Decode any hot-path query cold — six idioms cover the whole codebase. ~14 min.
You flagged Cypher as a gap, and forty lessons have quoted it at you. Here's the good news: the entire codebase's Cypher is built from about six recurring idioms. Learn to recognize them and you can read any hot-path query cold — and spot when one breaks a hard rule. This is a skills lesson: every query below is real, and you've already met most of them in context. Now we read them as Cypher.
(a)-[:REL]->(b) is "an a connected to a b by a REL edge." MATCH is SELECT … WHERE;
MERGE is INSERT … ON CONFLICT (upsert); SET is UPDATE; RETURN is the projection. Nodes in
(), edges in [], properties in {}. That's 90% of it.
graph_id-scopedEvery single query starts by pinning a node by id and graph_id — never a bare MATCH (n). The
read surface's point lookup (rules/node_lookup.go):
MATCH (n:Entity {id: $id, graph_id: $graph}) RETURN n.node_risk_score, n.symbol, …
graph_id scopes every query because multiple independent graphs share one Memgraph (L2). And the anchor on
id is index-served — O(1), not a scan. A query that starts MATCH (n) with no anchor is the cardinal sin:
it scans all ~2M nodes. If you read a hot-path query and it isn't pinned to a node or an edge type up front, that's the
first thing to flag.
UNWIND a listTo read N nodes in one round-trip, UNWIND a parameter list into rows and match each (the batched form of idiom 1,
from the same file):
UNWIND $ids AS nid MATCH (n:Entity {id: nid, graph_id: $graph}) RETURN …
UNWIND turns a list into rows — the inverse of collect(). You saw the write side of this everywhere (L41's
coalescing produces exactly UNWIND $rows AS row <template>); here it's the read side. One idiom, both directions.
The graph's signature query — the partial-graph spine walk that loads a focus token's neighborhood (L23/L38,
risk/graph_partial.go):
MATCH (root:Entity {id: $root, graph_id: $graph}) OPTIONAL MATCH (root)-[r:<spine edge types>*1..6]-(n:Entity {graph_id: $graph})
Three things to read here, each a hard rule made concrete:
*1..6 | bounded variable-length path: follow the relationship 1 to 6 hops — never unbounded (* alone would walk the whole component). This is L2's "bound the depth." |
-[r:…]- | undirected (no >): traverse the edge in both directions. CLAUDE.md: "query both edge directions" — a directed -> here would silently miss half the neighborhood. |
OPTIONAL MATCH | a left-join: root is still returned even if it has no spine neighbors. A plain MATCH would drop isolated roots entirely. |
collect()L32's conservation read gathers every holder balance into one list (risk/verify_balance_conservation.go):
MATCH (t:Entity {graph_id: $graph, id: $token})-[r:HOLDS]->(:Entity) RETURN collect(r.quantity_raw) AS qtys
Here the arrow is directed (->): HOLDS has a known direction (token→holder), so reading it one way is correct.
collect() folds the matched rows into a single list returned in one row — the read-side inverse of UNWIND. The
target node is (:Entity) with no variable because we only need the edge property, not the holder.
MERGE, never CREATE; and the anti-joinEvery write is idempotent because delivery is at-least-once (L7/L9). Two flavours. The batched SET (L38's
node_risk_score):
UNWIND $updates AS u MATCH (n:Entity {id: u.id, graph_id: $graph}) SET n.node_risk_score = u.nrs, n.governance_risk = u.gov
And the anti-join idempotent edge create (L27's oracle bridger) — create the edge only if it's missing:
MATCH (token)-[:LENDING_COLLATERAL]->(market), (token)-[:ORACLE_DEP]->(oracle) WHERE NOT EXISTS { MATCH (market)-[:ORACLE_DEP]->(oracle) } MERGE (market)-[:ORACLE_DEP]->(oracle)
MERGE = "match it or create it" (upsert) — re-running is a no-op, which is why every write uses it (idempotency,
L4/L9). CREATE would duplicate on replay — you'll almost never see it on the hot path. And note L26's deliberate choice
to MATCH (not MERGE) the token before merging an oracle edge: MATCH requires the node to already exist, so a
binding to a delisted token can't accidentally stub a phantom node. Picking MATCH vs MERGE is a correctness decision,
not a style one.
coalesce (no APOC)Memgraph has no APOC, and a node's kind can live under any of three properties, so the codebase reads it through a
coalesce fallback chain (seen in chainref / rules scope):
WHERE coalesce(dep.type, dep.subcategory, dep.category) IN ['pool', 'vault', …]
coalesce(a, b, c) returns the first non-null — so "the node's kind" is "whichever of type / subcategory /
category is set first." You'll also see the write-side don't-clobber idiom SET r.subcategory =
coalesce(r.subcategory, $new) — "set it only if it isn't already set."
graph_id; (2) anchored, never a bare MATCH (n) full scan; (3) variable-length
paths bounded (*1..6, not *); (4) edges queried both directions unless the direction is genuinely known;
(5) kind via coalesce (no APOC); (6) writes are MERGE + idempotent, and bulk writes go through the
graph-writer (a forbidigo lint blocks raw ExecuteWrite, L9/L41).
MATCH (n:Entity {... graph_id: $graph}) rather than MATCH (n:Entity)?(root)-[r:<spine>*1..6]-(n), what does *1..6 specify?-[r:…]- (no arrowhead). What does the missing direction mean?collect(r.quantity_raw). What does collect() do?MERGE rather than a CREATE?WHERE NOT EXISTS { MATCH (market)-[:ORACLE_DEP]->(oracle) } before its MERGE. What's that for?coalesce(n.type, n.subcategory, n.category) instead of just n.type?MATCHes the focus token but MERGEs the oracle node. Why MATCH the token?*1..6, undirected) and say what it traverses and how far.UNWIND ↔ collect as the row/list inverses, on both read and write sides.Grounded in real queries: pkg/rules/node_lookup.go (anchored point + UNWIND-by-id reads), pkg/risk/graph_partial.go (MATCH (root…) OPTIONAL MATCH (root)-[r:<spine>*1..6]-(n…) bounded undirected walk), pkg/risk/verify_balance_conservation.go (collect(r.quantity_raw)), pkg/risk/node_risk_score.go (UNWIND $updates … SET), pkg/enrichment/oracle_bridger.go (WHERE NOT EXISTS{…} MERGE anti-join) + navlink_refresher.go (MATCH-token/MERGE-oracle), coalesce(type,subcategory,category) kind idiom; CLAUDE.md Cypher hard rules + glossary. Verify against source — the code is the truth.