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
| Feature | Status | Supported form and boundary |
|---|---|---|
| Node matching | Supported | MATCH (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 matching | Supported | Outgoing ->, incoming <-, undirected -; relationship variables, properties and type alternatives [:LINK|BLOCKS]. |
| Optional patterns | Supported | OPTIONAL 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 paths | Supported | [:LINK*], *2, *1..3, *..3, *1.., *0..3; named paths route = (...). Resource/traversal limits still apply. Prefer explicit bounds. |
| Shortest paths | Partial | shortestPath((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 predicates | Supported | WHERE (ticket)-[:LINK]->(other) and negation. |
| Filtering | Supported | WHERE, 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. |
| Projection | Supported | RETURN, aliases, RETURN *, DISTINCT; WITH projection, aggregation, filtering and subsequent query stages. |
| Ordering and pagination | Supported | Multiple 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 sources | Supported | UNWIND over lists, expressions or parameters, including lists of maps and concatenated or sliced sources such as UNWIND range(1, 2) + [3] AS x. |
| Unions | Supported | UNION removes duplicates; UNION ALL preserves them. This is not a claim of modern named-column UNION … BY NAME support. |
| Insert | Supported | CREATE nodes and relationships, with property expressions. Repeated relationships have the storage restriction below. |
| Upsert | Supported | MERGE, ON CREATE SET, ON MATCH SET, per incoming row; undirected matching. A merge is not permission to create duplicate constrained keys. |
| Property/label updates | Supported | SET 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. |
| Deletion | Supported | DELETE, DETACH DELETE. Ordinary deletion cannot leave attached relationships dangling. |
FOREACH | Supported | Body 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 inspection | Supported extension | EXPLAIN query and PROFILE query; their operator names/output are Patina-specific, not Neo4j plan objects. |
| Index hints | Unsupported | MATCH (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 types | Unsupported | MATCH (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
| Form | Status | Exact scope |
|---|---|---|
CALL { … } | Supported | Correlated 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 syntax | Unsupported | CALL (ticket) { … } and CALL () { … }. Use the importing-WITH form. |
EXISTS { … } | Partial | A 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 extension | Counts 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 { … } | Unsupported | Use aggregation collect(expression) or an appropriate CALL subquery instead. |
| Procedure call | Supported | CALL namespace.name(arguments) with explicit YIELD column [AS alias]; results can feed subsequent clauses. Only registered procedures exist; see Procedures. |
| Index-await procedure | Supported since 0.16.0 | CALL 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 call | Partial | CALL 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 * | Partial | Only a standalone procedure call, with no surrounding clauses, including no trailing RETURN. Use explicit field names inside a query. |
| Batched write subquery | Partial extension | CALL { … } 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 procedures | Partial extension | Registered 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
| Feature | Status / boundary |
|---|---|
| Literals | Null, booleans, signed integers, floats/exponents, hexadecimal/octal integers, single/double-quoted strings, lists and maps. |
| Quoted names | Every 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 expressions | Simple 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 operations | Indexing including negative indexes, slices with omitted bounds, comprehensions [item IN source WHERE predicate | expression], reduce(accumulator = initial, item IN source | expression). |
| Map access | Map literals, map.field, nested access, expression indexing. |
| Map projection | Partial: 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. |
| Quantifiers | any, all, none, single over a list with WHERE. Scalar-list forms exist; do not assume full entity-list/edge-case compatibility. |
| Aggregation | count(*), 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 expressions | Nested 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.
| Family | Names |
|---|---|
| Conversion | toString, toInteger, toFloat, toBoolean, toStringOrNull, toIntegerOrNull, toFloatOrNull, toBooleanOrNull |
| Strings | toLower, toUpper, trim, lTrim, rTrim, split, replace, substring, left, right, reverse |
| Numeric | abs, ceil, floor, round, sqrt, sign, log, exp, sin, cos, tan, atan, asin, acos, atan2, pi, degrees, radians, rand |
| Lists/nulls | size, isEmpty, head, tail, last, range, reverse, coalesce |
| Graph values | id, elementId, labels, keys, properties, type, nodes, relationships, length, startNode, endNode |
| Temporal constructors | date, time, localtime, datetime, localdatetime, duration, timestamp |
| Temporal namespaces | date/time/localtime/datetime/localdatetime.truncate; their .statement, .transaction, .realtime forms; duration.between, .inSeconds, .inMinutes, .inHours, .inDays, .inMonths; datetime/localdatetime.fromepoch, .fromepochmillis |
| Spatial | point, 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 |
| Vector | vector.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:
INandNOT INaccept expression-valued left operands, for examplesplit(node.issue_key, '-')[0] IN $projectsafter a procedure’sWITH node, score. This does not make arbitrary predicates index-seekable; simple property membership retains its existing planner path. - Grouped predicates: a grouped
WHEREpredicate followed by= trueis not generally accepted; use the predicate directly inWHERE. - 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
trueforisEmpty(null), not null. Do not use it as a drop-in replacement forIS NULL. Null left operands in membership tests remain unknown even for an empty list; consequentlyWHERE 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
DISTINCTis 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.
| Feature | Supported scope |
|---|---|
| Property indexes | Legacy 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. |
| Constraints | Node 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 indexes | Full-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 import | LOAD CSV [WITH HEADERS] FROM 'path' AS row [FIELDTERMINATOR 'c']; local files only, no HTTP(S). Server reads require admin authorization and configured file allowlisting. |
| History | Engrams, 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/routing | Documented 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/procedures | Only 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,ACYCLICorTRAILin 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:Openform. INSERT,LET,FILTER,FINISH,NEXT, queryUSE, SQL window functions, SQLGROUP 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.rsand 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.