Cypher reference

openCypher, with two extra time axes.

InvariantDB targets openCypher syntax for everything you know — MATCH, CREATE, MERGE, SET, DELETE, WITH, UNWIND, aggregates, list comprehensions, quantifiers. On top of that, every property is bitemporal, so AT VALID / AT RECORDED / OVER VALID BETWEEN are first-class keywords.

Reads

// Standard match
MATCH (p:Person {name: 'Alice'})-[:WORKS_AT]->(c:Company)
RETURN p.name, c.name

// At a point in time (real-world)
MATCH (p:Policy) AT VALID '2024-06-01'
WHERE p.holder = 'acme' RETURN p

// As the database knew it at a point in time (transaction)
MATCH (p:Policy) AT RECORDED '2024-06-01' RETURN p

// Both axes at once — "what was true on June 1 according to what we
// knew on June 1"
MATCH (p:Policy)
  AT VALID '2024-06-01'
  AT RECORDED '2024-06-01'
RETURN p

// Walk every version that overlaps a window
MATCH (p:Policy {id: $id})
  OVER VALID BETWEEN '2024-01-01' AND '2024-12-31'
RETURN p.premium, p._valid_from, p._valid_to
ORDER BY p._valid_from

Writes

CREATE (p:Person {name: 'Alice', email: 'alice@example.com'})

MERGE (p:Person {email: 'alice@example.com'})
ON CREATE SET p.created_at = datetime()
ON MATCH  SET p.last_seen = datetime()

// Map-merge: keep existing keys, overwrite supplied
SET p += {a: 1, b: 2}

DELETE p

// Cascade across edges
DETACH DELETE p

Aggregation

AggregateNotes
count(*), count(x)Empty input → 0 (Cypher spec).
count(DISTINCT x)Dedups before counting.
sum(x), avg(x)Empty input: sum=0, avg=null.
min(x), max(x)Empty input: null.
collect(x), collect(DISTINCT x)List collector.
WITH-stage aggregationWITH x, count(*) AS c WHERE c > 5 RETURN x, c works as expected.

Pattern features

// Variable-length paths
MATCH (a)-[*1..5]->(b) WHERE a.id = $start AND b.id = $end RETURN length(path)

// Named paths
MATCH p = (a)-[r]->(b)
RETURN length(p), nodes(p), relationships(p)

// shortestPath / allShortestPaths
MATCH (a:Person {id:$a}), (b:Person {id:$b})
MATCH p = shortestPath((a)-[*..6]-(b))
RETURN p

// Pattern comprehension
MATCH (a:Person {id:$id})
RETURN [(a)-[:KNOWS]->(friend) WHERE friend.active | friend.name] AS active_friends

// Quantifier predicates
MATCH (p:Project)
WHERE ANY(x IN p.tags WHERE x STARTS WITH 'urgent')
  AND ALL(c IN p.checks WHERE c.passed)
RETURN p

// reduce()
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x * x) AS sum_of_squares

WHERE conditions

// Standard comparison
WHERE n.score >= 0.5 AND n.label IN ['A', 'B']

// Regex
WHERE n.name =~ '(?i)alice.*'

// String predicates
WHERE n.name STARTS WITH 'A'
WHERE n.url ENDS WITH '.com'
WHERE n.body CONTAINS 'pricing'

// Not-equals
WHERE a.x <> b.x

// Null handling
WHERE n.email IS NULL
WHERE n.email IS NOT NULL

Aggregating subqueries

// COUNT { ... } — Cypher-5 count subquery
RETURN COUNT { MATCH (a:Person {name:'Alice'})-[:KNOWS]->() } AS friend_count

// CALL { ... } — sub-query with parameters
MATCH (a:Account {id:$id})
CALL {
  WITH a
  MATCH (a)-[:OWNS]->(p:Position)
  RETURN sum(p.value) AS total
}
RETURN a.id, total

Indexes

// Property index — current data
CREATE INDEX FOR (n:Person) ON (n.email)

// Vector index (HNSW)
// Schema-driven via /graphs/{name}/schema; see docs.html

// Drop
DROP INDEX FOR (n:Person) ON (n.email)
Historical backfill: CREATE INDEX now backfills every snapshot in history so AT VALID '<past>' queries hit the index too. See the migration note in the ADR index.

Procedures

Time + history

ProcedurePurpose
db.changes(from_ns, to_ns)Every mutation in a window.
db.delta(from_ns, to_ns)Per-entity summary of changes in a window.
db.changeRate(window_seconds)Mutations-per-second over the recent window.
db.propertyHistory(nodeId, prop)Per-property bitemporal version history.
db.history(nodeId)Full per-version record dump.
db.delta(from_ts, to_ts)Per-entity summary of changes in a window.

Provenance + lineage

db.propertyProvenance(nodeId, prop)Provenance (actor / source / reason / confidence) at every version.
db.linkDerivedFrom(node, sources, kind?)Wire derivation edges into the graph.
db.derivedFrom(nodeStrId, maxDepth?)Walk derivation lineage.

Agent memory

db.recordEpisode(session, kind, text, payload)Append an event to a session log.
db.replayEpisodes(session, limit)Replay a session's events.
db.consolidateSession(session, opts)Roll old episodes into a summary.
db.sessionSummaries(session)List summaries for a session.
db.recordBelief(agent, key, content, evidence)Record a belief + evidence.
db.reviseBelief(key, content, evidence)Revise a belief; previous version stays.
db.beliefLineage(key)Every version of a belief, with evidence.
db.applySalienceDecay(session, opts)Decay session-wide salience.
db.reinforceSalience(episodeId, amount)Boost a single memory.
db.embeddingModelDrift()List embedding models in the graph + drift signal.

Compliance

db.aclEvents(sinceId?, principal?, limit?)Read ACL audit chain.
db.verifyAclChain()Verify the hash chain.
db.destroySubjectKey(subject)GDPR Art. 17 — crypto-shred.
db.proveErasure(subject)Signed receipt + audit_seq.
db.subjectExport(subject)GDPR Art. 20 — full per-subject export.
db.dpBudget(principal?)Differential-privacy budget snapshot.
db.dpEvents(principal?, from_ns?, to_ns?)DP audit log replay.
db.dpVerifyChain()Verify the DP audit hash chain.

Graph algorithms

gds.shortestPath(source, target, edgeType?)BFS shortest path.
gds.pageRank(maxIters?, damping?)Standard PageRank.
gds.connectedComponents()Union-Find components.
gds.degreeCentrality(direction?)In/out/total degree.
gds.fulltextSearch(label, query, k?)BM25 top-K.
gds.vectorSearch(label, prop, query, k?)HNSW top-K.
gds.semanticMatchExpand(label, prop, query, k, hops, edgeTypes, threshold?)Top-K seeds + N-hop graph expansion. Single call replaces a RAG pipeline.

Inline vector functions

// In WHERE
MATCH (d:Doc) WHERE cosine(d.embedding, $query) > 0.75 RETURN d

// In RETURN
MATCH (d:Doc) RETURN d.title, cosine(d.embedding, $query) AS sim
ORDER BY sim DESC LIMIT 10

// Available: cosine, euclidean, dotProduct

When a vector index is declared, the planner pushes the WHERE cosine() > t filter into HNSW automatically.

Parameter substitution

// Anywhere a literal goes — including LIMIT / SKIP
MATCH (p:Person) WHERE p.role = $role RETURN p LIMIT $page_size

EXPLAIN + EXPLAIN ANALYZE

EXPLAIN MATCH (n:Person) WHERE n.email = $e RETURN n
// → planner text, no execution

EXPLAIN ANALYZE MATCH (n:Person) WHERE n.email = $e RETURN n
// → actual row counts + per-stage timing, the query runs

Things to watch

Backticks for reserved-ish labels. :CONTAINS works as a label name; backtick when in doubt: :`weird name with spaces`.
OPTIONAL MATCH chains. If an OPTIONAL MATCH introduces a new variable that you later filter on, make sure the filter handles null; otherwise rows from the optional side disappear.
Pure aggregate RETURN with no rows. Returns one row with identity values (count→0, sum→0, avg→null). This matches Cypher spec but surprises people coming from SQL.
Private-mode keys (DP) refuse non-aggregate returns. RETURN nPRIVATE_MODE_NON_AGGREGATE_RETURN. See DP details.

Quickstart →  ·  SDK →  ·  MCP tools →  ·  All features →