Skip to content

NornicDB Cypher Compatibility

Date: November 26, 2025
Status: Complete β€” production ready
Purpose: Comprehensive audit of Cypher implementation against Neo4j


Currently Implemented

Core Clauses

  • βœ… MATCH - Pattern matching with property filters
  • βœ… MATCH...CREATE - Create relationships between matched nodes (like Neo4j's variable scoping)
  • βœ… CREATE - Node and relationship creation
  • βœ… MERGE - Upsert operations with ON CREATE/ON MATCH
  • βœ… DELETE - Node deletion
  • βœ… DETACH DELETE - Delete with relationship removal
  • βœ… SET - Property updates
  • βœ… SET += - Property merging
  • βœ… REMOVE - Property removal
  • βœ… RETURN - Result projection
  • βœ… WHERE - Filtering
  • βœ… WITH - Intermediate result projection
  • βœ… UNWIND - List expansion
  • βœ… OPTIONAL MATCH - Outer join equivalent
  • βœ… UNION / UNION ALL - Query combination
  • βœ… FOREACH - Iteration with updates

Schema Management

  • βœ… CREATE CONSTRAINT - All constraint families (see below)
  • βœ… CREATE INDEX - Property indexes
  • βœ… CREATE FULLTEXT INDEX - Fulltext search indexes
  • βœ… CREATE VECTOR INDEX - Vector similarity indexes
  • βœ… DROP CONSTRAINT / DROP INDEX - Schema deletion

Constraint Types

All constraint DDL supports IF NOT EXISTS for idempotent creation and named or unnamed forms.

Node constraints (FOR (var:Label)):

Constraint Syntax
Uniqueness REQUIRE var.prop IS UNIQUE
Existence REQUIRE var.prop IS NOT NULL
Node key REQUIRE (var.p1, var.p2) IS NODE KEY
Property type REQUIRE var.prop IS :: TYPE
Temporal no-overlap REQUIRE (var.key, var.from, var.to) IS TEMPORAL NO OVERLAP
Domain/enum REQUIRE var.prop IN ['val1', 'val2']

Cardinality constraints (FOR ()-[var:TYPE]->() or FOR ()<-[var:TYPE]-()):

Constraint Syntax
Max outgoing FOR ()-[var:TYPE]->() REQUIRE MAX COUNT N
Max incoming FOR ()<-[var:TYPE]-() REQUIRE MAX COUNT N

Limits the number of outgoing or incoming edges of a given type per node. Direction is encoded in the FOR clause arrows.

Relationship endpoint policies (FOR (:SrcLabel)-[var:TYPE]->(:TgtLabel)):

Constraint Syntax
Allowed pair FOR (:Src)-[var:TYPE]->(:Tgt) REQUIRE ALLOWED
Disallowed pair FOR (:Src)-[var:TYPE]->(:Tgt) REQUIRE DISALLOWED

ALLOWED policies form a union whitelist: once any ALLOWED policy exists for a relationship type, only declared (source, target) label pairs are permitted. DISALLOWED policies are a blacklist and take precedence over ALLOWED.

Relationship constraints (FOR ()-[var:TYPE]-()):

Constraint Syntax
Uniqueness REQUIRE var.prop IS UNIQUE
Composite uniqueness REQUIRE (var.p1, var.p2) IS UNIQUE
Existence REQUIRE var.prop IS NOT NULL
Relationship key REQUIRE (var.p1, var.p2) IS RELATIONSHIP KEY
Property type REQUIRE var.prop IS :: TYPE
Temporal no-overlap REQUIRE (var.key, var.from, var.to) IS TEMPORAL NO OVERLAP
Domain/enum REQUIRE var.prop IN ['val1', 'val2']

Temporal no-overlap, domain/enum, cardinality, and endpoint policy constraints are NornicDB extensions not available in Neo4j. Uniqueness and key constraints on relationships automatically create owned backing indexes. SHOW CONSTRAINTS returns all constraint types with entity type, direction, maxCount, sourceLabel, targetLabel, and policyMode columns where applicable. For operational guidance on NornicDB-specific schema features, including REQUIRE { ... } block contracts and SHOW CONSTRAINT CONTRACTS, see Managing Constraints.

CALL Procedures

  • βœ… db.labels() - List all labels
  • βœ… db.propertyKeys() - List all property keys
  • βœ… db.relationshipTypes() - List all relationship types
  • βœ… db.indexes() - List indexes
  • βœ… db.constraints() - List constraints
  • βœ… db.index.vector.queryNodes() - Vector similarity search
  • βœ… db.index.fulltext.queryNodes() - Fulltext search
  • βœ… apoc.path.subgraphNodes() - Graph traversal
  • βœ… apoc.path.expand() - Path expansion

SHOW Commands

  • βœ… SHOW INDEXES - Display indexes
  • βœ… SHOW CONSTRAINTS - Display constraints
  • βœ… SHOW CONSTRAINT CONTRACTS - Display block-style constraint contract metadata
  • βœ… SHOW PROCEDURES - List procedures
  • βœ… SHOW FUNCTIONS - List functions
  • βœ… SHOW DATABASE - Database info

Aggregation Functions

  • βœ… COUNT() - Count aggregation
  • βœ… SUM() - Sum aggregation
  • βœ… AVG() - Average aggregation
  • βœ… MIN() / MAX() - Min/max aggregation
  • βœ… COLLECT() - List collection

Scalar Functions (52 total)

  • βœ… String functions: substring, replace, trim, upper, lower, split, etc.
  • βœ… Math functions: abs, ceil, floor, round, sqrt, sin, cos, etc.
  • βœ… List functions: size, head, tail, last, range, etc.
  • βœ… Type functions: toInteger, toFloat, toString, toBoolean
  • βœ… Spatial functions: point, distance
  • βœ… Date/time functions: date, datetime, timestamp

Bolt Handshake Compatibility for cypher-shell

NornicDB speaks Bolt directly, and most Bolt drivers work without special handling. One notable edge case is Neo4j's cypher-shell, which may reject an otherwise valid Bolt connection if the Bolt HELLO success metadata does not advertise a Neo4j-style server string.

For that case, NornicDB includes an explicit compatibility override:

export NORNICDB_BOLT_SERVER_ANNOUNCEMENT="Neo4j/5.26.0"
cypher-shell -a bolt://localhost:7687 -u admin -p password

Equivalent YAML setting:

server:
  bolt_server_announcement: "Neo4j/5.26.0"

This changes only the announced Bolt server string used during handshake compatibility checks. It does not change Cypher behavior or query semantics. Leave it unset unless you specifically need compatibility with cypher-shell or another strict Neo4j client.


Recently Verified Features

1. ORDER BY Clause

Status: Implemented
Impact: Full sorting support

MATCH (n:Node)
RETURN n.name, n.age
ORDER BY n.age DESC, n.name ASC

Features:

  • βœ… Single and multiple sort fields
  • βœ… ASC/DESC modifiers
  • βœ… String and numeric sorting
  • βœ… Integration with LIMIT/SKIP

2. LIMIT / SKIP Clauses

Status: Implemented
Impact: Full pagination support

MATCH (n:Node)
RETURN n
ORDER BY n.created DESC
SKIP 10
LIMIT 20

Features:

  • βœ… LIMIT with any number
  • βœ… SKIP with any number
  • βœ… Combined SKIP + LIMIT for pagination
  • βœ… Works with ORDER BY

3. DISTINCT Keyword

Status: Implemented
Impact: Full deduplication support

MATCH (n:Node)-[:KNOWS]->(m)
RETURN DISTINCT n.name

Features:

  • βœ… RETURN DISTINCT
  • βœ… Deduplication of result rows
  • βœ… Works with aggregations

4. AS Aliasing in RETURN

Status: Implemented
Impact: Full aliasing support

MATCH (n:Node)
RETURN n.name AS personName, n.age AS personAge

5. Variable-length Paths

Status: Implemented

MATCH p=(a:Person)-[:KNOWS*1..3]->(b:Person) RETURN p

6. EXISTS Subqueries

Status: Implemented

MATCH (n:Person)
WHERE EXISTS { MATCH (n)-[:KNOWS]->(m) }
RETURN n

7. COUNT Subqueries

Status: Implemented

MATCH (n:Person)
RETURN n.name, COUNT { MATCH (n)-[:KNOWS]->(m) } AS cnt

8. Map Projections

Status: Implemented

MATCH (n:Person) RETURN n {.name, .age}

9. List Comprehensions

Status: Implemented

RETURN [x IN range(0,5) WHERE x % 2 = 0 | x*2] AS evens

10. WHERE after YIELD

Status: Implemented (6 passing tests)

CALL db.index.vector.queryNodes('idx', 10, $vector)
YIELD node, score
WHERE score > 0.8
RETURN node

-- Also works with CONTAINS, <>, =
CALL db.labels() YIELD label WHERE label CONTAINS 'Person'

Implemented November 26, 2025

11. CASE Expressions

Status: Complete
Files: pkg/cypher/case_expression.go (376 lines)

-- Searched CASE
MATCH (n:Person)
RETURN n.name,
  CASE
    WHEN n.age < 18 THEN 'minor'
    WHEN n.age < 65 THEN 'adult'
    ELSE 'senior'
  END AS ageGroup

-- Simple CASE
MATCH (n:Person)
RETURN CASE n.age
  WHEN 30 THEN 'thirty'
  WHEN 25 THEN 'twenty-five'
  ELSE 'other'
END AS ageLabel

Features Implemented:

  • βœ… Searched CASE with WHEN/THEN/ELSE
  • βœ… Simple CASE with value matching
  • βœ… NULL handling (IS NULL, IS NOT NULL)
  • βœ… Comparison operators (<, >, <=, >=, =, <>)
  • βœ… Nested expression evaluation
  • βœ… Multiple WHEN clauses
  • βœ… Optional ELSE clause (returns NULL if omitted)

12. shortestPath() / allShortestPaths()

Status: Complete (16 passing tests)
Files: pkg/cypher/shortest_path.go (372 lines), pkg/cypher/traversal.go (617 lines)

-- shortestPath with MATCH variable resolution
MATCH (start:Person {name: 'Alice'}), (end:Person {name: 'Carol'})
MATCH p = shortestPath((start)-[:KNOWS*]->(end))
RETURN p, length(p) AS pathLength

-- allShortestPaths
MATCH (start:Person {name: 'Alice'}), (end:Person {name: 'Carol'})
MATCH p = allShortestPaths((start)-[:KNOWS*]->(end))
RETURN p

-- Path functions
MATCH p = shortestPath((a)-[*]-(b))
RETURN nodes(p), relationships(p), length(p)

Features Implemented:

  • βœ… BFS shortest path algorithm (unweighted)
  • βœ… allShortestPaths() - finds all paths of minimum length
  • βœ… Variable resolution from MATCH clause (like Neo4j's LogicalVariable)
  • βœ… Direction support (outgoing ->, incoming <-, both -)
  • βœ… Relationship type filtering
  • βœ… Max hops limiting (*..max)
  • βœ… Path functions: nodes(p), relationships(p), length(p)
  • βœ… Cycle detection

Recent Fix: shortestPath now correctly resolves variable references (e.g., start, end) from the preceding MATCH clause, matching Neo4j's behavior where variables are "in scope" and referenced, not re-queried.

13. Transaction Atomicity

// Transaction support with full rollback
tx := engine.BeginTransaction()

// All operations are buffered
tx.CreateNode(&storage.Node{...})
tx.CreateEdge(&storage.Edge{...})
tx.UpdateNode(nodeID, &storage.Node{...})
tx.DeleteNode(nodeID)

// Atomic commit - all or nothing
err := tx.Commit()

// Or rollback to discard all changes
tx.Rollback()

Features Implemented:

  • βœ… BeginTransaction() - Start new transaction
  • βœ… Commit() - Atomically apply all buffered operations
  • βœ… Rollback() - Discard all buffered operations
  • βœ… CreateNode/UpdateNode/DeleteNode - Node operations in transaction
  • βœ… CreateEdge/DeleteEdge - Edge operations in transaction
  • βœ… GetNode() - Read-your-writes consistency
  • βœ… IsActive() - Check transaction status
  • βœ… Isolation - Uncommitted changes not visible to other operations
  • βœ… Atomicity - All operations succeed or all fail together

14. Composite Indexes

Status: Complete
Files: pkg/storage/schema.go

Features:

  • βœ… Multi-property indexes
  • βœ… SHA256-based composite keys
  • βœ… Efficient prefix lookups
  • βœ… Full and partial key matching
  • βœ… Neo4j-compatible behavior

15. MATCH...CREATE

Status: Complete
Files: pkg/cypher/create.go (427 lines)

-- Create relationship between existing matched nodes
MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})
CREATE (a)-[:KNOWS]->(b)

Key Feature: Like Neo4j, variables from MATCH are "in scope" - CREATE only creates what's NEW. If variables reference matched nodes, use those existing nodes (not create new ones).


16. EXPLAIN / PROFILE

Status: Complete (27 passing tests)
Files: pkg/cypher/explain.go (560 lines), pkg/cypher/explain_test.go

-- EXPLAIN - show execution plan without executing
EXPLAIN MATCH (n:Person) RETURN n
EXPLAIN MATCH (n:Person) WHERE n.age > 25 RETURN n ORDER BY n.name LIMIT 10

-- PROFILE - execute and show plan with statistics
PROFILE MATCH (n:Person) RETURN n
PROFILE MATCH (n:Person)-[:KNOWS]->(m) RETURN n, m

Features Implemented:

  • βœ… EXPLAIN mode (shows plan, doesn't execute)
  • βœ… PROFILE mode (executes and shows plan with stats)
  • βœ… Execution plan tree structure
  • βœ… Operator types: NodeByLabelScan, AllNodesScan, NodeIndexSeek, Filter, Expand, Sort, Limit, ProduceResults, etc.
  • βœ… Estimated rows per operator
  • βœ… DB hits estimation
  • βœ… Actual rows and timing (PROFILE only)
  • βœ… Visual plan formatting

Example Output:

+--------------------------------------------------------------+
| PROFILE Query Plan                                           |
+--------------------------------------------------------------+
| Total Time: 1.234ms                                          |
| Total Rows: 3                                                |
| Total DB Hits: 2006                                          |
+--------------------------------------------------------------+
| +- ProduceResults (Return results)                           |
| |   Est: 100, Actual: 3, Hits: 100                          |
|   +- NodeByLabelScan (Scan all :Person nodes)               |
|   |   Est: 1000, Actual: 3, Hits: 2000                      |
+--------------------------------------------------------------+

⏺️ Optional Features (Not Critical)

1. Multi-database Support 🟒 LOW PRIORITY

Status: NOT IMPLEMENTED
Impact: Single database only

-- Not supported
USE database2
CREATE DATABASE mydb
SHOW DATABASES

Estimated Effort: 1-2 weeks
Priority: LOW (most deployments use single database)


Implementation Status Summary

Feature Status Tests Coverage
CASE expressions Complete 7+ tests 376 lines
shortestPath() Complete 16 tests 372 lines
allShortestPaths() Complete 16 tests included
Transaction Atomicity Complete 12 tests 521 lines
WHERE after YIELD Complete 6 tests integrated
MATCH...CREATE Complete 16+ tests 427 lines
Composite Indexes Complete multiple integrated
EXPLAIN/PROFILE Complete 27 tests 560 lines

Test Coverage

Package Tests Coverage
pkg/cypher 863 tests 82%+
pkg/storage 308 tests 85.2%
Total 1,171 tests ~83%

Current Status Summary

Compatibility: 100% β€” production ready
Status: All critical features implemented
Deployment: Ready for production use

Complete Feature Set

Core Query (100%):

  • βœ… All 16 Cypher clauses implemented and tested
  • βœ… All result modifiers (ORDER BY, LIMIT, SKIP, DISTINCT, AS)
  • βœ… All pattern types (variable-length, bidirectional, multiple)
  • βœ… All subqueries (EXISTS, COUNT)
  • βœ… All collections (map projections, list/pattern comprehensions)
  • βœ… WHERE after YIELD filtering

Advanced Features (100%):

  • βœ… CASE expressions (searched and simple)
  • βœ… shortestPath() and allShortestPaths() with MATCH variable resolution
  • βœ… Variable-length path traversal
  • βœ… Composite indexes with prefix lookup
  • βœ… MATCH...CREATE with variable scoping (like Neo4j)

Transaction Support (100%):

  • βœ… BeginTransaction/Commit/Rollback
  • βœ… Atomic operations (all-or-nothing)
  • βœ… Read-your-writes consistency
  • βœ… Transaction isolation

Schema & Indexes (100%):

  • βœ… All constraint families: UNIQUE, EXISTS, NODE KEY, RELATIONSHIP KEY, property type
  • βœ… Constraints on both nodes and relationships with full write-path enforcement
  • βœ… NornicDB extensions: temporal no-overlap, domain/enum, cardinality, endpoint policy constraints
  • βœ… IF NOT EXISTS idempotent creation, owned backing indexes
  • βœ… Property indexes (single and composite)
  • βœ… Fulltext indexes (BM25 scoring)
  • βœ… Vector indexes (cosine/euclidean/dot similarity)

Functions (100%):

  • βœ… 52 scalar functions
  • βœ… 5 aggregation functions
  • βœ… 10 CALL procedures

⏺️ Optional (Not Required for Most Deployments)

Low Priority:

  • ⏺️ Multi-database - Not needed

Recent Changes (November 26, 2025)

shortestPath Variable Resolution Fix

Problem: shortestPath((start)-[:KNOWS*]->(end)) was not correctly resolving start and end variables from the preceding MATCH clause.

Solution: Implemented Neo4j-style variable resolution:

  1. Parse the first MATCH clause to extract variable bindings
  2. Resolve which nodePatternInfo each variable maps to
  3. Find actual nodes matching those patterns
  4. Use those specific nodes for shortestPath calculation

Reference: Neo4j uses LogicalVariable references in their query planner to bind variables from MATCH before using them in subsequent clauses.

Transaction Atomicity Implementation

Added: Full transaction support with:

  • Buffered operations (Write-Ahead Log pattern)
  • Atomic commit (all operations applied together)
  • Rollback support (discard all buffered changes)
  • Read-your-writes consistency
  • Transaction isolation

Last Updated: November 26, 2025 (Post EXPLAIN/PROFILE implementation)
Status: Production ready
Test Results: 1,171 tests passing