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

Appendix B: Query-Language Grammar

The complete grammar of the openCypher subset, as the parser accepts it. The dashboard labels its query tab GraphQL; the language is not GraphQL but an openCypher subset, and this appendix is its reference. Chapter 4 explains what each construct is for; this one states exactly what parses.

Notation: | alternatives, […] optional, {…} zero or more, '…' a literal token. Keywords are case-insensitive (match = MATCH); identifiers are not.

Statement

statement      ::= read-query | write-statement

A read compiles to a logical plan and returns rows; a write mutates the plane and returns change counts.

Reads

read-query     ::= source { beam } [ where ] return
                   [ order-by ] [ skip ] [ limit ] [ as-of ]

Every source binds exactly one node pattern and may continue with a relationship tail, so a typed hop follows a retrieval or algorithm seed exactly as it follows a MATCH node.

source         ::= 'MATCH'  pattern
                 | 'SEARCH' node-pat vector-seed  tail
                 | 'SEARCH' node-pat keyword-seed tail
                 | 'HYBRID' node-pat { hybrid-part } tail
                 | 'CALL'   ident '(' [ call-args ] ')' 'ON' node-pat tail

tail           ::= { rel-pat node-pat }

vector-seed    ::= [ 'ON' ident ] 'NEAR' vec-arg [ 'METRIC' metric ] [ 'TOPK' uint ]
keyword-seed   ::= 'ON' ident 'MATCHING' string [ 'TOPK' uint ]

hybrid-part    ::= 'VECTOR'  [ 'ON' ident ] 'NEAR' vec-arg
                     [ 'METRIC' metric ] [ 'WEIGHT' number ]
                 | 'KEYWORD' 'ON' ident 'MATCHING' string [ 'WEIGHT' number ]
                 | 'GRAPH'   'HOPS' uint [ 'DECAY' number ]
                     [ 'SEEDS' uint ] [ 'WEIGHT' number ]
                 | 'CANDIDATES' uint
                 | 'TOPK' uint

call-args      ::= call-arg { ',' call-arg }
call-arg       ::= ident ':' value

HYBRID requires at least one of VECTOR or KEYWORD — GRAPH only boosts what those find. Parts may appear in any order, each at most once.

Traversal

beam           ::= 'BEAM' node-pat direction [ ':' ident ] [ 'ON' ident ]
                   'NEAR' vec-arg [ 'METRIC' metric ] 'WIDTH' uint 'DEPTH' uint

direction      ::= 'OUT' | 'IN' | 'BOTH'

pattern        ::= node-pat { rel-pat node-pat }
node-pat       ::= '(' [ ident ] [ ':' ident ] ')'
rel-pat        ::= [ '<' ] '-' [ '[' rel-body ']' ] '-' [ '>' ]
rel-body       ::= [ ident ] [ ':' ident ] [ var-len ]
var-len        ::= '*' [ uint ] [ '..' [ uint ] ]

A relationship’s direction comes from its arrows: -[…]-> out, <-[…]- in, -[…]- both. <-…-> is rejected. A relationship variable parses but is not bound. var-len must have an upper bound (*1..3, *2, *..4); an unbounded * or *2.. is a clear error.

Tail clauses

where          ::= 'WHERE' expr
return         ::= 'RETURN' [ 'DISTINCT' ] return-item { ',' return-item }
return-item    ::= ident | '*'                (* the rows themselves *)
                 | ( expr | aggregate ) [ 'AS' ident ]
aggregate      ::= 'count' '(' '*' ')'
                 | agg-func '(' [ 'DISTINCT' ] expr ')'
agg-func       ::= 'count' | 'sum' | 'avg' | 'min' | 'max' | 'collect'
order-by       ::= 'ORDER' 'BY' order-key { ',' order-key }
order-key      ::= ( expr | ident ) [ 'ASC' | 'DESC' ]
skip           ::= 'SKIP' uint
limit          ::= 'LIMIT' uint
as-of          ::= 'AS' 'OF' ( uint | string | 'TIME' int )

RETURN n or RETURN * hands back whole records and must name the pattern’s last variable. Any other item projects: it becomes a column named by its AS alias or, without one, by the item as written, and it may read any variable the pattern bound. A node cannot share a RETURN with columns — it is not a value.

An aggregate folds each group; the grouping key is every column that is not one. Folds skip nulls and values they cannot use; over nothing, count and sum are 0, avg/min/max are null, and collect is the empty list.

In a projecting query DISTINCT compares whole projected rows, ORDER BY names one of the returned columns (by alias, or by the expression it returned), and SKIP/LIMIT count projected rows. Over RETURN n all four keep their node reading. AS OF is last: a bare integer is a commit sequence, a quoted string an RFC-3339 instant, and TIME takes unix-epoch milliseconds.

Expressions

Loosest to tightest:

expr           ::= or-expr
or-expr        ::= and-expr { 'OR' and-expr }
and-expr       ::= not-expr { 'AND' not-expr }
not-expr       ::= 'NOT' not-expr | comparison
comparison     ::= additive [ 'IS' [ 'NOT' ] 'NULL'
                            | 'IN' '[' [ expr { ',' expr } ] ']'
                            | cmp-op additive ]
cmp-op         ::= '=' | '<>' | '!=' | '<' | '<=' | '>' | '>='
additive       ::= multiplicative { ( '+' | '-' ) multiplicative }
multiplicative ::= unary { ( '*' | '/' ) unary }
unary          ::= '-' unary | primary
primary        ::= '(' expr ')' | param | literal | term
term           ::= ident '.' ident            (* a property *)
                 | ident ':' ident            (* a label test *)
                 | function
function       ::= 'score' '(' ')'
                 | 'hops' '(' ')'
                 | 'key' '(' ident ')'
                 | ( 'similarity' | 'distance' )
                   '(' ident '.' ident ',' vec-arg [ ',' metric ] ')'

score() is the row’s score channel — a seed’s relevance, or an algorithm’s per-node result. hops() is the path length. key(n) is the node’s external key; on the source variable, key(n) = "…" and key(n) IN […] compile to a key seek rather than a scan-and-filter. x IN [a, b] is sugar for equalities.

A WHERE condition may reference only one pattern variable — the compiler places it at that variable’s position in the pipeline.

Writes

write-statement ::= 'CREATE' create-path { ',' create-path }
                  | 'MERGE'  create-path { merge-on }
                  | 'MATCH'  pattern [ 'WHERE' expr ] mutate-op { mutate-op }

merge-on       ::= 'ON' ( 'CREATE' | 'MATCH' ) 'SET' set-item { ',' set-item }

mutate-op      ::= 'SET' set-item { ',' set-item }
                 | 'REMOVE' remove-item { ',' remove-item }
                 | [ 'DETACH' ] 'DELETE' ident { ',' ident }
                 | 'CREATE' create-path { ',' create-path }
                 | 'MERGE'  create-path { merge-on }

set-item       ::= ident '.' ident '=' value
                 | ident ':' ident
                 | ident '+=' prop-map
remove-item    ::= ident '.' ident | ident ':' ident

create-path    ::= create-node { create-rel create-node }
create-node    ::= '(' [ ident ] [ ':' ident ] [ prop-map ] ')'
create-rel     ::= [ '<' ] '-' '[' ':' ident [ prop-map ] ']' '-' [ '>' ]
prop-map       ::= '{' [ prop-entry { ',' prop-entry } ] '}'
prop-entry     ::= ident ':' value

In a create-node, a string-valued key: entry sets the node’s external key rather than becoming a property. A created edge must be directed and must name a type. MATCH … mutate-op operates on the pattern’s terminal variable.

Terminals

ident          ::= ( letter | '_' ) { letter | digit | '_' }     (* Unicode letters *)
                 | '`' { any character except '`' } '`'
uint           ::= digit { digit }
int            ::= [ '-' ] uint
number         ::= [ '-' ] digit { digit } [ '.' digit { digit } ] [ exponent ]
exponent       ::= ( 'e' | 'E' ) [ '+' | '-' ] digit { digit }
string         ::= '"' { string-char | escape } '"'
                 | "'" { string-char | escape } "'"
string-char    ::= any character except the closing quote or '\\'
escape         ::= '\\' ( '\'' | '"' | '\\' | 'n' | 't' | 'r' | 'b' | 'f' | 'u' hex hex hex hex )
vector         ::= '[' [ number { ',' number } ] ']'
vec-arg        ::= string | vector
metric         ::= 'cosine' | 'dot' | 'l2'
param          ::= '$' ident
literal        ::= number | string | 'true' | 'false' | 'null' | vector
value          ::= param | literal

Inside a string a backslash starts an escape — \', \", \\, \n, \t, \r, \b, \f, \uXXXX — so a quote of the same kind is written escaped, a literal backslash is written \\ (a Windows path is "C:\\data"), and an unknown escape or an unterminated string is a syntax error. Identifiers are Unicode words, or anything between backticks when the plain form cannot spell the name. Whitespace between tokens is insignificant; there are no comments. A $name parameter stands where a value does, resolved from the caller’s parameter map at parse time — the injection-safe way to pass values.

Defaults

OmittedValueWhy
ON <property> before NEARembeddingwhat the ingestion pipeline writes
METRICcosine
TOPK10
HYBRID … CANDIDATES100per-channel pool before fusion
GRAPH … DECAY0.5matches the RPC, MCP and CLI surfaces
GRAPH … SEEDS10top hits per channel used as seeds
WEIGHTvector 1.0, keyword 1.0, graph 0.5
CALL pagerank argsdamping: 0.85, iterations: 20, tolerance: 1e-6
CALL louvain argsmax_levels: 10, min_gain: 1e-9
CALL shortest_path diroutfrom and to are required

MATCHING has no property default: keyword indexes are declared per (label, property) and those properties follow no convention, so ON is required — as is a label, for the same reason.

Algorithms

CALL 'pagerank'      ( [ damping: number ] [ iterations: uint ] [ tolerance: number ] )
CALL 'components'    ( )                      (* alias: connected_components *)
CALL 'louvain'       ( [ max_levels: uint ] [ min_gain: number ] )
CALL 'shortest_path' ( from: key-or-id , to: key-or-id [ , dir: string ]
                       [ , weight: string ] )

key-or-id is a string (an external key) or a whole number (a node id). Each algorithm reports its per-node result through score(): the rank for pagerank, a dense 0-based group index for components and louvain, and the node’s position along the path for shortest_path. An unknown algorithm or argument name is an error, never a silently ignored setting.

Not supported

Each of these is a clear error, never a silent miscompile:

  • WITH pipelining — a projection is a tail, so nothing follows it.
  • returning the rows of any variable but the pattern’s last (RETURN p after a hop); its values project fine (RETURN p.name).
  • ordering a non-projecting query by any variable but the pattern’s last.
  • predicates spanning two variables — WHERE p.year < q.year.
  • reusing a pattern variable, which would express a graph constraint.
  • branching patterns — one linear path per statement.
  • unbounded variable-length relationships — *, *2...
  • undirected or untyped created edges.
  • comments and WITH.