Skip to content

Querying Unity Catalog Data as a Graph

Summary

In this tutorial, you will:

  • Build and start an open-source Unity Catalog server.
  • Create Delta tables under Unity Catalog and load example data through its CLI.
  • Start a PuppyGraph container, connect it to Unity Catalog, and query the Delta tables as a graph.

Self-contained Unity Catalog Data

This tutorial uses the open-source Unity Catalog server, which you build from source and run on the host alongside the PuppyGraph container. PuppyGraph reads Delta tables registered with Unity Catalog directly.

In real deployments, PuppyGraph points at your existing Unity Catalog (open source or Databricks-hosted) using the same schema shape. See Connecting to Delta Lake for the connection reference.

Prerequisites

  • docker is available on the host. Verify with:

    docker version
    
  • A working JVM toolchain (JDK 11+) and git so you can build the Unity Catalog server from source.

  • Accessing the PuppyGraph Web UI requires a browser. The schema upload and query steps also have CLI alternatives via curl and the bundled Gremlin console.

Setup

Build and start Unity Catalog

▶ Clone and build Unity Catalog:

git clone https://github.com/unitycatalog/unitycatalog
cd unitycatalog
build/sbt package

▶ From the same unitycatalog/ directory, start the server on port 9000:

./bin/start-uc-server -p 9000

Leave this running in its own shell.

Data Preparation

▶ Open a new shell so the server keeps running, then step back to the parent of the cloned unitycatalog/ directory. The setup script below resolves storage paths relative to that parent, so it must be run from there:

cd /path/to/parent-of-unitycatalog   # e.g., `cd ..` if you are currently inside `unitycatalog/`

▶ Create a file setup-uc.sh with the following content. The script provisions a Unity Catalog catalog, schema, four Delta tables, and seeds each table with modern-shaped sample data:

setup-uc.sh
#!/usr/bin/env bash
set -euo pipefail

unity_dir="$(pwd)/unitycatalog"
cli="${unity_dir}/bin/uc --server http://localhost:9000"

${cli} catalog create --name puppygraph
${cli} schema create --name modern --catalog puppygraph

${cli} table create \
  --full_name puppygraph.modern.software \
  --columns "id STRING, name STRING, lang STRING" \
  --storage_location "file://${unity_dir}/etc/data/external/puppygraph/modern/software/" \
  --format DELTA
${cli} table create \
  --full_name puppygraph.modern.person \
  --columns "id STRING, name STRING, age INT" \
  --storage_location "file://${unity_dir}/etc/data/external/puppygraph/modern/person/" \
  --format DELTA
${cli} table create \
  --full_name puppygraph.modern.created \
  --columns "id STRING, from_id STRING, to_id STRING, weight DOUBLE" \
  --storage_location "file://${unity_dir}/etc/data/external/puppygraph/modern/created/" \
  --format DELTA
${cli} table create \
  --full_name puppygraph.modern.knows \
  --columns "id STRING, from_id STRING, to_id STRING, weight DOUBLE" \
  --storage_location "file://${unity_dir}/etc/data/external/puppygraph/modern/knows/" \
  --format DELTA

${cli} table write --full_name puppygraph.modern.software
${cli} table write --full_name puppygraph.modern.person
${cli} table write --full_name puppygraph.modern.created
${cli} table write --full_name puppygraph.modern.knows

▶ Run it:

chmod +x setup-uc.sh
./setup-uc.sh

The Unity Catalog table write command populates the tables with auto-generated rows. The Unity Catalog repo's random value generator is configured to produce a small range of values, so the rows form a connected graph that matches the modern shape. The schema below uses the same column names regardless of the specific generated values.

id name age
STRING STRING INT
id name lang
STRING STRING STRING
id from_id to_id weight
STRING STRING STRING DOUBLE
id from_id to_id weight
STRING STRING STRING DOUBLE

Start PuppyGraph

▶ Start PuppyGraph and mount the Unity Catalog data directory into the container so it can read the Delta files referenced by Unity Catalog:

docker run -d --name puppygraph --rm \
  -p 8081:8081 -p 8182:8182 -p 7687:7687 \
  -e PUPPYGRAPH_USERNAME=puppygraph \
  -e PUPPYGRAPH_PASSWORD=puppygraph123 \
  -v "$(pwd)/unitycatalog:$(pwd)/unitycatalog" \
  --pull=always puppygraph/puppygraph:latest

The host bind for unitycatalog/ is mounted at the same path inside the container so the file:// storage locations in Unity Catalog resolve identically from both sides.

Default password

PUPPYGRAPH_PASSWORD ships as puppygraph123 for convenience. Change it before running on a publicly accessible machine.

Modeling a Graph

We model the data as the TinkerPop modern graph: two node types (person, software) and two edge types (knows, created).

Modern Graph
Modern Graph

▶ First, log into the PuppyGraph Web UI at http://localhost:8081 with the credentials configured above:

Field Value
Username puppygraph
Password puppygraph123

There are two ways to define the schema in PuppyGraph: build it interactively in the Schema Builder, or upload a JSON file directly. Pick whichever you prefer; both produce the same graph.

Build the graph in the Schema Builder

The Schema Builder is the visual editor in the PuppyGraph Web UI for adding catalogs, nodes, and edges step by step. It's the recommended path when you're modeling a graph for the first time or want to inspect what each click produces. For a deeper visual walkthrough of every dialog and field, see Modeling a Graph through the Schema Builder. The summary below covers what's needed to build the modern graph against this tutorial's Unity Catalog-managed Delta tables.

PuppyGraph treats Unity Catalog as a Delta Lake catalog with a Unity metastore.

Connecting to Unity Catalog

▶ Click Create Catalog, then expand Data Lakes and pick Delta Lake.

▶ Fill in the connection form:

Field Value
Catalog name uc_data
Metastore type Unity Catalog
Host http://host.docker.internal:9000
Token test
Databricks Catalog Name puppygraph
Storage type Get from metastore

The host.docker.internal hostname lets the PuppyGraph container reach the Unity Catalog server running on the host. On Linux, you may need to add --add-host=host.docker.internal:host-gateway to the docker run command above.

Unity Catalog form
Unity Catalog form

▶ Click Create Catalog.

Adding nodes and edges

▶ Add the software and person nodes, then the created and knows edges, the same way as in the Postgres tutorial. For each edge, set the From / To Node to the appropriate node label, map from_id and to_id as the FROM / TO Select Column, and assign id as the edge identifier.

Select the software table for a node
Select the software table for a node
Configure the software node
Configure the software node
Configure the knows edge
Configure the knows edge
Completed modern graph schema
Completed modern graph schema

Upload a schema file

If you've already built the graph in the Schema Builder above, you can skip this section. The resulting schema is the same.

This method writes the full schema to a JSON file and uploads it directly. It's useful when you already have a schema for an environment and want to recreate it elsewhere (e.g. for CI, scripted setup, or copy-pasting between PuppyGraph instances).

▶ Create a file schema.json with the following content. Replace <UC_HOST> with the hostname or IP at which the Unity Catalog server is reachable from inside the PuppyGraph container (host.docker.internal on Docker Desktop; an explicit host IP on Linux):

schema.json
{
  "catalog": [
    {
      "name": "uc_data",
      "type": "deltalake",
      "metastore": {
        "type": "unity",
        "host": "http://<UC_HOST>:9000",
        "token": "test",
        "databricksCatalogName": "puppygraph"
      }
    }
  ],
  "node": [
    {
      "label": "software",
      "dataSourceGroup": {
        "externalDataSource": {
          "enabled": true,
          "catalog": "uc_data",
          "schema": "modern",
          "table": "software",
          "mappedField": [
            { "sourceFieldName": "id",   "targetFieldName": "id"   },
            { "sourceFieldName": "name", "targetFieldName": "name" },
            { "sourceFieldName": "lang", "targetFieldName": "lang" }
          ]
        }
      },
      "id":        [{ "name": "id",   "type": "STRING" }],
      "attribute": [
        { "name": "name", "type": "STRING" },
        { "name": "lang", "type": "STRING" }
      ]
    },
    {
      "label": "person",
      "dataSourceGroup": {
        "externalDataSource": {
          "enabled": true,
          "catalog": "uc_data",
          "schema": "modern",
          "table": "person",
          "mappedField": [
            { "sourceFieldName": "id",   "targetFieldName": "id"   },
            { "sourceFieldName": "name", "targetFieldName": "name" },
            { "sourceFieldName": "age",  "targetFieldName": "age"  }
          ]
        }
      },
      "id":        [{ "name": "id",   "type": "STRING" }],
      "attribute": [
        { "name": "name", "type": "STRING" },
        { "name": "age",  "type": "INT"    }
      ]
    }
  ],
  "edge": [
    {
      "label":         "created",
      "fromNodeLabel": "person",
      "toNodeLabel":   "software",
      "dataSourceGroup": {
        "externalDataSource": {
          "enabled": true,
          "catalog": "uc_data",
          "schema": "modern",
          "table": "created",
          "mappedField": [
            { "sourceFieldName": "id",      "targetFieldName": "id"      },
            { "sourceFieldName": "from_id", "targetFieldName": "from_id" },
            { "sourceFieldName": "to_id",   "targetFieldName": "to_id"   },
            { "sourceFieldName": "weight",  "targetFieldName": "weight"  }
          ]
        }
      },
      "id":        [{ "name": "id",      "type": "STRING" }],
      "fromKey":   [{ "name": "from_id", "type": "STRING" }],
      "toKey":     [{ "name": "to_id",   "type": "STRING" }],
      "attribute": [
        { "name": "from_id", "type": "STRING" },
        { "name": "to_id",   "type": "STRING" },
        { "name": "weight",  "type": "DOUBLE" }
      ]
    },
    {
      "label":         "knows",
      "fromNodeLabel": "person",
      "toNodeLabel":   "person",
      "dataSourceGroup": {
        "externalDataSource": {
          "enabled": true,
          "catalog": "uc_data",
          "schema": "modern",
          "table": "knows",
          "mappedField": [
            { "sourceFieldName": "id",      "targetFieldName": "id"      },
            { "sourceFieldName": "from_id", "targetFieldName": "from_id" },
            { "sourceFieldName": "to_id",   "targetFieldName": "to_id"   },
            { "sourceFieldName": "weight",  "targetFieldName": "weight"  }
          ]
        }
      },
      "id":        [{ "name": "id",      "type": "STRING" }],
      "fromKey":   [{ "name": "from_id", "type": "STRING" }],
      "toKey":     [{ "name": "to_id",   "type": "STRING" }],
      "attribute": [
        { "name": "from_id", "type": "STRING" },
        { "name": "to_id",   "type": "STRING" },
        { "name": "weight",  "type": "DOUBLE" }
      ]
    }
  ]
}

▶ In the Web UI, click Graph in the sidebar, then Upload Schema, and select schema.json.

Upload via CLI

You can also POST the schema directly:

curl -X POST -H "content-type: application/json" \
  --data-binary @./schema.json \
  --user "puppygraph:puppygraph123" \
  http://localhost:8081/schema

Querying the Graph

In the PuppyGraph Web UI, click Query in the sidebar. You can run graph queries in either Cypher or Gremlin.

The following query traverses two knows and created hops from any person:

MATCH path = (p:person)-[:knows]->()-[:created]->()
RETURN path
LIMIT 5;
g.V().hasLabel('person')
  .out('knows').out('created').path()
  .limit(5)

Unity Catalog seeds each table with random data, so the specific path values vary between runs. The query should return at least one path through any person → person → software hop.

Cleanup

▶ Stop PuppyGraph:

docker stop puppygraph

▶ Stop the Unity Catalog server with Ctrl+C in the shell where it was started.