Graph
Preview — the graph engine is v1 and the surface is deliberately narrow: one node table and one edge table per graph, integer node keys only, and a read-only Cypher subset. The shapes documented here are stable, but the follow-ups called out on each page (multi-table graphs, string keys, weighted paths) are not shipped. See Feature status.
The fourth dimension of a bucket. A graph is declared over tables you already
have — no separate graph store to load. Node rows come from an ordinary
table, edge rows from an ordinary table with src/dst columns; writes ride
the normal SQL write path, transactions included.
Graphs are Tables-pillar objects: there is no separate graph service to
enable. CREATE GRAPH / DROP GRAPH / SHOW GRAPHS go through
Tables.Execute like any other
SQL, and the wire endpoints + credentials come from the same
Connect story as every other engine.
Two-plane model
The graph engine is a derived artifact over table truth:
- Base truth — the node and edge tables. Every insert/update/delete is an ordinary SQL write.
- Serve plane —
CREATE GRAPHbuilds a compact CSR adjacency pinned at a specific table version and serves traversals from it. Table writes after the pin are not in the artifact until it rebuilds (which happens automatically when the edge table drains/compacts) — unless you ask for a'strong'read, which merges the not-yet-compacted writes into the traversal at query time.
nodes/edges tables ──CREATE GRAPH──► CSR artifact @ version V
│ │
│ writes (SQL INSERT/DELETE) ├── eventual read: artifact as-is
▼ │
write-ahead log ────────'strong' read────────┴── artifact ∪ un-drained writesSee Graph DDL for the exact lifecycle and freshness rules.
Quick tour
-- ordinary tables (edge tables need their own PK; the build reads only SRC/DST)
CREATE TABLE person (id INT PRIMARY KEY, name VARCHAR);
CREATE TABLE knows (edge_id INT PRIMARY KEY, src INT, dst INT);
-- declare the graph over them
CREATE GRAPH social NODES (person KEY id) EDGES (knows SRC src DST dst);
SHOW GRAPHS;
-- traverse: native table functions…
SELECT * FROM graph_khop('social', 1, 2); -- (node, hop_distance)
SELECT * FROM graph_neighbors('social', 2, 'both'); -- (neighbor)
SELECT * FROM graph_shortest_path('social', 1, 7); -- (step, node)
-- …or the Cypher spelling (same engine, same rows)
SELECT * FROM cypher('social',
'MATCH (a)-[:knows*1..2]->(b) WHERE id(a) = 1 RETURN b');
-- whole-graph analytics
SELECT * FROM graph_pagerank('social'); -- (node, rank)
SELECT * FROM graph_components('social'); -- (node, component)
SELECT * FROM graph_bfs('social', 1); -- (node, level)
-- graph + SQL in one statement: traverse in a subquery, aggregate outside.
-- e.g. total deal value across an account's 5-hop network
SELECT SUM(amount) FROM deals
WHERE account_id IN (SELECT node FROM graph_khop('accts', 1, 5));All of the above runs over the Postgres wire, Execute, or
dodil data sql -b <bucket> "…".
Three doors to one engine
| Door | What it speaks | Notes |
|---|---|---|
SQL — pg.uk-lon-1.dodil.io:5432, Execute RPC, dodil data sql | graph_* table functions + cypher() + graph DDL | The full surface — DDL, traversals, analytics, freshness control |
Bolt — bolt+s://bolt.uk-lon-1.dodil.io:7687, neo4j drivers, cypher-shell | The Cypher subset — Cypher only, no SQL, no analytics | Bolt 5.0–5.4 / 4.4. Use bolt+s:// — TLS on connect, verified against public roots. neo4j+s:// and neo4j:// are both refused as unrouted (a routing-protocol limit, not a TLS one). Autocommit RUN only, no query parameters; the Bolt database is the bucket name, required on every session. See the Bolt notes for the v1 graph-selection gap |
GraphQL — gql.uk-lon-1.dodil.io/graphql | {graph}_khop / {graph}_neighbors / {graph}_shortestPath root fields, plus a nested traversal field on each node-table type named after that graph’s edge table (with a depth argument) | Traversals only — no analytics. Composes with relational and vector fields in one query — see Converging engines |
The same traversal, spelled three ways:
cypher-shell
# -d is the DATABASE = your bucket name. Scheme is bolt+s:// (TLS, public
# roots). neo4j+s:// and neo4j:// both fail — they ask for routing, which
# this adapter refuses.
cypher-shell \
-a bolt+s://bolt.uk-lon-1.dodil.io:7687 \
-u "$DK_KEY_ID" -p "$DK_SECRET" \
-d kb-prod \
"MATCH (a)-[:knows*1..2]->(b) WHERE id(a) = 1 RETURN b"What the engine is (and isn’t)
- Directed multigraph over integer keys. Node keys are the node table’s
integer
KEYcolumn (v1: string keys are rejected loudly). Every edge-table row is an edge — parallel edges are kept, self-loops allowed. - Read-only query surface. There is no write Cypher:
CREATE/MERGE/SET/DELETEare rejected with a pointer to SQL — edges are ordinary rows, so youINSERT INTO knows …instead (and get transactions for free). - Single-start traversals + whole-graph analytics. k-hop expansion, neighbors, shortest path (unweighted BFS), PageRank, weak connected components, leveled BFS. No weighted paths, no arbitrary-pattern matching.
- Traversal direction is a query-time choice —
'out'(default),'in','both'— because the artifact stores both forward and reverse adjacency. - Results are key rows, not node objects. Traversals return
BIGINTnode keys (plus hop/step/level columns) — no Neo4jNode/Relationshipstructs. Rehydrate properties byJOINing the node table in the same statement (the traversal is served plane-side and spliced in as an inline relation — see Composing traversals with SQL), or let GraphQL’s nested fields do that join for you.
Where to next
- Graph DDL —
CREATE/DROP/SHOW GRAPHSexact grammar, build lifecycle, how table edits reflect, residency - Cypher & Traversals — the supported subset, anchoring rules, the honest unsupported list, Bolt specifics
- Analytics —
graph_pagerank,graph_components,graph_bfs: parameters and output shapes - Converging engines — KNN + graph + SQL + GraphQL over one bucket
- Connect & wire adapters — endpoints and credentials