Skip to main content

openCypher

RushDB accepts openCypher statements for reading and writing the property graph, over Protocol v1 and over the Bolt protocol. This page lists what is supported and tested, how records map to Cypher nodes, and what is out of scope. It is for developers coming from Neo4j or another Cypher database.

PropertyValue
StatusRelease candidate
Enable with--languages cypher (languages.cypher: true)
Wire idcypher
ProfileopenCypher property-graph profile
ImplementationGrafeo graph planner and executor
Accessread and write
Parametersnamed ($name)
TransportsProtocol v1 (HTTP, gRPC) and Bolt

Note: Cypher is a release candidate, not full openCypher. The engine reports noFullOpenCypherTckConformance. The full openCypher TCK runs in CI: of 3,897 scenarios, 1,466 test features outside the supported profile (see Conformance), and 1,385 of the remaining 2,431 pass. The other 1,046 hit known defects of the embedded Grafeo engine, listed below. Do not rely on behavior this page does not list as supported.

Data model mapping​

Cypher conceptRushDB
NodeA record
Node labelThe record's single primary label
RelationshipA relationship
Relationship typeThe relationship type
PropertiesRecord or relationship properties
DatabaseThe request's tenant

A returned node is the record as a JSON object with its properties plus _id (its dashed UUID) and __label. A returned relationship carries its properties plus _id, type, source, and target (dashed UUIDs). A returned path is {"nodes": [...], "edges": [...]} with those objects. Temporal values are ISO 8601 strings (1984-10-11, 12:31:14.645Z, 1984-10-11T12:31:14Z, P1Y2M). A statement without RETURN (an updating query) returns no rows.

Supported statements​

These cases are part of the tested conformance corpus:

FeatureExample
ExpressionsRETURN 1 + 1 AS n
Match and returnMATCH (n:FACTION) RETURN n
Inline property filterMATCH (n:FACTION {name: 'Rebel'}) RETURN n.name
Relationship patternsMATCH (a:FACTION)-[:OWNS]->(b:PLANET) RETURN b
WHERE with STARTS WITHMATCH (n:FACTION) WHERE n.name STARTS WITH 'R' RETURN n
ORDER BY, SKIP, LIMITMATCH (n:FACTION) RETURN n.name AS name ORDER BY name DESC SKIP 1 LIMIT 2
WITHMATCH (n:FACTION) WITH n.name AS name RETURN name
UNWINDUNWIND [1,2,3] AS x RETURN x
AggregationMATCH (n:FACTION) RETURN count(n) AS c
DISTINCTMATCH (n:FACTION) RETURN DISTINCT n.name
OPTIONAL MATCHMATCH (n:FACTION) OPTIONAL MATCH (n)-[:OWNS]->(p) RETURN n
UNIONMATCH (n:FACTION) RETURN n.name UNION MATCH (p:PLANET) RETURN p.name
List and map literalsRETURN [1,2,3] AS xs, RETURN {a: 1, b: 'x'} AS m
Nested aggregationMATCH (n:FACTION) RETURN n.name AS name, size(collect(n)) AS c
CREATECREATE (n:FACTION {name: 'Rebel'})
MERGEMERGE (n:FACTION {name: 'Hutt'})
SETMATCH (n:FACTION {name: 'Rebel Alliance'}) SET n.name = 'Alliance'
DELETE, DETACH DELETEMATCH (n:Person {name: 'Bob'}) DETACH DELETE n
Index DDLCREATE INDEX FOR (n:FACTION) ON (n.name), DROP INDEX FACTION_name

Multi-hop patterns and the standard functions size, date, datetime, toString, toUpper, abs, round, and range also execute.

Reading​

MATCH (n:FACTION) RETURN n.name AS name ORDER BY name
MATCH (p:Person) WHERE p.age >= $minAge RETURN p.name AS name ORDER BY name

Send parameters as a JSON object:

{
"protocolVersion": 1,
"operation": {
"kind": "query",
"query": {
"kind": "statement",
"language": "cypher",
"statement": "MATCH (p:Person) WHERE p.age >= $minAge RETURN p.name AS name ORDER BY name",
"parameters": { "minAge": 30 }
}
}
}

Integer parameters must fit a signed 64-bit integer.

text_match(n.prop, 'query') is true when a record matches the query in the text index on that property; text_score(n.prop, 'query') is its BM25 score. n.`*` names the index over every string of the record.

MATCH (d:Doc) WHERE text_match(d.title, 'quick fox')
RETURN d.title AS title
ORDER BY text_score(d.title, 'quick fox') DESC LIMIT 10

See Full-text search.

Writing​

Send writes with mutate.statement. The whole statement, however many nodes and relationships it touches, is published as one commit:

CREATE (a:Person {name: 'Alice'})-[r:KNOWS {since: 2025}]->(b:Person {name: 'Bob'})
RETURN a.name AS source, b.name AS target

This statement reports affected: 3 (two records and one relationship) and returns its RETURN rows in the statementMutation response.

Connect existing nodes:

MATCH (a:Person {name: 'Alice'}), (c:Person {name: 'Carol'})
CREATE (a)-[r:LIKES {weight: 2}]->(c)
RETURN r

Update properties and delete:

MATCH (n:Person {name: 'Alice'}) SET n.age = 35, n.city = 'London' RETURN n.age AS age
MATCH (n:Person {name: 'Bob'}) DETACH DELETE n

A write that changes nothing (a MERGE that matched, a SET over no rows) succeeds with affected: 0 and commits nothing. Over Bolt, RushDB decides whether a statement is a read or a write from its parsed plan, so you can send both with RUN.

Writes inside a transaction​

Cypher writes can be staged in a multi-request transaction with a statement mutation, exactly as shown for GQL. Over Bolt, BEGIN/COMMIT/ROLLBACK use the same mechanism.

Index DDL​

Neo4j-style index statements are recognized before the query is planned. CREATE and DROP must be sent with mutate.statement; SHOW INDEXES is a read.

CREATE INDEX [name] [IF NOT EXISTS] FOR (n:Label) ON (n.prop)
CREATE FULLTEXT INDEX [name] [IF NOT EXISTS] FOR (n:Label) ON EACH [n.prop]
[OPTIONS {indexConfig: {consistency: 'immediate'}}]
DROP INDEX name [IF EXISTS]
SHOW INDEXES

An unnamed index is called <Label>_<property>, so CREATE INDEX FOR (n:FACTION) ON (n.name) creates FACTION_name. Each index covers one label and one property. See Indexes and Full-text search.

Note: Composite indexes and constraint DDL are not supported.

Not supported​

These are outside the supported profile. Capability discovery reports each one in limitations.

FeatureBehaviorLimitation
Nodes without a label or with several (CREATE (), CREATE (:Person:Employee))Fails with "exactly one label"; nothing is published.singlePrimaryLabelPerRecord
Changing labels (SET n:Label, REMOVE n:Label)Fails; nothing is published. Use the native retypeRecord mutation. Setting the label a node already has, or removing one it lacks, succeeds with affected: 0.statementLabelMutationUnavailable
CALL procedures, including APOCFails with a typed error.noProcedures
The openCypher temporal type system: localdatetime/localtime, named time zones, nanosecond precision, week/ordinal/quarter constructors, truncate/between/inSecondsFails or returns a different value. date(), time(), datetime() and duration() from ISO 8601 text work and return ISO 8601 text.limitedTemporalTypes
Constraints (CREATE CONSTRAINT ...)Fails with a typed error.
Standalone collect over a literal (RETURN collect([1,2]))Fails; collect aggregates matched rows.
Write working setA write holds only what it creates or changes, refused above 100,000 records or 64 MiB of them (not the tenant's size).
Server-side cursorsResults are materialized; page with SKIP/LIMIT.

Known defects​

RushDB runs the openCypher implementation of the embedded Grafeo engine unchanged. Grafeo deviates from openCypher in the ways below. Each class is measured by the TCK on every CI run. Every failure is also rerun on plain Grafeo, without RushDB's storage or adapter, and fails there too. They are to be reported upstream; RushDB does not patch Grafeo.

DefectTCK scenariosWhat you see
Invalid statements accepted478A statement openCypher requires to fail (type errors such as NOT 'a', undefined variables in some positions, invalid argument types) runs and returns a result.
Different result118Wrong rows, order or null handling in some queries, for example an empty variable-length interval (*2..1) still expands.
Valid statements refused by the binder108Some variable scopes, OPTIONAL MATCH without input rows, DELETE of an expression.
Quantifiers92all/any/none/single give different results when nested, over lists containing nulls, and over some list literals.
Internal errors65Some statements fail with an internal error, mostly WITH ... ORDER BY (50 of 65).
Write column names65An updating statement names an aggregate column differently from its expression text (count(...) for count(*)).
Syntax57openCypher syntax the parser does not accept.
Operator precedence52Some operator combinations evaluate in a different order.
Lazy writes10SKIP/LIMIT after an updating clause also limits the updates.
Non-termination1A variable-length match around a cycle does not end; the request deadline cancels it.

Conformance​

The CI opencypher-tck job runs the whole openCypher TCK (2024.2) against a Cypher-enabled server. Every scenario's outcome is recorded in a ledger, and each failure is attributed to an exclusion or a defect in the profile opencypher-tck-v2024.2-profile.json. The job fails on any change against the ledger, and on a defect that passes on plain Grafeo (then RushDB caused it). Share of in-profile scenarios that pass, by area: triadic selection 100%, CASE and null handling 92%, strings 88%, literals 86%, WITH ... WHERE 83%, SKIP/LIMIT 80%, MATCH ... WHERE 77%, SET 76%, quantifiers 72%; weakest are MATCH 19% and boolean expressions 17% (both mostly invalid statements accepted), aggregation 17% and graph functions 6%. Details and per-category counts are in Query language conformance.

Errors​

SituationError codeBolt code
Cypher not enabledlanguage_disabledNeo.ClientError.Statement.SyntaxError
Syntax not accepted by the parserunsupportedNeo.ClientError.Statement.SyntaxError
Statement error (undefined variable, type error, a write sent with query.statement)invalid_request, with the engine's reasonNeo.ClientError.Statement.SyntaxError
More than 8,192 clauses in one statementinvalid_requestNeo.ClientError.Statement.SyntaxError
Index DDL sent with query.statementinvalid_requestNeo.ClientError.Statement.SyntaxError
Transaction write conflictwrite_conflictNeo.TransientError.Transaction.DeadlockDetected
Cancelled or deadline elapsed (also mid-statement: a running statement's reads fail)cancelledNeo.ClientError.Transaction.TransactionTerminated

See Error codes for the full mapping.

See also​