Cypher Queries
GestaltDB exposes an expanding openCypher subset through
GraphDB.query(cypher, parameters=None, read_snapshot=None). The grammar-based frontend supports
comments, Unicode and backtick-escaped names, source-located syntax errors, and
standard expression precedence. Execution covers indexed node and relationship
scans, typed relationship expansion, filtering, ordering, chained MATCH
clauses, writes, and a persisted node-constraint catalog.
Relationship types come from edge.properties["type"]. Node labels are stored
on Node(labels=[...]).
Read-only queries automatically use one unified mutable-state backend snapshot
when supported, preventing index scans, entity hydration, and traversal from
mixing horizons. Pass read_snapshot=True to require the guarantee or
False to opt out. LMDB supports verifiable snapshots; transactional PyRex
supports pinned reads; LevelDB and default PyRex fail explicit requests closed.
Basic Result Shape
GraphDB.query returns a result with columns and records. Iterating the
result yields record dictionaries.
result = graph_db.query('MATCH (n:Drug) RETURN n.id, n.name LIMIT 10')
print(result.columns)
for record in result:
print(record["n.id"], record["n.name"])
Node Scans
Label scans use the label index. Inline property maps may contain multiple
entries. The first eligible indexed property selects an index-backed candidate
set; exact and range predicates in WHERE reuse the same indexes, and all
predicates are then checked. Independent comma-separated node patterns start
with the smallest labeled scan.
graph_db.create_node_property_index("name")
graph_db.query('MATCH (n:Drug) RETURN n')
graph_db.query('MATCH (n:Drug {name: "Aspirin"}) RETURN n.id')
graph_db.query('MATCH (n:Drug {name: "Aspirin", approved: true}) RETURN n.id')
graph_db.query('MATCH (n:Drug:Approved) RETURN n.id')
graph_db.query('MATCH (n) RETURN n.id LIMIT 5')
Typed Relationship Traversal
Use an anchored pattern when you know the start node ID.
{id: ...} in any node pattern is a GestaltDB extension that addresses
the stable entity ID; it accepts a string literal or parameter. It composes
with labels, other properties, and relationship patterns, and keeps
MERGE idempotent on every endpoint.
graph_db.query('MATCH (d {id: "drug-1"})-[:binds]->(p) RETURN p.id')
graph_db.query('MATCH (p {id: "protein-1"})<-[:binds]-(d) RETURN d.id')
graph_db.query('MATCH (n {id: "x"})-[:related]-(m) RETURN m.id')
Unanchored typed relationship scans are also supported.
graph_db.query('MATCH (a)-[r:binds]->(b) RETURN a.id, r.id, b.id')
graph_db.query('MATCH (a)-[r:binds|inhibits]->(b) RETURN r.id ORDER BY r.id')
Relationship patterns accept inline property maps with literal or parameter
values, which filter matched edges in both typed and untyped expansions. A
typed pattern can use a configured composite type/property index for an inline
property or eligible WHERE predicate.
graph_db.query('MATCH (a)-[r:binds {score: 0.9}]->(b) RETURN r.id, b.id')
Variable-length patterns (-[*]->, -[*1..3]->, -[*..2]->,
-[*2..]->, -[*2]->) match trails of the given length range without
repeating relationships. Multi-edge matches bind the relationship variable
to a list of edges ([] for zero-length matches such as *0..1).
graph_db.query('MATCH (d {id: "drug-1"})-[:binds*1..3]->(p) RETURN p.id')
Shortest paths select minimal-length trails. MATCH SHORTEST [k] emits
up to k shortest matches (one by default), MATCH ANY SHORTEST
behaves the same, and MATCH ALL SHORTEST emits every match at the
minimal length. shortestPath and allShortestPaths evaluate a
single-hop pattern between bound endpoints to path values (None or
[] when unreachable).
graph_db.query('MATCH SHORTEST (d {id: "drug-1"})-[*]->(p) RETURN p.id')
Named paths bind a whole match to a path value holding endpoint-to-endpoint
nodes and edges. Use nodes(), relationships(), and length() to
inspect path values; bound paths remain usable downstream like any variable.
graph_db.query('MATCH p = (d:Drug)-[:binds]->(t:Target) RETURN length(p) AS hops')
Bitemporal Matches
Append FOR VALID_TIME AS OF <expression> to MATCH or OPTIONAL
MATCH to query immutable temporal versions instead of mutable current graph
records. Add FOR SYSTEM_TIME AS OF <expression> to select what was known at
a historical system instant; a system-time qualifier requires a valid-time
qualifier. Both expressions must produce datetime values and may use
literals, parameters, and scalar functions, but not row variables or
aggregates.
result = graph_db.query(
'MATCH (p:Person)-[r:WORKS_FOR]->(c:Company) '
'FOR VALID_TIME AS OF datetime($validAt) '
'FOR SYSTEM_TIME AS OF datetime($knownAt) '
'RETURN c.name, versionId(r), validFrom(r), validTo(r), systemFrom(r)',
parameters={"validAt": valid_at, "knownAt": known_at},
)
Qualifiers are query-wide. Every chained or optional match, UNION branch,
correlated subquery, fixed or variable path, and shortest path uses one valid
instant and one read view captured at query start. Repeated qualifiers must
resolve to the same instant. Temporal queries are read-only and reject all
write clauses. versionId, validFrom, validTo, and systemFrom
return temporal version metadata, propagate null, and return null for entities
from unqualified current-graph queries; validTo is also null for an
open-ended interval.
Temporal matches use temporal node and endpoint catalogs plus typed temporal
adjacency. Deferred temporal writes therefore require
rebuild_temporal_indexes() or rebuild_deferred_indexes() before they can
be queried. Queries without qualifiers retain current-graph behavior.
Writes
CREATE builds nodes and relationships. Bound variables are reused while
fresh ones are created (nodes get a UUID unless the properties supply
id); created relationships need exactly one type. SET updates
properties (n.prop = expr), adds labels (n:Label), merges maps
(n += map), or replaces all properties (n = map). REMOVE drops
properties or labels. All updates copy on write, apply per input row with
snapshot input semantics, and treat None targets as a null-safe no-op.
graph_db.query('CREATE (d:Drug {id: "drug-9", name: "New"}) RETURN d.id')
graph_db.query('MATCH (d {id: "drug-9"}) SET d.score = 0.9 RETURN d.score')
Write queries run inside a backend transaction when one is supported (LMDB, transactional PyRex) and roll back on failure; other backends apply best-effort direct writes. Later clauses in the same query always observe earlier writes.
DELETE removes nodes and relationships. Deleting a node with remaining
relationships fails unless DETACH DELETE cascades to incident edges.
graph_db.query('MATCH (d {id: "drug-9"}) DETACH DELETE d')
MERGE matches a pattern or creates it when absent, running ON MATCH
actions in the first case and ON CREATE actions in the second. It is
idempotent: repeating the same MERGE changes nothing after the first
execution.
graph_db.query('MERGE (d:Drug {id: "drug-9"}) ON CREATE SET d.score = 0 RETURN d.id')
FOREACH runs write clauses per element of a list without changing the
row stream. Loop variables never escape, and entity bindings refresh
afterwards so later clauses observe loop writes.
graph_db.query('MATCH (d:Drug) FOREACH (tag IN ["a", "b"] | SET d.tag = tag) RETURN d.id')
Node Constraints
CREATE CONSTRAINT registers a persisted single-label, single-property
UNIQUE or IS NOT NULL constraint. Existing data is validated before the
catalog changes. Cypher CREATE, MERGE, SET, and REMOVE validate
the final node state, including ON CREATE SET actions. SHOW CONSTRAINTS
returns name, type, label, and property columns, and
DROP CONSTRAINT removes a definition by name.
graph_db.query(
'CREATE CONSTRAINT drug_name FOR (d:Drug) REQUIRE d.name IS UNIQUE'
)
graph_db.query(
'CREATE CONSTRAINT drug_source FOR (d:Drug) REQUIRE d.source IS NOT NULL'
)
constraints = graph_db.query('SHOW CONSTRAINTS')
graph_db.query('DROP CONSTRAINT drug_source')
Constraints currently cover node label/property values written through Cypher; direct object and columnar writes do not enforce the catalog.
SHOW INDEX and SHOW INDEXES list configured property indexes in
deterministic node-then-relationship order. The result columns are
entityType and properties; automatic label, relationship-type,
composite, and range representations are not separate catalog entries.
indexes = graph_db.query('SHOW INDEXES')
General fixed-length patterns may be unanchored and may filter every node in the path. Anonymous nodes and relationships, omitted relationship types, and bracketless relationships are supported. Untyped expansion scans canonical edge records and is therefore less efficient than typed adjacency expansion.
graph_db.query(
'MATCH (a:Drug {approved: true})-[:binds]->()-[:affects]->(d:Disease {active: true}) '
'RETURN a.id, d.id'
)
graph_db.query('MATCH (a)-->(b) RETURN a.id, b.id')
graph_db.query('MATCH ()-[r]-(b) RETURN r.id, b.id')
Multiple comma-separated pattern parts share one MATCH scope. Shared
variables join the parts; disconnected parts form a Cartesian product.
graph_db.query(
'MATCH (a:Drug)-[:binds]->(p), (p)-[:associated_with]->(d:Disease) '
'RETURN a.id, d.id'
)
Within one path or comma-separated MATCH, one stored relationship cannot be
used twice. A subsequent MATCH clause starts a new relationship uniqueness
scope.
Filtering
WHERE supports property-to-property and property-to-value comparisons,
arithmetic, parentheses, NOT, AND, XOR, OR, IN, IS
NULL, IS NOT NULL, STARTS WITH, ENDS WITH, CONTAINS, and
regular-expression matching with =~.
Predicates use Cypher three-valued logic. In particular, n.value = null and
n.value <> null do not pass WHERE; use IS NULL or IS NOT NULL.
Predicate contexts require Boolean or None values rather than coercing
numbers or strings. Membership in an empty list is always false, including
null IN [].
graph_db.query('MATCH (n:Drug) WHERE n.name = "Aspirin" RETURN n.id')
graph_db.query('MATCH (n:Person) WHERE n.age >= $age RETURN n.id', parameters={"age": 35})
graph_db.query('MATCH (n) WHERE n.kind IN ["drug", "protein"] RETURN n.id')
graph_db.query('MATCH (n) WHERE n.name IS NOT NULL RETURN n.id')
graph_db.query('MATCH (n) WHERE n.name STARTS WITH "A" OR n.score * 2 >= n.minimum RETURN n.id')
Relationship predicates work in anchored traversals and unanchored relationship scans. When an edge property index exists, exact and range predicates on typed relationship scans can use the composite type/property index.
graph_db.create_edge_property_index("score")
graph_db.query('MATCH (a)-[r:binds]->(b) WHERE r.score >= 0.8 RETURN r.id, b.id')
Extended Expressions
RETURN, WITH, ORDER BY, and WHERE accept CASE,
subscripts, slices, list comprehensions, reduce, map projections,
quantified predicates, and exists over properties. Comprehension
variables are local to their expression and shadow outer variables of the
same name. Missing values propagate as None.
graph_db.query('MATCH (n:Drug) RETURN CASE WHEN n.score >= 0.8 THEN "hit" ELSE "miss" END AS call')
graph_db.query('MATCH (n:Drug) RETURN n.synonyms[0] AS first, n.synonyms[1..3] AS rest')
graph_db.query('MATCH (n:Drug) RETURN [s IN n.synonyms WHERE s STARTS WITH "A" | s] AS matches')
graph_db.query('MATCH (n:Drug) RETURN reduce(total = 0, s IN n.scores | total + s) AS total')
graph_db.query('MATCH (n:Drug) RETURN n{.*, name: n.name} AS drug')
graph_db.query('MATCH (n:Drug) WHERE all(s IN n.scores WHERE s > 0) RETURN n.id')
graph_db.query('MATCH (n:Drug) WHERE exists(n.score) RETURN n.id')
Aggregates must still be top-level projection expressions; they cannot hide
inside CASE or comprehensions. Pattern comprehensions and
exists() with a pattern argument are not yet supported.
Scalar Functions
The engine provides the core openCypher scalar functions: coalesce,
id/elementId, type, labels, startNode/endNode,
properties, head/last, size/length, the toBoolean/
toInteger/toFloat/toString conversions, trim/lTrim/
rTrim, toUpper/toLower, replace, split, substring,
left/right, abs, ceil, floor, round, sqrt,
pow, sign, exp, log, log10, sin, cos, tan,
pi, e, rand, randomUUID, range, reverse, tail,
and keys.
Conversions return None for unconvertible inputs, most other functions
propagate None, and misused functions raise typed errors. Scalar calls
group like any other non-aggregate projection expression.
graph_db.query('MATCH (n:Drug) RETURN toUpper(n.name) AS name, size(n.synonyms) AS total')
graph_db.query('MATCH (a)-[r:binds]->(b) RETURN type(r) AS rel, startNode(r).name AS source')
Temporal Values
Cypher expressions support immutable, microsecond-precision date, time,
localtime, datetime, localdatetime, and exact duration values.
Each constructor takes one strict ISO string or component map. Constructors
propagate null; they deliberately do not provide a no-argument current-clock
form, so repeated queries remain deterministic. datetime requires an
explicit numeric offset and normalizes to UTC. Named timezones and calendar
month/year durations are not supported.
result = graph_db.query(
"UNWIND [1] AS x "
"RETURN date('2025-01-31') + duration('P2D') AS due, "
"datetime('2025-01-31T12:30:00+02:00').hour AS utc_hour"
)
Temporal values support component access, same-category comparison,
DISTINCT, grouping, ORDER BY, CASE, collections, parameters, and
canonical toString conversion. Exact duration arithmetic is supported for
dates, times, and datetimes; date arithmetic requires whole days. Aware Python
datetime/time parameters become offset temporal values, while naive
ones become local values; Python date and timedelta parameters are also
normalized.
These values persist recursively in node and relationship properties, including
nested lists and maps. Pickle, JSON, MessagePack, and Protobuf use one portable
tagged representation, and old untagged records remain readable. Configured
exact and range property indexes support temporal scalars; fixed-offset time
values are indexed by their UTC-equivalent time so equality matches Cypher
semantics. Temporal graph views and historical MATCH remain separate from
these scalar values. Array-native sampler snapshots do not export arbitrary
graph properties.
graph_db.create_node_property_index("observed")
graph_db.query(
"CREATE (e:Event {id: 'e1', observed: datetime('2025-01-31T12:30:00Z')})"
)
graph_db.query(
"MATCH (e:Event) WHERE e.observed >= datetime('2025-01-01T00:00:00Z') "
"RETURN e.id, e.observed"
)
Projection and Result Shaping
Use aliases, RETURN *, DISTINCT, ORDER BY, SKIP, and LIMIT.
RETURN and ORDER BY accept general expressions, including arithmetic,
literals, parameters, lists, and maps. Unaliased expressions use a
deterministic rendered column name, so prefer AS aliases for readability.
ORDER BY accepts a projected alias. SKIP and LIMIT accept either a
non-negative integer literal or a parameter containing one.
graph_db.query('MATCH (n:Drug) RETURN n.id AS id, n.name AS name ORDER BY name LIMIT $count', parameters={"count": 10})
graph_db.query('MATCH (n) RETURN DISTINCT n.kind ORDER BY n.kind')
graph_db.query('MATCH (a)-[r:binds]->(b) RETURN * LIMIT 10')
GestaltDB retains these extension projections for entity metadata:
n.idandr.idreturn entity IDs.n.labelsreturns node labels.r.sourceandr.targetreturn relationship endpoints.Missing properties project as
None.
Chained MATCH Clauses
Multiple MATCH clauses execute as a row pipeline. Reusing a variable enforces
that it refers to the same entity. WHERE may follow any MATCH clause
and filters that stage before later clauses run.
graph_db.query(
'MATCH (d:Drug {name: "Aspirin"}) '
'MATCH (d)-[r:binds]->(p) '
'RETURN d.id, r.score, p.id'
)
WITH and Variable Scope
WITH projects intermediate values, replaces the variable scope with its
outputs, and may carry its own WHERE, DISTINCT, ORDER BY,
SKIP, and LIMIT. Only projected variables and aliases survive; later
MATCH clauses may traverse from retained entity variables. Each MATCH
starts a new relationship uniqueness scope.
graph_db.query(
'MATCH (p:Person) '
'WITH p ORDER BY p.age DESC LIMIT 10 '
'MATCH (p)-[:member_of]->(t:Team) '
'RETURN p.name AS name, t.name AS team'
)
OPTIONAL MATCH
OPTIONAL MATCH works as a left-outer join: input rows without a match
survive with newly introduced variables bound to None. A WHERE clause
after OPTIONAL MATCH filters the joined rows, so null-extended rows are
removed unless the predicate keeps them (for example with OR m IS NULL).
Later MATCH clauses can still traverse from bound variables, while
patterns referencing null-bound variables match nothing; unrelated patterns
form Cartesian products as usual. Aggregates ignore None inputs except
for count(*).
graph_db.query(
'MATCH (d:Drug) '
'OPTIONAL MATCH (d)-[:binds]->(p:Protein) '
'RETURN d.id, p.id'
)
UNWIND
UNWIND expands each input row into one row per element of a list
expression, which may come from a literal, a parameter, or a bound variable.
None and empty lists produce no rows. UNWIND may open a query or
appear after MATCH, OPTIONAL MATCH, or WITH stages.
graph_db.query('UNWIND ["Aspirin", "Ibuprofen"] AS name RETURN name')
UNION
UNION and UNION ALL combine the results of two or more independent
read queries. Every branch must return exactly the same columns in the same
order; branch ORDER BY/SKIP/LIMIT apply within their branch.
UNION ALL concatenates branch results in branch order, while each
UNION step deduplicates everything accumulated so far, preserving
first-seen order. Mixed operators fold left.
graph_db.query(
'MATCH (d:Drug) RETURN d.id AS id '
'UNION MATCH (p:Protein) RETURN p.id AS id'
)
CALL Subqueries
CALL { ... } runs a correlated subquery once per input row. The inner
query sees outer bindings, and its RETURN outputs merge into the outer
row (shadowing same-named variables); outer rows with an empty subquery
result are dropped. Parameters work everywhere, including SKIP/LIMIT,
regular expressions, IN lists, and inside subqueries.
graph_db.query(
'MATCH (d:Drug) '
'CALL { MATCH (d)-[:binds]->(p:Protein) RETURN p } '
'RETURN d.id, p.id'
)
Registered Procedures
Top-level registered procedure calls use CALL qualified.name(...) YIELD.
Yielded fields may be aliased, arguments may use parameters, and unknown
procedure names or fields produce source-located semantic errors. Generalized
syntax does not enable arbitrary dispatch. pg.sample_typed_paths and the
bounded temporal-epistemic kg.entails procedure are registered.
result = graph_db.query(
'CALL pg.sample_typed_paths($seeds, $pattern) '
'YIELD path AS sampled RETURN sampled LIMIT 1',
parameters={
"seeds": ["drug-1"],
"pattern": [{"edge_type": "binds", "direction": "out", "sample_size": 2}],
},
)
kg.entails accepts agent, proposition, modal operator, and options. It can
yield status, confidence, and explanation. See
Bounded Modal and Epistemic Evaluation for its world, temporal, formula, and limit semantics.
Aggregation and Implicit Grouping
count, collect, sum, avg, min, and max work in
WITH and RETURN, including count(*) and argument-level
DISTINCT such as count(DISTINCT n.age). Non-aggregate projection
expressions become implicit grouping keys; an all-aggregate projection has
one global group. Null inputs are ignored except by count(*), and empty
global inputs return 0 for count, [] for collect, 0 for
sum, and None for avg, min, and max.
graph_db.query('MATCH (p:Person) RETURN count(*)')
graph_db.query('MATCH (p:Person) RETURN p.department AS department, count(*) AS total ORDER BY total DESC')
ORDER BY in an aggregate query must reference projected outputs.
Aggregates cannot appear in WHERE (filter an aggregating WITH instead),
and cannot nest. Aggregate results may be composed with scalar expressions and
functions, such as count(*) + 1 or toString(count(*)).
ORDER BY is stable and applies Cypher’s value hierarchy across supported
maps, entities, lists, paths, strings, Booleans, numbers, and nulls. Nulls sort
last ascending and first descending. Conversion functions reject unsupported
input types; malformed strings passed to numeric/Boolean conversions return
None. Temporal constructors (such as date and duration) and the
spatial point constructor produce explicit unsupported-function errors.
Current Limitations
Syntax and semantic failures are ValueError subclasses with source
locations. The current Cypher API does not yet support:
mutating clauses beyond
CREATE,SET,REMOVE,DELETE,MERGE, andFOREACHrelationship, multi-property, and direct-object-write constraint enforcement
pattern comprehensions and
exists()with a pattern argumentGQL quantified relationships/path patterns; errors suggest legacy
*min..maxsyntaxscalar functions beyond the documented core set
procedures other than the registered
pg.sample_typed_pathsandkg.entailscalls