Release Notes — v1.4.1 (Draft)
Status: Draft for review. Not for publication yet. Scope: All changes between tag
v1.4.0(22b90d2d) and the upcomingv1.4.1— 97 commits onmainplus the Cypher 25 shared-grammar branch (assumed merged). Figures below are from commit inspection, not a packaged build.
Highlights
- Cypher 25 shared grammar (foundation slice).
LET,FILTER, andFORreading clauses execute in the existing shared pipeline, with an optionalCYPHER 5/CYPHER 25header. No separate executor, router, or text-rewrite fallback. - Correctly rounded math.
^/power()(andexp/log/log2/exp2/ hyperbolic functions) now return Neo4j's correctly rounded results, fixing the 1-ulp^divergence (#981). - A large Neo4j 5.26.30 compatibility sweep (#907) across operators, literals, functions, UNION, DELETE, temporal values, and MERGE.
- Lazy Bolt auto-commit reads stream rows as the driver
PULLs them (#939). - Storage and concurrency correctness for label scans, record errors, and concurrent unique-key MERGE (#961).
1. Cypher 25 — Shared Grammar Foundation
NornicDB previously rejected CYPHER 25. This release adds the first implemented slice of the Cypher 25 conformance plan.
What is included
LET— bindings with retained scope, including chainedLETclauses. Aggregate expressions are rejected.FILTER— a standalone reading clause filtering rows, with optionalWHERE.FOR x IN expr— iteration over a list, equivalent toUNWIND expr AS x(including scalar coercion:FOR x IN 1returns one row).- Optional language header —
CYPHER 5/CYPHER 25are accepted and discarded by both parsers; callers pass statements as written.
FOR x IN [1, 2, 3]
LET scaled = x * 10
FILTER scaled > 10
RETURN x, scaled ORDER BY xOne pipeline, two front ends
- The SRD parser (default) accepts
LET/FILTER/FORand correlated unscopedCALLwithout a header — a permissive, deterministic extension. - The ANTLR parser keeps the strict Cypher 5.26 contract: those additive clauses require
CYPHER 25, and implicit unscopedCALLimports remain rejected. The ANTLR grammar now also parses theCYPHER 5/CYPHER 25preamble directly.
Both front ends feed the same pipeline clause kinds and operators; ANTLR is a syntax front end, not a second execution path.
Rule enforcement
LET/FOR redeclaration is rejected (VariableAlreadyBound), non-boolean FILTER predicates fail as in Neo4j (including the list-to-boolean coercion error), aggregates are rejected in FILTER, and conflicting CYPHER 5 CYPHER 25 is an ArgumentError.
This is the foundation slice of the Cypher 25 conformance plan — not full 2026.09 conformance. The remaining inventory (SEARCH, graph types, typed VECTOR/UUID values, standalone ORDER BY after LET, and more) is tracked in the plan and stays open.
2. Correctly Rounded Math (#981)
Go's math package (fdlibm) is up to 1 ulp away from Java's Math for some inputs, so 3 ^ 2.5 returned 15.588457268119894 instead of Neo4j's 15.588457268119896.
- Ported musl's double-precision
exp,exp2,log,log2, andpow(Arm optimized-routines, MIT) and the six hyperbolic functions intopkg/math/libm. - The package mirrors the standard library
mathAPI, so everymathimport became an import alias tolibmwith no call-site changes. Sin/Cos/Tan/inverse-trig/Log10/Log1p/Expm1are fdlibm-derived identically in musl and Go, so they are re-exported unchanged.
3 ^ 2.5 and 9007199254740993 ^ 2.5 now match Neo4j exactly.
3. Neo4j 5.26.30 Compatibility Sweep (#907)
Dozens of correctness fixes from a differential sweep against neo4j:5.26.30-community. Notable groups:
- Operators —
AND/OR/XOR/NOTfollow Neo4j's rules through one implementation; unary plus;^static types. - Literals —
true,false,null,NaN, andInfinityare literals even where a variable has that name; numeric literals group digits with underscores;0X/0Oprefixes kept as a NornicDB extension; a NaN MERGE property fails. - UNION — matches columns by name;
UNIONworks insideEXISTS/COUNT/COLLECT; result-column checks. - DELETE & properties() — deleted entities read as Neo4j's empty entity;
properties()on a deleted relationship fails; map keys stay DELETE targets; a non-DETACH DELETE of a connected node is checked at commit. - Functions — argument counts and compile-time types from catalog signatures;
trimspecifier forms; temporal functions' run-time argument errors;isEmpty(null);point.withinBBoxon non-points;reducewith a literal accumulator. - CALL subqueries —
OPTIONAL CALLkeeps rows the call produces nothing for; an unscoped body's implicit imports are kept as a NornicDB extension; branch-local declarations. - SET / MERGE — SET applies items in order; MERGE takes repeated
ON CREATE/ON MATCHand multi-relationship paths through one action parser. - Aggregates — duration
sum/avg;sumswitches to float on overflow;avgis a running mean; equal-length durations order by months; aggregate value types. - Framing —
FINISHafterWITHandYIELD;finishas a valid variable name; a variable read anywhere in aRETURN/WITH/UNWINDexpression must be bound. - Ordering — maps, nodes, relationships, and paths order as Neo4j; tied rows and
keys()/labels()compared the way Neo4j defines them (#931).
Documented intentional differences
keys()returns property names in alphabetical order on every route, where Neo4j's order is unspecified and unstable (property-store layout / map hash order). Cypher defines no order forkeys(), and the openCypher TCK grades it ignoring list order (#994).- Retained extensions from the sweep remain documented rather than removed.
4. Bolt & Protocol
- Lazy auto-commit reads (#939): the server produces an auto-commit read's rows as the driver
PULLs them, instead of materializing the whole result up front. - An error on a row after the first 1,000 fails the
PULLthat reaches it (#939).
5. Storage & Concurrency
- Label scans and bounded scan tails fail on unreadable records; one-pass record errors are reported in visitation order.
- Concurrent
MERGEof a uniquely constrained key matches the creator's node (#961); a multi-MERGE UNIQUE race is retryable like single-node MERGE. - Embeddings follow the embedded text; storage hands callers their own node copies (#963, #965).
- Search results are keyed by a search-index revision so a rebuilt index invalidates cached results.
6. Search
db.index.vector.queryNodesreturns when the HNSW search reaches fewer thanknodes (#974).- A filtered search fills its page; an empty filter list matches nothing.
7. MCP
- Pin tools to a database through the URL path (
/mcp/{database}/…); the pinned database is injected and cannot be overridden by the payload. - The standalone
tasktool is folded intotasks(management arguments create/update/complete/delete; filter arguments list tasks).
8. Performance
- OPTIONAL MATCH / OPTIONAL CALL keyword checks without allocating.
- ORDER BY … LIMIT over a property index starts at the bound WHERE puts on the sort property.
- Reduced shared WITH LIMIT source overhead; plain RETURN keeps its captured result local to its row-source branch.
- Equivalence keys of single values are built in one allocation.
- A new allocation-ratchet gate compares fixed-count benchmark workloads against the baseline, so parsing stays allocation-free.
9. Infrastructure & Build
- Upgrade Debian runtime packages during Docker builds.
- Differential conformance now runs a fixed corpus against the pinned Neo4j reference and refreshes known differences as fixes land.
Upgrade Notes
- No breaking changes. Existing Cypher 5 statements and NornicDB extensions continue to work unchanged.
CYPHER 5/CYPHER 25headers are optional. The SRD parser accepts the new clauses without a header; the ANTLR parser requiresCYPHER 25for them.^/power()results may change in the last ulp, matching Neo4j instead of Go'smath.Pow.keys()remains alphabetical (see the documented difference above).