Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cypher Query Reference

mushroomdb supports a Cypher subset for pattern matching, filtering, aggregation, ordering, and writes (CREATE, SET, DELETE, DETACH DELETE, MERGE). This page documents the full grammar, with a dedicated section on variable-length paths and shortestPath.


Key idiom — how to filter and read node keys

The node’s key lives in the id map, not as a stored property. These spellings read it (and the label) anywhere a scalar does — WHERE, ORDER BY, RETURN, an inline pattern filter, and after a grouping WITH:

SpellingGives
n.key / n.idthe node’s key (a stored property of that name wins)
n.labelthe node’s label (stored label wins)
key(n) / id(n)always the id-map key; id(n) aliases key(n)
labels(n)a one-element list holding the label

n.key, n.id, and n.label are a fallback: a stored property of that name always wins, so a graph that really has an id or key property keeps reading it. key(n) / id(n) always return the id-map key (id(n) is the function alias of key(n): one argument, same errors on a non-node).

MATCH (n {id: 'alice'}) is not the property: it is a key selector (ScanKey) on MATCH/MERGE/CREATE. CREATE may still stamp a stored id property; that node then reads n.id from the store, which equals the key.

-- filter by key
MATCH (n:Person) WHERE n.id = 'alice' RETURN n.id, n.name
MATCH (n:Person) WHERE n.key = 'alice' RETURN n.key, n.name
MATCH (n:Person {id: 'alice'}) RETURN n.name
MATCH (n:Person) WHERE n = 'alice' RETURN n        -- the older spelling, still correct

-- read key and label
MATCH (n:Person) RETURN n.id, id(n), n.key, n.label AS kind ORDER BY n.key

-- and after a grouping WITH, which used to flatten `c` to a scalar
MATCH (t:Talent)-[:FIT]->(c:Company)
WITH c, count(DISTINCT t) AS n WHERE n >= 2
RETURN key(c), n ORDER BY n DESC

A bare RETURN n also works: the result row value for a node variable is the key string, not a node object. In Python, row["n"] is "alice", not a dict.

Performance note. WHERE n.key / n.id / key(n) / id(n) against a literal or $param plans as ScanKey (plus a stored-property filter so stored-wins still holds), not a label scan. n.label / labels(n) do not: those still scan the label. Pattern {id: 'x'} remains a key lookup.


Basic patterns

MATCH (n:Person)-[:KNOWS]->(m:Person)
WHERE n.name = 'Alice'
RETURN m.name
ORDER BY m.name
LIMIT 10

Node patterns

(var:Label {key: 'value'})
  • var — optional binding name
  • :Label — optional label filter
  • {key: 'value'} — zero or more property equality filters. {id: 'alice'} is a key lookup (ScanKey). The names key and label fall back to the node’s own key and label when no stored property of that name exists (see Key idiom above)

Relationship patterns

-[r:TYPE]->    right-directed
<-[r:TYPE]-    left-directed
-[r:TYPE]-     undirected (matches either direction)

The relationship variable (r) is optional. Omitting the brackets entirely (-->) is also accepted.

Multiple patterns in one MATCH

Patterns separated by commas inside a single MATCH are equivalent to the same patterns as consecutive MATCH clauses — they plan and execute identically. Sharing a variable between them is a join; sharing none is a cartesian product, which is the openCypher meaning.

-- the intersection: a talent joined to a company by all three types
MATCH (t:Talent)-[:INDUSTRY_ALIGNMENT]->(c:Company),
      (t)-[:SPECIALTY_MATCH]->(c),
      (t)-[:LOCATION_FIT]->(c)
WHERE t.status = 'published'
WITH c, count(DISTINCT t) AS n WHERE n >= 14
RETURN key(c), n ORDER BY n DESC

count(DISTINCT …) is what makes that correct: three edge types between the same pair yield three rows per talent under an alternation, and only DISTINCT counts the talent once. A company joined by only two of the three types does not appear at all.

Comma-separated patterns after CREATE are still a named parse error.


Variable-length paths

Add *min..max inside the relationship brackets to match paths spanning multiple hops:

MATCH (a:N)-[r:T*1..5]->(b:N)
RETURN b

Range syntax

SyntaxMeaning
*1 to 10 hops (bare star default)
*nExactly n hops
*min..maxmin to max hops (both inclusive)
*..max1 to max hops

Hard cap: max hops ≤ 10. Patterns with an unbounded upper end (*min..) or with max > 10 are rejected at parse time with the error:

variable-length paths are capped at 10 hops

Minimum hop count ≥ 1. Zero-length paths (*0, *0..N) are not supported and are rejected at parse time with the error:

zero-length variable-length paths are not supported; minimum hop count is 1

Semantics

  • BFS expansion — each hop expands all neighbours of the current frontier.
  • Per-path edge uniqueness — a relationship may not appear twice within the same path (Cypher relationship isomorphism). This guarantees termination on cyclic graphs.
  • Intermediate-row budget — if the output row count reaches 1,000,000 the query is aborted with an error naming the limit. Use tighter ranges or a WHERE filter to stay within budget on dense graphs.

Executor routing

Variable-length plans always use the staged execution path regardless of a LIMIT clause. LIMIT is applied after materialisation.

Accessing path length

The relationship variable bound in a *min..max pattern exposes a virtual .length field:

MATCH (a:N)-[r:T*1..5]->(b:N)
RETURN r.length

No other fields on r are available for variable-length relationships.


shortestPath

shortestPath finds the shortest directed path between two already-bound nodes:

MATCH (a:N {id: 'alice'}) MATCH (b:N {id: 'bob'})
MATCH shortestPath((a)-[r:T*..5]->(b))
RETURN r.length

Bind the two endpoints with separate MATCH clauses. Comma-separated patterns in a single MATCH (MATCH (a), (b)) are not supported and fail to parse.

  • Both endpoints must be bound before the shortestPath clause. Unbound forms are rejected at planning time.
  • A minimum hop count greater than 1 (e.g., *2..5) is rejected at planning time with the error "shortestPath does not support a minimum hop count". Use a plain variable-length pattern if you need a minimum.
  • BFS stops at the first path found (shortest hop count).
  • Tie behavior: When two or more paths reach the destination at the same BFS depth, exactly one row is returned. The choice is arbitrary (internal storage order) and is not guaranteed to be stable across re-opens or inserts.
  • The *..max range cap still applies (max ≤ 10).
  • Returns zero rows when no path exists within the hop limit.

WITH pipelines

WITH turns the executor into a multi-stage pipeline: each clause projects, filters, sorts, and/or limits the working set before passing rows into the next stage.

Basic projection and aliasing

MATCH (p:Person)
WITH p, p.city AS city
RETURN city
ORDER BY city

Aggregation with HAVING (WHERE after WITH)

MATCH (p:Person)
WITH p.city AS city, COUNT(*) AS cnt
WHERE cnt > 2
RETURN city, cnt
ORDER BY cnt DESC

The WHERE clause after an aggregating WITH acts as a HAVING filter — evaluated against the group result, not the raw matched rows.

A grouping key that is a bare variable stays a node through the group, so key(c), c.key and c.label all still read after WITH c, count(*) AS n. A computed key (c.city, toLower(c.name)) becomes a scalar, as it must.

An aggregating WITH projects its RETURN whether or not a WHERE follows it:

MATCH (p:Person)-[:WORKS_AT]->(c:Company)
WITH c, COUNT(p) AS n
RETURN key(c), n * 2 AS doubled

A WITH alias is in scope for its own WHERE

An alias a non-aggregate WITH introduces is visible to the WHERE, ORDER BY, SKIP and LIMIT that follow it, because the stage projects before it filters:

MATCH (t:Talent)
WITH t, t.years_of_experience AS x
WHERE x > 11
ORDER BY x DESC
RETURN key(t), x

An unknown name in that WHERE is still an error (unbound variable `nope` in WHERE). A name the WITH dropped is accepted and reads null at runtime, so the predicate is false rather than wrong.

ORDER BY and LIMIT inside WITH

MATCH (p:Person)
WITH p, p.age AS age
ORDER BY age DESC
LIMIT 5
RETURN p.name AS name, age

Chained stages with re-entry MATCH

MATCH (p:Person)
WITH p.city AS city, COUNT(*) AS cnt
WHERE cnt > 1
MATCH (p2:Person)
WHERE p2.city = city
RETURN p2.name AS name, city

Each WITH stage produces a new working set. A subsequent MATCH clause joins against that set — use WHERE to correlate.


UNWIND

UNWIND expands a list into one row per element. Null or empty lists produce zero rows.

MATCH (p:Person)
UNWIND [1, 2, 3] AS x
RETURN p.name AS name, x

List source forms:

  • Literal list: UNWIND [1, 2, 'a'] AS x
  • Property containing a list value: UNWIND p.tags AS tag
  • A named alias from a prior WITH: UNWIND items AS item

Passing a non-list value (e.g., a plain integer property) is a named error. Intermediate rows produced by UNWIND count against the 1,000,000-row budget.


Aggregation

MATCH (a:N)-[:T]->(b:N)
RETURN COUNT(*)

MATCH (a:N)-[r:RATED]->(b:N)
RETURN AVG(r.score)

Supported functions: COUNT(*), COUNT(var), SUM(var.field), AVG(var.field), MIN(var.field), MAX(var.field).

DISTINCT inside an aggregate

count(DISTINCT x) and collect(DISTINCT x) deduplicate within each group before aggregating, keyed by the same normalisation grouping uses (so an Int 1 and a Float 1.0 are one value).

MATCH (t:Talent)-[:FIT|:NEAR]->(c:Company)
RETURN key(c), count(DISTINCT t) AS talents

count(DISTINCT *) is a named parse error. A variable actually named distinct still parses as an ordinary argument.

Grouped aggregation

One or more non-aggregate RETURN items act as group keys; one or more aggregate functions are computed per group:

MATCH (n:Person)-[:KNOWS]->(m:Person)
RETURN n.city, COUNT(*) AS friends
ORDER BY friends DESC
LIMIT 5
  • Multiple group keys and multiple aggregates are supported in the same RETURN clause.
  • NULL group-key values group together (openCypher semantics).
  • Empty input with no group-key items yields exactly one row (openCypher semantics: COUNT=0, other aggregates null).
  • Group count is capped at 1,000,000 distinct keys per query.
  • ORDER BY + LIMIT sort the finished group table (top-k groups), not the input rows; the LIMIT push-down optimisation is intentionally disabled for grouped queries.
  • Multiple aggregates without any group-key items (e.g. RETURN COUNT(*), SUM(r.score)) is also supported and returns exactly one row.
  • Numeric group-key values are compared as floats: 1 (integer) and 1.0 (float) land in the same group. Caveat: integers whose absolute value exceeds 2^53 may be incorrectly unified due to float-representation rounding.

ORDER BY / SKIP / LIMIT

MATCH (n:Person)-[:KNOWS]->(m:Person)
RETURN m.name
ORDER BY m.name DESC
SKIP 10
LIMIT 5

Write statements

mushroomdb supports a Cypher write subset. Every write statement flows through the same mutation path as the Rust API — the rule engine fires incrementally and the WAL commits with a single fsync before the response is returned.

Write results are always one row with three columns:

ColumnMeaning
creatednodes inserted
properties_setindividual set_prop calls applied
deletededges deleted

CREATE

CREATE (n:Person {id: 'alice', score: 1})
  • The id property is required and becomes the node key.
  • Multiple nodes with an edge between them use chain syntax (comma-separated lists are not supported — attempting them returns a named parse error):
    CREATE (a:Org {id: 'acme'})-[:MEMBER]->(b:Person {id: 'bob'})
    

MATCH … SET

MATCH (n:Person {id: 'alice'})
SET n.score = 99
  • SET RHS must be a literal (integer, float, string, boolean), a $param reference, or arithmetic (n.x + 1); bare property-to-property copy is a named error.
  • Multiple SET clauses in one statement are allowed.
  • Combined MATCH … SET … RETURN is supported: the write commits, then RETURN projects from the post-write view.
  • When a SET touches a property that a rule evaluates, the rule engine re-evaluates incrementally: derived edges appear or retract within the same WAL frame.

MATCH … DELETE (edge)

MATCH (a:Org)-[r:MEMBER]->(b:Person)
DELETE r
  • Only manual (user-inserted) edges may be deleted.
  • Attempting to delete a derived (rule-owned) edge returns: cannot delete derived edge; retract via the rule or change the property

MATCH … DETACH DELETE (node)

MATCH (n:Person) WHERE n.id = 'alice'
DETACH DELETE n

Deletes the node and all incident edges (manual and derived). Derived edges are retracted via the rule engine; top-k backfill fires automatically for any source whose top-k slot was occupied by the removed node.

MATCH … DELETE (isolated node)

MATCH (n:Person) WHERE n.id = 'solo'
DELETE n

Bare DELETE n on a node variable succeeds when the node has no incident edges (openCypher semantics). If any edges remain the executor returns: Cannot delete node … because it still has incident edges. Use DETACH DELETE…

MERGE

MERGE (n:Person {id: 'alice'})
  • Match-or-create by a single key property. An optional ns names the namespace the create arm writes into (MERGE (n:Doc {id: 'x', ns: 'tenant-a'})).
  • A role bound to exactly one namespace stamps that namespace when the pattern does not name ns. A role bound to two or more must name one.
  • ON CREATE SET / ON MATCH SET apply inside the same write batch as the insert-or-skip. Both may appear on one MERGE.
  • Other extra properties are not supported.

OPTIONAL MATCH

OPTIONAL MATCH applies left-outer-join semantics. Rows that satisfy the outer MATCH always appear in the result; if the optional pattern finds no match, its bindings are null.

MATCH (a:Person)
OPTIONAL MATCH (a)-[:KNOWS]->(b)
RETURN a, COUNT(b)

Edgeless nodes return COUNT(b) = 0 — the outer row is never dropped.

WHERE inside OPTIONAL MATCH

A WHERE clause inside an OPTIONAL MATCH filters the optional candidates, not the outer row:

MATCH (a:Person)
OPTIONAL MATCH (a)-[:KNOWS]->(b) WHERE b.score > 5
RETURN a, b

If no b passes the filter, b is null for that outer row.

Chained OPTIONAL MATCHes

Multiple OPTIONAL MATCH clauses may follow one MATCH:

MATCH (a:Person)
OPTIONAL MATCH (a)-[:FRIEND]->(b)
OPTIONAL MATCH (a)-[:COLLEAGUE]->(c)
RETURN a, b, c

Each clause is independent; a miss in one does not affect the others.

Composes with aggregation

OPTIONAL MATCH composes correctly with grouped aggregation and WITH pipelines.


Query parameters

Parameters substitute $name placeholders with values supplied at query time. They are safe (the value is never interpreted as Cypher) and avoid string interpolation in your application code.

Rust API

#![allow(unused)]
fn main() {
// Convenience wrapper — takes a slice of (&str, Value) pairs.
let rs = db.query_with_params(
    "MATCH (n:Person) WHERE n.age > $min RETURN n",
    &[("min", Value::Int(18))],
)?;

// Low-level form — BTreeMap<String, Value>.
use std::collections::BTreeMap;
let mut params = BTreeMap::new();
params.insert("min".to_string(), Value::Int(18));
let rs = db.query("MATCH (n:Person) WHERE n.age > $min RETURN n", &params)?;
}

Parameters also work in SET clauses:

MATCH (n:Person) WHERE n.age = 30 SET n.age = $newage

HTTP API

POST /query
{
  "cypher": "MATCH (n:Person) WHERE n.age > $min RETURN n",
  "params": { "min": 18 }
}

Error: unknown parameter

If $name appears in the query but is not supplied, the executor returns:

missing parameter `name`

Scalar functions

Scalar functions are available in WHERE expressions and RETURN / WITH projections. Null propagation: if any argument is null, the result is null — except coalesce, which skips nulls.

FunctionInputOutputNotes
toLower(s)StringStringASCII casefold
toUpper(s)StringStringASCII casefold
size(x)String or ListIntsize(str) is bytes; lists are element count
coalesce(a, b, …)anyfirst non-nullnever null unless all args are null
type(r)RelStringrelationship type label
abs(n)Int or Floatsame typeabsolute value
round(f)FloatFloatrounds to nearest integer as Float
textMatches(s, q)String/List, StringBoolper-row scratch full-text; see fulltext.md
contains(s, sub)String, StringBoolsubstring test
startsWith(s, p)String, StringBoolprefix test
endsWith(s, p)String, StringBoolsuffix test
toInteger(x)Int/Float/StringIntparse/truncate; unparseable → null
toFloat(x)Int/Float/StringFloatparse; unparseable → null
toString(x)scalarStringscalar to text
decay(base, age, halflife)Int/Float ×3Floatbase * 0.5^(age / halflife); error if halflife <= 0
key(n)node variableStringalways the id-map key. Non-node argument is a named error
id(n)node variableStringalias of key(n) (always the id-map key). n.id is stored-wins property access
labels(n)node variableLista one-element list holding the node’s label; n.label reads it as a scalar

Aggregations: count, sum, avg, min, max, and collect(x) (gather each row’s value into a list, per group when grouping keys are present). count and collect accept DISTINCT.

CASE WHEN <cond> THEN <value> … [ELSE <value>] END is supported anywhere a scalar expression is (read or write RETURN/WITH/WHERE/SET). UNION and UNION ALL combine read queries with matching column names. Relationship patterns accept type alternation: (a)-[:A|:B]->(b).

Calling an unknown function name returns:

unknown function `name`; supported: toLower, toUpper, size, coalesce, type, abs, round, textMatches, contains, startsWith, endsWith, toInteger, toFloat, toString, decay, key, id, labels

Infix string predicates

STARTS WITH, ENDS WITH and CONTAINS are infix spellings of the startsWith / endsWith / contains functions above, and share their null and non-string handling: a missing property is false, not an error.

MATCH (c:Company) WHERE c.name STARTS WITH 'Acme'   RETURN c.name
MATCH (c:Company) WHERE c.name ENDS WITH 'Works'    RETURN c.name
MATCH (c:Company) WHERE NOT c.name CONTAINS 'Design' RETURN c.name

STARTS or ENDS without its WITH is a named parse error.

List subscripts

A subscript reads one element of a list-valued property or expression. A negative index counts from the end.

MATCH (n:Place) RETURN n.location[0] AS lat, n.location[1] AS lon
MATCH (n:Place) WHERE n.tags[-1] = 'primary' RETURN key(n)

Out of range, a non-list base and a non-integer index all evaluate to null — never an error, and never a match. An unaliased subscript names its own column (n.location[0]), so two subscripts of one list are two distinct columns. A write-statement RETURN evaluates a subscript the same way a read RETURN does.

Examples

MATCH (n:Person) RETURN toLower(n.name)
MATCH (n:Person) RETURN size(n.tags)
MATCH (n:Person) RETURN coalesce(n.nickname, n.name)
MATCH (a:Person)-[r]->(b:Person) RETURN type(r)
MATCH (n:Person) RETURN abs(n.score)
MATCH (n:Measurement) RETURN round(n.value)
MATCH (n:Person) RETURN key(n) AS id, labels(n) AS kinds

Cypher coverage

Tested against the current binary (2026-08-24, release build, maturin develop –release); the forms below — node key and label, comma-separated patterns, infix string predicates, list subscripts, DISTINCT aggregates, WITH alias scoping — are covered by the workspace test suite. Classification: Supported = executes without error; Named-error = rejected with a clear, actionable message; Absent = not implemented (not tested here).

Supported

FormExample
MATCH (n:Label)MATCH (n:Person) RETURN n.name
MATCH (n {key: val}) inline property filterMATCH (n:Person {city: 'Austin'}) RETURN n
n.key / n.id / n.label / key(n) / id(n) / labels(n)MATCH (n:Person) WHERE n.id = 'alice' RETURN n.id, id(n), labels(n) — n.key/n.id/n.label stored-wins; key(n)/id(n) always the id-map key
Comma-separated patterns in one MATCHMATCH (t)-[:A]->(c), (t)-[:B]->(c) RETURN key(c) — identical to consecutive MATCH clauses; no shared variable is a product
STARTS WITH / ENDS WITH / CONTAINS (infix)WHERE c.name STARTS WITH 'Acme' — missing property is false, not an error
List subscript x[i], negative indexRETURN n.location[0], n.tags[-1] — out of range / non-list / non-integer index → null
count(DISTINCT x) / collect(DISTINCT x)WITH c, count(DISTINCT t) AS n RETURN key(c), n
WITH … WHERE <alias> (non-aggregate)WITH t, t.age AS x WHERE x > 11 RETURN key(t), x — the alias is in scope for WHERE, ORDER BY, SKIP and LIMIT
Aggregate WITH projecting its RETURNWITH c, COUNT(p) AS n RETURN key(c), n * 2 AS d — projects with or without a HAVING clause
MATCH (n)-[r:TYPE]->(m) directed edgeMATCH (a)-[:KNOWS]->(b) RETURN a
MATCH (n)-[r:TYPE]-(m) undirected edgeMATCH (a)-[:KNOWS]-(b) RETURN a
MATCH (n)-[r]->(m) any relationship typeMATCH (n)-[r]->(m) RETURN type(r)
WHERE =, <>, <, >, <=, >=WHERE n.age > 18
WHERE n.prop IN [a, b] / IN $listWHERE n.city IN ['Austin', $c]; $cities is Value::List
RETURN DISTINCTRETURN DISTINCT n.city — hashed after Project; numeric Int/Float unify like grouping; cap 1,000,000 distinct rows
WHERE AND / ORWHERE n.a = 1 AND n.b = 2
ORDER BY prop ASC / DESCORDER BY n.name DESC
SKIP n LIMIT nSKIP 10 LIMIT 5
$param in WHEREWHERE n.age > $min
$param in SETSET n.score = $val
COUNT(*), COUNT(var), SUM, AVG, MIN, MAX (single)RETURN COUNT(*), SUM(n.score)
Grouped aggregation (single or multiple keys, multiple aggs)RETURN n.city, COUNT(*) AS c ORDER BY c DESC
WITH projection + aliasingWITH n.name AS nm RETURN nm
WITH … WHERE (HAVING)WITH city, COUNT(*) AS c WHERE c > 2 RETURN city
WITH … ORDER BY … LIMITWITH n, n.age AS a ORDER BY a DESC LIMIT 5 RETURN n.name
WITH … MATCH re-entryWITH city MATCH (p:Person) WHERE p.city = city RETURN p
UNWIND list_literal AS xUNWIND [1, 2, 3] AS x RETURN x
UNWIND n.prop AS x (list-valued property)UNWIND n.tags AS tag RETURN tag
UNWIND alias AS x (from prior WITH)WITH n.tags AS ts UNWIND ts AS t RETURN t
UNWIND … WHERE (filter after UNWIND)UNWIND [1,2,3] AS x WHERE x > 1 RETURN x
OPTIONAL MATCH (left-outer-join)MATCH (a) OPTIONAL MATCH (a)-[:R]->(b) RETURN a, b
OPTIONAL MATCH … WHERE (filter in optional scope)OPTIONAL MATCH (a)-[r]->(b) WHERE b.score > 5
Multiple chained OPTIONAL MATCHOPTIONAL MATCH (a)-[:R]->(b) OPTIONAL MATCH (a)-[:S]->(c)
OPTIONAL MATCH + aggregationOPTIONAL MATCH (a)-[r]->(b) RETURN a, COUNT(r)
Variable-length *1..n, *n, *..n, bare *MATCH (a)-[r:T*1..5]->(b) RETURN r.length
shortestPath((a)-[r*..n]->(b))MATCH (a {id: 'x'}), (b {id: 'y'}) MATCH shortestPath((a)-[r*..5]->(b)) RETURN r.length
CREATE (n:L {id: 'x', …})CREATE (n:Person {id: 'alice', age: 30})
CREATE (a:L {id: 'x'})-[:T]->(b:L {id: 'y'})node-edge chain
MATCH … SET n.prop = literalMATCH (n) WHERE n.id = 'x' SET n.score = 99
MATCH … SET n.prop = $paramMATCH (n) WHERE n.id = $id SET n.score = $val
MATCH … SET … RETURNMATCH (n {id:'a'}) SET n.x = 2 RETURN n.x — write commits, then RETURN from post-write state. CASE and list subscripts (n.tags[0]) evaluate the same as a read RETURN; n.key / n.id / n.label use identity fallback
MERGE … ON CREATE SET / ON MATCH SETMERGE (n:L {id:'new'}) ON CREATE SET n.born = 1 ON MATCH SET n.hit = 1 RETURN n
MATCH … DELETE r (manual edge)MATCH (a)-[r:KNOWS]->(b) DELETE r
MATCH … DETACH DELETE nMATCH (n) WHERE n.id = 'x' DETACH DELETE n
MATCH … DELETE n (isolated node)MATCH (n:Tmp) WHERE n.id = 'x' DELETE n
MERGE (n:L {id: 'x'}) (single-key upsert)MERGE (n:Person {id: 'alice'})
MERGE (n:L {id: 'x'}) RETURN …MERGE (n:Person {id: 'alice'}) RETURN n — returns node whether created or matched
CREATE … RETURN …CREATE (n:Person {id: 'alice'}) RETURN n.id, id(n), n.key — single-statement create + projection
WHERE … IS NULL / IS NOT NULLWHERE n.score IS NULL, WHERE b IS NOT NULL — null-check predicate; composes with AND/OR
Boolean literals true / false (any case)WHERE n.active = true, WHERE n.active <> false, MATCH (n {active: true}), SET n.active = false, RETURN true AS t — a node with no such property matches neither = … nor <> …. true and false are reserved: naming a variable or an alias either one is a parse error. A property or a map key may still be called that (n.true)
Binary arithmetic (+, -, *, /) in RETURN, WHERE, SET, function argsRETURN n.age + 1 AS next, WHERE n.score * 2 > 10, SET n.x = n.x + 1 — precedence: *// over +/-; parentheses supported; null propagates; integer div by zero is a named error
Scalar functions (toLower, size, contains, startsWith, endsWith, toInteger, toFloat, toString, decay, key, …)RETURN abs(n.score), toString(n.weight)
collect(x) aggregationMATCH (c:City)<-[:IN]-(p:Person) RETURN c.name, collect(p.name) AS residents
CASE WHEN … THEN … [ELSE …] ENDRETURN CASE WHEN n.age >= 65 THEN 'senior' ELSE 'other' END AS band — also in MATCH … SET … RETURN
UNION / UNION ALLMATCH (a:A) RETURN a.id AS id UNION MATCH (b:B) RETURN b.id AS id
Multi-relationship-type [:A|:B]MATCH (a)-[:KNOWS|:LIKES]->(b) RETURN b
View-maintained properties queryable like any propertyMATCH (c:City) WHERE c.pop > 1000 RETURN c.name — pop is a degree view maintained incrementally; reads like a stored prop
textMatches(n.field, "query") in WHERE (per-row scratch scan)MATCH (a:Article) WHERE textMatches(a.bio, 'rust embedded') RETURN a.key — correct for any graph size; O(scan) per row. Prefer db.search() for large indexed fields.

Named-error

Forms rejected with a clear, actionable error message (executor returns a typed error; no silent misbehavior).

FormError message (excerpt)
Bare RETURN r for a relationship variablecannot return relationship variable '…' bare; return its properties (….field) instead
MERGE (n:L {id: 'x', extra: 2}) (multi-property other than ns)MERGE supports exactly one key property (got 2)
MERGE (n:L {id: 'x'}) by a role bound to two namespacesrole-bound token: MERGE create requires the role to name one namespace
DELETE n when node has incident edgesCannot delete node … because it still has incident edges. Use DETACH DELETE…
DELETE r on derived (rule-owned) edgecannot delete derived edge; retract via the rule or change the property
CREATE (a), (b) comma-separated formparse error: unexpected tokens after CREATE pattern (found Comma)
Variable-length *0..n (zero-length min)zero-length variable-length paths are not supported; minimum hop count is 1
Variable-length *n.. (unbounded upper)variable-length paths are capped at 10 hops
UNWIND without preceding MATCHparse error: expected MATCH (found Unwind)
UNWIND scalar (non-list value)execute: UNWIND requires a list; got … value for …
Multi-statement / unknown top-level keywordparse error: expected MATCH (found …)
shortestPath with unbound endpointsplan: shortestPath: source node … is not bound; bind both endpoints before shortestPath
Unknown function nameexecute: unknown function …; supported: toLower, toUpper, size, coalesce, type, abs, round, textMatches, contains, startsWith, endsWith, toInteger, toFloat, toString, decay, key, id, labels
STARTS / ENDS without WITHexpected WITH after STARTS
count(DISTINCT *)named parse error — DISTINCT needs an expression, not *
Unknown name in a WITH … WHEREunbound variable `nope` in WHERE
$param referenced but not suppliedexecute: missing parameter …
SET n.prop = n.other (bare property-to-property copy)SET RHS: bare property/variable reference is not supported; use a literal, $parameter, or arithmetic expression
Integer division by zeroexecute: division by zero
Writing to a view-maintained propertyproperty is managed by view "…" and cannot be written directly
Any write on an open_at (as-of) instanceas-of instances are read-only

Absent

Forms not implemented and not tested. Behavior is not guaranteed — these may produce an unexpected-token parse error or other unspecified result.

Form
Pattern comprehension `[(n)-[r]->(m)
List comprehension [x IN list WHERE …]
EXISTS { … } subquery predicate
CALL procedure invocations
Schema operations (CREATE INDEX, CREATE CONSTRAINT)

Limitations

FeatureStatus
Multi-statement transactionsNot supported (one write statement per query in v1)
Combined MATCH … SET … RETURNSupported — write commits, then RETURN projects from post-write state
SET RHS expressionsLiterals, $param references, and arithmetic expressions (n.x + 1, n.score * 1.5); bare property-to-property copy (SET n.x = m.y) is a named error
MATCH … DETACH DELETE (node)Supported — removes node + all edges
Bare DELETE on node with edgesError — use DETACH DELETE
MERGE ON CREATE / ON MATCHSupported in the same MERGE / write batch
Grouped aggregationSupported (multiple keys and multiple aggregates allowed; group count capped at 1,000,000)
WITH pipeline stagesSupported — projection, aliasing, WHERE (HAVING, and an alias the stage itself introduced), ORDER BY, LIMIT, and re-entry MATCH
Multiple patterns in one MATCHSupported, comma-separated — identical to consecutive MATCH clauses. Still a named error after CREATE
n.key / n.id / key(n) / id(n) equality vs lit/$paramScanKey (plus stored-wins). n.label / labels(n) are not this fold — they still scan the label
UNWINDSupported — list literals, list-valued properties, and scalar aliases from prior WITH; non-list → named error
Variable-length paths: max hopsCapped at 10
shortestPath with unbound endpointsRejected at planning time
Intermediate result budget1,000,000 rows (WITH/UNWIND intermediate rows count against this cap)