Skip to content

Cypher Reference ​

Purple8 Hyper Graph (v0.72.0, pip install purple8-hyper-graph) implements a practical subset of the openCypher specification alongside its REST API and MCP server (125 MCP tools across 16 namespaces). It is not a drop-in replacement for a complete openCypher implementation. The sections below describe the supported syntax and its limits.

Execution cancellation ​

Read-only Cypher execution has a cooperative 30-second execution deadline by default. Administrators can configure P8G_QUERY_TIMEOUT_SECONDS to a positive, finite duration in seconds. The deadline is shared across nested graph reads, not restarted for each expansion. Expiry raises QueryDeadlineExceeded; REST returns 504 with error: "query_deadline_exceeded", never partial successful results. This is an execution deadline, not yet a queue-admission deadline.

Native adjacency expansion reads at most 256 adjacency entries per page. Filters do not cause an individual page to scan the entire adjacency list. Python decodes edges lazily, so LIMIT can stop before decoding the rest of a page. Traversal uses the same path and deadline. Concurrent writes retain non-snapshot read semantics; paging does not create transaction snapshots.

Trusted in-process callers can use purple8_graph.query_control.query_scope() and call the yielded control's cancel() from another thread. Cancellation is query-local and raises QueryCancelled; it does not terminate the database process. Native adjacency loops check the shared atomic cancellation flag. These features require the matching rebuilt purple8_core extension.

Read-only WITH projections without ordering or aggregation, UNWIND, and OPTIONAL MATCH pass rows lazily to subsequent clauses. Unordered DISTINCT also streams, retaining only keys for values already seen. SKIP and LIMIT operate after projection, filtering, and deduplication; a downstream limit can stop upstream reads without collecting every match. Consequently, expressions on rows that are never consumed are not evaluated. Ordering still requires buffering, distinct-key memory can grow with unique values, and the public execution method still collects the final result list. This is not a per-query memory cap. Write queries retain eager mutation barriers: limiting returned rows does not skip preceding writes.

Aggregation consumes all input rows but no longer retains the input bindings. count, sum, avg, min, and max maintain running state per group. Integer sums retain integer precision, and floating sums preserve Python 3.12 summation behaviour, including compensated float addition and mixed numeric inputs. collect necessarily retains its collected values; aggregate DISTINCT retains seen-value keys. Grouping also retains grouping values and state for each distinct group. These can still grow with the query's data: this removes redundant row buffering, not the need for future memory budgets. Cancellation is checked while consuming input and producing aggregate groups.

This does not guarantee preemption of every native operation: ANN calls, lock waits, parsing, sorting, provider calls, and other operations need their own bounded/cancellable implementations. Intermediate-row memory limits, bounded query admission, and transactional write cancellation remain separate hardening work. Write Cypher is not automatically cancelled mid-mutation; attempting it inside an explicit cancellable read scope is rejected before any mutation.

Supported clauses ​

ClauseSinceNotes
CREATEv0.1Node and relationship creation
MATCHv0.1Pattern matching with optional predicates
OPTIONAL MATCHv0.5Returns null for non-matching patterns
MERGEv0.4Create if not exists
SETv0.1Update node/edge properties
DELETEv0.2Remove nodes and edges
DETACH DELETEv0.3Delete node + all its relationships
RETURNv0.1Project results
WITHv0.5Pipeline between query steps
WHEREv0.1Filter predicates
ORDER BYv0.3Sort results
LIMIT / SKIPv0.3Pagination
UNWINDv0.6Expand a list into rows
CALLv0.8Procedure calls (see below)

Pattern syntax ​

cypher
-- Node pattern
MATCH (n)
MATCH (n:Person)
MATCH (n:Person {name: "Alice"})

-- Relationship pattern
MATCH (a)-[:KNOWS]->(b)
MATCH (a)-[r:KNOWS {since: 2020}]->(b)

-- Undirected
MATCH (a)-[:KNOWS]-(b)

-- Variable-length path (v0.22+)
MATCH (a)-[:KNOWS*1..4]->(b)

-- Any relationship type
MATCH (a)-[r]->(b)

Each MATCH pattern supports a single node or two nodes connected by one relationship, including a variable-length relationship. Combine patterns with commas or successive MATCH clauses; shared aliases retain their existing identity. WHERE on a comma-separated pattern list is evaluated after the bindings have been joined. Multiple labels require every requested label, and inline node and relationship properties are predicates, including on bound aliases.

Variable-length matches preserve distinct paths, not just distinct endpoints. A node may be revisited, but the same relationship cannot be reused within one path. Relationship aliases on variable-length patterns hold lists, including an empty list for a zero-length match. Paths remain subject to the engine's traversal-depth cap. Use DISTINCT when only unique endpoints are required.

WHERE predicates ​

cypher
-- Comparison
WHERE n.age > 30
WHERE n.name = "Alice"
WHERE n.name <> "Bob"

-- Null checks
WHERE n.email IS NOT NULL
WHERE n.deleted_at IS NULL

-- String operators
WHERE n.name STARTS WITH "Al"
WHERE n.email ENDS WITH "@acme.com"
WHERE n.bio CONTAINS "engineer"

-- Boolean operators
WHERE n.age > 30 AND n.active = true
WHERE n.role = "admin" OR n.role = "owner"
WHERE NOT n.deleted = true

-- IN list
WHERE n.status IN ["active", "pending"]

-- Range
WHERE n.age >= 18 AND n.age <= 65

Non-aggregate expressions in RETURN or WITH define the grouping keys. count(expression) ignores nulls; count(*) counts rows. DISTINCT is supported inside aggregate calls, for example count(DISTINCT n.author). An empty ungrouped aggregate produces one row (count and sum are zero, collect is empty); an empty grouped aggregate produces no groups. ORDER BY, SKIP, and LIMIT apply to the projected groups as well as ordinary rows. LIMIT 0 returns no rows.

Aggregation functions ​

cypher
MATCH (n:Document)
RETURN
  COUNT(n)                AS total,
  COUNT(DISTINCT n.author) AS unique_authors,
  AVG(n.word_count)       AS avg_words,
  SUM(n.word_count)       AS total_words,
  MIN(n.published_at)     AS earliest,
  MAX(n.published_at)     AS latest,
  COLLECT(n.title)        AS titles

WITH — multi-step pipelines ​

cypher
MATCH (p:Person)-[:AUTHORED]->(d:Document)
WHERE d.published_at > "2024-01-01"
WITH p, COUNT(d) AS doc_count
WHERE doc_count > 5
RETURN p.name, doc_count
ORDER BY doc_count DESC
LIMIT 10

MERGE — upsert pattern ​

cypher
-- Create the node if it doesn't exist, match if it does
MERGE (p:Person {email: "alice@acme.com"})
ON CREATE SET p.created_at = timestamp()
ON MATCH  SET p.last_seen  = timestamp()
RETURN p

MERGE also supports a single fixed-length relationship pattern:

cypher
MATCH (p:Person {id: "person-1"})
MERGE (p)-[r:ENROLLED]->(c:Course {id: "course-1"})
ON CREATE SET c.title = "Graph fundamentals"
ON MATCH SET r.reviewed = true
RETURN c, r

The complete pattern is matched first. If it matches, all matching bindings are returned and ON MATCH SET is applied. Otherwise, unbound pattern elements are created and ON CREATE SET is applied. Bind both existing endpoints with MATCH before merging a new relationship between them; an existing explicit node ID in an unbound element is not permission to replace that node.

Multiple comma-separated MERGE patterns, multi-relationship chains, and variable-length MERGE patterns are rejected. Compound Cypher writes are not wrapped in a query-level transaction; MERGE is not a uniqueness constraint across concurrent queries.

Parameters ​

Use parameters for user-supplied values. Values are bound to the parsed query, never interpolated into query text. Missing parameters are rejected before any clause executes, including writes. This does not make multi-clause writes transactional, and query-plan caching is not currently implemented.

python
from purple8_graph.query_language import QueryLanguage

QueryLanguage(engine).execute(
    "MATCH (p:Person {email: $email}) RETURN p",
    parameters={"email": "alice@acme.com"},
)
cypher
-- In raw Cypher, parameters are referenced with $name
MATCH (p:Person {email: $email})
WHERE p.age > $min_age
RETURN p.name, p.age

Parameters can contain strings, numbers, booleans, nulls, lists, and maps. They work in predicates, property values, projections, scalar functions, arithmetic, UNWIND, and procedure arguments. SKIP $offset and LIMIT $limit require non-negative integers (not booleans). Parameters cannot replace labels, property names, relationship types, or other query syntax.

REST uses POST /query with {"cypher": "...", "parameters": {...}}. The MCP graph.query tool accepts the same fields. The HTTP SDK uses client.query.cypher(query, parameters={...}); its asynchronous counterpart is await client.search.cypher(query, parameters={...}). GraphQL exposes an optional JSON parameters argument on executeCypher, with its existing permission gate. POST /query/explain also accepts parameters and never executes the query.

Query/search HTTP SDK calls use the authenticated credentials' tenant. Passing a per-call tenant_id raises ValueError; an ignored body field must not be mistaken for tenant selection.

CALL — built-in procedures ​

The following procedures are the complete list implemented in query_language.py. There is no db.bm25.search or db.hybrid.search Cypher procedure — full-text and hybrid (vector + BM25 + graph) search are REST/MCP/Python-API-only today (engine.fulltext_search(), rag.hybrid_query MCP tool, POST /rag/query). If you need hybrid retrieval, call those instead of trying to reach it through Cypher.

cypher
CALL db.vector.search('Label', $embedding, k)
YIELD node, score

-- Example: top-5 documents similar to a query vector
CALL db.vector.search('Document', $vec, 5)
YIELD node AS doc, score
RETURN doc.title, doc.author, score
ORDER BY score DESC
ArgumentTypeDescription
LabelSTRINGNode label to search within
$embeddingLIST<FLOAT>Query vector (must match index dimensionality)
kINTEGERNumber of nearest neighbours to return

There is no options map (e.g. {slot: ...} or {alpha: ...}) — only positional (label, vector, k) arguments are supported.

With filters (post-filter, applied in WHERE after YIELD):

cypher
CALL db.vector.search('Document', $vec, 20)
YIELD node AS doc, score
WHERE doc.status = "published" AND doc.year >= 2023
RETURN doc.title, score
ORDER BY score DESC
LIMIT 5

db.vector.searchText ​

Auto-embeds a raw text string using the engine's configured GenAI provider, then delegates to db.vector.search. Requires engine.set_genai_provider(...) to have been called — raises a QueryError (not a Python exception) if no provider is attached.

cypher
CALL db.vector.searchText('Document', 'transformer attention mechanism', 10)
YIELD node AS doc, score
RETURN doc.title, score
ORDER BY score DESC

Schema introspection procedures ​

cypher
CALL db.labels()             YIELD label
CALL db.propertyKeys()       YIELD propertyKey
CALL db.relationshipTypes()  YIELD relationshipType
CALL db.nodeCount()          YIELD count
CALL db.edgeCount()          YIELD count

Named embedding slots ​

Multiple named vector properties per node (e.g., title_vec + body_vec) are not addressable from Cypher's db.vector.search today — it always searches against the engine's single configured embedding index. To search a specific named vector field, use engine.vector_search() directly from the Python API or graph.vector_search via MCP, both of which support explicit field selection.

Functions ​

FunctionDescription
id(n)Internal node ID
labels(n)List of node labels
type(r)Relationship type string
properties(n)All properties as a map
keys(n)List of property keys
size(list)Length of a list
toString(x)Cast to string
toInteger(x)Cast to integer
toFloat(x)Cast to float
toBoolean(x)Cast to boolean
toLower(s)Lowercase string
toUpper(s)Uppercase string
trim(s)Strip whitespace
split(s, delim)Split string to list
substring(s, start, len)Extract substring
timestamp()Current UNIX epoch (ms)
date()Current date string (YYYY-MM-DD)
coalesce(a, b, ...)First non-null value
head(list)First element
tail(list)All but first element
last(list)Last element
range(start, end)Integer range list

Not supported ​

The following openCypher features are not implemented in the current release:

FeatureWorkaround
FOREACHUNWIND + WITH
List comprehensions [x IN list WHERE ...]UNWIND + WHERE + COLLECT
Pattern comprehensions [(a)-->(b) | b.name]MATCH + COLLECT
CALL { ... } (subquery)Split into WITH steps
CALL { ... } IN TRANSACTIONSUse engine.batch_query()
EXISTS { ... } subqueryOPTIONAL MATCH + null check
APOC proceduresNo APOC support — use Python SDK
shortestPath() / allShortestPaths() (Cypher syntax)Not exposed as a Cypher function. Use engine.shortest_path(source, target) (Python API) or the graph.shortest_path MCP tool instead — the algorithm itself is implemented, just not reachable through MATCH shortestPath(...) syntax
db.bm25.search / db.hybrid.search (Cypher CALL)Not implemented as Cypher procedures. Use engine.fulltext_search(), rag.hybrid_query (MCP), or POST /rag/query (REST)
Full-text index synonymsUse BM25 + custom tokenizer

Cypher vs SQL at a glance ​

cypher
-- SQL:  SELECT p.name FROM people p JOIN authored a ON p.id=a.person_id
--       JOIN documents d ON d.id=a.doc_id WHERE d.year=2024
-- Cypher equivalent:
MATCH (p:Person)-[:AUTHORED]->(d:Document {year: 2024})
RETURN p.name

Purple8 Hyper Graph v0.72.0 · AI-Native Execution Infrastructure · NVIDIA Inception Member · Red Hat Technology Partner.