Cypher.
Cypher reads the same rows SQL does. What it adds is a pattern grammar: instead of joining tables on keys, you draw the shape you are looking for - (a)-[:rel]->(b) - and the engine walks it.
Reach for it when the question is "what is this row connected to, and what is that connected to". A three-hop question is three arrows in Cypher and three joins in SQL, and the arrows stay readable. For aggregation, string matching, arithmetic or anything with a GROUP BY in it, use SQL - this dialect deliberately does not compete there.
POST /v1/tenants/:t/cypher with a body of { cypher, default_schema?, params? }. Reads and writes both go here. No SDK wraps this route yet, so every tab on this page builds the request by hand - which is three lines of boilerplate and then plain query strings.
Before you start: the graph shape.
A relationship is not a table. It is a column on the row that holds another row's primary key, plus a [[relations]] block naming it. Declare the block and every write to that table starts maintaining the edge for you. Graph walks through the modelling in depth; here is the shape these examples use.
Register shop.customers first - a relation's target table must already exist when the pointing table is registered. It refers to itself through referred_by, which is what makes a variable-length referral chain legal later on.
namespace = "shop"
table = "customers"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str"
required = true
[[columns]]
name = "name"
ty = "str"
[[columns]]
name = "country"
ty = "str"
[[columns]]
name = "referred_by"
ty = "str" # another customer's id - or absent
# A customer points at the customer who referred them. Source and target
# are the SAME table, which is what makes variable-length paths legal.
[[relations]]
name = "referrer"
from_col = "referred_by"
target = { namespace = "shop", table = "customers", pk = "id" }
bidirectional = true
# REQUIRED for MATCH (c:customers {id: '...'}) to resolve.
[[indexes]]
name = "by_id"
columns = ["id"] namespace = "shop"
table = "orders"
primary_key = ["id"]
[[columns]]
name = "id"
ty = "str"
required = true
[[columns]]
name = "customer"
ty = "str" # a shop.customers id
[[columns]]
name = "amount_cents"
ty = "i64"
[[columns]]
name = "status"
ty = "str"
[[columns]]
name = "notes"
ty = "str"
[[columns]]
name = "placed_ms"
ty = "u64"
# The edge: an order points at the customer who placed it.
[[relations]]
name = "placed_by"
from_col = "customer"
target = { namespace = "shop", table = "customers", pk = "id" }
bidirectional = true
[[indexes]]
name = "by_id"
columns = ["id"] MATCH (o:orders {id: "..."}) resolves through an index named by_<primary key column>. Without that [[indexes]] block the point match does not resolve, and since every traversal has to start from a pinned node, nothing on this page will work. Declare it on any table you intend to query with Cypher.