Skip to content

Quine Cypher vs. Neo4j Cypher

Quine Enterprise implements a dialect of OpenCypher v9. If you have experience writing Cypher for Neo4j, most of your knowledge transfers directly. However, Quine Enterprise is a streaming graph, not a database, and several behaviors differ in important ways. This page covers every major difference so that queries written for Quine Enterprise work correctly the first time.

Fundamental Differences

All Nodes Always Exist

In Neo4j, CREATE (n:Person {name: "Alice"}) adds a new node to the database. In Quine Enterprise, nodes are never created — they always exist. You address a node by its ID and start using it. This means:

  • CREATE on a node doesn't fail or produce a duplicate — it simply addresses the node at the generated ID.
  • There is no need to check whether a node exists before writing to it.
  • Two separate data streams can reference the same node simultaneously without coordination.

The MATCH ... WHERE id(n) = idFrom(...) pattern is the standard way to address nodes:

-- Neo4j style (don't use in Quine Enterprise)
MERGE (customer:Customer {customerId: "CUST-123"})
SET customer.name = "Alice"

-- Quine Enterprise style
MATCH (customer)
WHERE id(customer) = idFrom("customer", "CUST-123")
SET customer:Customer, customer.name = "Alice"

No Indexes — Use idFrom() Instead

Neo4j uses indexes to look up nodes by property value. Quine Enterprise has no indexes. Instead, idFrom() deterministically computes a node ID from input values, giving O(1) direct access to any node.

Any query that searches for nodes by property without an id(n) = idFrom(...) constraint will scan every node in the graph, which is extremely slow at scale.

-- Neo4j style: property lookup via index (don't use in Quine Enterprise)
MATCH (user:User {email: "alice@example.com"})
RETURN user

-- Quine Enterprise style: direct ID lookup via idFrom()
MATCH (user)
WHERE id(user) = idFrom("user", "alice@example.com")
RETURN user

Always include at least one id(n) = idFrom(...) constraint in ad-hoc and ingest queries. The only exception is standing queries, which use incremental matching and do not require idFrom().

idFrom() Basics

idFrom() accepts any number of arguments and deterministically produces a node ID:

idFrom("user", "alice@example.com")        -- single key
idFrom("sensor-reading", sensorId, timestamp) -- composite key

Always prefix with a type string to prevent collisions between different node types:

-- Good: different types won't collide even if IDs overlap
id(customer) = idFrom("customer", "123")
id(order) = idFrom("order", "123")

-- Bad: these resolve to the same node if the IDs match
id(customer) = idFrom("123")
id(order) = idFrom("123")

Stay consistent across all ingest streams. If one stream uses idFrom("customer", id) and another uses idFrom("cust", id), they will create separate nodes for the same logical entity.

For details, see Quine Indexing.

Behavioral Differences

MATCH Does Not See Same-Query Updates

In Neo4j, a SET operation updates the node immediately and subsequent clauses in the same query can see the change. In Quine Enterprise, nodes found in a MATCH do not reflect updates made later in the same query.

-- In Neo4j, this works as expected.
-- In Quine Enterprise, the WHERE clause sees the ORIGINAL value of n.status,
-- not the value set by the preceding SET.
MATCH (n) WHERE id(n) = idFrom("order", "123")
SET n.status = "shipped"
WITH n
MATCH (n) WHERE n.status = "shipped"  -- may not match in Quine Enterprise
RETURN n

The same node can even be aliased under two different variable names, and those aliases may show different property values if there were intervening writes.

Workaround: Split the operation into separate queries, or use standing queries that react to the updated state.

CREATE Is Idempotent for Edges

In Neo4j, running CREATE (a)-[:KNOWS]->(b) twice creates two separate :KNOWS edges. In Quine Enterprise, an edge is uniquely identified by its direction, label, and endpoints. Running the same CREATE twice has no effect the second time — the edge already exists.

-- Running this twice in Neo4j creates 2 edges.
-- Running this twice in Quine Enterprise creates 1 edge.
MATCH (a), (b)
WHERE id(a) = idFrom("person", "Alice") AND id(b) = idFrom("person", "Bob")
CREATE (a)-[:KNOWS]->(b)

This means there can never be multiple edges with the same label and direction between the same two nodes.

Edges Have No Properties

Neo4j supports properties on relationships. Quine Enterprise does not. If you need to attach data to a relationship, model it as an intermediate node:

-- Neo4j style (won't work in Quine Enterprise)
CREATE (a)-[:PURCHASED {amount: 99.99, date: "2024-01-15"}]->(product)

-- Quine Enterprise style: use an intermediate node
MATCH (customer), (order), (product)
WHERE id(customer) = idFrom("customer", $that.customerId)
  AND id(order) = idFrom("order", $that.orderId)
  AND id(product) = idFrom("product", $that.productId)
SET order.amount = $that.amount, order.date = $that.date
CREATE (customer)-[:PLACED]->(order)-[:CONTAINS]->(product)

Edges Have No IDs

In Neo4j, RETURN id(e) returns an edge's internal ID. In Quine Enterprise, edges do not have IDs. MATCH (n)-[e]->(m) RETURN id(e) does not return a useful value. Look up edges from one of the endpoint nodes instead.

Labels Are Not Indexed

In Neo4j, MATCH (p:Person) uses a label index to efficiently find all Person nodes. In Quine Enterprise, labels are just properties — there is no label index. A query like MATCH (p:Person) RETURN p scans every node in the graph to check for the :Person label.

Labels are still useful for organization and for standing query pattern matching, but they do not make ad-hoc queries faster.

Counting All Nodes Is Expensive

In Neo4j, MATCH (n) RETURN count(*) is a fast metadata lookup. In Quine Enterprise, this query scans the entire graph and should be avoided.

Unsupported Features

Feature Neo4j Quine Enterprise Alternative
Edge properties Supported Not supported Use intermediate nodes
Multiple edges (same label/direction/endpoints) Supported Not supported N/A — edges are idempotent
shortestPath in MATCH/MERGE patterns Supported Not supported Use as expression: RETURN shortestPath((a)-[*]->(b))
allShortestPaths Supported Not supported
Variable-length patterns in standing queries Supported Not supported Use in ad-hoc queries only
DETACH DELETE on a path Supported Not supported Delete nodes individually
percentileCont, percentileDisc, stDev, stDevP Supported Not supported
sum/avg on durations Supported Not supported
Query hints Supported Silently ignored
Cypher commands (system management) Supported Not supported Use REST API

shortestPath Usage

shortestPath works in Quine Enterprise, but only as an expression — not inside a MATCH pattern. Bind the start and end nodes first:

-- Neo4j style (won't work in Quine Enterprise)
MATCH p = shortestPath((a:Person)-[*]->(b:Person))
WHERE a.name = "Alice" AND b.name = "Bob"
RETURN p

-- Quine Enterprise style
MATCH (a), (b)
WHERE id(a) = idFrom("person", "Alice") AND id(b) = idFrom("person", "Bob")
RETURN shortestPath((a)-[*]->(b))

The default maximum path length is 10 hops. Override with range syntax: shortestPath((a)-[*..20]->(b)).

Standing Query Constraints

Standing queries use a restricted subset of Cypher. These constraints do not apply to ad-hoc or ingest queries.

DistinctId Mode (Default)

  • MATCH patterns must be tree-shaped or linear — no cycles
  • Edges cannot be aliased to variables (-[:LABEL]-> is fine, -[e:LABEL]-> is not)
  • Edges must be directed, have exactly one label, and cannot be variable-length
  • WHERE clauses only support: literal comparisons, IS NULL, IS NOT NULL, regex, and id(n) = idFrom(...)
  • Must return exactly one value: RETURN DISTINCT id(n) or RETURN DISTINCT strId(n)

MultipleValues Mode

Relaxes DistinctId constraints:

  • Can return multiple values including property values (e.g., RETURN n.name, id(m))
  • WHERE clause supports broader expressions on node properties
  • Does not support DISTINCT
  • Still cannot use variable-length patterns, sub-queries, or procedures in WHERE

Quine Enterprise-Specific Features

These features exist in Quine Enterprise but not in Neo4j.

Atomic Property Updates

In a streaming system, concurrent operations can cause race conditions. Quine Enterprise provides atomic procedures to safely update properties:

-- Atomically increment a counter (avoids read-modify-write races)
CALL int.add(node, "count", 1) YIELD result

-- Atomically add to a set
CALL set.insert(node, "tags", "important") YIELD result

-- Atomically merge sets
CALL set.union(node, "categories", ["A", "B"]) YIELD result

These lock the node for the duration of the operation. See Atomic Property Updates.

Composite Property Types

Quine Enterprise can store MAP and LIST OF ANY as node properties — not just scalar types:

SET n.metadata = {source: "kafka", topic: "events"}
SET n.tags = ["urgent", "reviewed"]

Use castOrThrow.map() or castOrNull.map() to hint the type to the query compiler when using composite values in subsequent operations. See Casting Property Types.

Temporal Functions

Quine Enterprise includes functions for working with time that are not in standard OpenCypher:

  • datetime(), localdatetime(), date(), time(), localtime(), duration()
  • Temporal arithmetic and comparison
  • reify.time() procedure for materializing time as graph nodes

See Temporal Functions.

Exploring Without Known IDs

When you don't know specific node IDs, use recentNodes or recentNodeIds to sample recently accessed nodes:

-- Get 20 recently accessed nodes
CALL recentNodes(20)

-- Use recent nodes to anchor a larger query
CALL recentNodeIds(1000) YIELD nodeId
MATCH (n)-[:KNOWS]->(m)
WHERE id(n) = nodeId
RETURN n.name, m.name
LIMIT 10

Common Mistakes

Searching by Property Instead of ID

-- WRONG: scans all nodes (slow at any scale)
MATCH (user:User)
WHERE user.email = "alice@example.com"
RETURN user

-- CORRECT: direct lookup by computed ID
MATCH (user)
WHERE id(user) = idFrom("user", "alice@example.com")
RETURN user

Using MERGE Like Neo4j

-- WRONG: MERGE does a property-based search, causing a full scan
MERGE (customer:Customer {customerId: "CUST-123"})
SET customer.name = "Alice"

-- CORRECT: address by ID, set properties and labels separately
MATCH (customer)
WHERE id(customer) = idFrom("customer", "CUST-123")
SET customer:Customer, customer.name = "Alice", customer.customerId = "CUST-123"

Putting Properties on Edges

-- WRONG: edge properties are not supported
CREATE (a)-[:SENT {amount: 100, currency: "USD"}]->(b)

-- CORRECT: use an intermediate node to hold the data
MATCH (sender), (tx), (receiver)
WHERE id(sender) = idFrom("account", $that.from)
  AND id(tx) = idFrom("transaction", $that.txId)
  AND id(receiver) = idFrom("account", $that.to)
SET tx.amount = $that.amount, tx.currency = $that.currency, tx:Transaction
CREATE (sender)-[:SENT]->(tx)-[:RECEIVED_BY]->(receiver)

Inconsistent idFrom Arguments

-- WRONG: these create two separate nodes for the same customer
-- Stream 1:
MATCH (c) WHERE id(c) = idFrom("customer", $that.customer_id) ...
-- Stream 2:
MATCH (c) WHERE id(c) = idFrom("cust", $that.customerId) ...

-- CORRECT: use identical type prefix and field across all streams
-- Stream 1:
MATCH (c) WHERE id(c) = idFrom("customer", $that.customer_id) ...
-- Stream 2:
MATCH (c) WHERE id(c) = idFrom("customer", $that.customer_id) ...

Using shortestPath in a MATCH Pattern

-- WRONG: shortestPath cannot be used in MATCH patterns
MATCH p = shortestPath((a)-[*]->(b))
WHERE id(a) = idFrom("node", "start")
RETURN p

-- CORRECT: bind endpoints first, use shortestPath as expression
MATCH (a), (b)
WHERE id(a) = idFrom("node", "start") AND id(b) = idFrom("node", "end")
RETURN shortestPath((a)-[*]->(b))