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

openCypher Compatibility Reference

This is the support inventory for the 0.15.1 source tree, reviewed on 2026-09-09 at 6fb6d521. It describes the implemented language, not a claim of complete openCypher, Neo4j Cypher 5/25, GQL, APOC or GDS compatibility. Bolt connectivity and neo4j:// routing do not imply language compatibility.

Supported below means that the stated form is implemented; it does not mean that every combination, invalid input or error code matches Neo4j. Partial means that the boundary matters to application correctness. Unsupported means that applications must not use that form. Parsing successfully is not sufficient evidence of equivalent execution, especially for the subquery restrictions below.

Use this chapter for porting decisions, Cypher Support for examples, and Limitations for storage and operational differences.

Clauses and graph patterns

FeatureStatusSupported form and boundary
Node matchingSupportedMATCH (ticket:Ticket {key: $key}); anonymous nodes, multiple labels :Ticket:Open, comma-separated patterns and property maps. Labels and relationship types are static identifiers, not parameters.
Relationship matchingSupportedOutgoing ->, incoming <-, undirected -; relationship variables, properties and type alternatives [:LINK|BLOCKS].
Optional patternsSupportedOPTIONAL MATCH, including chained optional clauses. Its attached WHERE filters the optional side before null-extension; it is not an ordinary post-join filter.
Variable-length pathsSupported[:LINK*], *2, *1..3, *..3, *1.., *0..3; named paths route = (...). Resource/traversal limits still apply. Prefer explicit bounds.
Shortest pathsPartialshortestPath((first)-[:LINK*]->(last)) and allShortestPaths(...) are implemented for unweighted traversal between bound endpoints. allShortestPaths as an expression returns a list of paths; do not assume every Neo4j shortest-path query shape or cardinality is interchangeable.
Pattern predicatesSupportedWHERE (ticket)-[:LINK]->(other) and negation.
FilteringSupportedWHERE, parentheses, AND, OR, XOR, NOT; =, <>, !=, <, <=, >, >=, IN, NOT IN, IS NULL, IS NOT NULL; label tests on node/relationship values, including entity-valued list elements and projections (UUID values remain scalars); STARTS WITH, ENDS WITH, CONTAINS, =~. Regex behavior is not a promise of Java-regex compatibility.
ProjectionSupportedRETURN, aliases, RETURN *, DISTINCT; WITH projection, aggregation, filtering and subsequent query stages.
Ordering and paginationSupportedMultiple sort expressions, ASC/DESC and long spellings, SKIP, LIMIT; parameterized offsets/limits. NULLS FIRST/NULLS LAST is an additional accepted form. Without ORDER BY, result order is not an application contract. Use a unique tie-breaker for pagination.
Row sourcesSupportedUNWIND over lists, expressions or parameters, including lists of maps and concatenated or sliced sources such as UNWIND range(1, 2) + [3] AS x.
UnionsSupportedUNION removes duplicates; UNION ALL preserves them. This is not a claim of modern named-column UNION … BY NAME support.
InsertSupportedCREATE nodes and relationships, with property expressions. Repeated relationships have the storage restriction below.
UpsertSupportedMERGE, ON CREATE SET, ON MATCH SET, per incoming row; undirected matching. A merge is not permission to create duplicate constrained keys.
Property/label updatesSupportedSET ticket.prop = expression, SET ticket = map, SET ticket += map, SET ticket:Label; entity-valued map replacement/merge; REMOVE ticket.prop, label removal. Setting a property to null removes it.
DeletionSupportedDELETE, DETACH DELETE. Ordinary deletion cannot leave attached relationships dangling.
FOREACHSupportedBody accepts the full updating-clause inventory — CREATE, MERGE (with ON CREATE/ON MATCH SET), property SET and SET n:Label, REMOVE (property and label), DELETE/DETACH DELETE — applied in written order per loop item through the same constraint enforcement as the top-level clauses. Entity-valued list items (collected nodes, path elements) bind as real entities; a scalar DELETE target raises InvalidDelete. A nested FOREACH is not supported.
Planner inspectionSupported extensionEXPLAIN query and PROFILE query; their operator names/output are Patina-specific, not Neo4j plan objects.
Index hintsUnsupportedMATCH (ticket:Ticket) USING INDEX ticket:Ticket(key) …, including USING INDEX SEEK. Remove the hint; the planner chooses access paths automatically. Creating an index does not add hint syntax.
Dynamic labels / relationship typesUnsupportedMATCH (ticket:$(expression)), MATCH ()-[link:$(expression)]->(), dynamic CREATE, and SET/REMOVE ticket:$(expression). $() is not an alternative parameter spelling; labels/types must be static identifiers.

Why labels and types are static

Not supporting :$(expression) is a deliberate design decision, not an undocumented compatibility promise. A static label/type gives the planner a known target for label/type-specific access paths and eligible property-index seeks, rather than requiring runtime dispatch across varying labels/types. It does not guarantee an index seek for every query; indexes, predicates and planner decisions still matter. This is the design rationale, not a measured performance comparison against dynamic Cypher in another database.

For mixed-label/type input, group items in the client by their label/type and submit one static query template per group, using UNWIND $rows for values. Select templates from an application-controlled allowlist; never interpolate untrusted labels/types into query text. Grouping into separate commits changes atomicity; use an explicit transaction when the supported operations must commit together. This keeps runtime variability in parameter values, not graph schema identifiers.

Subqueries and procedures

FormStatusExact scope
CALL { … }SupportedCorrelated subquery using an importing WITH, for example WITH 7 AS outerValue CALL { WITH outerValue RETURN outerValue AS innerValue } RETURN innerValue. Read and write subqueries exist.
New variable-scope syntaxUnsupportedCALL (ticket) { … } and CALL () { … }. Use the importing-WITH form.
EXISTS { … }PartialA single pattern, optional MATCH, optional attached WHERE. Scalar/projection use is supported. Following WITH/RETURN clauses execute, including filtering, DISTINCT, aggregation and LIMIT. Other general inner-query syntax remains unsupported.
COUNT { … }Partial extensionCounts matches of a single pattern with optional MATCH and WHERE. Following WITH/RETURN clauses execute and COUNT returns the resulting row count. It is not a general Cypher 5 count-subquery implementation.
COLLECT { … }UnsupportedUse aggregation collect(expression) or an appropriate CALL subquery instead.
Procedure callSupportedCALL namespace.name(arguments) with explicit YIELD column [AS alias]; results can feed subsequent clauses. Only registered procedures exist; see Procedures.
Index-await procedureSupported since 0.16.0CALL db.awaitIndexes() or CALL db.awaitIndexes(300): succeeds without output columns once the local deferred-build marker is clear. Normal synchronous index maintenance needs no additional wait. Pending builds wait up to the timeout; storage errors and query deadlines propagate. Not a replica-catch-up barrier. See Procedures.
Parenthesis-free callPartialCALL namespace.name takes implicit query parameters; nothing may precede it. An explicit YIELD and following projection are allowed. It is not an in-query row-dependent call.
YIELD *PartialOnly a standalone procedure call, with no surrounding clauses, including no trailing RETURN. Use explicit field names inside a query.
Batched write subqueryPartial extensionCALL { … } IN TRANSACTIONS [OF n ROWS] [ON ERROR CONTINUE|BREAK|FAIL]; final clause only, default chunk size 1000 rows. Independent chunk commits, not one atomic statement. FAIL does not undo previously committed chunks. No IN CONCURRENT TRANSACTIONS or REPORT STATUS AS syntax.
User-defined proceduresPartial extensionRegistered read-only WASM procedures through CALL wasm.name(...). Despite CREATE FUNCTION naming, these are not scalar UDFs usable in RETURN name(...). No Java/Python/JavaScript procedure runtime.

For batching, file access, explicit Bolt transaction restrictions and partial commit handling, consult Bulk Loading, Cypher Support, and Bolt. Do not assume batching also bounds every outer-query sort, aggregation or row materialization.

Expressions and values

FeatureStatus / boundary
LiteralsNull, booleans, signed integers, floats/exponents, hexadecimal/octal integers, single/double-quoted strings, lists and maps.
Quoted namesEvery symbolic name — label, relationship type, property key, map key, variable, alias, parameter, procedure, index, constraint and function name — may be backtick-quoted: `K` and K are the same name, and a doubled backtick is one literal backtick (`a``b` is the name a`b). Results report the decoded name; SHOW INDEXES re-quotes compound definitions so they parse back. An empty quoted name is a syntax error, and names beginning with `pdb_ are reserved for internal bindings. A quoted name containing a . cannot be used as a property-path segment (n.`a.b`).
Parameters$name and positional-style $0; values can be lists/maps. Parameters are not textual substitution for labels, property names or entire query fragments.
Arithmetic+, -, *, /, %, ^, unary minus and parentheses. String/list concatenation is supported through expression evaluation.
Case expressionsSimple CASE value WHEN …, including complete Boolean values such as CASE n.x IS NULL WHEN true …, and searched CASE WHEN predicate …; optional ELSE. A NULL simple-CASE value does not match any WHEN value, including NULL.
List operationsIndexing including negative indexes, slices with omitted bounds, comprehensions [item IN source WHERE predicate | expression], reduce(accumulator = initial, item IN source | expression).
Map accessMap literals, map.field, nested access, expression indexing.
Map projectionPartial: ticket {.key, .summary}. Not the general Neo4j projection language: ticket {.*}, computed entries ticket {extra: expression}, and mixed variable/literal selectors are not supported.
Pattern comprehension[(first)-[:LINK]->(other) WHERE predicate | expression]. Entity-list and nesting edge cases are not fully TCK-conformant.
Quantifiersany, all, none, single over a list with WHERE. Scalar-list forms exist; do not assume full entity-list/edge-case compatibility.
Aggregationcount(*), count, sum, avg, min, max, collect, stDev, stDevP, percentileCont, percentileDisc; DISTINCT inside aggregates. Grouping is implicit in non-aggregate projections; no SQL GROUP BY/HAVING clauses.
Aggregate expressionsNested functions/arithmetic around aggregate results are implemented. This is not a guarantee for every implicit-grouping or aggregate-ordering edge case.

Function arguments support postfix list slices/indexes and map-field access: RETURN size([1,2,3][1..]) returns 2, and RETURN abs({nested: {value:-7}}.nested.value) returns 7. Omitted and negative slice bounds use the same expression semantics as an intermediate WITH value. This does not imply full Neo4j expression compatibility in every grammar position; test the exact expression when porting a query.

Built-in scalar function inventory

These are the function names registered by the core grammar/evaluator. A name here does not imply every Neo4j overload, coercion or invalid-input error is implemented. Unknown scalar names are not a plugin discovery mechanism.

FamilyNames
ConversiontoString, toInteger, toFloat, toBoolean, toStringOrNull, toIntegerOrNull, toFloatOrNull, toBooleanOrNull
StringstoLower, toUpper, trim, lTrim, rTrim, split, replace, substring, left, right, reverse
Numericabs, ceil, floor, round, sqrt, sign, log, exp, sin, cos, tan, atan, asin, acos, atan2, pi, degrees, radians, rand
Lists/nullssize, isEmpty, head, tail, last, range, reverse, coalesce
Graph valuesid, elementId, labels, keys, properties, type, nodes, relationships, length, startNode, endNode
Temporal constructorsdate, time, localtime, datetime, localdatetime, duration, timestamp
Temporal namespacesdate/time/localtime/datetime/localdatetime.truncate; their .statement, .transaction, .realtime forms; duration.between, .inSeconds, .inMinutes, .inHours, .inDays, .inMonths; datetime/localdatetime.fromepoch, .fromepochmillis
Spatialpoint, distance, withinBBox, point.distance, point.withinBBox; extensions point.distance.ellipsoidal, polygon, within, contains, intersects, polygon.within, polygon.contains, polygon.intersects, polygon.withinBBox — see Spatial for types and semantics
Vectorvector.similarity.cosine, vector.similarity.euclidean — see Vector Search for score definitions

range(start, end [, step]) includes reachable endpoints and stops before the next step would overflow a signed 64-bit integer. A zero step is rejected; materialized results remain subject to the configured aggregate-row limit.

Not present as core scalar functions: randomUUID, log10, e, cot, haversin, char_length, character_length, normalize, valueType, toIntegerList/toFloatList/toBooleanList/toStringList, and arbitrary apoc.* functions. This is an explicit inventory, not “and all other Neo4j functions.” Named procedure equivalents, where documented, use CALL instead.

Vector similarity: supported without an index

RETURN vector.similarity.cosine([1,0], [0,1]) AS score returns 0.5. This is normalized cosine (1 + raw cosine) / 2, not raw cosine: identical nonzero vectors score 1, orthogonal vectors 0.5, opposite vectors 0. vector.similarity.euclidean([0,0], [3,4]) returns approximately 1/26, using 1 / (1 + squared distance).

Both are scalar functions over equal-length numeric lists, not CALL procedures and not nearest-neighbour searches. They require no vector index. Evaluation converts components to f32; null arguments propagate null, dimension mismatches and non-numeric components error. This is not a guarantee of all Neo4j invalid-vector edge cases. Use db.index.vector.queryNodes for indexed Top-k search; see Vector Search.

Important semantic differences

  • Expression membership: IN and NOT IN accept expression-valued left operands, for example split(node.issue_key, '-')[0] IN $projects after a procedure’s WITH node, score. This does not make arbitrary predicates index-seekable; simple property membership retains its existing planner path.
  • Grouped predicates: a grouped WHERE predicate followed by = true is not generally accepted; use the predicate directly in WHERE.
  • Identity: nodes use UUIDs; elementId(node) is the portable string handle. id(node) is not a Neo4j integer ID. Relationship identity is the endpoint/type triple. Two relationships of the same type between identical endpoints cannot coexist; Cypher duplicate creation errors, while bulk import has a separate overwrite contract. See Data Model.
  • Synthetic label: an otherwise unlabelled stored node uses the primary label Node. This is not a fully label-free Neo4j storage model; the TCK comparator normalizes this difference, so TCK results do not disprove it.
  • Strings: the scalar evaluator’s size(string) counts UTF-8 bytes, not Unicode characters. For example, size('é') is 2. Other string operations need not use the same unit; test non-ASCII application text explicitly.
  • Nulls: the scalar evaluator returns true for isEmpty(null), not null. Do not use it as a drop-in replacement for IS NULL. Null left operands in membership tests remain unknown even for an empty list; consequently WHERE null NOT IN [] does not retain the row.
  • Temporal clocks: .statement(), .transaction() and .realtime() route to the same current-time constructor family. They do not establish Neo4j’s distinct statement/transaction clock lifetimes. Extended years, timezone serialization and very large durations also need compatibility testing.
  • Numeric errors: integer division/modulo by zero and checked integer arithmetic overflow raise arithmetic errors; floating-point operations can produce infinity/NaN. Integer/float equivalence for grouping and DISTINCT is documented in Value semantics.
  • Error compatibility: not every invalid argument, deleted-entity access, procedure signature or semantic error has the same validation phase/status as Neo4j. A successful syntax check is not a compatibility certificate.

Schema, administration and non-openCypher extensions

These features are useful, but are separate from an openCypher language claim.

FeatureSupported scope
Property indexesLegacy CREATE INDEX ON :Label(prop) and node CREATE [RANGE] INDEX [name] [IF NOT EXISTS] FOR (node:Label) ON (node.prop); compound fields, drop/show. TEXT, LOOKUP, and explicitly named BTREE index types are rejected.
ConstraintsNode and relationship uniqueness, existence, key and property-type constraints; modern FOR … REQUIRE, legacy ON … ASSERT, and documented shorthand. Bare composite uniqueness without existence and LIST<T> refinement are not supported. See Constraints.
Specialized indexesFull-text, vector, point and edge-sorted indexes with their documented DDL. Options and implementation differ from Neo4j; e.g. accepting an HNSW option does not turn the vector index into HNSW.
CSV importLOAD CSV [WITH HEADERS] FROM 'path' AS row [FIELDTERMINATOR 'c']; local files only, no HTTP(S). Server reads require admin authorization and configured file allowlisting.
HistoryEngrams, diffs, AS OF and history procedures are Patina extensions, not openCypher.
Maintenance status (unreleased)Server-only SHOW JOBS lists local tag-snapshot jobs for the selected database; admin required. No YIELD, WHERE, ORDER BY or cluster aggregation. See Maintenance Jobs.
Databases/auth/routingDocumented server-side database/user/role commands and routing procedures. Not the full Neo4j administration language; selecting a database through Bolt/REST is distinct from unsupported query USE database.
Algorithms/proceduresOnly the built-in procedure catalog and registered WASM procedures. Not the complete APOC or Neo4j GDS libraries.

Explicit non-goals of this subset

  • Modern quantified path patterns/relationships and selectors such as SHORTEST k, ALL SHORTEST, ANY, ACYCLIC or TRAIL in their newer Cypher/GQL syntax; use the documented legacy patterns instead.
  • Dynamic-label/type syntax such as :$(expression); general label expressions such as (node:Ticket&Open) are not the supported :Ticket:Open form.
  • INSERT, LET, FILTER, FINISH, NEXT, query USE, SQL window functions, SQL GROUP BY/HAVING, and arbitrary multi-statement scripts in one query.
  • Triggers, unrestricted server-side scripting, writable WASM UDFs, and a Neo4j plugin ABI.

Evidence and maintenance

The implementation inventory is grounded in these source files:

  • crates/patinadb/src/cypher/cypher.pest: accepted syntax and function names.
  • cypher/parse_query.rs, parse_expr.rs, parse_ddl.rs: validation and lowering boundaries; grammar acceptance alone is not the support decision.
  • plan/eval/scalars.rs, plan/eval/shortest_path.rs, plan/exec/expr_eval.rs: expression behavior and the semantic differences called out above.
  • execution/in_transactions.rs and the server dispatchers: batching versus ordinary statement/transaction behavior.
  • crates/patinadb/tests/manual_cypher_contract.rs: executable examples and negative controls for selected documented unsupported forms.
  • crates/patinadb/tests/opencypher_tck.rs: broader conformance inventory.

The TCK runner is informational, not a strict all-green gate. Its comparison normalizes synthetic labels and some value representations; identity correlation across cells is not fully modeled. One pinned Gherkin parsing limitation also prevents a feature’s scenarios from running. Such scenarios must be counted as not passing, not silently excluded. A fresh TCK run still cannot replace this feature-by-feature contract or tests of the backend’s exact queries.

The 2026-09-09 run on the revision above reports 3755 passed, 113 executed failures, and 29 never-run scenarios: 3755 of 3897, with 142 not passing (#858 inventory). The process exits successfully even with those failures. This replaces the older 3754-pass inventory; it is not an all-green result.

Recheck after language changes:

cargo test --locked --release -p patinadb --test manual_cypher_contract
cargo test --locked --release -p patinadb --test opencypher_tck

Archive the complete TCK output, source revision and never-run scenario count. Do not publish an old pass percentage as the current release’s certification.

Counting independent optional branches

For supported independent one-hop OPTIONAL MATCH branches, count(*) can count their combinations without retaining the combined rows. Empty optional branches still contribute one null-extended row. This path can complete when the count exceeds the materialized-row limit; queries that retain those rows still use the configured limit. EXPLAIN/PROFILE identify this execution as agg.independent_optional_count. Other optional shapes retain normal execution.