Changelog¶
All notable changes to Grafeo, for future reference (and enjoyment).
[0.6.3] - Unreleased¶
SPARQL transactions release: SPARQL in a transaction reads one snapshot, conflicting updates fail at commit and graph operations run inside transactions; the plan cache, the per-query memory limit and path searches are bounded. Results that change on upgrade: a statement over the per-query memory limit fails with GRAFEO-S001 in builds with disk spilling, a path search over its budget fails with GRAFEO-S001 instead of GRAFEO-Q006, and a standalone type that a graph type lists keeps applying in the default graph.
Added¶
Config::with_path_search_budget(#492): sets the bytes one variable-length, shortest-path or RDF property path search may hold, never above the memory limit.Config::with_plan_cache_limit(#674): bounds the plan cache in bytes, by a deep estimate of each plan. The default is a sixty-fourth of the memory limit, at least 4 MiB and at most 64 MiB; 0 turns the cache off.validate()names the nodes a listed type would refuse (#764): the warningLISTED_TYPE_ROWSgives, per node type and graph not bound to a graph type that lists it, the count and the first ids of the nodes that break the type, with the steps to make it a type of its own again. Run it after upgrading a file in which 0.6.2 took a standalone type over.
Changed¶
- The documentation has a new structure: its pages are grouped in seven sections, and every old link redirects to its new page.
- A path search over its budget fails with
GRAFEO-S001(#492): it names the path search budget and its setting, in GQL, Cypher and SPARQL; it wasGRAFEO-Q006. An RDF property path reads the same budget as the others, no longer narrowed to the memory free when it was planned. - The plan cache is bounded by bytes and reported deeply (#674): it held 1,000 plans whatever their size, and
memory_usage()counted only its map. Now one byte bound holds parsed and optimized plans, a plan larger than the bound runs uncached, a repeated statement shares the cached plan instead of copying it, andmemory_usage().cachesreports what the plans hold plusplan_cache_limit_bytes. - A standalone type a graph type lists stays in force (#764): a type made with
CREATE NODE TYPEorCREATE EDGE TYPEkeeps applying by its label in every graph once a graph type lists it, and also in that graph type's graphs, which reverses 0.6.2 for this case;DROP GRAPH TYPEleaves it,DROP NODE TYPEof it is refused while a graph type lists it, and a pattern may name it only as it is. Types a graph type declares itself, and files in which 0.6.2 already took a type over, keep the 0.6.2 rule. - Conflicting SPARQL triple updates fail at commit (#718): when two transactions write the same triple in one graph and one of them deletes it, the second to commit fails with
GRAFEO-T001and is rolled back; both committed before, so a delete could silently undo the other's insert. Two inserts of one triple both commit, and a transaction also conflicts with a bulk load (load_rdf_text,load_rdf_into,batch_insert_rdf) that commits while it is open; the load itself is never refused. - SPARQL graph operations in a transaction (#718):
CREATE,DROP,CLEAR,COPY,MOVEandADDapply at the commit in order with the transaction's triple updates, and its later statements see them; 0.6.2 refused any SPARQL statement after one withGRAFEO-T004. Graph operations on different graphs, or a graph operation and a triple update in one graph, no longer conflict; two transactions changing one graph still fail withGRAFEO-T001. - The per-query memory limit covers every operator that holds rows (#674):
Config::with_query_memory_limitnow caps what one statement's sorts, groupings, hash and nested-loop joins, distincts, set operations, top-k heaps, correlated subqueries, eager reads and stored procedure results hold together, their strings and lists included, where it covered sorts and groupings only. At the cap a GQL sort spills first; the others fail withGRAFEO-S001naming the limit, and the statement changes nothing. The limit holds in builds with disk spilling, such as the Python package, grafeo-server and the CLI; the defaultgrafeocrate,edgeand WebAssembly builds do not enforce it yet. - SPARQL reads at a viewing epoch are refused: with
set_viewing_epochset, a SPARQL query or SHACL validation fails withGRAFEO-Q004(RDF data keeps no history) instead of reading the present.
Deprecated¶
GrafeoDB::rdf_store(): writes through the RDF store bypass transactions and snapshot reads; use SPARQL updates in a session or transaction, orbatch_insert_rdf. Removed in 0.7.0.
Fixed¶
- A plan chosen on an empty graph replans after a bulk load (#674): a cached plan is made again once a count the planner reads (nodes, edges, a label's nodes, an edge type's edges) doubles or halves by at least 1,000 rows.
- A plan built before a schema change is no longer cached after it (#674): a statement planned while an index, constraint, type or graph changed runs once and is planned again next time; a schema change queued in an open transaction no longer drops other sessions' plans, and a rollback keeps them.
- The default path search budget is at most 256 MiB (#492): a quarter of the memory limit, so a 64 GiB limit let one search hold 16 GiB before it failed.
- SPARQL reads in a transaction are snapshot isolated (#718): every SPARQL query in a transaction reads the triples and named graphs as they were when the transaction began, with its own updates, at the same point as its GQL and Cypher reads; an auto-commit query reads one committed state for its whole run. It saw triples, graphs and clears that other sessions committed meanwhile.
- RDF in checkpoints, copies and exports is the epoch's (#718): a checkpoint,
save, a copy orexport_snapshotthat runs while SPARQL updates commit writes the RDF triples and named graphs as of its epoch, with commits going on while it writes.rdf_schema()lists the named graphs (it returned an empty list) and reads at one epoch. - The CLI opens every database the default
grafeobuild writes (#763):cargo install grafeo-cliand the pre-built binaries now have the same features, vector, text and hybrid search, change data capture and graph algorithms included, so a file with a vector or text index opens. A database holding data the build cannot read, such as RDF triples, is refused with thecargo install grafeo-cli --features ...line that adds the feature. - SQL/PGQ ranges on one-kind columns use the range scan again: a
<,<=,>,>=orBETWEENon a property whose values are all of the bound's kind reads its index by range and skips chunks by their stored bounds, as in 0.6.1. A column holding values of several kinds still scans and raises 22G04. - Concurrent sorts and groupings keep their memory consumers: each spillable sort and aggregate registers under a name of its own and unregisters only that one when it finishes or fails.
[0.6.2] - 2026-11-08¶
Correctness release: comparisons, nulls and operands of the wrong kind follow ISO GQL and openCypher more closely, a refused permission has its own code, a session can be kept from reading local files, and a graph cleared and written back keeps its size. Results that change on upgrade: a string never equals a number, {k: NULL} in a pattern finds nothing, an operator given a value of the wrong kind raises 22G12 where it answered null, and a file database checkpoints by itself (Changed lists each). Plus join orders from sampled degrees, cycles closed by intersection and lookups that need no index.
Performance¶
- Planner: comma patterns pick their join order from sampled degrees, cycles close by lookup or intersection, and same-type chains up to 6 hops count without listing paths. Common queries stay at their speed; the first plan of a comma pattern costs a little more.
- Pinned far nodes and
IN: a hop to a pinned node starts at that end when it costs less, a key on several nodes reads the shorter side, and a long constantINlist is a set. - Lookups with no index: an equality on a property reads its column directly, a
LIMITstops early, and reopened columns skip chunks by their stored bounds. - Typed graphs: a read no longer pays for a graph type's checks, which only a write needs.
- Constraints:
UNIQUEandNODE KEYbuild an index on their first property, so an insert under one no longer reads the whole label. - Weighted algorithms: Dijkstra, SSSP, A*, Bellman-Ford, Floyd-Warshall, Kruskal, Prim,
max_flowandmin_cost_max_flowread each edge's weight directly instead of loading the whole edge; Dijkstra takes about a third fewer instructions. - Smaller default builds: the
walfeature no longer pulls in tokio; onlyasync-storageandserverdo.
Changed¶
Queries that ran in 0.6.1 and now fail or behave differently.
CREATE INDEXholds its label only: writes on other labels no longer pay for it. A query on another label scans, with the same rows and a note inEXPLAIN; a query with no label reads the label indexes and scans only the rows they do not cover. Existing indexes stay as they are, andcreate_property_indexstill covers every label. A 0.6.1 release opens such a file with the same rows, without those indexes, and a checkpoint there drops them.- Closed graph types:
REMOVEfails withG2000when it would take a node's last label or the label that declares a property it holds, and so does removing a label an edge type's endpoints need, wherever that rule applies.import_tsv,import_tsv_strandimport_mmiofail the same way when the graph's rules refuse_Importednodes or the edge, as does dropping or replacing a property that nodes or edges still hold.DROP NODE TYPEandDROP EDGE TYPEare refused while a closed graph type bound to a graph lists the type. Files already holding such data still open. - Writes:
|,!,%and parentheses in the labels of anINSERTor a single-nodeMERGEare an error naming the statement, as they name no labels. - Rules: a rule added over data that breaks it fails with
GRAFEO-V001, and so does a commit whose writes break a rule added since the transaction began. - Graph types: a type a graph type lists applies only in graphs bound to it, so its rules and defaults no longer reach other graphs;
validate()reports affected types, and Python warns at open. Dropping a graph type drops the types only it listed, and a second, different declaration of a type is refused. See the upgrade note. - Transactions:
START TRANSACTION,COMMITandROLLBACKon a per-callexecuteare refused,CREATE GRAPH ... AS COPY OFis refused in a transaction, and no SPARQL statement runs after a SPARQL graph operation until the commit. - A string and a number are never equal:
'5' = 5is false and'5' <> 5true, in GQL and Cypher, also in indexes, joins,IN,MERGEand upserts;id(n) = '5'finds nothing. In GQL, a node compared with=to another kind of value is false instead of an error. - Lists and records containing a null can compare as unknown:
[null] = [null]and[1] = [null]drop the row in aWHERE, while a definite mismatch such as[1, 2] = [null, 3]is false. A value containing a null matches nothing in a join or a lookup.DISTINCT, grouping andUNIQUEstill treat nulls as one value. - Comparing values of two kinds with
<,<=,>and>=: GQL orders them as ORDER BY does (a string before every number, a boolean before every number), with or without an index; Cypher and Gremlin answer null; SQL/PGQ raises 22G04, as do its=,<>andIN, whichever plan reads them (an index or key-column read returned rows where the scan raised). - Wrong kinds: an operator or list index given an operand of a kind it does not take (
true + 1,-'a',[1, 2, 3][1.5]) raises invalid value type (22G12) instead of answering null; a sign on a non-number reports 22G12 rather than 22G03. A null operand still gives null. +of a string and another kind in GQL raises invalid value type (22G12) in either order, for any non-null value that is not a string ('x' + 3gave'x3','is:' + truegave'is:true').- Procedure arguments: a value outside what an argument allows raises invalid parameter value (22023), in CALL (the load procedures included) and in the Python
db.algorithmsmethods;grafeo.pagerank(2.0), a negativemax_iterations, a NaN tolerance or a negativekran with meaningless results or clamped values (in Python,pagerank(damping=2.0)returned scores like 39,602,142,754). A missing required argument, or null for it, raises 22023 too, naming the argument and its type (it raised 22000). In the Python algorithm methods,Nonefor an optional argument means its default, andNonefor a required one raisesGrafeoError22023 instead ofTypeError. The load procedures take their arguments in the map or the positional form (path, thenlabels, ortype,sourceandtarget), raise 22023 for a delimiter that is not one character, an unknown column type, a wrong key insourceortarget(an unknown key was ignored) or aprojection(it was ignored), and 22G03 for an argument of the wrong type. - Edge weights: weighted algorithms refuse an edge value they cannot use and name the edge and property: a non-number raises 22G12 (it read as 1.0); NaN, an infinity, or a negative weight in Dijkstra, A*, SSSP or a flow capacity raises 22023.
- Procedure timeouts: graph algorithm, search, schema and CSV load procedures, and the Python algorithm methods, stop at the query timeout with the usual timeout error, never a partial answer; a run used to hold its thread until it ended.
grafeo.max_flowandgrafeo.min_cost_max_flowrows: one row per edge that carries flow, so parallel edges each get a row; they gave one row per node pair.grafeo.topological_sorton a graph with a cycle raises a data exception (22000) naming one cycle, such as3 -> 5 -> 8 -> 3; it returned a row in an undocumentederrorcolumn. The Python method still returnsNonefor a cycle.- Minimum spanning trees of equal weight:
grafeo.kruskalandgrafeo.primreturn the one with the lowest edge ids (it depended on the order edges were read). - A WAL with an empty segment, which a crash in 0.6.1 could leave while starting one, is refused at open with an error naming
wal_salvage: reopen it once withwal_salvage, which sets the empty file aside and loses no commit (#725). - Incremental backups that cannot continue their chain fail with
GRAFEO-S005, naming the cause: no backup cursor, a chain behind the cursor, or log that was removed. Some of these raised an invalid-input error. A new full backup starts a new chain (#673). - Automatic checkpoints are on: a file database checkpoints by itself once its WAL holds 256 MiB past the last checkpoint, on a background thread that starts when it first has work; turn it off with
checkpoint_wal_size=None(Python),checkpointWalSize: null(Node.js) orConfig::without_checkpoint_wal_size(). The WAL's segment size is a setting (wal_segment_size, 1 MiB to 1 GiB, 64 MiB by default), and a checkpoint size below 1 MiB is refused. - The default memory limit follows the process's real ceiling (#751): 75% of the smaller of the machine's memory and any cgroup (Linux, at the process's cgroup or a parent) or job-object (Windows) limit. Windows and macOS now read their memory instead of assuming 1 GiB; WASM keeps 768 MiB.
Config::with_memory_limitis enforced (#751): every write that grows the store (a node, edge or value write in GQL, Cypher, the direct API, a bulk load or a CALL, an RDF triple in each form, a property, text or vector index build) is granted against the limit, with an eviction pass that spills what it can first, and one that passes it is refused withGRAFEO-S001; reads that fit their query-memory budget still run. A database file above its limit opens with a warning (MEMORY_LIMIT_PASSED) and replays regardless of it, so its first write is refused. Under pressure a file database spills its older row groups' indexed embedding columns and keeps the newest's; a write into a spilled column needs no room for it.- A per-query memory limit,
Config::with_query_memory_limit(#751): it caps what one statement's operators hold, defaults to the memory limit and never exceeds it; a sort or grouping past it spills first and, where it cannot spill, fails withGRAFEO-S001naming the limit and changes nothing.memory_usage()reports it asquery_limit_bytes, in every binding. Config::with_memory_fractionapplies per open: it is taken of the memory detected when the database opens, and a database file no longer stores its bytes;with_memory_limitis stored as before.file:URLs follow RFC 8089: inLOAD CSV,LOAD DATAand the load procedures,file:///srv/data.csvandfile://localhost/srv/data.csvname the absolute path/srv/data.csv(file:///was stripped and the rest read as a relative path), and afile:URL with another host is refused.- Permission refusals have their own codes: GQLSTATUS
42501, withGRAFEO-Q007for a write, DDL or writing procedure that the session's role or graph grants refuse andGRAFEO-Q008for a statement that reaches outside the database on a session without external access; both wereGRAFEO-Q002with42000. Python raisesGrafeoPermissionError, a subclass ofGrafeoError. memory_usage()reports what the store holds: its store, index, version and string-pool figures are the counted bytes, kept as each write grows them, instead of estimates.- A negative
SKIPorLIMITafter a GQLCALLraises 22G02, as in every otherRETURN; 0.6.1 ignored the count. TierOverride::ForceDiskis refused on sections that cannot spill: onlyVectorStorecan move to disk, soForceDiskon another section (TextIndex,PropertyIndex,LpgStore,RdfRing,CompactStoreand the rest) now failsConfig::validate, where it was accepted and did nothing.- Temporal literal cleanup: a
LOCAL DATETIMEproperty refusesDATETIME '...'with an offset, and a schema error names the value as the query writes it (DATETIME '...', offset kept).
Added¶
- Graph procedures:
CALL grafeo.ktruss()andgrafeo.ktruss(k),grafeo.total_triangles()andgrafeo.subgraph_isomorphism(pattern_edges, pattern_nodes), and the Python methodsktruss(k=None)andsubgraph_isomorphism_count(pattern_edges, pattern_nodes). - Query warnings: a statement that uses a deprecated form or has something worth knowing returns its rows with warnings, each with a GQLSTATUS code (class 01), a kind and a message. They are on
QueryResult::warningsand on streaming results, in every binding (Python also raises them throughwarnings.warn), and the CLI prints them on stderr. Experimental. - SQL/PGQ
||concatenates strings and lists. - Procedures in every build:
CALL grafeo.procedures()and the CSV load procedures are listed in every build, with each argument's type, default and allowed values where it declares them. A call of a procedure this build leaves out fails withGRAFEO-Q004, naming the feature that brings it, instead of as an unknown name. - Sessions without external access:
Identity::with_external_access(false)refuses every statement that reads or writes outside the database, such as the CSV loads, before it runs. - The memory limit and spill settings at open, from every binding (#751): C (
GrafeoOptionswithgrafeo_open_with_options), Node.js, Python, Go, C# and Dart take the settingsConfigtakes, and WASM takesDatabaseOptions.memoryLimitandhistoryRetention;memory_usage()reads them back in each binding (C reports them and the open's warnings as JSON,MEMORY_LIMIT_PASSEDamong them), an open past the limit warns and still reads, and a snapshot import takes them too (Database.importSnapshot(data, options)in WASM,GrafeoDB::import_snapshot_with_configin Rust). - Sessions in the bindings:
db.session(read_only=..., external_access=...)in Python,db.session({ readOnly, externalAccess })in Node.js andgrafeo_session_openin C run queries with those limits;external_access=Falserefuses every statement that reads or writes outside the database. - The procedure listing says what each procedure does:
CALL grafeo.procedures()lists whether it writes the graph (writes, a BOOLEAN) and what it reaches outside the database (external_access:'none','reads'or'writes'). grafeo.pagerankwarns (01Y01) when it runs out of iterations before its scores settle withintolerance.- WAL retention limit: after a backup, checkpoints keep the WAL the next incremental backup needs, up to
Config::with_wal_retention_limit(1 GiB by default; Pythonwal_retention_limit, NodewalRetentionLimit). Past the limit the oldest log is removed with a warning, and the next incremental backup fails withGRAFEO-S005.release_backup_cursor()(Rust, Python, Node, C, C#) stops keeping log for backups (#673).
Deprecated¶
ZONED DATETIME '...'andZONED TIME '...'literals: writeDATETIME '...'orTIME '...'with an offset. They warn with 01X01 and 01X03, and 0.7.0 refuses them.+on two lists in GQL and SQL/PGQ: use||. It warns with 01X02, and 0.7.0 refuses it.
Fixed¶
- Label expressions on a path's first node:
IS,&,|,!,%and parentheses were ignored there, soMATCH (x IS A)-[:R]->(t)also matched nodes withoutA, and aSET,REMOVEorDELETEafter it changed them. All GQL and SQL/PGQ path patterns now check them (#736). - Label expressions in writes:
INSERT (n IS Person),INSERT (n:A&B)andMERGE (n IS Person)created a node with no labels;IS AandA & Bnow set those labels. Databases written before this may hold such nodes, and nodes changed or deleted by the query above, which no fix can recover: find unlabelled nodes withMATCH (n) WHERE size(labels(n)) = 0 RETURN n. UNIQUErefuses0.0and-0.0as duplicates whether or not the property has an index.UNIQUE,NODE KEYandMERGEtreat1,1.0e0and the decimal1.0, and[1]and[1.0], as one value, andUNIQUEandNODE KEYcount every NaN, of either sign, as one value.- SPARQL joins: fixed a slowdown in multi-way joins from 0.6.1.
- SPARQL parsing: a
.after aFILTER,OPTIONAL,MINUS,GRAPHor nested group parses again (#737). - SPARQL subqueries and paths: a sub-select inside
GRAPH ?gbinds?g(#739),BIND(EXISTS {...} AS ?v)evaluates (#740), andEXISTSandNOT EXISTSread each row's values, as the SPARQL 1.2 draft says.?s :p? :owith a constant object returns:oand the subjects that reach it, each once (it returned every subject of:p, repeated), a subquery with an aggregate gives all its rows, and nested groups keep their rows. - SPARQL graphs: inside
GRAPH ?g { ... },?gis bound only after the group, as SPARQL says: aFILTERon?g,?gused as a term, and a nestedGRAPH ?ginsideEXISTS,NOT EXISTS,OPTIONALorMINUSno longer see it bound early, which gave missing or extra rows.GRAPH <iri>over a graph the dataset does not hold gives no row (#738),FROMover two graphs returns a shared triple once, andFROMwithoutFROM NAMEDexposes no named graphs. A variable repeated within one triple pattern holds the same term in each position (?x :r ?x,?g ?p ?ginsideGRAPH ?g, any pairing of subject, predicate, object and graph, also insideOPTIONAL); every matching triple was returned, andCOUNT(*)counted them all. - Booleans:
<,<=,>and>=order false below true in every plan; they could return null and lose rows. - Comparing lists:
<,<=,>and>=on two lists or two paths compare element by element in GQL and Cypher;[1] < [1]is false (it was null). - Lists: GQL
||concatenates lists, and Cypher+appends or prepends a single value to a list, a null included ([1, 2] + 3,[1, 2] + null); both gave null before. +with a string in Cypher: a number and a string join in either order (1 + 'a'is'1a', written astoString()writes it; it was null).- Grouping by a parameter: a grouping key built from a parameter (
RETURN $p + 1 AS x, count(*),$m.k,$list[0],size($list)) no longer fails with an undefined variable. - Schema statements roll back with their transaction, also across a crash (#727).
- Conditions that can fail, such as
100 / n.age > 0, no longer fail on rows a label or an earlier condition rejects. - Graph type patterns: naming an existing type no longer wipes its properties, and several patterns of one edge type no longer lose all but the last.
- Closed graph types in a schema: a node or edge type added with
ALTER GRAPH TYPE ... ADDis held like one listed byCREATE GRAPH TYPE, soDROP NODE TYPEandDROP EDGE TYPErefuse it while listed, andALTER GRAPH TYPE ... DROPtakes it out, in files written by 0.6.1 too. - Backups: a restore checks the full backup's checksum, as it does an incremental backup's, and refuses a damaged file by name before it writes anything (#744); incremental backups and restores of a large WAL stream the log a segment and a frame at a time instead of reading it into memory (#673).
- A null optional procedure argument, however computed, takes the parameter's default instead of failing.
grafeo.min_cost_max_flowgives each parallel edge its own capacity and cost (two edges costing 1 and 5 reported 50 where 30 is right), and refuses a negative-cost cycle the source reaches (22023), where the call never returned.grafeo.kruskalandgrafeo.primtake the lightest of the edges between two nodes, in either direction, andgrafeo.primfrom a start node returns the whole tree; Kruskal kept the first edge it met and Prim missed a lighter edge pointing back, both reporting a heavier tree, and Prim followed only outgoing edges.grafeo.topological_sortgives the same order on every run (the lexicographically smallest by node id).- GQL statements after
CALL ... YIELD: a statement that starts with a procedure call goes on with any clause,MATCH,OPTIONAL MATCH,FILTER,LET,FOR,ORDER BY, anotherCALL,SETorINSERT, as ISO GQL allows (14.3); onlyRETURNwas accepted, and a followingMATCHwas a syntax error. - GQL
NEXTthenCALL: a procedure call afterNEXTkept the earlier statement's columns, grouping the result by them; it now returns only its own columns, and the incoming fields stay in scope. SKIPandLIMITafter a GQLCALL:SKIP $nandLIMIT $nin theRETURNafter a procedure call take effect; they were ignored.HAVINGandFINISHafter a GQLCALLare accepted; they were a syntax error.- SQL/PGQ
CALL ... LIMIT $ntakes its limit; it was ignored. - Stored procedures run in builds without the graph algorithms (the
lpgandedgeprofiles, the CLI):CREATE PROCEDUREsucceeded there and itsCALLsaid the procedure was unknown. - A null in a property map (
MATCH (n {k: NULL}), also on an edge, in GQL, Cypher and SQL/PGQ) finds nothing, as an equality with null does; it found the elements without the key. A null in aMERGEpattern still matches an absent property. - A
DATETIMEliteral keeps its offset:DATETIME '2024-01-01T10:00:00+01:00'is a ZONED DATETIME at+01:00andZis+00:00, as theZONED DATETIME '...'deprecation names it; 0.6.1 gave the UTC time without the offset. Without an offset it stays a local datetime. - SQL/PGQ
EXPLAINreturns the plan; it ran the statement and returned its rows, in Python'sexplain_sqltoo. INTERSECTandEXCEPTinsideCALL { }: the columns they return are visible after the call, as withUNIONandOTHERWISE; reading them raised "Undefined variable".- A
MERGEafter anOTHERWISEbody inCALL { }uses the node the body returns; it raised "Variable already declared outside the CALL subquery". YIELD: a column a procedure does not have is refused when the call is planned, naming its columns; it failed only after the algorithm had run (4.5 s on 4,000 nodes).- WAL segments: a panic while the WAL started a new segment no longer makes every later checkpoint fail until a reopen (#725).
- Checkpoints check free disk space first and fail with a clear error naming the space needed and available, changing nothing; a checkpoint that fails partway gives back the disk space it took (#726).
- Go and C# bindings: a database, transaction, graph, stream or point-in-time view could be freed by the garbage collector while a call on it was still running, which could crash the process (for example on the last
Nextof a stream in Go); every native call now keeps its handle alive. In C#, an async call on a database disposed meanwhile throwsObjectDisposedException. - WASM
importRdfis in every build with the RDF store, thefullpackage (@grafeo-db/wasm) included; it was missing there. - Vector search with a filter on a quantized index could return fewer than
kresults, or none, when the allowed nodes lay far from the query; it now searches among the allowed nodes. - Opening and closing without writes leaves the file unchanged:
close()checkpoints only when something changed since the last checkpoint, so a database opened only for reads keeps its file byte for byte (its header was rewritten on every close). - The C header declares the backup functions:
grafeo_backup_full,grafeo_backup_incremental,grafeo_restore_to_epochandgrafeo_clear_plan_cache, exported since earlier releases, are now declared ingrafeo.h. - A crashed query's spill files are removed at open: the next read-write open deletes the
<file>.spill/query_*directories a crash left, with the vector cache, before it measures the spill volume. - A graph cleared and written back keeps its size: the slots of deleted rows (edge ends, property values, adjacency offsets) go back once a whole vector of 2,048 rows below the live ones is gone, in a session and across reopens. In 0.6.1 each clear and rewrite of 2,400 nodes and 4,800 edges added about 500 KB within a session.
Security¶
- Queries can be kept from reading local files: a session without external access refuses
LOAD CSV,LOAD DATA, the CSV load procedures and SPARQLLOADbefore any file is opened; through a server, give remote sessions none.
Contributors¶
[0.6.1] - 2026-11-01¶
Correctness and speed release. Backups and restores survive a crash, stored procedure bodies run as their statements do, SPARQL follows SPARQL 1.1 in OPTIONAL, MINUS, BIND and FILTER EXISTS, GQL quantified patterns repeat as ISO GQL defines them, the query timeout stops every query, and dates, times, bytes and NaN keep their type through every binding. Plus a CSV and an RDF file loader, schema() with value types, and faster SPARQL lookups, Cypher multi-hop patterns, degree counts and bulk updates by key.
Changed¶
Queries and calls that ran in 0.6.0 and now fail or return another type (each fix is under Fixed). Results that change because a wrong answer was fixed are under Fixed only; every one in the differential test corpus is listed, with its reason, in scripts/difftest/reviewed/0.6.1.txt.
- Casts:
CASTof text that is no number fails with22018, to aDECIMAL(p, s)that is too small with22003, and a float outside the integer range, an infinity or NaN to an integer with22003; they gave null, the largest integer or 0.toIntegerandtoFloatof text that is no number still give null. - GQL patterns: a quantified pattern of more than one edge without an upper bound or over 16 repetitions fails, an unbounded walk around a cycle fails at the path budget instead of stopping at 100 hops, and
{3,1},*3..1or a quantifier both inside and after an edge's brackets is a syntax error. - SPARQL: an undeclared prefix other than
rdf,rdfs,xsd,owlandfn, a variable or quoted triple inINSERT DATAorDELETE DATA, andBINDto a variable in scope are syntax errors; N-Triples,INSERT DATAandbatch_insert_rdfrefuse a literal subject or a predicate that is not an IRI. - Indexes:
CREATE INDEXwith a name another index has fails (IF NOT EXISTSkeeps the existing one). A file with a named vector index cannot be opened by 0.6.0. - Transactions:
SERIALIZABLEis refused withGRAFEO-Q004(it ran as snapshot isolation);START TRANSACTION,COMMITandROLLBACKthrough a transaction object'sexecutefail with25000; in C#,Rollback()after a commit throws. - Files and backups: a file already open fails with
GRAFEO-S004(08000) and a restore without its manifest, cursor or full backup withGRAFEO-V001; an incremental backup into another directory, a snapshot whose catalog does not apply and a parameter nested over 128 levels are refused;hybrid_searchwithout an index it can read fails. - Calls and config: a procedure call with an unknown map key or an extra positional argument fails naming the parameters;
TierOverride::ForceDiskfor a section that cannot move to disk is refused byConfig::validate. - Bindings: Python
node["k"]andnode.get("k")return native values. In Go, C# and Dart a DATE, TIME, DURATION or ZONED DATETIME result is a typed value, and in Node.js its tag object ({ $time: '12:30:00+02:00' }); they were strings or maps. A C#DateTimeOffsetparameter is a ZONED DATETIME and a Gotime.Durationa DURATION.
WASM: faster (and larger)¶
- Built for speed: queries run about 1.6 times and inserts about 4.8 times as fast as in 0.6.0;
@grafeo-db/wasm-litegrows from about 940 KB to 1.1 MB gzipped and@grafeo-db/wasmfrom 1.6 to 1.7 MB. - The full package uses the lightweight regex engine, as lite does, about 260 KB smaller;
=~and SPARQLREGEXrun about as fast as in 0.6.0.
Added¶
schema()describes the data's shape: each label and edge type with its properties and their value types, and each edge type with the labels it connects, in Rust and every binding.- A native CSV loader:
load_nodes_csvandload_edges_csv(Python, Rust, sessions) and thegrafeo.load_nodes_csvandgrafeo.load_edges_csvprocedures load a typed CSV file in one transaction, checking its keys, about 2.7 times as fast as batch inserts. Experimental. - Loading RDF files and text:
load_rdf(path)andload_rdf_text(text, format)in Python and Rust load Turtle or N-Triples into the default or a named graph as one bulk write, about three times as fast as a batch insert in 0.6.0. - Query timeouts in Python and Node.js:
query_timeoutandexecute(..., timeout=)in Python,queryTimeoutin Node.js. - Errors say what to do in every binding: Python raises
GrafeoLockedErrorfor a database open elsewhere, Node.js errors carryerrorCode,gqlStatusandisRetryable, WASM errors their GQLSTATUS, and a Node.jsTransactionrolls back when disposed. - SQL/PGQ graph patterns are GQL's: label expressions, an element's own
WHERE, quantifiers,ANY SHORTESTandALL SHORTEST, path modes and path variables withPATH_LENGTH,NODESandEDGESinsideGRAPH_TABLE. A shortest path over more than one edge pattern is not supported yet. - SPARQL looks triples up from bound terms: a pattern, an
OPTIONAL, an alternative path or aUNIONon a bound subject or object finds its triples through the indexes, a group starts at its most selective pattern, and a property path from bound terms searches from them. LDBC IS1 on RDF runs about 7 times and IS4 about 24 times as fast, IC13 in under a millisecond (it took 6 s), and?s (v:KNOWS|^v:KNOWS) ?ffrom a bound?sin about 0.04 ms (it took 80 ms). - SPARQL keeps its statistics across small writes: a query after a write or a load no longer reads the whole store (an
INSERT DATAand a query: 1.4 ms, was 178 ms), and the RDF store's indexes are keyed by term ids, for 27 to 31% less memory (#659). - Faster queries: multi-hop patterns stream (
LIMIT 10over two hops in under a millisecond and no memory, was 0.4 s and 440 MB), Cypher patterns over several relationships are 2 to 4 times as fast, degree counts read the adjacency (about 3.5 times as fast grouped, 2 to 2.8 times without grouping), property reads, filters and grouping 1.1 to 1.3 times, and a shortest path between two nodes stops sooner (LDBC IC13 about 0.25 ms per pair, was 6.3).
Deriva¶
Changes for Deriva, which keeps its graph in an embedded Grafeo database through the Python binding.
- Bulk updates and reads:
update_nodes(labels, rows, key="id")updates existing nodes by key in one call (about 6 ms for 1,500 rows, where the same query took 500 ms; through an index on the key, 0.25 ms for 40 rows), andget_properties_batch(ids, keys)reads several properties of many nodes, also in Rust. grafeo[arrow]: the extra that installs pyarrow fornodes_to_arrow(),edges_to_arrow()andto_arrow().- Updates and merges by key without an index join once:
UNWIND $rows AS r MATCH (n:Method {id: r.id}) SET ...is about 60 times as fast, andMERGE(withupsert_nodes) of 1,500 rows takes about 20 ms (it took 5 s). - Faster label queries: grouping by
labels(n)about 27 times as fast,count(*)of a label from the store's count, anORof labels reads only those labels (about 2.5 times), an edge check of one type stops at the first edge (about 200 times on busy nodes), an index lookup looks past label checks (about 45 times), and a lookup by an indexed key on two labels checks both in the lookup. EXISTSruns once over the rows it checks, also insideORandNOT(about 3 times as fast), and anOPTIONAL MATCHfrom bound nodes reads only their edges (LDBC IC13 under a millisecond, where it took over 30 s).- Whole nodes from Python queries cost less: about 1.4 times
get_nodes_by_label, was 1.8 times.
Fixed¶
Every query result in the differential test corpus that differs from 0.6.0 is listed, with its reason, in scripts/difftest/reviewed/0.6.1.txt.
- Backups and restores survive a crash (#667, #677): a restore is on disk before it returns, so commits after it no longer replay onto the old data, and a failed one can be retried; a backup is durable before the log it covers is let go; backups run one at a time; an incremental backup with nothing new succeeds, and a broken chain fails with the new
GRAFEO-S005. - A restore replaces the catalog, and a restore that fails changes nothing.
- Transactions (#670): a failed commit ends the transaction in every binding; rolled-back writes no longer block others; a direct call that fails partway leaves nothing; checkpoints and copies keep raw store writes; time travel never sees a commit in progress, and inside a transaction checks the retained history.
- A write above a join reads the data as the statement started (#677), so it no longer changes a row twice or matches what it just created.
- Vector and text indexes, searches and edge-list imports follow the selected graph (#677).
- Migrating a 0.5.x file keeps its indexes, and compares the new file with the old database before replacing anything.
- No more aborts:
PROFILEof a count over a multi-hop pattern and of a SPARQLDELETE WHEREwithout variables, a SPARQL syntax error that looped until memory ran out, a parameter nested over 128 levels, thousands of conditions on a path or Gremlin branches (#668), a two-hop pattern through a node with over 65,536 paths, and values that wrapped around (the largest id, a WAL record over 4 GiB, an RDF ring over 2^32 terms). - GQL quantified patterns (#678): a
WHEREinside holds for each step, a segment of several edges repeats within its bounds, an unbounded walk is no longer cut at 100 hops,{,2}starts at 0, and(n:%)matches only labeled nodes. OPTIONAL MATCHandEXISTS: anOPTIONAL MATCHkeeps its rows when itsWHEREholds a subquery, and anEXISTSwith a condition no longer runs over nodes the query never asked about.- Procedures (#678):
OPTIONAL CALLkeeps the row with nulls; a stored procedure returns as many values as it declares, and aYIELDin another order picks by name; procedure columns follow the arguments; a body runs with the schema and constraint checks of its statements,USE GRAPHwith its grant checks, and a limit of 16 nested calls. - Expressions: aggregates inside other expressions and
LETvariables inside lists, maps andCASE(#539), severalLETdefinitions in order, negative list slices,IS TYPED LIST<NODE>,size()andtoList()of a vector (#585),all_differentandsameby value,Infinityin text, a float%by zero is NaN, and Cyphersum()of no values is 0. - Numbers:
toInteger(' 42 ')is 42 andtoInteger('4.2')4, a float to an integer no longer wraps around, and decimalsumandavgno longer depend on the order of the rows, nor do products and remainders that fit fail (#669). - Ordering and grouping: a TIMESTAMP and a ZONED DATETIME of one instant order and group together in GQL and SQL/PGQ,
GROUP BYwithoutORDER BYgives its groups in first-row order on every run and every database, also when they spill, and aggregates over a chain of edges read the variable they name. - Syntax: GQL
//comments, non-reserved words and double-quoted names as names, a laterINSERTcreating its new nodes, doubled backticks in Cypher names (#579), and compound types in Cypher DDL (#678). - Cypher and Gremlin:
shortestPathfrom a node to itself is no path, a variable-length relationship whose upper bound is below its lower bound (*3..1) matches nothing, as in openCypher, undirected variable-length counts no longer fail on the path budget, and Gremlincoalesce()andoptional()choose a branch per traverser (#668). - SQL/PGQ:
NODES(p)andEDGES(p)return nodes and edges (#678),ORDER BYan outer alias, parameters named after keywords, andLIMIT $nandOFFSET $n, with errors that name their clause. - A subquery that returns an edge in one part and a node in another returns each as what it is.
- The query timeout stops every query, also sorts, aggregates, joins and large
UNWINDs;range()near the largest integer stops at its end. - Vector search: the most similar rows above a threshold come first, a similarity filter gives the same rows with and without the index, embeddings written as lists are indexed (#587), and typed arrays are vectors in Node.js and WASM (#586).
- Indexes (#678): a name stands for one index, vector indexes keep their name, a text index drops as itself, names with a colon no longer collide, a multi-property index drops whole,
CREATE VECTOR INDEXtakesIF NOT EXISTS, andSHOW INDEXESandlist_indexes()also list the indexes made through the API. - Graph types: a node type takes every occurrence's properties, a graph typed by another schema's type is checked against it,
DROP GRAPHforgets its type,LIKEan untyped graph is the open type, andSHOW NODE TYPESwrites DDL. - Introspection:
schema()anddb.labels()list only what is in use, stored procedure errors say "Procedure", and EXPLAIN and PROFILE show the whole plan as it runs, with each operator's numbers on its own line, also with subqueries and index pushdowns. - SPARQL 1.1 (#701, #702, #703): a
FILTERinsideOPTIONALreads the whole solution, a group that opens withOPTIONALorMINUSstarts from one empty solution,FILTER EXISTSandNOT EXISTSonly filter,BINDto a variable in scope is refused, an unbound variable joins with what follows, andGRAPH ?gover a pattern without?ggives a row per named graph. - SPARQL
DELETE WHEREdeletes only what a match of its whole pattern holds: without a match it no longer deletes its triples without variables, and every triple of a match goes, not only the first. - SPARQL syntax: the standard prefixes are predefined,
[]is a blank node,VALUESafter the query andORDER BYa function call parse,EXPLAIN,EXPLAIN ANALYZEandPROFILEwork after the prologue, IRIs with\uescapes match, alternative paths with an inverse branch run,xsd:floatstays a float, andLOADsays it is not supported. N-Triples and Turtle read a dot right after a blank node or language tag and\u,\U,\band\fescapes. - Durations with a negative fraction of a second:
DURATION 'PT-1.5S'is minus one and a half seconds (it read as -0.5 s), and a duration of mixed signs is writtenP1MT-1.5S(it wasP1MT-1.-5S) and reads back in every binding; a duration part past 64 bits or a signed time offset (12:00+-1:00) is refused instead of wrapping around. - Dates, times and durations keep their GQL type both ways in Go, C#, Dart and Node.js: Go
time.Timeand the newgrafeo.Date,TimeandDuration; C# microseconds,DateOnly,TimeOnly,DateTimeOffset, and the newGrafeoTimeandGrafeoDuration; Dart's newGrafeoDate,GrafeoTime,GrafeoDurationandGrafeoZonedDateTime; Node.js the tag objects. - Bytes, NaN and infinities travel as
$bytesand$floattags in the C, Go, C# and Dart bindings and arrive as native values; Go decodes tags in nodes, edges, streams and AsOf reads, and its streams give numbers asjson.Number. - Parquet:
LOAD DATA FORMAT PARQUETreads DATE, TIMESTAMP, TIME and DECIMAL columns as their types, the Python wheel loads Parquet (uncompressed or Snappy), and another codec is named in the error. - Bindings: Node.js ES module imports name every export, Go frees finished transactions, Dart
rollback()after a failed commit does nothing, and WASM hasdetachDeleteNode(#678). - Data: decimal weights in graph algorithms (#678) and CSV imports with quoted line breaks (#678).
- Smaller fixes: a write conflict says so once; a procedure argument that fails keeps its GQLSTATUS; a damaged incremental backup is
GRAFEO-S002;PreparedCommit::commitreturns its own epoch; CDC events of named-graph triples name the graph;list_graphsis sorted; a JSON parameter with a tag beside other keys is a map; builds withtriple-storealone compile. - Docs: SPARQL reads in a transaction are not yet snapshot isolated (#718, planned for 0.6.2).
Deprecated¶
GrafeoDB::store(): its writes skip the write-ahead log, transactions and change capture. Write through a session or the direct API, and read throughsnapshot().- The
async-storagefeature, withGrafeoDB::async_wal_checkpointandasync_write_snapshot, which only wrapped a blocking call: callwal_checkpoint()fromtokio::task::spawn_blocking. From 0.7.0 theenterpriseprofile is metrics and tracing. Config::with_threads,with_query_logging,with_schema_constraints,without_schema_constraintsand their getters, which never had an effect.RdfStore::copy_graph: use SPARQLCOPYin a session, which is logged.CatalogError::InvalidCheck: nothing returns it; CHECK constraints come back later as DDL.
All are removed in 0.7.0.
Contributors¶
- Thanks to teipsum for reporting the SPARQL
OPTIONAL,MINUSandBINDbugs (#701, #702, #703), each with its measurements and the section of the spec. - Thanks to temporaryfix for decimal weights in the graph algorithms (#681), the test of a failed RDF commit (#683), the refusal of an incremental backup with a gap (#684), crash-safe backups (#685) and restores (#687, #689), backups that run one at a time (#690), time-travel reads that never see a commit in progress (#686), claims released after a rollback (#693) and time travel inside a transaction that checks retained history (#695), CSV fields with line breaks (#692), aggregates inside other expressions (#682),
OPTIONAL MATCHpatterns that read the rows before them (#698),OPTIONAL CALLof a procedure (#699), stored procedure results that fit their columns (#700), index lists with the indexes made through the API (#704), decimal products and remainders that fit (#691), test fixtures that build with any feature set (#694), direct calls that leave nothing when they fail, with raw store writes that checkpoints keep (#696), SQL/PGQ path nodes and edges (#705), compound types in Cypher type DDL (#706), anddetachDeleteNodein WASM (#708).
[0.6.0] - 2026-10-23¶
Storage release: a new single-file format on a new storage engine, with online checkpoints and backups, snapshot isolation on every read path and time travel over a retained history. Plus exact decimals, a store contract for backends of your own, binding parity across C, Go, C#, Dart, Node.js and WASM, and a large round of correctness fixes in GQL, Cypher, SQL/PGQ and SPARQL.
0.6.0-beta.1 (2026-10-11) was a preview of this release; this section covers everything since 0.5.44.
Breaking release. As a minor release before 1.0, 0.6.0 breaks things (see Versioning). Before you upgrade:
- Database files are migrated on first open: the first read-write open converts a 0.5.x database to the new format and keeps the old files as
.pre-0.6. 0.5.x cannot open the result, so stop every 0.5.x process first; 0.7.0 will not read 0.5.x files. Backups and snapshots have new versions: take a full backup after the upgrade. - Minimum Rust version 1.99.0 (was 1.91.1).
- Some query results change: a GQL or SQL/PGQ number with a point is an exact decimal, overflow and division by zero are errors, GQL sorts nulls last, SPARQL returns RDF terms, and some statements that gave a wrong answer now fail. See the Breaking entries under Changed, and Result changes.
- Rust API: a new store contract (
ReadView,GraphBackend,Snapshot, the row-group store in place ofLpgStore), direct reads that returnResult, and#[non_exhaustive]configuration and result types. - Bindings: a read that fails is an error, never an empty answer; some C, Go, C#, Dart and Python signatures change, and Python RDF terms no longer equal plain strings.
- CLI: standard output holds only results, and
--quiethides only status lines. - Deprecations: what 0.6.0 replaces is deprecated and removed in 0.7.0 (see Deprecated).
Upgrading to 0.6 lists every breaking change and what to change.
Added¶
- Time travel: a database can keep its history for a retention window (
Config::with_history_retention:HistoryRetention::Duration(..)orForever; off by default and free when off), stored in the file and kept through checkpoints, crashes and reopens.GrafeoDB::as_of(point)gives a read-onlyAsOfViewas of an epoch, a time, a GQLZONED DATETIMEorTimePoint::ago(duration), withexecute_as_ofandepoch_as_of; a point older than the retained history fails with "history before ... is not retained". Python, Node.js, WASM, C, C#, Go and Dart set the retention at open and haveas_of,execute_as_ofandepoch_as_of; a point is never guessed from a bare number (Python: anintepoch, an awaredatetimeor atimedelta; Node.js and WASM: abigint, aDateor{ ago: milliseconds }; C:GRAFEO_POINT_*; C#, Go and Dart:TimePoint). - Exact decimals (#610): up to 38 digits with their scale (
1.50), exact in arithmetic, comparison, grouping and aggregates. GQLCAST(x AS DECIMAL(10, 2))andDEC,toDecimal(x[, precision, scale])in GQL, Cypher and SQL/PGQ,DECIMAL(p, s)property types in type DDL, and SQL/PGQCAST(x AS type)for every SQL type it knows, withDECIMAL,NUMERIC,DATEandTIMEcolumns inCREATE PROPERTY GRAPH. Python reads and writesdecimal.Decimal, Node.js and WASM{"$decimal": "1.50"}, C#, Dart and Go a decimal type of their own, and the Arrow exportDecimal128. - Backends of your own (#416):
GrafeoDB::with_store(backend, Config::persistent(path))runs the database's transactions in aGraphBackendof your own, with rollbacks, savepoints and change data capture, and is durable through its own WAL,<path>.wal, replayed after a crash from the epoch the backend reports durable.grafeo::storeholds the contract, and theconformancefeature publishes the test suite the built-in store passes (conformance_suite!(MyStore, make, GAPS)); thegrafeocrate's tests hold a complete example backend. Implementing the contract is experimental through 0.6.x. - API stability levels and an upgrade guide (#493): every public API is Stable unless its documentation marks it
# Stability: Experimental, in Rust and in the binding docs that wrap it; API Stability lists them and CI keeps both in step. Upgrading to 0.6 lists every breaking change from 0.5.x. - The GQLSTATUS of every error (#639, #643): Python
GrafeoError.gql_status, Node.js and WASMgqlStatus(TypeScriptGrafeoError), Cgrafeo_last_error_gql_status(), Go's new*grafeo.Error(still matchingErrDatabase), C#GrafeoException.GqlStatusand DartGrafeoException.gqlStatusgive the ISO GQL status (22003,22012,42001, ...) next to the Grafeo code. The Node.js package's entry module is nowgrafeo.js(typesgrafeo.d.ts). - New error codes:
GRAFEO-S002for a damaged file or WAL, naming the file and offset (PythonGrafeoCorruptionError, RustError::Corruption, which replacesStorageError::Corruption);GRAFEO-T008after a commit that panicked midway, until the database is reopened (#412);GRAFEO-T009andGRAFEO-T010for a commit whose WAL write failed (see Fixed); andGRAFEO-Q004for what the build or database cannot run. - GQL
USE g <statement>(grafeo-web#55): a query or write afterUSE [GRAPH] gruns on graph g, in a transaction or outside, and leaves the session's graph as it is; each operand of aUNIONorNEXTchain may repeat it. - Query language additions: GQL
=~regular expression match (a Grafeo extension, see Conformance); GQLGROUP BYon aRETURNalias, with items computed from the keys; CypherRETURN *, r.years AS y; Gremlin anonymous traversals__.(#631); SQL/PGQDROP PROPERTY GRAPH; and the numbers.5,5.and1_000. - Binding parity (#441): C gets node and edge batches with properties, batch vector search, write counters, change history, upserts with their summary and graph handles (
grafeo_graph(db, name)), and Go, C# and Dart wrap all of it. Node.js gets graph handles (db.graph(name)). WASM gets writecounters, batch vector search and, in thelpgandfullbuilds, node and edge batches, upserts anddb.graph(name);executeRawWithParamsandexecuteRawWithLanguageAndParams(#574); andexecutionTimeMson raw results. - A node's edges without a query (#402):
get_node_edges(node, direction, edge_types)returns(neighbor, edge)pairs, in Rust, Python, Node.js, C (typed id arrays), Go, C#, Dart and every WASM build.detach_delete_node(id)(#465) removes a node and its edges as one change in every binding; change data capture marks each removed edge withdeleted_with. - Automatic checkpoints by WAL size and interval (#426):
set_checkpoint_triggers(Rust, Python, Node.js, C, Go, C#, Dart) andConfig::with_checkpoint_wal_size, off by default.checkpoint_status()reports the triggers, the last checkpoint and failed automatic checkpoints with the last error; a failure is logged and retried. - Query metadata (#461, #464):
PROFILEresults hold the operator tree as data (QueryResult::profile,result.profile, WASMprofile; experimental), andPROFILEnames an operator a fast path absorbed instead of listing it with 0 rows.rows_scannednow counts the rows read from storage, and every query, in every language, reportsexecution_time_ms.QueryResult::kind(grafeo::ResultKind) says whether a result holds rows, a boolean (SPARQL ASK) or a graph (CONSTRUCT).EXPLAINnames the index a MERGE uses (#288). - Graph algorithms on projections and named graphs (#566):
projection=indb.algorithms, aprojectionargument for algorithm procedures, anddb.graph(name).algorithms. Undirected PageRank (directed=False,{directed: false}), andkey=to key per-node results by a node property, which also makes PageRank, Louvain and label propagation independent of load order (see Determinism). - Search filters and text index options (#397, #351):
hybrid_searchandtext_searchtake the property filters ofvector_search, applied before fusion so a scoped search returns up tokresults. A text index takes BM25k1andb, a tokenizer (simple,standard,cjk_bigram) and stop words, through every binding andCREATE INDEX ... USING TEXT {...}, kept with the index. ZONED DATETIME,LOCAL DATETIMEand typed lists as property types (#569), in every form of type DDL, checked on write and kept after a reopen.- Rust API additions:
Session::statement_writes(whether a statement writes, without running it),grafeo::import::import_nodes(CSV or JSON Lines, behind the Python, Node.js and CLI imports),GrafeoDB::set_stored_config,Configgetters for every setting and the builderswith_wal(),without_wal(),without_schema_constraints(),with_cdc_retention(),with_encryption()andwith_wal_salvage(). Thegrafeocrate exportsStorageFormatandCdcRetentionConfig, and its newencryptionfeatureEncryptionConfig,KeyChain,PasswordKeyProviderandrestore_to_epoch_withfor encrypted backups. The first two calls are experimental. - C library release archives with every feature (#636):
grafeo-c-<tag>-<target>-defaultand-allarchives with the library,grafeo.hand aFEATURES.txt;allexports every query language. Thegrafeo-<tag>-<target>archives holdgrafeo.htoo, andchecksums.txtlists every archive. - Python 3.15 is tested and listed as supported; the existing wheels install on it.
Changed¶
- Breaking: a new single-file format, migrated on first open (#555): every database is one file, whatever its extension, with its WAL in
<path>.walwhile it is open. The first read-write open of a 0.5.x.grafeofile migrates it and keeps it as<file>.pre-0.6(WAL<file>.pre-0.6.wal); a 0.5.x WAL directory becomes a file at the same path, with its indexes (#401), and is kept as<path>.pre-0.6/. Read-only opens leave a 0.5.x database as it is. A migrated vector index gets the default parameters (cosine distance), with a warning: create it again if it used others. Properties 0.5.x stored as null, counter values among them, are gone after the migration. The directory must be writable, and builds without thewalfeature refuse a 0.5.x database whose WAL holds changes. Opening a directory that is not a 0.5.x database fails, an empty one included, andsave()fails on an existing path. See Upgrading from 0.5. - A new storage engine: the graphs live in a row-group store (#432), written in chunks of at most 65,536 rows and 1 MiB (#555), so checkpoints and opens never hold a whole section in memory (see Memory) and a node can have any number of labels. The catalog is stored as typed records (#517), the WAL is a chain of checksummed segments (encrypted per segment for an encrypted database), RDF triples are stored as term ids, and a file records its format revision, which a build that does not know it refuses. Vector and text indexes are built from the data when a database opens, so an open with large indexes takes longer.
- Upgrading from 0.6.0-beta.1: a beta WAL that still holds commits is replayed and checkpointed on the first read-write open. A beta file with RDF triples is rewritten to the term-id format and the old file and WAL are kept as
<path>.pre-0.6.0(the beta cannot open the rewritten file; a read-only open reads it as it is). Beta snapshots and incremental backups still restore. - A database file keeps the configuration it was created with (#551): the graph model, backward edges, schema constraints, WAL and sync mode, memory limit, tier overrides and an in-directory spill path apply to every open that does not set them. The graph model and backward edges are fixed at creation: an open that sets another value fails. Change the stored values with
GrafeoDB::set_stored_config. - Checkpoints no longer hold writers off, and write only what changed (#430, #449): commits wait only while a checkpoint takes its snapshot and writes the catalog and RDF data, and it writes only the changed chunks of the graph (after one value set in 70,000 nodes, two chunks).
- Backups and snapshots:
backup_full()runs online from a snapshot, without a checkpoint first (#362), and so dosave(),to_memory()andexport_snapshot();save()andbackup_full()write to<target>.partialand rename it when complete. Incremental backups copy WAL segments (backup file version 2): take a full backup after the upgrade. Breaking: the backup manifest and cursor are JSON, which 0.5.x cannot read. Snapshots are version 5 and keep the whole catalog (constraint and index names, index options, graph types, schemas, procedures) (#551); version 4 snapshots still import. - Breaking (Rust):
compact()returns aCompactReportand no longer builds a separate store: it checkpoints a persistent database, drops the versions no reader needs and encodes the property columns in memory, which reads keep using without decoding. Transactions, indexes and named graphs work the same before and after it, and it no longer needs thecompact-storefeature.layered_store(),compact_tiered(), thecompact_tieredmodule andadmin::CompactionStatsare removed. A database compacted by 0.5.44 or older opens with its compacted data folded in (see Compact Store). - Commits are pipelined: commits of several sessions that wait for the disk in
Syncmode share one sync, and a transaction that starts while another commit is in progress reads the last published commit instead of waiting; a write to what that commit wrote fails with the retryableGRAFEO-T001. Change data capture timestamps events in commit order. - Writes outside a transaction are transactions of their own (#536): every write statement, direct call and batch call with no transaction open commits on its own and leaves nothing when it fails, also with auto-commit off; one made while a transaction is open runs beside it and conflicts (
GRAFEO-T001) with what that transaction changed first.DROP GRAPHand the SPARQLCLEAR,DROP,COPY,MOVEandADDfail withGRAFEO-T001while an open transaction has changes in the graph. The planner's statistics count committed data only. - Reads of an earlier epoch read the retained history:
execute_at_epoch,execute_as_of,Session::set_viewing_epochand the*_at_epochreads see the values, labels, nodes and edges of then, in every build, and fail for an epoch older than what the database keeps (without a retention window, any epoch before the last write). Breaking (Rust, Python):get_node_at_epoch,get_edge_at_epochandget_node_property_at_epochreturn aResult,Ok(None)for absent and an error (PythonGrafeoError) for an epoch not retained. - Breaking: a GQL or SQL/PGQ number with a point is an exact decimal (#610):
0.1 + 0.2 = 0.3holds and1.50keeps its digits.1.5e0and the suffixesFandDmake a float,Ma decimal. Decimal with integer gives a decimal, with float a float;sqrt,sinand the other float functions return a float. Write1.0e0 / 0.0e0for an infinity. Cypher1.5stays a float. - Breaking: integer and decimal overflow and division by zero fail the query (#639): in GQL, Cypher and SQL/PGQ they fail with GQLSTATUS
22003or22012(codeGRAFEO-Q006), where they gave null; a sum adds in 128 bits and fails only when the final value does not fit. Float operations keep their IEEE results, null stays null, andANDandORskip their right side when the left decides, soWHERE n.v <> 0 AND 10 / n.v > 1is safe. - Breaking: SPARQL numbers follow the XPath type promotion of SPARQL 1.1 (#610):
1.5is an exact decimal,5 / 2is2.5(it was2), and integer arithmetic past 64 bits goes on as decimals. Computed numbers are literals in canonical form, the XPath casts (xsd:decimal(...),xsd:integerand the others) now convert, and comparisons with NaN are false. - Breaking: SPARQL results hold RDF terms (#464): each IRI, blank node and literal comes back as the exact term with its lexical form, language tag and datatype, where it was a string or a number (Rust
Value::RdfTerm; Pythongrafeo.IRI,LiteralandBlankNode, subclasses ofstr; RDF/JS terms in Node.js and WASM;RdfTermin C#, Go and Dart; a$termtag in the C library's JSON). ASK gives one boolean. GraphQL over RDF still gives plain values, and the Arrow, pandas and polars exports give a literal of a known XSD type as its native value. Breaking (Python): the term classes compare and hash as RDF terms, as rdflib's do, soIRI("http://example.org/a") != "http://example.org/a"; compare withstr(term). - Breaking: new limits and refusals: a GQL
WHEREorFILTERright afterINSERT,CREATE,MERGEorDELETEis an error (it filtered before the write); a statement nests at most 64 levels and its plan at most 128 (#573), where longAND,ORandUNIONchains andINSERTlists do not count; type DDL refuses a property type Grafeo does not support (INT32, a typo), which declaredANY; property values nest at most 128 levels (a 0.5.x database that holds a deeper one fails to migrate: change it with 0.5.x first); andSESSION SET TIME ZONEtakes a UTC offset only ('+01:00','Z'), failing with 22009 for a named zone, which was stored and never used. RustSession::set_time_zonereturns aResult. - Breaking: GQL sorts nulls last by default (#571), ascending and descending; Cypher keeps the openCypher order.
- Error codes say what kind of mistake it is (#588): a mistake in the query text is
GRAFEO-Q002, what the build or database cannot runGRAFEO-Q004, a value or name a call gives that does not fitGRAFEO-V001, a statement that fails on its dataGRAFEO-Q006, a direct write to a missing node or edgeGRAFEO-V002orGRAFEO-V003, andGRAFEO-X001now means a bug in Grafeo. Every query error message starts with its code.QueryResult::gql_statusis00001for a statement without a result. - Faster planning around indexes and labels (#456, #457, #455, #540, #546, #463): an indexed
INinside anANDchain uses the index, a label inWHERE(or anORof labels) scans only that label, a multi-label pattern scans its smallest label, a laterMATCHjoined by property values uses a hash join (300 files and 3,000 type definitions: 303 ms to 3 ms), function calls resolve once per query,schema()counts labels without building nodes, andDISTINCTover a variable-length pattern finds each reached node once. Edge upserts pinned byid(),WHERE id(tgt) IN $idson an edge target and aWHEREafterOPTIONAL MATCH ... WITHno longer scan every node.EXPLAINshows what is used. - Faster execution: a repeated statement without parameters skips parsing, a statement with parameters is planned once and refilled per call (as prepared statements are), filters and projections read only the property they need (2.5 to 3 times faster than 0.5.44 over many rows), label scans check 64 rows' visibility at once, edge reads skip the other end, SPARQL joins,
DISTINCTandCONSTRUCTrun on term ids (20 to 66% faster), conditions on variable-length edges prune the search as it walks, and a file database no longer creates a spill directory per statement (#565). - Direct single writes are slower than in 0.5.44:
create_node,set_node_propertyandcreate_edgetake about 0.1 to 0.2 microseconds more per call, as each commits like a transaction. Batch calls and upserts commit once and are the fast path. - Breaking (Rust): the store contract (#416, #432):
GrafeoDB::store()returns the row-group store (Result<Arc<RowGroupStore>>, experimental, the unsupported error on a backend of your own), andLpgStore,LpgStoreSection,PropertyStorage,NodeRecord,EdgeRecord,GraphStoreMut,ExternalTarget,UndoSupport,WriteTargetandgraph_store_mut()are removed: write through sessions or the direct API.GraphStorehas one family of reads, each taking aReadViewand returning aResult(GraphStoreSearchis merged in), and graph algorithms take a store and a view.GrafeoDB::with_storetakes anArc<dyn GraphBackend>.snapshot(),GraphHandle::snapshot(),AsOfView::snapshot()andprojection(name)give a pinnedSnapshot, replacinggraph_store()andselected_graph_store(), and algorithms read through one.iter_nodes()anditer_edges()return aResult. The upgrade guide has before and after tables. - Breaking (Rust and every binding): a read the store cannot do is an error, never an absent answer (#416): the direct reads, counts and reports of
GrafeoDB,SessionandAsOfView(get_node,node_count,info,schema,validateand the rest) return aResult, a direct read of a dropped graph fails, and a store without history answers history reads with the unsupported error. Python raisesGrafeoError, Node.js and WASM throw. C:grafeo_node_countandgrafeo_edge_countwrite to a newsize_t*,grafeo_get_node_labelsandgrafeo_as_of_get_node_propertyto a newchar**, theget_nodeandget_edgecalls returnGRAFEO_OKwith NULL for an absent one, andgrafeo_inforeturns NULL with the error. Go:NodeCount()andEdgeCount()return(int, error)and an absent node or edge isErrNotFound. C#NodeCountandEdgeCountthrow on a failed read andGrafeoGraph.GetNodeandGetEdgereturn null; DartgetNode,getEdgeandgetNodeLabelsreturn null for an absent one. - Breaking (Rust): other API changes:
grafeo::Erroris 32 bytes with boxed payloads (build variants withError::fromorError::type_mismatch);Configand the option and result types are#[non_exhaustive](build them with their constructors andwith_*methods, name fields in patterns with..);drop_graph,create_property_index, the index drops,batch_insert_rdfandcreate_projectionreturnResult(in C the drop and projection calls return -1, the projection calls asint32_t);hybrid_searchandtext_searchtakefilters(passNone);GrafeoDB::wal()andGrafeoDB::file_manager()return the new WAL writer and file manager; andConfig::wal_flush_interval_ms,GrafeoDB::adaptive_config(),admin::DumpMetadataand the Block-STM metrics (block_stm_*, also on grafeo-server's/metrics) are removed. - Rust implementation crates:
QueryProcessor::with_graphandTransactionState::Committingare new,QueryError::semanticandQueryError::unsupportedbuild errors andErrorisClone,Term::from_ntriplesreturnsResult<Term, TermParseError>, the engine'swalfeature enablesgrafeo-file, and the deprecatedTieredStoretrait is removed. - Breaking (bindings): Python
db.algorithms.kcore()returns{"core_numbers": {...}, "max_core": n}(#563); GoDropVectorIndexreturns(bool, error); and the Arrow export (Pythonto_arrow(),nodes_df()and the polars calls, Node.jstoArrowIpc(), the CLI's Arrow dump) keeps lists as lists, maps as structs and durations as structs ofmonths,daysandnanos, where they were text. - Breaking (CLI): standard output holds only results: progress and status lines go to standard error and
--quiethides only those, so-qno longer hides query rows. With--format jsonevery command prints JSON. The commands that only read (info,stats,schema,validate,data dump,index list,index stats,wal status, a query that does not write) open the database read-only, so they no longer migrate a 0.5.x file. - Python: long-running calls release the GIL: queries, commits, algorithms, searches, batch writes, imports, exports, backups, checkpoints and
close()let other threads run, so several threads can query one database. Acommit()orrollback()while another thread runs a query of the same transaction raisesRuntimeError. embed: ONNX Runtime bindings 2.0.0-rc.14: Grafeo builds the process's one ONNX Runtime environment when it loads its first model; an application that builds its own first cannot load Grafeo embedding models in that process.- Dependencies:
rustls-webpki0.103.16 andwebpki-root-certs1.0.10 for RUSTSEC-2026-0382 and RUSTSEC-2026-0383 (reached only by the model download of the opt-inembedfeature),crc32fast1.5.3,unicode-normalization0.1.26, and the Python bindings on PyO3 0.30 (no API change).
Deprecated¶
All of these are removed in 0.7.0.
- Cargo features that enable nothing:
temporal(history is the retention setting, in every build, and the calls it gated are always there),compact-store(every build that opens files reads a compacted 0.5.x database),temporalandtiered-storageofgrafeo-coreandgrafeo-common, andlpgofgrafeo-core,grafeo-adapters,grafeo-engineandgrafeo-cli(every build has the property graph store; thelpgprofile of thegrafeocrate and the bindings stays). Python'sgrafeo.features()no longer lists them. - Rust: the public fields of
Config: read a setting with its getter and set it with its builder; a field written directly does not count as set, so a database file's stored value wins over it. - Rust:
Session::set_auto_commitandSession::auto_commit(#536): a write outside a transaction always commits on its own; group writes withbegin_transaction. - Single-file and adaptive leftovers:
StorageFormat::SingleFileandWalDirectory, the C, Go and Dartgrafeo_open_single_file,OpenSingleFileandGrafeoDB.openSingleFile(use the plain open), andConfig::adaptive,with_adaptive,without_adaptiveandAdaptiveConfig, which never had an effect. recompact(), which callscompact(), and RustBackupCursor::log_sequence(usewal_lsn).- Rust (
grafeo-core):InvertedIndex::with_tokenizer(usewith_optionswith aTokenizerKind), and the per-transaction methods ofRdfStore(insert_in_transaction,commit_transactionand the rest): run updates in a session transaction.
Fixed¶
- A commit whose WAL write fails is no longer reported as committed (#498): it is rolled back and fails with the retryable
GRAFEO-T009, or withGRAFEO-T010(outcome unknown) when the write landed but could not be synced. Before, it was logged as a warning and lost on the next restart. - Recovery stops at damage in the WAL (#425): an open that finds damage before the end of the log fails with
GRAFEO-S002, naming the file and offset, and changes nothing, where 0.5.x skipped past it and built a state that never existed. A torn tail is still cut.Config::with_wal_salvage()(Pythonwal_salvage=True, Node.jswalSalvage: true) replays the commits before the damage and keeps the rest in<file>.wal.damaged. - Commits that were lost on a crash or reopen: a write after
close()now fails withGRAFEO-T007(PythonDatabaseClosedError) instead of being accepted and lost; a read-only open replays what a crashed writer left in the WAL (a build withoutwalrefuses such a file); an open with the WAL turned off no longer drops a leftover WAL, and a new database no longer replays one from an earlier database at its path; imports and RDF batch inserts are durable once they return; SPARQLCOPY,MOVE,ADDandCLEAR ALLreplay as written; and indexes (#401), schema details (defaults, parent types, edge endpoints, KEY labels, ALTER additions) and graph drops survive a crash. - Files and backups: databases larger than 4 GiB work (#392) (0.5.42 and older wrote them corrupt); ids of deleted nodes and edges are never given out again after a reopen; epochs continue after a reopen and after the migration; timestamps keep their sub-millisecond part and counter values survive a checkpoint; a damaged file is refused instead of aborting the process; incremental backups work across a restart (#267); backups of a read-only database hold the state it shows and a restore replays every incremental backup after its full one (#362);
grafeo backup restore --forcereplaces the target only once the restore is complete;open_in_memory()no longer opens its source read-write; and the same data exports to the same snapshot bytes. - Data that 0.5.x silently dropped: encryption at rest did nothing, and now encrypts the file and its WAL (an open without the right key fails; encryption with a spill path,
TierOverride::ForceDiskor no path fails, and a migrated database's.pre-0.6copy stays unencrypted); spilled vector embeddings were not in the database file (#594) (see Spilled embeddings); and a build withouttriple-store,vector-indexortext-indexopened a database without that data and dropped it at the next checkpoint, and now refuses it (#544). - 0.5.x's compacted store is gone, with its bugs (#542, #558, #596): writes after
compact()are no longer lost, ids stay unique and every read sees the whole database. A node with several labels in a compacted file matches each of them again (#595). Whatcompact()of 0.5.40 to 0.5.44 lost stays lost (nodes without labels, indexes, typed lists and maps, deleted items that come back): see Compact Store. - Rust:
GrafeoDB::opencompiles with the default features (#379); withoutwal, commits reach the file at a checkpoint. - Snapshot isolation holds on every read path (#412): other sessions and direct reads no longer see an open transaction's values, labels or deletes, a transaction reads one state throughout (filters, index lookups, label scans, searches), and checkpoints,
close(),save(), backups,to_memory()andexport_snapshot()write the committed state while a transaction is open. Graph algorithms and the Python reads (get_nodes_by_label,as_networkx()) see committed data only, and a write during a commit no longer lands in the middle of it (#548). - Constraints and transactions: a
UNIQUE,PRIMARY KEYorNODE KEYvalue holds between concurrent transactions (the second writer getsGRAFEO-T001), and a transaction may set such a key to the value its own node holds. An edge to a node deleted in the same statement or concurrently is refused. A refused schema statement, a failed stored procedure that writes, and a SPARQL or GraphQL update that fails part way (#414) change nothing; a stored procedure that writes is refused where writes are. Labels set on a node created in the same transaction are kept, a rollback to a savepoint restores labels, a rollback no longer deadlocks with a delete, read-only databases refuse SPARQL updates, imports and snapshot restores, Pythonimport_dfand WASMimportRowsandimportLpgimport all or nothing, andgc()collects every graph. - A GQL
DELETEof a variable nothing binds deleted every node:DETACH DELETE nwithout aMATCHnow fails with "Undefined variable 'n'". A plainDELETEof a node with incoming edges in a database without backward edges now fails in release builds too. - Clauses after a write read the whole write (#455): a
RETURN,WITH,WHERE, aggregate,MATCH,OPTIONAL MATCH,MERGEor subquery after a write in the same statement read only what earlier rows wrote; they now read after the whole write, as ISO GQL and openCypher define a statement. ALIMITorFINISHno longer cuts the write short, a write takes values from nodes created earlier in the statement,FOREACHand unitCALLsubqueries keep the number of rows, and a statement that ends with a write returns no rows and no columns (#580). - WHERE, OPTIONAL MATCH and subquery scope (#455, #545): each condition is checked where every value it reads is bound; the
WHEREof anOPTIONAL MATCHis part of its pattern, so every row stays (filter withWITH * WHEREor GQLFILTER, which now filters every row); a later comma-separatedMATCHpart goes on from a variable bound before it; a vector or text search after aMATCHkeeps its rows;CALLsubqueries keep their imported variables after aWITH; anUNWINDafterWITH ... WHEREunwinds the rows kept; a condition without variables (WHERE $p = 1) no longer fails with "Empty plan"; and an index lookup at a past epoch no longer matches the values of now. - GQL statements in any order the standard allows (#483):
ORDER BY,OFFSETandLIMITbefore the finalRETURN,WHEREandFILTERbetweenMATCHstatements, writes afterWITHand after each other,LETandFILTERfirst, and a finalCALLthat writes. Also fixed:NEXTafter a write,SELECT ... FROM g,HAVINGright afterGROUP BY, backticked names inSETandREMOVE(they kept their backticks), doubled quotes in strings,collect_list,HOME_SCHEMAandHOME_GRAPH, andSESSION RESETto the session's home. - MERGE follows openCypher: a pattern is matched or created as a whole, a node the pattern does not connect is not reused, an anonymous node merges,
ON CREATE SET n:Labeladds the label, CypherSET n = {...}replaces the properties, and Cypher and GQLMERGEand CypherCREATEstored a left-pointing relationship backwards (find and reverse such relationships 0.5.x wrote). - Cypher follows openCypher:
<-[:T]->and<-->match either direction, a relationship without a type or a direction cannot be created,INandSTARTS WITHbind tighter than comparisons, a relationship is matched at most once perMATCH, chained comparisons (a < b <= c) work,exists((n)-->())tests the pattern, pattern comprehensions read the row,:A|:Bparses, a clause afterRETURNis an error, aCALLafter other clauses runs per row, an inlineWHEREon a variable-length relationship holds per edge, andCREATE CONSTRAINTandCREATE INDEXwork without a name and takeUSING TEXT,VECTORorBTREE(#344). - Paths (#590, #591, #572, #318): a path variable holds every node and edge of a multi-edge pattern;
DIFFERENT EDGESworks andTRAIL,ACYCLICandSIMPLEhold for the whole path; shortest-path searches bind their path and edge variables and apply the edge's conditions;MATCH ANYkeeps a path per pair of endpoints andSHORTEST kkeeps k; path modes after the path variable parse; aWHEREon a quantified edge holds per edge; horizontal aggregates (sum(e.w)over-[e]->{1,3}) work; andlength(p)reads a passed-on path. - Variable-length patterns end and stay in memory: a pattern on a graph with cycles could abort the process allocating tens of GB; rows now stream, and a search that would exceed a quarter of
memory_limitfails with advice. A path of 100,000 edges no longer overflows the stack, unbounded patterns no longer stop aftermin + 100hops, SPARQL+and*no longer stop after 50, and an undirected pattern takes a self-loop once.-[:A:B]->and a created edge with:A|Bare errors. - Very large statements run (#573): an
ANDorORof 10,000 conditions, anINSERTof 10,000 patterns or aUNIONof 10,000 queries overflowed the stack, aborting native builds and breaking the WASM module for the rest of the page; they now run in every query language. - Aggregates and grouping (#589, #602): values beside an aggregate with a computed key no longer turn into
0; a spilled group keeps aggregating; aggregates skip nulls; grouping andDISTINCTtreat3and3.0as one value and keep float keys as floats;minandmaxorder mixed types asORDER BYdoes;DISTINCTreaches the statistical aggregates; two-argument aggregates read their second argument;listaggandgroup_concattake their separator;HAVINGcomputes its own aggregates;RETURN count(*)withoutMATCHworks; and an aggregate in aWHERE, a wrong number of arguments or an item that mixes an aggregate with a non-key is an error, as are theGROUP BYmistakes, each with its reason. - ORDER BY: GQL and Cypher
ORDER BYread what openCypher's does (the input of a plainWITH, the columns of an aggregating one), the GQLORDER BYstatement also puts nulls last, and withDISTINCTonly returned values can be sorted on. - Values keep what they are: nodes and edges inside lists, maps and paths stay nodes and edges,
startNode(r)andendNode(r)return nodes, list and map literals keep a missing value as null, a property named_id,_labelsor the like no longer replaces the identity (#254), a list, map or bytes parameter returned as is no longer comes back as''(#574), a variable named like an internal one keeps its value,UNWINDkeeps its list variable,size()counts characters, andid,elementIdandpropertiesread any expression. - Functions (#570, #179): an unknown function or a wrong number of arguments fails before any row is read, also in
EXPLAIN, with the closest name as a hint, where it was null (includingtoStringOrNull,isEmpty,randomUUIDand other names Grafeo lacks);path_length(p)works; an invalid regular expression, a build without regex ortext-indexsupport andcreate_vector_indexwithoutvector-indexare errors; the clock functions read one instant per statement;rand()gives a new value per row, also in WASM; andnormalize,LIKEacross line breaks,cosine_distanceof equal and opposite vectors,absof the smallest integer (22003) and unary+and-(a type error for non-numbers) are fixed. - Temporal values (#584): zoned datetimes order by their instant and compare with timestamps;
date,timeanddatetimetake a zoned datetime;toStringof a local datetime has no zone; zoned datetime arithmetic keeps the offset; a duration multiplies and divides by any number; GQL compares zoned and local values at the session time zone; Cypherdatetime()andtime()are zoned and keep their offset, anddatetime({epochMillis: n})and temporal components work. - Comparisons and typed properties (#535, #568):
=returns the same rows with or without a property index; a node or edge no longer equals its id;IS TYPEDchecks every type of type DDL; aFLOAT64property takes integers; adding a label converts values to its types; an integer past 64 bits is an exact DECIMAL in imports, parameters and every binding; and type errors name types as DDL spells them. - Schema and DDL (#567, #423): a closed graph type refuses what it does not declare; types created in a schema check writes there; defaults may be negative, a default of the wrong type is refused, and graph type and edge type defaults apply;
DROP GRAPH TYPErefuses a type in use, and a graph type created under a dropped one's name no longer takes over its graphs; a graph type in braces declares its element types;MAPproperties take maps; property names likestartsandmatchcan be declared;CREATE GRAPH dst AS COPY OF srccopies the graph;SHOW INDEXESkeeps every row of a multi-property index after a reopen; refused writes reportG2000orG1001; andSHOWandEXPLAINof a write run in a read-only database or transaction. - Parameters, procedures and PROFILE: SPARQL parameters bind their variables; parameters beside an aggregate, in
HAVINGand in the second argument of a binary aggregate are filled in; a streamed statement with a missing parameter fails asexecutedoes; the plan cache keys a statement by its exact text;CALLarguments take parameters and expressions; stored procedure arguments are bound as values; a query past its timeout is the retryableGRAFEO-Q003in every plan; andPROFILEof a query that starts withoutMATCHno longer panics. - SQL/PGQ:
CREATE PROPERTY GRAPHcreates the types and graph it names; aggregates work insideSELECTandHAVINGexpressions;GRAPH_TABLEcolumns read through the table alias; property maps apply to every element;LIMITandOFFSETapply after grouping;-[:T*]->is one or more hops;HAVINGon a grouping column andRELATIONSHIPS(p)work; an aggregate inCOLUMNSis an error that says to aggregate in theSELECT; and the SQL/PGQ-only build compiles. - SPARQL terms and operators (#464):
DELETEremoves the stored term and templates write the bound one, literals of unknown datatypes keep them, operators andORDER BYfollow SPARQL 1.1, language tags survive expressions,ROUND, double division, date casts andisNumericfollow XPath, an IRI holds no space, and non-ASCII literals survive a reopen. - SPARQL queries:
GROUP BY (expr AS ?v),ORDER BYof any pattern variable, aggregates insideHAVING(GROUP_CONCATincluded),SELECTexpressions over aggregates,SUMandAVGof no values (0), emptyVALUES, unbound selected variables,MOVEandCOPYof a graph onto itself, andCLEAR NAMED(which cleared the default graph too). An empty named graph no longer takes 1.3 MB,execute_sparqlreports updates to change data capture, and sessions run SPARQL and GraphQLPROFILE. - Vector and text search (#593, #600, #391, #619): a query vector of the wrong size or with NaN fails with
GRAFEO-V001instead of crashing the process, and an indexed vector property refuses NaN; removing a vector, a full neighbor list or many identical vectors no longer leave others unfindable; equal scores rank by node id, so identical searches give identical results, also after a reopen; andmemory_usage()counts named graphs, indexes and what values hold. - Graph algorithms (#592): results come in node-id order and are bit-identical across runs, platforms and thread counts; algorithms no longer read uncommitted writes (betweenness centrality crashed);
CALL grafeo.dfsreports tree depth withdiscoveryandfinishcolumns; articulation points take linear time; and Pythonas_networkx(),as_solvor(),get_nodes_by_labelandget_property_batchfollowset_graph(). - Databases on a store of your own (
with_store,with_read_store): sessions are set up like any other, what only the built-in store has (indexes, searches, snapshots,to_memory()) is the unsupported error instead of a panic,Sessionreads read that store,validate()reports instead of panicking, and a label outside a projection's spec is found. - Imports: CSV and JSON Lines imports keep every column and key (spaces, punctuation, a byte order mark, late keys), and
import_jsonlworks in the Python and Node.js packages.grafeo data loadconnects edges to the nodes the file names, keeps plain values and runs in one transaction (#537). - CLI: JSON and CSV results keep their values and types, an error prints its cause,
grafeo index listshows the index's name, andgrafeo query --lang cypherwith--paramruns Cypher. - Bindings: Python
bytes, aware times and dates round-trip,nodes_df()gives the same values with pyarrow, and the polars exports work on polars 2.0. Node.js and WASM convert parameters and rows as properties do,ResultStream.next()has a valid type, WASM timings and clock functions work, andexecuteRawWithLanguageruns in the open transaction (#574). C fails on invalid params JSON, the C# NuGet package bundles every query language (#636), C# and Dart integer types arrive as integers, DartcurrentSchema()no longer crashes, and Gremlin nestedand(),or()andnot()filters apply.changes_betweenreturns a commit's events in write order.
Result changes¶
- Numbers: float
=and<>are exact (IEEE 754) and an integer equals only a float of exactly its value,<and>compare integers with floats exactly, and a float literal past the float range is an error (22003 in GQL, a syntax error in Cypher). Use decimals for exact fractions. - LIMIT, SKIP, OFFSET and WITH: a negative count is 22G02 and a non-integer 22G03, written or as a parameter; a
WITHwith two items of one name is an error (#371). - CAST, SUM and AVG: CAST trims spaces around a text and converts booleans and numbers into each other, in GQL and SQL/PGQ (Cypher keeps openCypher's rules);
sumandavgtake numbers or durations only (strings were summed), and durations now sum and average. text_matchandtext_scoreneed a text index (#308): on a property without one the query fails, naming theCREATE INDEXto run, where every node scored 0.- GQLSTATUS names the kind of error: an unresolved name stays
42002, any other rule a statement breaks is42000, and an unsupported feature hasGRAFEO-Q004. - SQL/PGQ
GRAPH_TABLE (g MATCH ...)reads graph g, where it read the session's graph; the form without a comma is accepted. - Graph algorithms (#563, #564, #566, #592): k-core returns core numbers; Louvain merges communities level by level (a 1,000-node path: 500 pairs at modularity 0.50 before, 31 communities at 0.94 now);
as_networkx(directed=False).pagerank()is undirected;db.algorithmsandcreate_projectionfollow the selected graph; clustering coefficients and stochastic block partition no longer change between calls; clustering, triangle counts and k-truss ignore self-loops; and strongly connected components are numbered in another order. - SPARQL:
COUNTin a transaction counts its own writes,DROP ALLdrops every graph, andASKreturns one row with a Boolean in the columnboolean.
[0.5.44] - 2026-10-04¶
Durability and consistency release. Crash-safe checkpoints and WAL recovery, indexes and constraints that survive a reopen, schema checks on every write path, per-graph conflicts and grants, commit and rollback in O(changes), and fixes for shortest paths, variable-length edges, list comprehensions, subqueries, OPTIONAL MATCH and MERGE. Plus graph handles, upserts by key and write counters.
Heads-up: 0.6.0 (first announced as 0.5.45) changes the on-disk format. 0.6.0 migrates a database automatically on first open (WAL-directory databases become a single
.grafeofile); after that, 0.5.44 and older can no longer open it, so keep a backup if you may need to go back.
Changed¶
- Breaking (Rust,
grafeo-engine): the direct write methods returnResult(create_node,create_edge,set_node_property,delete_node, the batch calls and the rest, onGrafeoDBandSession). Each call commits on its own with the checks of a query, so writing to a missing entity or endpoint, or deleting a node that still has edges, now fails. The bindings raise these errors. - Breaking (Rust,
grafeo-core): the mutation operators take aGraphWriterinstead ofwith_transaction_context,with_validatorandwith_write_tracker;SetPropertyOperator::with_labelsandwith_edge_typeare gone. - Breaking (Rust,
grafeo-core): operators that only order, cut, deduplicate or compare rows take no output schema (LimitOperator,SkipOperator,LimitSkipOperator,SortOperator,TopKOperator,DistinctOperator,ShuffleOperator,ExceptOperator,IntersectOperator). - Breaking (Rust,
grafeo-core): a sort key'sNullOrderno longer flips for a descending key (SortKey::descendingsetsNullsFirst), andvalue_utils::compare_values_with_nullsis replaced bycompare_sort_values. - Breaking (Rust,
grafeo-core):FilterExpression::ExistsSubqueryandCountSubqueryhaveend_varandedge_var, andExistsSubqueryreadsmin_hopsandmax_hops. - Breaking (Rust,
grafeo-engine): the transaction manager's write and read sets holdGraphEntity(graph plus node or edge), the logicalExpandOphas aquantifiedfield, andRdfPlanner::with_walis no longer public. - Breaking (Rust,
grafeo-engine):ChangeEventhas agraphfield (Nonefor the default graph), andCdcLogkeys its events by graph and entity, withhistory_inandhistory_since_infor a named graph. - Breaking (Rust,
grafeo-adapters): the GQLQueryClause::InlineCallhasscopeandcombinedfields, and the CypherClause::CallSubqueryis a struct variant withquery,scope,unionsandunion_all. - CDC label change events:
labelsis now always the labels after the change, and the newbefore_labelsholds the previous ones. - Some queries that ran now fail with an error, as in openCypher: an expression in
WITHwithout a name; aCALLsubquery that returns an outer variable or reads one it does not import (CALL () { ... }, a CypherCALL { ... }without an importingWITH); an importingWITHwith aWHERE, alias,ORDER BY,SKIPorLIMIT;UNIONmixed withUNION ALL; and a Cypher query that ends with aCALLsubquery that returns rows (add aRETURNafter it). Before, they gave wrong or silently null results, or failed with an internal error (see Fixed).
Fixed¶
Every query result in the differential test corpus that differs from 0.5.43 is listed, with its reason, in scripts/difftest/reviewed/0.5.44.txt.
- Checkpoints after
compact()left data out of the.grafeofile: the schema, indexes and RDF data were dropped (also atclose()), the periodic checkpoint wrote the pre-compaction store, andasync_write_snapshot()wrote little or nothing. Every checkpoint now writes the whole database. - A failed checkpoint could leave a
.grafeofile that no longer opened (#418). Checkpoints now write to<file>.checkpointfirst, so the last good state stays readable; this needs free disk space for a second copy of the file. - Edges appeared twice after a checkpoint followed by a crash (#417). Replay now skips entities the file already holds and repairs affected databases, and checkpoints trim the sidecar WAL.
- Direct writes were not durable until
close()(#395). Each call is now durable when it returns, and batch calls are recovered completely or not at all. - Concurrent transactions could corrupt WAL recovery (#411): one session's rollback could erase another's commit, and open or rolled-back writes came back on reopen. Each transaction is now logged as one group at commit.
- Two processes could open the same WAL-directory database (#405); it is now locked like a
.grafeofile. wal_checkpoint()lost data in WAL-directory databases (#419). It now only syncs the WAL there, and open replays every WAL file.DurabilityMode::Adaptivenever synced the WAL, so it behaved likeNoSync, and backups missed writes made while they ran; those now land in this backup or the next.- Schema changes, constraints and
create_graph()/drop_graph()were lost on WAL replay (#421, #422). An unknown WAL record now fails the open instead of being skipped. DROP CONSTRAINTdid nothing (#420). Constraints are now stored by name:CREATEandDROP CONSTRAINTsupportIF NOT EXISTS/IF EXISTS,SHOW CONSTRAINTSlists them, and unnamed ones get a name such asCity_name_not_null. Constraints from 0.5.43 files have no name and cannot be dropped by name.- Reopening a
.grafeodatabase orto_memory()lost its indexes (lookups on a reopened file scanned every node, #459), andto_memory()also dropped the schema, constraints and property history. WAL-directory databases still lose their indexes until 0.6.0 (#401). - Rust builds with only the
rdfprofile kept no data across a reopen (#544); the facade'srdfprofile now includes the LPG store. - Embeddings changed while their vector index was spilled reverted on reload (#522).
- Sessions and direct reads after
compact()missed parts of the database: SPARQL writes were lost, CDC missed queries, the selected graph was ignored, andget_node,node_count,info(),schema()and similar did not see pre-compaction data (or panicked on awith_storedatabase). Queries aftercompact()are still not written to the WAL, so a crash loses them; direct calls are logged. - Grants and read-only modes did not stop every write (#413):
use_graph()skipped the grant check, read-only grants allowed writes, and read-only transactions, roles and databases let non-GQL languages and the direct API write. A session whose graph was dropped also fell back to the default graph; it now fails. - Transactions: a failed statement inside a transaction kept its partial writes (it is now undone, and the transaction goes on); a commit that failed with a write-write conflict left the transaction active (#409); a transaction could not delete what it had created; transactions in different named graphs raised false write conflicts; and rolled-back nodes still counted in the planner's statistics.
- Writes that should fail went through:
SET,REMOVEand label changes on a node or edge deleted earlier in the statement or transaction (now... does not exist or has been deleted in this transaction, as in openCypher), writes from a session reading at an earlier epoch (set_viewing_epoch,execute_at_epoch), and writes aftercompact()on a database opened read-only. All of these now fail. - Writes skipped schema checks:
SET,REMOVE,SET n:LabelandMERGEdid not check property types,NOT NULL,UNIQUE,NODE KEY, edge types, endpoint labels orDEFAULTvalues,UNIQUEdid not hold within a transaction, and multi-propertyUNIQUEandNODE KEYwere checked one property at a time. Every write is now checked likeINSERT, and a failedALTER NODE TYPEorALTER EDGE TYPEapplies all or none. - Parameterized queries skipped schema and constraint checks (#526), so
execute(query, params)in Python and Node.js could insert a duplicateUNIQUEvalue. They now run exactly like the same statement with literals. - Parameters: a statement that used a parameter nobody supplied stored the text
$e(INSERT (:P {e: $e})) or failed with an internal error, and now fails withMissing parameter: $ebefore it writes anything; unaliased columns were named after a parameter's value (RETURN $xgave a column5) and are now named after the query text; andPROFILEwas ignored when parameters were passed (#460). - Removed properties stayed visible as null without the
temporalfeature (as in the Python and Node.js packages);REMOVE n.pandSET n.p = NULLnow remove the property. - Commit and rollback got slower as the database grew (#410): a one-node insert took 36 ms at 1M nodes. They now cost only as much as the change.
- Slow paths on large databases: setting a property got slower with the node's property count (
NodeRecord::props_countis now always 0), every hundredth commit scanned the whole store, and concurrent writes could deadlock withtiered-storage. - Lookups by
id()or an indexed property scanned every node (#454, #356), once per row for keys likeUNWIND $rows AS row MATCH (n {id: row.id}), and with a label (MATCH (n:File {id: 'x'})) every node of that label was collected. They now seek directly, also forINlists and edge endpoints. - Regular expressions and
LIKEpatterns were compiled for every row (#458); each is now compiled once. - Nodes and edges came back as IDs, as
0, or read another entity's properties (#482): afterORDER BY,SKIP,LIMIT,DISTINCT, a laterUNIONbranch, a secondMATCH,OPTIONAL MATCH,EXISTS,CALL,UNWIND, grouping or a write, a returned edge could be0, a node or edge a raw ID, and a property could come from the entity of the other kind with the same ID (MATCH (a)-[r]->(b) RETURN r ORDER BY r.w).collect()and group keys returned nodes and edges as IDs,nodes(p)andrelationships(p)returned internal ids,UNWINDof them or of a function call afterMATCHreturned no rows, andEXCEPTandINTERSECTover nodes or edges returned the wrong rows. Nodes, edges and lists of them now keep their kind through every clause, also in Gremlinunion(outE(), out()). keys(r)of an edge returned null; it now lists the edge's keys, andkeys()of a node or edge returns them sorted, likeproperties().- A variable used as both a node and an edge matched by a coincidence of IDs (
MATCH (r) MATCH ()-[r]->()); it now fails with an error that names the variable. ORDER BYover values of different types failed or depended on the input order: values of different types compared as equal, numbers among them and around NaN came out of order, lists and maps were not ordered, and such a sort (also inpercentileContandpercentileDisc) could fail withdoes not correctly implement a total order(aPanicExceptionin Python).ORDER BYnow uses the openCypher total order: maps, lists, paths, temporal values, strings, booleans, numbers, then null; integers and floats compare exactly and NaN sorts after infinity.- Other
ORDER BYfixes: GQLNULLS FIRSTandNULLS LASTwere reversed withDESC,RETURN * ORDER BY ...failed withVariable '*' not found in input, and sort keys could show up as extra columns (RETURN r AS e ORDER BY e.wreturned ane_wcolumn). - Patterns through variables bound earlier matched too much: a path back to an earlier node (
MATCH (a)-->(b)-->(a)) returned every two-hop path instead of the cycles, and a pattern through a bound edge (MATCH ()-[r]->() MATCH (x)-[r]->(y)) matched every edge of its shape, also afterWITH, inCALLsubqueries, in stored procedures and after a shortest path. Both now use what the variable holds. - Variable-length edges: the edge variable held only the last edge, a property map was checked only on the last hop, and a one-hop quantifier (
*1..1,*1, GQL{1,1}) bound a single edge, sosize(rs)returned null. The variable is now the list of the path's edges. - Shortest-path searches returned pairs without a path and ignored hop bounds (#514). Unreachable pairs no longer get a row,
->+finds the shortest cycle, and the path must fit the quantifier. - Clauses after a write or a
WITHin one statement (#479, #480): aMATCHafter a write missed the written data (and when it found nothing, the write did not run), and a GQLMATCHafterWITHfailed withVariable '...' not found in input. Clauses now read the rows the earlier ones pass on. EXISTSandCOUNTsubqueries gave wrong answers (#543): they checked only a path's first edge (missing multi-hop matches and minimums like*2..), ignored the rest of their pattern (EXISTS { MATCH (a)-[:KNOWS]->(b), (c:Robot) }held without anyRobot), used only their start node from the row (EXISTS { MATCH (b)-[:KNOWS]->(a) }held wheneverbknew anyone), ignored a tie to the row through a value ({id: s.id}, an innerWHERE), could run inWHEREbefore their variables were bound, and counted edges the query could not see. Each row now gets its own answer from the whole pattern, and a null node makesEXISTSfalse andCOUNT0.- GQL subqueries: in
EXISTS,COUNTandVALUE, eachMATCHclause matched on its own; anEXISTSthat starts withOPTIONAL MATCHfiltered rows;VALUE { ... RETURN count(x) }ignored its argument andDISTINCT; and aVALUEsubquery that is not a count gave every row the same answer, or failed inWHEREandWITH. They now behave as the same clauses do outside a subquery. - Cypher
OPTIONAL MATCHat the start of a query and inEXISTSandCOUNTfailed; it now runs, with one row of nulls when nothing matches. CALLsubqueries saw the wrong outer variables: a GQLCALLdid not see the outer row, so every row got every match; an importingWITH(in GQL, the firstWITHof the body) ignored itsWHEREandWITH a AS bimported the wrong value; and a CypherCALLwithout an importingWITHread outer variables as null. A GQLCALLnow sees every outer variable and itsWITHis an ordinary one, a CypherCALLsees those its importingWITHlists, and the scope clauseCALL (a, b) { ... }(CALL (*),CALL ()), which failed to parse, names them in both languages.- Nodes and edges a
CALLsubquery returned were copies of their properties: a laterMATCHfrom them failed or matched everything, andid(b)andtype(r)were null.RETURN *in aCALLalso returned nothing, and a two-edge chain in aCALLno rows. The subquery now passes on the nodes and edges themselves, andRETURN *returns the variables it binds. - Cypher
CALLbodies rejectedORDER BY,SKIP,LIMIT,UNION,DELETE,MERGE,REMOVE,FOREACHand a nestedCALL; they now run, as in openCypher (an orderedLIMITgives the top rows per outer row). A GQLCALLbody takesUNION,EXCEPT,INTERSECTandOTHERWISE. - An
OPTIONAL MATCHcondition that reads a variable bound before it dropped rows: inMATCH (a)-[:KNOWS]->(b) OPTIONAL MATCH (b)-[:KNOWS]->(c) WHERE c.age > a.age, abwhose matches all failed the condition lost its row instead of keeping it with nulls. The condition now decides which matches count, in GQL and Cypher. MERGEbound only the first of several matches: with two(:Tag {k: 1})nodes,MERGE (t:Tag {k: 1}) SET t.seen = trueset one of them. As in openCypher,MERGEnow binds every match, one row each, also for relationships and inupsert_edges.- GQL
NEXTdid not pass rows on: the statement afterNEXTmatched from every node and returned the first statement's columns too. It now reads the rows the one before returns, and only the lastRETURNis the result. - CDC history mixed graphs:
history(id)andchanges_betweenreturned the events of every graph's entity with that id. Each event now names its graph (graph, also in Python and Node.js),historyreads the database's default graph or the session's current graph, andchanges_betweenreturns every graph's events. - A variable a
WITHleft out failed with an internal error (MATCH (a) WITH 1 AS x RETURN a.name); it now fails withUndefined variable 'a'. - A Cypher pattern comprehension inside a function call, an aggregate or a map projection failed (
RETURN size([(a)-->(b) | b])); it now runs. - A transaction on a
.grafeofile, a WAL directory or a compacted database did not see the type of an edge it had created until the commit. - List comprehensions, list predicates and
reduce()lost items (#538) when the expression called a function, read a property of arelationships(p)item or used another row variable. EXCEPT,INTERSECTandOTHERWISEdid not check their branches' columns (#481), and SQL/PGQ checked none of its set operations; they now fail likeUNION.- Cypher
=~matched any part of the string; it now has to match the whole string, as in openCypher. - Cypher and SQL/PGQ checked only the first label of some node patterns (#513), e.g.
MATCH ()-[r]->(n:A:B). - GQL
INSERTwith unlabeled or already bound endpoints failed withUndefined variableor attached edges to the wrong node. - A
LOAD CSVrow returned whole was null instead of the row's map. - Times with UTC offsets compared inconsistently; they now compare by instant.
toString()returned an internal form for temporal values, lists and maps; it now gives ISO 8601 text and literal form.- Very long
^chains or runs of signs orNOTs could overflow the parser stack. - Docs: fixed the Discord invite and the GQL constraint examples (#344).
Deriva¶
Changes for Deriva, which generates ArchiMate models from software repositories and keeps its graph in an embedded Grafeo database through the Python binding.
- Deterministic Louvain: the same partition on every run, with communities numbered from 0 by their smallest node id.
- Python:
grafeo.features()andgrafeo.build_info(): the compiled-in languages and capabilities, plus version, git commit and build profile. - Dotted access into map properties:
n.meta.route(also chained) in GQL and Cypher; a missing key gives null. - The direct API follows the selected graph set with
set_graph(), likeexecute(). - Graph handles:
db.graph(name)scopes queries, the direct API and transactions to one graph, usable side by side and across threads. - Direct writes are checked against the schema and constraints like
INSERT, and advance the epoch, sochanges_betweenandget_node_at_epochsee them in order. - CDC events say what changed: labels, edge type and endpoints, the last properties on delete, and one create event per entity created in a transaction.
- Write counters:
result.countersreports nodes, edges, properties and labels created, set or removed. - Upserts by key:
upsert_nodes(labels, rows, key="id")andupsert_edges(...)create or update many entities by a key property and report created, updated and skipped rows. shuffle_unorderedtest option: returns rows withoutORDER BYin random order, to catch code that relies on an order that is not guaranteed.- Batch edges with properties, batch nodes with several labels (#462).
- Cypher pattern predicates in
WHERE:WHERE (d)-[:CONTAINS]->()andWHERE NOT (d)-->().
Internal¶
- CI gate and policy checks (#511): a single
CI Gatecheck to require before merging, and aPolicyjob for crate boundaries, toolchain pins and public-text rules. - CI toolchain pins restored (#509); Dependabot no longer bumps the Rust toolchain.
- Pull request eligibility check: a
PR Policycheck applies the contribution rules in CONTRIBUTING.md. - Differential test:
scripts/difftestruns a GQL and Cypher query corpus on the previous release and on a release build of a commit, and fails on any changed result that was not reviewed for the release; it also compares GQL with Cypher within one run. - Browser WASM size limit: the CI limit for the gzipped browser build is now 760 KB (warning at 740 KB); the build is about 730 KB after this release's fixes.
[0.5.43] - 2026-09-27¶
Stabilization release. Fixes for silent wrong results (ORDER BY + LIMIT, UNION, aggregates, duplicate column names, SPARQL named graphs and property paths), queries whose trailing statements were ignored, rollbacks that did not undo changes on persistent databases or SPARQL updates, databases over 4 GiB written corrupt, edges lost after compact() and HNSW vector updates, plus dependency and security updates.
Added¶
- Gremlin negated text predicates:
notRegex(),notContaining(),notStartingWith()andnotEndingWith()inhas()filters, e.g.g.V().has('city', notStartingWith('Am')). (#336, #340, @jakeboone02)
Changed¶
- Breaking (Rust API):
QueryResult::new,with_typesandfrom_rowsreturnResultand reject repeated column names (#371). Add?or.unwrap()at call sites. - Unaliased columns are named after their expression, as written:
RETURN id(a), n.a + 1, count(b)yieldsid(a),n.a + 1andcount(b)instead ofid(...),exprandcount(...). Aliased columns are unchanged. (#350, #372) restore_to_epoch()refuses to overwrite an existing database or its.walsidecar; restore to a fresh path instead. (#363, @teipsum)- Node.js: transaction queries run on a worker thread, like
Database.execute(), so they no longer block the event loop.commit()androllback()now throw while a query from the same transaction is still running. - Queries with trailing input are rejected (#380): GQL, Cypher and Gremlin fail with a syntax error on text after the statement, including
;-separated statements (a trailing;is fine) and GQL clause orders not supported yet (e.g.SET ... DELETE). These used to run only the first part. - Breaking (Rust,
grafeo-core):ColumnCodec::write_to,write_to_v2,write_to_v3andCsrAdjacency::write_toreturnResult, failing instead of writing a truncated size (#392). - Rust toolchain pinned to 1.98.1 for local builds and CI. The MSRV stays 1.91.1, and the
grafeocrate now declares it (#390).
Fixed¶
- GQL ran only the first statement and ignored the rest (#380):
INSERT ... INSERT ...(as in the quickstart) created only the first node andINSERT ... RETURNignored itsRETURN; both now work. Gremlin and Cypher had similar gaps. - GQL
^(power) was not parsed:RETURN 2 ^ 10returned2. It now computes the power, binding tighter than*and right associative. ALTER NODE TYPE / ALTER EDGE TYPE ... ADD PROPERTY name TYPEadded a property calledPROPERTYof typenameand dropped the real type;DROP PROPERTY namedropped the wrong property.PROPERTYis now an optional keyword.- Databases over the storage format's limits were written corrupt (#392): an LPG section over 4 GiB or 65,535 blocks, or over 65,535 labels on one node or versions of one property, wrapped a size field, and the file failed to open with
CRC mismatch. Such a checkpoint now fails with an error naming the limit and keeps the sidecar WAL, so nothing is lost. Larger sections are planned for 0.6.0. - Rolling back a transaction on a persistent database did not undo
SET,REMOVEor label changes, and single-file databases wrote them to disk onclose(). ORDER BY ... LIMITover a whole-nodeRETURNreturned raw NodeIds instead of node maps, both for property sort keys (#335) and for expression keys such astext_score(...),CASEor arithmetic (#347). (#337, #349, @temporaryfix)- Duplicate column names silently lost data (#371), e.g.
RETURN id(s), id(t). Distinct expressions now get distinct names, and a result that would still repeat a name is an error asking for an alias; in SPARQL, anAS ?xthat repeats a projected or bound name is a parse error. (#372, @teipsum; #350, @temporaryfix) UNIONwith differing branches (#365): SPARQL now returns every variable from every branch, and GQL and Cypher reject branches with different columns instead of padding or truncating them. (#366, @teipsum)- Aggregates next to aliased or later items (GQL and Cypher):
RETURN n.city AS city, count(n)failed withUndefined variable '_agg_0', columns came back in the wrong order, and unreturnedGROUP BYkeys appeared as extra columns. - SPARQL updates ignored open transactions:
INSERT/DELETE ... WHEREapplied immediately and was not undone byrollback(), andINSERT DATA/DELETE DATAon a named graph was dropped on commit. Updates now apply on commit, and queries in a transaction see its earlier writes. - SPARQL
FROMandWITH <g>did not apply inside subqueries: a nestedSELECTread the default graph instead of the query's dataset. - SPARQL updates on named graphs went to the wrong graph or were lost (#367):
INSERT/DELETE ... WHEREwithGRAPH <g>orWITH <g>now targets the named graph and survives a restart.USING/USING NAMEDis rejected until it is supported. (#368, @teipsum) - SPARQL
path+returned only direct neighbours (#369); it now returns the full transitive closure. (#370, @teipsum) - Edges disappeared after
compact()once an endpoint was modified (#345). Deleted edges also no longer reappear or show up as neighbours. (#346, @temporaryfix) - Updating an indexed vector could make other vectors unfindable (#374). Removing or replacing the HNSW entry point also no longer leaves the upper index layers unreachable. (#375, @jarmen423)
- Filters on edge properties or map values returned no rows when nodes had a property with the same name, e.g.
MATCH ()-[r]->() WHERE r.id = 'e1'orUNWIND [{id: 'a'}] AS m WHERE m.id = 'a', because they were answered from node statistics. - Property maps on anonymous edges were ignored:
()-[:T {since: 2020}]->()in Cypher and GQL matched everyTedge. - Deleted nodes stayed in property and vector indexes, so
find_nodes_by_propertyand vector search could still return them, and the lookup API returned uncommitted nodes. Rolling back a delete orSETnow restores the index entries. - Computed property values in
CREATE,INSERTandMERGE:CREATE (:X {id: toString(i)})failed with an internal error, andMERGEsilently usednull, so every row matched or created the same node. They are now evaluated per row. MERGEcreated duplicate relationships when several rows of one statement merged the same edge (UNWIND [1, 2] AS i MATCH (a), (b) MERGE (a)-[:T]->(b)created two). Anullin a relationship'sMERGEpattern now also matches an absent property, as it already did for nodes.=and<>on dates, times, datetimes, durations and vectors were always false / true in expressions, e.g.RETURN date('2024-01-01') = date('2024-01-01'),x IN [date(...)]or a comparison afterWITH. Filters answered directly from a node property were not affected.- Python: naive
datetimevalues were read as local time but returned as UTC; they are now UTC both ways. Microseconds are no longer rounded and dates before 1970 work on Windows. - Storage docs: the
.grafeosidecar WAL is removed on a cleanclose(), not after every checkpoint. (#364, @teipsum)
Security¶
- Fixed RUSTSEC-2026-0190 (anyhow), RUSTSEC-2026-0186 (memmap2), RUSTSEC-2026-0204 (crossbeam-epoch), RUSTSEC-2026-0258 (h2) and RUSTSEC-2026-0285 (rustls), and replaced two yanked crates (chacha20, der).
- Python bindings on PyO3 0.29, fixing RUSTSEC-2026-0176 and RUSTSEC-2026-0177. No Python API changes.
Dependencies¶
- Arrow and Parquet 59, aes-gcm 0.11, comfy-table 8, tikv-jemallocator 0.7 and hf-hub 1.0, among others (#396). The
embedfeature now downloads models through hf-hub's new client; models already in the Hugging Face cache are reused.
Thanks to @teipsum for six PRs and the detailed reports behind them, to @temporaryfix for #346, #349 and #350 and the root-cause analysis on #335, to @jakeboone02 for the Gremlin predicates, to @jarmen423 for the HNSW fix, and to @stiff, @halaharvi, @GanbaruTobi and @cuongvo for their reports. The filter, index and MERGE fixes come from reports by the Deriva project.
[0.5.42] - 2026-05-04¶
End-to-end tiered storage: section data (LPG, RDF Ring, vector topology) can spill to mmap-backed disk under memory pressure or explicit configuration, with per-section tier overrides, introspection, and reload. Plus per-block columnar zone maps for selective range scans, paged HNSW topology, packed RDF Ring on disk, a WAL overlay for mutating mmap'd compact stores and a streaming top-K operator that fuses ORDER BY ... LIMIT into a single bounded-heap pass.
Added¶
- Per-section storage tier configuration:
Config::with_section_tier(SectionType, TierOverride)pins individual sections to RAM or disk.ForceDiskspills at db open;ForceRamis hard-enforced (skipped in every spill loop).Auto(default) defers to the buffer manager. db.storage_tiers()introspection: returns a map of section type to current tier (InMemory/OnDisk/Uninitialized). NewMemoryConsumer::current_tiertrait method backs it with authoritative state.db.reload_eligible(target_fraction): brings spilled sections back into RAM in priority order, stopping when projected usage exceeds the target. Returns the count reloaded.- Python bindings for tier control:
GrafeoDB(path, section_tiers={'VectorStore': 'force_disk'}),db.storage_tiers(),db.reload_eligible(). Eight new pytest cases. - Paged HNSW topology (vector v2):
VectorStoreSectionswitches from bincode to a packedGVSTenvelope with per-indexGTOPpaged topology.MmapTopologyserves neighbor lookups directly fromBytesslices;HnswIndexdispatches search throughTopologyBackend { Heap, Mmap }. Heap drops >10x under mmap, recall is bit-identical. - Packed RDF Ring (ring v2): bincode
RdfRingSectionreplaced by aGRFRenvelope (sorted term dictionary + three packed wavelet trees + two permutations + CRC32).swap_to_mmapshares refcountedBytesslices from the spill mmap. - Per-block columnar zone maps + iterator bounds: LPG compact-store columns lay out as 4 KiB blocks with per-block min/max.
find_in_range_iterskips blocks via zone maps;RangeScanOperatorstreams matches into the push pipeline. Large speedups on selective range scans. - WAL overlay for mutable mmap'd LPG: when the compact base is
OnDisk, mutations route to a liveLpgStoreoverlay; periodic merge folds it back viaLayeredStore::merge_overlay_in_place. Newmerge_guardmakes concurrent reads + writes + merges race-free. Persistent file + spilled base + overlay mutations round-trip across restarts. SectionType::OverlayDeletions: persistsLayeredStore's deletion log so deletions of base entities survive close/reopen without an explicitcompact(). Omitted from the container directory when empty. Markedrequired: false; meaningful once the directory parser learns to skip non-required unknowns (planned for 0.5.43).tracingevents on tier transitions (withtracingfeature): info events undergrafeo::bufferandgrafeo::tierfor spill, reload, and ForceDisk applications.- Storage Tiers documentation page:
docs/architecture/memory/storage-tiers.mdcovering tiers, overrides, introspection, reload, and tracing conventions. PageFetchertrait +MmapPageFetcherimpl: indirection layer that lets sections receive paged byte access without knowing the source, opening a future swap to vmcache or an explicit pager.- Streaming top-K operator (
TopKOperator): bounded-heap of size k, O(k) memory regardless of input cardinality, ~12x faster than the unfusedSort+Limitat N=1M. Planner fuses literalLIMIT koverSortinto a single physical operator; vector/text top-K still takes precedence and PROFILE-mode plans bypass the fusion for entry-count parity. (#326, @temporaryfix) var.prop IN [literals]property-index fast path: per-value index lookups unioned viaNodeListOperator, deduped, label/MVCC-filtered. ~56x speedup onWHERE id IN $idsagainst a snapshot-loaded database. (#326, @temporaryfix)LogicalOperator::map_children: child-recursive optimizer passes descend without enumerating every variant.
Changed¶
- Vector store and RDF Ring section formats bumped to v2 (paged
GVSTand packedGRFRenvelopes). Existing v1 bincode files keep loading via magic-byte detection and upgrade to v2 on the next checkpoint. TierOverride::ForceDiskenforcement is now targeted: previously triggeredspill_all()for every consumer; nowspill_consumer_by_name(...)per matching section, so unrelated consumers stay InMemory.- Filter pushdown extended through
Filter,LeftJoin,Apply,Union,Unwind: predicates insideOPTIONAL MATCHand correlated subqueries now reach the scan. New pre-pushdown pass propagates filter predicates acrossLeftJoinshared-variable boundaries so the right-side subtree of an OPTIONAL MATCH picks up the sameWHEREconstraints as the mainMATCH(~8x onfeed.hydrate-shaped queries). SiblingFiltercommute is gated by a variable-scope check so predicates over path variables likeLENGTH(p) >= 1don't push past a label filter into a subtree where the path isn't yet bound. (#326, @temporaryfix)
Fixed¶
MERGE ... ON CREATE/ON MATCH SETcould not reference the MERGE variable (#317): expressions likeON MATCH SET c.description = coalesce(c.description, 'fallback')failed at the binder, and any non-trivial action expression that did pass through was silently lowered toNull. Binder now scopes the MERGE variable into ON CREATE / ON MATCH (per ISO/IEC 39075:2024 §15.5); planner emits aPropertySource::Expressionfor action expressions; operators evaluate them against an augmented row containing the merged entity. Reported by @Fraenkstan.TierOverride::ForceRamwas silently a no-op: prior versions accepted the config but the buffer manager spilledForceRamconsumers anyway under pressure. The spill loop now consults aforce_ram_consumersset and skips matching consumers inrun_eviction_internal,spill_all, andspill_consumer_by_name.- Persistent reopen lost the compact base after
compact(): directory parser had noSectionType::CompactStorearm andload_from_sectionshad no handler, so reopens fell back to a legacy path that surfaced as a checksum mismatch. Newextract_compact_base+wire_layered_after_loadhelpers rebuild the LayeredStore and consumer wiring on open. - Concurrent merge could lose writes: stress test with 4 readers + writer + merger lost 426/500 writes in one run. Fixed via
merge_guard: RwLock<()>(mutators take read, merge takes write). GRAFEO-X001: snapshot checksum mismatchopening v2 section files (#323, #324):read_snapshotwas unconditionally a v1 reader and CRC'd zero bytes against the v2 directory CRC when the engine's open path fell through. Reader now early-returns on a v2 header (snapshot_length == 0with non-empty header), letting the engine's v2 dispatch take over. Reported by @teipsum against an embedded production database.- Silent corruption masking in
read_section_directory(#323 follow-up): the v2 directory parser swallowedfrom_byteserrors with a wildcard, routing truncated directories and torn-page writes into the v1 read logic and surfacing as a misleading snapshot-checksum error. Parser now propagates errors with the failing offset, treats "v2 header but file too short" as an error rather than a v1 fallback, and verifies the directory page CRC. Regression tests cover both paths. LayeredStoredeletions silently reverted on reopen (#323 follow-up): base nodes/edges deleted aftercompact()were tracked only in memory, so reopen made them reappear until the nextcompact(). NewOverlayDeletionssection persists the deletion log; open path seeds the layered store from it. Round-trip integration tests lock in the behaviour.- Per-query spill directory leak (#323 follow-up):
<spill_path>/query_<id>/directories were created per query and never removed; one production day accumulated 358 empty subdirectories.SpillManager/AsyncSpillManagernow exposewith_owned_dir()soDropremoves the directory non-recursively (preserving unexpected siblings). active_db_headerdocstring drift: the function picks the higher-iteration slot unconditionally; checksum validation lives in the readers. Doc rewritten to match.- Equality-scan regression from the Bytes-backed codec refactor:
BitPackedInts,DictionaryEncoding, andColumnCodec::{Float64,RawI64}switched to a two-variantInline(Vec<T>)/Mapped(Bytes)store so in-RAM builds keep native slice iteration and mmap loads stay zero-copy. Closes the 19-29% CodSpeed regression oncompact/find_nodes_by_property/{int_eq,dict_eq}. - Property-index fast path silently disabled after
compact():LayeredStore::has_property_indexnow delegates to the overlay (was using the trait defaultfalse). Snapshot-loaded databases stop falling back to the ~3.5x-slower label-first scan. (#326, @temporaryfix) ORDER BYagainst aWITHalias dropped byRETURN: e.g.WITH x AS s ... RETURN x ORDER BY sfailed withVariable 's' not foundbecause the augmented-projection path skipped Variable sort keys. They now pass through alongside Property keys. (#326, @temporaryfix)PROFILEpanicked on fused fast-path operators: property/range/IN-list paths absorbed their childNodeScaninto one physical op, leavingbuild_profile_treeshort an entry. Affected paths now emit a syntheticNodeScanentry; the factorized expand-chain fusion is also gated underPROFILE. (#326, @temporaryfix)
Deprecated¶
TieredStoretrait (grafeo_common::memory::buffer::TieredStore): never implemented anywhere; theSectiontrait (withswap_to_mmap+reload_to_ram) plusMemoryConsumercover the same lifecycle. Marked#[deprecated(since = "0.5.42")]and scheduled for removal in 0.6.0. TheStorageTierenum it shipped alongside is kept.
Thanks to @teipsum (Michael Lewis Cram) for reporting #323 with byte-level analysis from a production embedded database, and for #324's read_snapshot fix which lands cherry-picked with authorship preserved. The follow-up work in this release (stricter read_section_directory validation, durable LayeredStore deletions via the new OverlayDeletions section, and the per-query spill directory cleanup) addresses the deeper root causes the original report surfaced.
Thanks to @temporaryfix for the top-K operator, IN-list fast path, LeftJoin filter propagation, and the cluster of fixes around compact() and PROFILE in #326, reshaped to fit grafeo's map_children recursion pattern before merging.
[0.5.41] - 2026-04-24¶
Compact-store correctness (post-compact() read path, signed integer round-trip), search procedures, disk-backed compact base, silent-hybrid-on-persistent-DB fix, memory introspection for RDF and CDC, and test-infrastructure hardening (proptest, persistent spec variants, CodSpeed CI).
Added¶
CALL grafeo.search.*procedures: first-class procedure entry points for text and vector search, with scalar similarity available as an expression in projections. Routes through the existingGraphStoreSearchsurface so WAL/CDC wrappers andLayeredStoreall work.- WASM transactions +
close(): explicit transaction API for the browser bindings, plusclose()to release handles deterministically instead of waiting for GC. - Tamper-evident WASM snapshots:
exportSnapshotSigned(key)/importSnapshotSigned(data, key)wrap snapshots with aGSN1magic header and an HMAC-SHA256 tag over magic + payload, with 128 MiB input cap and constant-time verification.importSnapshot()refusesGSN1-prefixed payloads so the two entry points can't be confused. - Property-based round-trip coverage for
compact()(#303): proptest generates arbitrary LPG graphs and asserts equivalence across 28 GQL queries per case on a compacted vs fresh database. 128 cases by default,PROPTEST_CASES=1024for local investigation. Surfaced the two compact-store bugs fixed in this release (#301, #302). - Persistent spec-test variants (#309): every
.gtestcase requiringtext-indexorvector-indexnow auto-generates a_persistentsibling that opensGrafeoDB::open(tempdir), exercising the WAL-wrapped read path used by every on-disk session. 49 persistent variants land with this release. - CodSpeed continuous benchmark regression (#304): all seven Criterion suites run under Callgrind on every PR via
cargo codspeed, posting a diff vsmainas a PR comment. Fork PRs skip cleanly; plaincargo benchcontinues to work unchanged. ColumnCodec::RawI64(#306): native signed 64-bit codec for columns containing at least one negative value, with i64 comparison infind_eq/find_in_rangeand signed zone maps. Non-negative columns continue to use the more compactBitPackedencoding.- wasm32 simd128 distance kernels (#305):
std::arch::wasm32implementations of all four HNSW distance metrics (dot product, squared Euclidean, cosine, Manhattan), 4.36x to 5.35x faster on 384-dim f32 vectors. Enabled by default for wasm32 builds via.cargo/config.toml(target-feature=+simd128); runtime requirement Chrome 91+ / Firefox 89+ / Safari 16.4+. Relaxed-simd FMA deliberately skipped: spec permits runtime-defined rounding, and wasmtime regresses vs plain add+mul. - RDF and CDC memory breakdown:
db.memory_usage()gainsrdf(triple count, term dictionary, optional Ring index, named-graph count) andcdc(entity count, event count) blocks alongside the existing store/index/MVCC/cache totals. Feature-gated and skipped from JSON when empty. - CLI
:memorymeta-command: prints the hierarchical memory breakdown in the REPL, omitting zero-valued and feature-disabled components. - Disk-backed compact base (
compact-store + mmapfeatures):CompactStoreTieredwraps the columnar base in a two-stateInMemory/OnDisk(mmap) machine.compact()registers aCompactStoreConsumerwith theBufferManager; under memory pressure the base serialises to<spill_path>/compact_base.grafeoandArcSwappublishes the fresh mmap-backedArcthroughLayeredStore, so queries see no discontinuity and the old heap allocation drops. - Contributor docs for CDC, query planner, and MVCC visibility: module-level
//!guides covering the CDC event model and epoch relationship, the planner's rewrite and filter-pushdown ordering, and theEpochId::PENDING/ visibility /TransactionWriteTrackerflow.
Changed¶
- On-disk codec format extended: databases written by 0.5.41+ may contain columns under the new
RawI64discriminant (6). Earlier 0.5.4x binaries reject these withunknown codec discriminant. One-way format break, in line with the 0.5.35 precedent.
Fixed¶
- Post-
compact()writes invisible toMATCH(#307, closes #302):LayeredStoreget/versioned/epoch/property/type/visibility methods now fall through to the overlay when the base doesn't recognise the id;edges_from/neighborsalways consult the overlay so cross-layer edges surface. Results dedup byEdgeIdto handle promoted edges. - Signed Int64 columns stringified after
compact()(#306, closes #301): negative-containingInt64columns were routed toInferredType::Dict, silently becomingValue::String(WHERE n.num = 100returned zero rows). Signed columns now use the newRawI64codec, preserving type and ordering through compaction. - Silent text/vector search on file-backed DBs (#309, closes #308):
WalGraphStoreandCdcGraphStorefell through to theGraphStoreSearchtrait defaults (all no-ops), so every index lookup on aGrafeoDB::open()session silently returned "no index." Both wrappers now delegate everyGraphStoreSearchmethod toself.inner. - Cypher aggregate substitution inside CASE WHEN (#300): aggregates wrapped in
CASE WHEN ... THEN sum(...) ...now substitute the reduced value post-aggregation instead of leaving an unresolved reference. - VectorScan
k=Nonerisked HNSW overflow (#299 follow-up): unbounded k was being passed asusize::MAX, degrading HNSW to full traversal and risking overflow in quantized rescore. The planner now bounds k to the label's node count via a newnodes_by_label_counttrait method. - Native SIMD kernels read past
bon mismatched slice lengths (#312, closes #311): AVX2, SSE, and NEON kernels drove the main loop froma.len()and raw-pointer-loadedb, guarded only by adebug_assert_eq!. The four public*_simddispatchers now assert length equality in release. rustls-webpki0.103.13: clears RUSTSEC-2026-0104 (reachable panic in CRL parsing), pulled transitively viahf-hub -> ureq -> rustls.- GQL schema DDL:
CREATE GRAPH TYPEbare references no longer corrupt the catalog (#316):NODE TYPE PersonandEDGE TYPE KNOWSinside a graph-type body are now treated as references to existing catalog entries per ISO/IEC 39075:2024, not as empty inline redeclarations. The previous behavior silently wipedNOT NULLand other property constraints on the referenced types. References to undefined types now error cleanly atCREATE GRAPH TYPEtime.
Dependencies¶
proptest1.x added as workspace dev-dep and enabled ongrafeo-enginefor property-based tests.codspeed-criterion-compat3.x added as workspace dev-dep and enabled ongrafeo-common,grafeo-core,grafeo-storage,grafeo-engine. Drop-in for Criterion; pass-through outsidecargo codspeed run.tempfileadded as a dev-dep ongrafeo-spec-testsfor the_persistentvariants.arc-swap1.x added to the workspace and enabled ongrafeo-coreunder thecompact-storefeature, backing theLayeredStorebase pointer for lock-free atomic swap.
Thanks to @temporaryfix for substantial work this cycle: the two compact-store correctness fixes (#306, #307) and the property-based suite that surfaced them (#303), the hybrid-on-persistent fix (#309), the Cypher CASE aggregate fix (#300), the VectorScan k bounding follow-up to #299, the native SIMD safety assert (#312, closes #311), CodSpeed benchmark CI (#304), and the wasm32 simd128 distance kernels (#305).
[0.5.40] - 2026-04-20¶
Unified hybrid queries (graph + vector + text), lazy streaming results, structured Python errors, catalog hierarchy hardening, and compact-store fixes.
Added¶
- Unified hybrid queries:
text_score()andtext_match()usable as filter expressions, with planner pushdown of score predicates toTextScan/VectorScanoperators, compound AND/OR joins, top-K recognition, and score projection. Inspired by #287 (@temporaryfix); reimplemented via theGraphStoreSearchsubtrait. - BM25 text scan operator:
TextScanOperatorwith top-K and threshold modes.InvertedIndexgainsscore_document,search_with_threshold,bm25_term_score. (#287, @temporaryfix) - Native Float64 and Float32Vector codecs: CompactStore stores them directly instead of falling back to dictionary encoding. Mixed
Int64+Float64columns coalesce to Float64. (#286, @temporaryfix) - Streaming query results (experimental):
Session::execute_streamingreturns aResultStreamthat pulls oneDataChunkat a time, bounded memory regardless of result-set size. Exposed across bindings: Pythonexecute_lazy(), Node.jsexecuteStream(), C#ExecuteStream(), DartexecuteStream(), and Go and C FFI equivalents. Rejects mutations, EXPLAIN/PROFILE, session commands, and push-only plans. - Python
GrafeoErrorexception: subclass ofRuntimeErrorcarryingerror_code("GRAFEO-Q001") andis_retryable. Legacyexcept RuntimeError:paths keep working. - Error codes reference: user-guide page documenting every
GRAFEO-*code, retry semantics, and a Python retry-loop sample. - Catalog hierarchy docs (ISO/IEC 39075): user-guide page covering schemas, named graphs, session state, isolation, and cross-schema transactions.
Changed¶
DatabaseStats.memory_bytesreflects the full heap breakdown: now equalsmemory_usage().total_bytes(store + indexes + MVCC + caches + string pools + buffer manager) instead of just buffer-manager-tracked bytes.- Schema and graph names reject
/:CREATE SCHEMA/CREATE GRAPHnow fail on names containing/, which Grafeo uses internally as the compoundschema/graphstorage-key separator.
Fixed¶
- Multi-schema transaction atomicity:
SESSION SET SCHEMAmid-transaction no longer loses pre-switch writes on COMMIT. Fix centralizes "touched graph" tracking inside the session setters so every active-key change is recorded. - Commit failure auto-rollback: a failed COMMIT now discards pending writes and returns the session to a clean state, instead of leaving the transaction in-flight with uncommitted writes still visible.
- Parser keyword anti-pattern:
try_accept_keywordhelpers fix fourCREATE CONSTRAINT ... FOR,ON REPLACE, and similar sites where identifier-fallback tokenization accepted the wrong keyword. - Vector strict pushdown boundary leak:
euclidean_distance(...) < tandmanhattan_distance(...) < tnow correctly exclude rows at exactly the threshold; strict comparisons attach a residual filter above the vector scan, which was previously dropped. - MERGE index lookup:
MERGE (n:Label {prop: value})now uses property indexes when available, eliminating O(n) scan on large graphs. (#288) - Index and search after
compact(): ~26 vector/text index methods no longer panic with "no built-in LpgStore" or silently return empty results. (#286, @temporaryfix) LayeredStorenew-node visibility:get_node/get_node_propertyfall back to the overlay for nodes added aftercompact(). (#286)- Named graphs across
compact()/recompact(): graphs existing before compaction are carried into the new overlay;list_graphs,drop_graph,create_graph,set_current_graphsee them. - Layered scan lock holding:
nodes_by_labelacquiresdirty_node_idsonce per scan instead of re-locking in the chunk loop. (#278, @temporaryfix)
[0.5.39] - 2026-04-16¶
Block-STM conflict partitioning, push-based query execution, AES-256-GCM encryption at rest, runtime metrics with Prometheus export, and a writable layered compact store.
Added¶
- Block-STM conflict partitioning: groups conflicting transactions into disjoint clusters for parallel re-execution.
- Encryption at rest (
encryptionfeature): AES-256-GCM for WAL records and.grafeosections. Password-based (Argon2id) or raw-key setup. Zero overhead when disabled. - Push-based pipeline execution: filter, sort, aggregate, limit, and distinct queries execute through a push pipeline, reducing per-row overhead.
- Runtime metrics (
metricsfeature): query, transaction, session, cache, and GC counters with Prometheus text export. Pythondb.metrics()/db.metrics_prometheus()and Node.js equivalents. - C# enterprise APIs: schema management, backup/restore, compact, projections, CDC toggle, plan cache.
IGrafeoDBandITransactioninterfaces for DI. - Resource limits: default 30-second query timeout (
Config::with_query_timeout()), 16 MiB property value size limit (max_property_size), HNSWmax_elementsbound. - Layered store (
compact-storefeature):compact()produces a writable two-layer store (columnar base + overlay) instead of a read-only snapshot.recompact()merges the overlay back. Versioned section format with CRC32 integrity and ID-preserving builds. - WAL benchmarks: Criterion benchmarks for write throughput, batch commit, and recovery replay.
Changed¶
compact()is now non-destructive: creates a writable layered store instead of converting to read-only mode.- Cast clippy lints re-enabled:
cast_possible_truncation,cast_sign_loss,cast_possible_wrappromoted towarnworkspace-wide. - Leaner WASM builds: removed
grafeo-storage,crc32fast,anyhowfrom WASM targets. Binary size: 650 KB gzipped. CI threshold: 660 KB warn, 700 KB fail. - Expand locality optimization: sorts input chunks by source node ID before adjacency lookups on large traversals.
- CI hardening: MSRV verification (1.91.1), typos check, supply-chain audit as required status check, benchmark regression gating for core paths, Node.js matrix reduced to 22/24.
- Release pipeline hardening: explicit publish errors, pre-publish version consistency gate. Removed stale
deny.tomlskips, updatedrustls-webpki(RUSTSEC-2026-0098).
Fixed¶
- SSI validation race: concurrent commits could miss read-write conflicts due to a gap between state update and epoch recording. Both locks now held atomically.
- Transaction lock ordering: consistent write-lock ordering in
commit()andgc(), eliminating a potential deadlock from the previous read-then-upgrade pattern. - EXPLAIN/PROFILE nesting bypass: recursive EXPLAIN/PROFILE in GQL and Cypher now counts toward the 128-level nesting limit.
- Session commit atomicity:
touched_graphsclone-then-clear replaced with atomicmem::take(). - WAL encryption nonces: fixed reuse on restart, collisions across sections, and u64-to-u32 truncation. Old log file now fsynced before rotation.
- HKDF key derivation: added domain-separation salt, preventing cross-protocol key reuse.
- Parser overflow hardening: integer overflow in Cypher, SQL/PGQ, Gremlin, GraphQL now returns errors instead of silently producing
0. Float overflow (1e999) in GraphQL rejected. - Numeric cast safety: arena allocator, block serializer, buffer manager, DPccp optimizer, temporal constructors, and
toInteger()all use checked arithmetic instead of unchecked casts. - DPccp join optimizer: fixed
BitSetoverflow for 64+ relations, added 100K iteration budget to prevent stall on large joins. - DISTINCT hash collisions: content-based hashing for List, Map, Vector, and Path values.
- Parameters in subqueries:
$paraminsideEXISTS/COUNT/VALUEsubqueries now substituted correctly. (@temporaryfix) - SHACL SPARQL injection: IRI validation prevents breakout via crafted
$thisvalues. - CDC history without permission: now requires RBAC read permission.
- SIMD and arena checks: promoted debug-only vector length and alignment validation to release builds.
- Buffer manager TOCTOU race: replaced non-atomic check-then-allocate with
compare_exchangeloop. - RDF dictionary panic: graceful error on u32::MAX entry overflow instead of panic.
- Windows memory detection: reads actual physical memory instead of falling back to 1 GB.
- Python
execute_sqllanguage mismatch: standardized to"sql"across all bindings. - Gremlin
range(5, 2)overflow: returns error instead of panic when end < start.
[0.5.38] - 2026-04-13¶
Hardening, ISO compliance, and vector search improvements driven by persona-based exploratory testing. Parser security limits prevent stack overflow attacks, all six query languages gain EXPLAIN support, Unicode identifiers bring GQL closer to ISO 39075, and quantized vector indexes cut memory usage up to 4x for large embedding workloads.
Breaking: QueryBuilder.param() now raises ValueError on unsupported types instead of silently dropping them: code that relied on silent fallthrough will see exceptions and should fix the type conversion. Parser error messages now include a language prefix (e.g., [GQL] Unexpected token): code that pattern-matches on error strings may need updating. SPARQL SERVICE clauses now return an explicit error instead of silently executing the inner pattern against the local store: queries that appeared to work but returned incorrect results will now fail with a clear message. SPARQL property path +/* expansion depth raised from 10 to 50: queries on deep hierarchies will return more complete results, which may increase result set sizes and execution time.
Added¶
- Quantized vector indexes:
create_vector_index()acceptsquantizationparameter ("scalar","binary","product") for 4x memory reduction on large vector datasets.VectorIndexKindenum unifies plain and quantized indexes throughout the engine. All bindings (Python, Node.js, WASM, C) updated. - EXPLAIN/PROFILE for all 6 query languages: Gremlin, GraphQL, and SQL/PGQ now support
EXPLAINandEXPLAIN ANALYZEprefix, matching existing GQL, Cypher, and SPARQL support. Pythonexplain(),explain_cypher(),explain_sql(),explain_gremlin()convenience methods added. - Unicode identifiers: GQL, Cypher, and SQL/PGQ parsers now accept Unicode letters in identifiers (e.g.,
CREATE (:人物 {名前: 'Alix'})), per ISO GQL 39075. Gremlin and GraphQL already supported this. - Unicode string escapes:
\uXXXX(4-digit BMP) and\UXXXXXXXX(8-digit full range) escape sequences in string literals across all query languages. - CONSTRUCT output serialization: Python
QueryResult.to_ntriples()andto_turtle()methods for SPARQL CONSTRUCT results. - NetworkX ID round-tripping:
from_networkx()preserves original node IDs via_networkx_idproperty,to_networkx()restores them. Returns a node mapping dict. - GQL
!=operator: accepted as alias for<>(not-equal comparison).
Changed¶
- Parser error messages now identify the language: all 6 parsers prefix errors with
[GQL],[Cypher],[SPARQL],[Gremlin],[GraphQL],[SQL/PGQ]. - SPARQL property path depth raised to 50:
+/*paths now expand to 50 hops (up from 10), covering most real-world taxonomies and org charts. QueryBuilder.param()raisesValueError: previously silently dropped unsupported types, now raises with a descriptive message.execute_async()documentation: docstring now explains that it usesspawn_blocking(releases GIL, uses thread pool, not truly non-blocking I/O).
Fixed¶
- Parser recursion depth limits: all 6 parsers now enforce a 128-level nesting limit, preventing stack overflow on deeply nested malicious input (DoS vector).
- SPARQL SERVICE clause returned wrong results silently: now returns an explicit error instead of executing the inner pattern locally.
- GQL integer overflow produced confusing errors: overflow on integer literals now reports the value and valid i64 range.
- NetworkX
in_degree()was O(V*E): replaced full-graph scan with direct adjacency index lookup. AsyncQueryResultmissingnodes()/edges(): entity extraction now runs post-spawn_blocking, matching syncQueryResult.- RDF blank node collisions across imports: Turtle parser now prefixes blank node IDs per-import (
_:imp{N}_b0), preventing cross-file collisions. - Incremental backup always failed after full backup (#267): the backup cursor stored the active WAL file's sequence without rotating, so post-backup writes stayed invisible to incremental. Both
backup_fullandbackup_incrementalnow rotate the WAL after completing, ensuring new writes land in a file the next incremental will pick up. - Edge variables in multi-hop queries returned as raw IDs (#268):
plan_expand_chainandplan_factorized_aggregatedid not register edge columns in the planner's tracking set, causing RETURN to emitNodeResolveinstead ofEdgeResolve. Edge variables now resolve to full maps with_id,_type,_source,_target, and properties. - Arrow/DataFrame export dropped user properties named
source/target/id/type(#272): structural columns inedges_to_arrow(),edges_df(),nodes_to_arrow(), andnodes_df()collided with user property names, silently dropping them. Structural columns are now underscore-prefixed (_id,_type,_source,_target,_labels) to match the engine'sedge_to_map()/node_to_map()convention. Breaking: code referencingdf["source"]must change todf["_source"]. - Weighted hybrid search inverted vector ranking:
hybrid_search()withfusion="weighted"applied min-max normalization to raw vector distances, causing the farthest node to score highest. Vector distances are now negated before fusion so that closer vectors rank higher.
Documentation¶
- Search score conventions: new table in the Vector Search guide clarifying return value semantics across all search methods (
vector_searchreturns distances, lower = better;hybrid_searchreturns fusion scores, higher = better;mmr_searchreturns distances in MMR selection order;text_searchreturns BM25 scores, higher = better). - Text Search guide: new dedicated page covering BM25 index creation, searching, auto-sync behavior, and when to rebuild.
- Hybrid Search guide: new dedicated page covering RRF vs weighted fusion, prerequisites, graceful degradation, and the common pitfall of treating fusion scores as distances.
- MMR Search guide: new dedicated page covering Maximal Marginal Relevance parameters, lambda tuning, and when to use MMR vs vector search.
- Index auto-sync clarified:
rebuild_vector_index()andrebuild_text_index()docs now explain that indexes auto-sync onset_node_property()and batch operations; explicit rebuild is rarely needed. Updated across Rust doc comments, Python/Node.js binding docstrings, and API reference pages.
[0.5.37] - 2026-04-12¶
RDF Semantic Web overhaul with improved SPARQL support, RDF performance improvements and SHACL validation.
Added¶
- SPARQL compliance pass: spec gaps closed.
CONSTRUCT,BIND,OPTIONAL,MINUS,UNION,FILTER,EXISTS/NOT EXISTS. Named graph CRUD and SPARQL UPDATE. Composite indexes (SP, PO, OS) for O(1) multi-bound lookups. new W3C tests. - Ring Index planner (
ring-index): wavelet-tree compact triple index wired into SPARQL planner. Leapfrog WCOJ for multi-way star joins, hash join fallback when LANG/DATATYPE columns needed. - Ring Index persistence: bincode serialization with post-load invariant validation.
RdfRingSectionpersists to.grafeocontainer, eliminating rebuild on restart. - Dictionary encoding infrastructure:
TermDictionarymaps terms to u32 IDs.DictResolveOperatorresolves at result boundaries. Built lazily, invalidated on mutation. - COUNT(*) fast paths: O(1) for unbound scans via
store.len(), O(log sigma) for bound patterns via Ring Index. - RDF query optimizer: per-predicate cardinality estimates, cached statistics, cost-based join reordering.
- SPARQL EXPLAIN / EXPLAIN ANALYZE: physical plan tree without executing, or profiled execution with per-operator timing. Python
explain_sparql()binding. - SHACL validation (
shacl): W3C Shapes Constraint Language with all 28 Core constraint types, SHACL-SPARQL (sh:sparql), 7 property path types with cycle detection,ValidationReportwithto_triples()RDF materialization.session.validate_shacl(shapes_graph)in Rust,db.validate_shacl("graph")in Python. Inrdfpersona andserverprofiles. - Arrow bulk export (#260):
nodes_to_arrow()/edges_to_arrow()(pyarrow Table),nodes_to_polars()/edges_to_polars()(Polars DataFrame),nodes_to_pandas()/edges_to_pandas()(pandas DataFrame via Arrow). Builds RecordBatch in Rust, serializes to IPC: ~10-100x faster than per-elementnodes_df()/edges_df()at scale. Existingnodes_df()/edges_df()auto-use the Arrow fast path when pyarrow is available.
Changed¶
- RDF store indexes upgraded to
foldhash: replacedahashwithfoldhash::fast::RandomStatefor all RDF HashMap indexes. TxIdrenamed toTransactionId: consistent naming across the codebase.
Fixed¶
- Incremental backup could skip WAL records: backup cursor was not advanced, causing duplicate replay on restore (#258).
- File manager leaked temp files on checkpoint failure: temp files now cleaned up in the error path (#258).
[0.5.36] - 2026-04-11¶
Authentication at engine level with RBAC, per graph access grants and several query language improvements.
Added¶
- Role-based access control:
Identity,Role(Admin,ReadWrite,ReadOnly), andStatementKindtypes for scoping sessions to specific permission levels.db.session_with_identity(identity)creates a session bound to an identity,db.session_with_role(role)is a convenience shorthand. Permission checks run after parsing but before execution across all query languages (GQL, Cypher, Gremlin, GraphQL, SQL/PGQ, SPARQL). No credentials or crypto at this layer: the caller is trusted to assign the correct role. - Graph projections: read-only filtered views of a graph store via
ProjectionSpecandGraphProjection. Filter by node labels and edge types to create virtual subgraphs for algorithms and queries. Manage withcreate_projection()/drop_projection()/list_projections()in Rust, Python, Node.js, WASM, and C. GQL syntax:CREATE PROJECTION name LABELS (...) EDGE_TYPES (...),DROP PROJECTION name,SHOW PROJECTIONS. - Gremlin
repeat().times()/.emit(): parse and executerepeat(out()).times(n)for fixed-depth traversal andrepeat(out()).emit()for all-depths traversal. Maps to the existingVariableLengthExpandoperator.until()predicates,path(),simplePath(), andloops()remain pending. - CSV/JSON Lines import: CLI
grafeo import csv/grafeo import jsonlcommands, Pythonimport_csv()/import_jsonl(), Node.jsimportCsv()/importJsonl(). - Per-graph access grants:
Granttype scopes an identity's access to specific named graphs.Identity::with_grants([Grant::new("social", Role::ReadWrite)])restricts access to listed graphs only.USE GRAPH,CREATE GRAPH,DROP GRAPHenforce grants when present. Empty grants = unrestricted (backward compatible).
Changed¶
- Unified aggregate accumulator: push-based aggregate operator now uses the same
AggregateStateas the pull-based operator, gaining support for all 30+ aggregate functions (COLLECT, LAST, STDEV, percentiles, regression, etc.) that previously returned NULL in push mode. session_read_only()deprecated: usesession_with_role(Role::ReadOnly)instead. The old method remains as an alias.
Fixed¶
- Release workflow missing
grafeo-storage: the crate publish sequence now includesgrafeo-storagebeforegrafeo-engine, fixing cascading publish failures. - Permission bypass in parameterized queries:
_with_paramsmethods used a text heuristic to gate write permissions, which had false negatives for languages like GraphQL. Restricted identities now use plan-based mutation detection. - Projection
neighbors()ignored edge-type filter: neighbors connected via excluded edge types were incorrectly returned. - Projection
edge_type()leaked hidden edges: edges whose endpoints were excluded by label filtering could still have their type queried. - Spill serialization dropped DISTINCT semantics: DISTINCT aggregate variants are now serialized via finalized-value fallback to avoid corrupting results after reload.
- Gremlin
times()accepted negative values: negative loop counts silently wrapped to huge values, now returns a parse error. - Gremlin nested repeat modifiers:
.times()/.until()/.emit()now work insideunion(),coalesce(), and other nested traversals. - Projections retained stale store after
compact():compact()now clears all projections to prevent stale data and memory leaks. randRUSTSEC-2026-0097: updated to 0.10.1.
[0.5.35] - 2026-04-11¶
Breaking: QueryResult.rows is now private (use rows()/into_rows()), all public enums are #[non_exhaustive] (add _ => arms), old feature profiles (embedded, browser, server, full) are deprecated in favor of persona-based profiles (lpg, rdf, analytics, ai, edge, enterprise) and the on-disk storage format changed from bincode blobs to block-based sections (databases created with 0.5.34 or earlier must be re-created).
Added¶
- Persona-based feature profiles: new named profiles
lpg,rdf,analytics,ai,edge,enterprisedescribe use cases instead of deployment targets. Compose them freely:features = ["lpg", "ai"]for a graph app with search,features = ["rdf", "analytics"]for a knowledge graph with algorithms. Old profiles (embedded,browser,server,full) remain as deprecated aliases with unchanged behavior, scheduled for removal in 0.7.0. - Python named graph management:
create_graph(),drop_graph(),list_graphs(),set_graph()/reset_graph()/current_graph(),set_schema()/reset_schema()/current_schema()(#241, #243 by @Michaelzag) - Python per-transaction CDC override:
begin_transaction_with_cdc(True|False)(#242, #244 by @Michaelzag) - Arrow IPC export (
arrow-export): zero-copy export to Arrow IPC for DuckDB, Polars, pandas, DataFusion interop - GEXF + GraphML export: graph interchange for Gephi, Cytoscape, NetworkX, yEd, igraph. CLI
--export-format gexf|graphml - Section-based container format:
.grafeofiles use a section directory with checksummed, independently addressable sections. Checkpoint writes only dirty sections, recovery loads in parallel. grafeo-storagecrate: persistence I/O extracted fromgrafeo-adapters.grafeo-coreandgrafeo-storageare now siblings (both depend only ongrafeo-common).- Unified flush model: checkpoint,
CHECKPOINT, and memory-pressure eviction share one code path - Regression + memory benchmarks: 11 Criterion benchmarks with per-benchmark thresholds, 5 memory benchmarks with CI bounds checking
- Section serializers: Vector Store (HNSW topology) and Text Index (BM25 postings) persist to container, eliminating index rebuild on open
- Per-section memory config:
SectionMemoryConfigwithmax_ramcaps andTierOverrideper section type - Mmap for index sections: zero-copy read via
memmap2, CRC-verified, cross-platform lifecycle - BufferManager section consumers: sections register as
MemoryConsumers for accurate pressure tracking - Periodic checkpoint timer: background flush at
Config::checkpoint_interval, bounds WAL size - Container format spec:
docs/architecture/storage/container-format.md - Vector spill to disk: vector columns drain to
MmapStorageunder memory pressure, search reads transparently from mmap - BufferManager spill integration: eviction calls
spill()on consumers after in-memory eviction exhausted - PropertyColumn eviction:
drain_values(),evict_values(),restore_values()withspilledflag - Block-based LPG section format (v2): replaces bincode blob with a structured layout: string table, packed node/edge arrays, columnar property blocks, label assignments, per-block CRC. Enables mmap for data sections.
- Block-based RDF section format (v2): replaces bincode with string-table-deduplicated triple storage, per-block CRC, named graph sub-sections.
- WAL overlay: in-memory mutation layer (
WalOverlay) for tracking inserts, updates, deletes on top of mmap'd base data. Supports drain/clear for checkpoint merge. - TieredStore trait:
StorageTierenum (InMemory, OnDisk, Uninitialized) andTieredStoretrait definingpersist(),open_mmap(),reload_to_ram()lifecycle ingrafeo-common. - CDC retention and eviction:
CdcRetentionConfigwithmax_epochsandmax_eventslimits.CdcLogimplementsMemoryConsumerfor BufferManager-driven eviction. Pruning hooks into MVCC GC cycle. (#250) - EpochAdvance WAL record: new
WalRecord::EpochAdvance { epoch }logged after eachTransactionCommit.is_metadata()trait method onWalEntry. Enables epoch-bounded WAL replay for point-in-time recovery. - Incremental backup:
backup_full(),backup_incremental(),restore_to_epoch()onGrafeoDB. Backup manifest tracks the chain, backup cursor in WAL directory prevents premature log truncation. CLI commands:grafeo backup full,grafeo backup incremental,grafeo backup status,grafeo backup restore-to-epoch. Exposed in Python, Node.js, and C bindings.
Changed¶
VersionChainusesVecinstead ofVecDeque: eliminates 4-slot minimum allocation per entity, reducing per-entity memory 14-31% (#251)- Schema DDL types decoupled from GQL parser: shared types (
SchemaStatement,PropertyDefinition, etc.) moved toquery::schemamodule, allowing Cypher to compile without thegqlfeature (#234) QueryResult.rowsis now private: userows()for borrowed access,into_rows()for ownership,push_row()/from_rows()for construction#[non_exhaustive]on 95 public enums: downstreammatchmust add_ =>wildcard arms- Python abi3 wheels: single wheel per platform supports Python 3.12+
rdffeature renamed totriple-store: deprecatedrdfalias kept for one release,rdfnow names the persona profilelpgfeature flag: LPG model is now explicit in grafeo-core/engine/adapters, symmetric totriple-store- Crate restructure: storage backends moved to
grafeo-storage,grafeo-adaptersis parser-only,grafeo-core/src/storage/renamed tocodec/ - Removed tokio from grafeo-core: async spill moved to
grafeo-engine/src/execution/spill/ - CI: benchmark job adds per-benchmark thresholds and baseline persistence on main
Fixed¶
- WAL not replayed on reopen: data written via mutations was lost across restarts when no explicit checkpoint was called. Session commits now log
TransactionCommit+EpochAdvanceto WAL, and the close path only removes the sidecar WAL after verifying the checkpoint wrote data. (#252) - CDC event log unbounded memory: the CDC log stored every mutation with full property snapshots and never pruned. Added epoch-based and count-based retention (
CdcRetentionConfig), integrated with BufferManager for memory-pressure eviction. (#250) VecDeque::first_mutcompile error: fixed tofront_mut()in MVCCVersionChain(tiered-storage feature path)- Graph/schema context validation: reject nonexistent targets,
drop_graph()auto-clears active context (#245, #246 by @Michaelzag) - C binding
grafeo_reset_schema: propagates errors instead of silently discarding - WASM
setSchema: returns proper JSErrorinstead of plain string - CI benchmark
--save-baseline: scoped to Criterion crates only - Aggregate grouping hash collision: wildcard arm now hashes
std::mem::discriminantto distinguish futureValuevariants edges_df()/nodes_df()column overwrite: properties named after structural columns (source,target,type,id,labels) no longer silently replace them (#254)
[0.5.34] - 2026-04-07¶
Pre-RC hardening: query engine fixes from external integration testing, format stability, feature matrix CI.
Added¶
- GQL schema hierarchy (ISO/IEC 39075 Section 4.2.5):
CREATE SCHEMA/DROP SCHEMA,SESSION SET SCHEMA, full data isolation between schemas - Streaming RDF triple sink:
TripleSinktrait withBatchInsertSink(bounded memory) andCountSink(dry-run) - Streaming Turtle/N-Triples load:
load_turtle_streaming(),load_ntriples_streaming()insert incrementally - Golden fixture tests: snapshot v4,
.grafeofile format, and WAL frame backward-read + byte-equality checks - Deterministic snapshot export: nodes/edges/labels/properties sorted by ID/name for reproducible exports
- Feature matrix CI: per-profile build+test jobs (gql-only, gql+vector, gql+rdf, embedded, browser)
- Serialization benchmarks: snapshot export/import and
Valuebincode round-trip
Changed¶
LabelRegistrycombined lock: mergedlabel_to_id+id_to_labelinto oneRwLock<LabelRegistry>, reducing write-path lock acquisitions#[non_exhaustive]on 13 public enums: future variants can be added without breaking semvermissing_errors_doc/missing_panics_doclints enabled: public functions now document error/panic conditions- MVCC types hidden:
VersionChain/VersionInfore-exports marked#[doc(hidden)]
Fixed¶
- WAL sync counter race:
fetch_subafter sync instead ofstore(0), preserving concurrent increments - Multi-aggregate extraction:
sum(a) + count(b)now extracts all aggregates - Mixed
WITH ... WHERE/HAVING: non-aggregate conjuncts stay as WHERE, aggregate parts become HAVING references_anycompleteness: allLogicalExpressionvariants handledCREATE SCHEMAduplicate WAL record: only logged when graph is actually createdBatchInsertSinkzero batch_size: defensivemax(1)clampdelete_node_edgesself-loop: deduped viaHashSet, batch edge lock, batch adjacency lockcypherfeature needinggqldependency (#232, #233 by @Michaelzag)- Node.js napi cfg-gated methods: moved methods into per-feature
#[napi] implblocks BitPackedInts::from_bytes:bits_per_value > 64now returnsErrinstead of panicking- WAL
log_files: directory read errors now propagated instead of swallowed - Per-feature compilation: missing cfg gates across
vector-index,rdf,mmap,regex - Algorithm unreachable panics:
expect("node in index")replaced withenumerate/let-elsein functions - Null property pattern matching:
MATCH (n {key: null})now matches nodes where the property is absent or explicitly null (MERGE and MATCH) - MERGE null key matching:
MERGE (n:T {a: null, b: 'x'})correctly finds existing nodes with absent/nulla - SET += {key: null} removes property:
SET n += {price: null}now removes the property instead of keeping it as null - Negative LIMIT clamped to 0:
LIMIT -1returns empty result instead of raising a syntax error (GQL and Cypher) - i64 MIN literal parsing:
-9223372036854775808now parses correctly by folding-<integer>at parse time (GQL and Cypher) - NaN/Inf float literals:
NaN,Inf,Infinityrecognized as IEEE 754 special float values (GQL and Cypher) nodes(p)resolves to property maps:nodes(path)now returns node maps with properties instead of raw Int64 IDs, enabling[n IN nodes(p) | n.name]- Cyclic VLP pattern matching:
MATCH p=(s)-[:R*]->(s)now filters expanded targets to match the source node viaid()equality - VLP default depth raised: unbounded
[*]now expands up tomin_hops + 100(was+10)
[0.5.33] - 2026-04-05¶
GraphChallenge benchmark suite, RDF-to-LPG bridge, and a large round of query engine correctness fixes.
Added¶
- GraphChallenge algorithms (DARPA/MIT IEEE HPEC 2026): k-truss decomposition, parallel triangle counting, subgraph isomorphism (VF2), stochastic block partition, partition quality metrics (Rand index, NMI, precision, recall)
- TSV/MMIO bulk import:
import_tsv(),import_mmio(),import_tsv_rdf()for fast graph loading bypassing per-edge transaction overhead RdfGraphStoreAdapter: bridgesRdfStoretoGraphStore, giving RDF graphs access to all graph algorithms- grafeo-cli PyPI publish workflow (#222)
Fixed¶
- CompactStore multi-table edge types: same edge type across multiple label pairs now produces separate
RelTables. Addedrel_tables_for_type()(#221, #225 by @Imaclean74) - WAL deadlock on property mutations: store mutation now applied before WAL logging, matching lock ordering of create/delete methods
- GQL
CREATE INDEX ... FORparsing:FORaccepted whether lexed as keyword or identifier round()/floor()/ceil(): float inputs returnFloat64instead of truncating toInt64CALL ... YIELDwith aggregation: aggregates now work over procedure results- Cypher keyword-as-label:
Order,By,Skip,Limitusable as node labels - CompactStore edge type statistics: counts aggregated across multiple rel tables
CAST(bool AS INT):truecasts to1,falseto0- List
+concatenation:[1, 2] + [3, 4]returns[1, 2, 3, 4] - Parameter substitution in multi-statement queries:
$paramvariables now forwarded to intermediate statements - ORDER BY + LIMIT/SKIP: SKIP and LIMIT now apply after ORDER BY
- MIN/MAX aggregate output type: uses
Anyinstead ofInt64, fixing coercion for Float64 and Date values - Cypher ORDER BY after aggregation: property references resolve correctly after GROUP BY
- JOIN column deduplication: multi-pattern MATCH with shared variables no longer produces duplicate columns
- SET self-reference:
SET n.value = n.value + 1pre-computes expressions before the property write size(collect())nested aggregate: no longer panics during aggregate extractionWITH ... WHEREon aggregate alias: WHERE predicate correctly promoted to HAVINGSUM()on empty result set: returnsnullper ISO GQL
Performance¶
- Triangle counting: oriented adjacency built directly from
GraphStore, improving cache efficiency on CSR-backed stores - WAL
sync_all()outside lock: reduces lock contention under concurrent writes - Kahan compensated summation:
sum()uses Kahan algorithm to reduce floating-point rounding errors
[0.5.32] - 2026-04-03¶
Correctness hardening, Jepsen readiness, and Hybrid Logical Clock for causal consistency.
Added¶
GrafeoDB::compact(): converts a live database to a read-onlyCompactStorein one call. Available asdb.compact()in Python, Node.js, WASM;grafeo_compact(db)in C. Included inembeddedandbrowserprofiles by default (#199)- Hybrid Logical Clock (HLC):
HlcTimestamppacks physical ms (48-bit) + logical counter (16-bit) into a u64 with lock-free CAS for monotonic timestamps. Replaces wall-clockSystemTime::now()in CDC events - CDC for session mutations:
CdcGraphStoredecorator buffers CDC events during transactions, flushes on commit (discards on rollback). Session-driven mutations via GQL/Cypher now generate CDC events - Session CRUD methods:
set_node_property(),set_edge_property(),delete_node(),delete_edge(),create_edge_with_props()on Session for transaction-aware direct mutations - Gremlin
valueMap()andelementMap()with no arguments: returns all properties (or id + label + all properties) as a map - Stress and crash tests: WAL-disabled crash injection, concurrent MERGE, mixed read/write contention, concurrent schema mutations, and 5 epoch monotonicity stress tests for CDC
- Expanded gtest suite: 4 real-world datasets (e-commerce, movies, IT infrastructure, transportation), gap tests for all languages (GQL, Cypher, Gremlin, SQL/PGQ, SPARQL), Rosetta cross-language fidelity, production coverage (data type round-trips, mutation patterns, input validation), parameter substitution, catalog diagnostics, index correctness, temporal queries, and algorithm tests (Dijkstra, PageRank, centrality, BFS, SCC)
Fixed¶
- Sibling CALL block scope collision: same-named variables in sibling
CALLblocks no longer clobber each other (#213) - GROUP BY hash collisions:
hash_value()now uses discriminant tags for allValuevariants, preventing cross-type collisions; addedDate,Time,Timestamp,Duration,ZonedDatetime,Bytes,Mapvariants toGroupKeyPart - Cypher ORDER BY zeros with relationship traversal: planner now resolves to the existing projected column instead of returning zeros (#218)
Changed¶
- CDC is now opt-in per session: no longer unconditionally active when compiled in.
Config::with_cdc()andGrafeoDB::set_cdc_enabled()control the default (off). Fixes +251% regression on single-node inserts. Python:GrafeoDB(cdc=True). Node.js:db.enableCdc(). C:grafeo_set_cdc_enabled(db, true) - CompactStore native codec scans:
find_eq()andfind_in_range()push checks into the codec's native domain instead of decoding toValueper row. Thanks to @temporaryfix (#216)
Internal¶
- SPARQL ORDER BY STR() tests tightened: removed error-accepting fallback;
NullGraphStoreis correct for expression evaluation - Vector search
$ne/$ninNULL semantics: documented and regression-tested (SQL three-valued NULL semantics)
[0.5.31] - 2026-04-01¶
CompactStore: a read-optimized columnar graph store for memory-constrained environments. Thanks to @temporaryfix for the design, prototype and implementation (#199, #204). Also, all remaining syntax gaps covered by the gtest suite are now fully implemented!
Added¶
compact-storefeature flag: opt-in columnar read-only store for WASM, edge workers and embedded devices. Per-labelNodeTables with typed columns, double-indexedCsrAdjacencyfor O(degree) traversal, zone-map skip optimization, and a fluentCompactStoreBuilderAPI with build-time validation. Integrates viaGrafeoDB::with_read_store(Arc<dyn GraphStore>), all query languages work through it- Benchmark:
compact_benchescriterion group withnodes_by_label,get_node_property, andedges_frombenchmarks for CompactStore execute_language(language, query, params)in Python and Node.js bindings: generic dispatch for non-standard language keys (e.g."graphql-rdf") without needing dedicated methods- SQL/PGQ UNION, INTERSECT, EXCEPT: full set operation support between GRAPH_TABLE queries, with optional ALL modifier
- GraphQL multiple root fields and variable substitution:
{ person { name } company { name } }now translates all root fields via Union instead of dropping all but the first;$variablereferences emitLogicalExpression::Parameterwith default value propagation from query declarations - Binding spec runner
params:support: Python, Node.js, Go, and C# test runners now pass gtestparams:fields to parameterized execution methods DatabaseInfo.features:db.info()now returns afeaturesarray listing all compiled feature flags (e.g.["gql", "cypher", "algos", "vector-index"]), available in all bindings (Python, Node.js, WASM, C, Go, C#, Dart)- WASM
lpgandrdfbuild profiles: two new named profiles joinbrowserandfull.lpgbundles all LPG query languages plus AI search;rdfbundles SPARQL and GraphQL over the RDF model
Fixed¶
- GQL list slice and path search:
[1..3],[..2],[3..]slices now work (one-char lexer bug);MATCH ANY p = ...andMATCH p = ANY SHORTEST ...path search prefixes now use the existing shortest-path BFS operator - SPARQL pattern matching: MINUS with disjoint variables returns left side unchanged per spec;
<p>*/<p>?property paths include zero-length reflexive match; VALUES with UNDEF produces correct partial bindings; anonymous blank node[]as subject expanded correctly - SPARQL function and type evaluation: STRLEN, CONCAT, IF, COALESCE, arithmetic work in SELECT/BIND projections;
STRDT()produces typed values;DATATYPE()companion columns track original XSD types through scans; subquery aggregation propagates to outer queries - SPARQL graph management:
GRAPH ?gscans only named graphs per spec 13.3;FROM/FROM NAMEDrestrict visible graphs per spec 13.1-13.2;CLEAR ALLclears both default and named graphs;DESCRIBEreturns Concise Bounded Description - SPARQL updates and literals:
DELETE { ... } WHERE { ... FILTER(...) }applies the filter correctly; language-tagged literal comparison checks both value and tag - Gremlin traversal fixes: multi-hop dead end no longer causes "Column not found";
values()with no keys returns all properties; scalar values in union branches no longer coerced toNodeId(0);path()on empty traversal returns empty result set - Cypher
CREATE INDEX/DROP INDEX/SHOW INDEXES: indexes now registered in the catalog, persisting across statements - GraphQL aggregation:
personCount,personAggregate { sum_age }, and_countfield patterns now emit proper aggregate operators - RDF GraphQL: per-test
language: graphql-rdfdispatch for mutation rejection testing;first/limit/skip/offsetpagination in the RDF translator
Performance¶
- RDF schema type propagation:
plan_operatorthreads concreteLogicalTypes through the entire plan tree instead ofLogicalType::Any, keeping triple scan data inVec<ArcStr>(8 bytes/entry) through joins, sorts, and projections instead ofVec<Value>(40 bytes/entry)
Internal¶
- Spec runner feature detection: all 6 binding spec runners (Python, Node.js, WASM, C#, Dart, Go) now use
db.info().featuresto detect available capabilities instead of probing for individual methods, eliminating false skips for non-language features likealgosandvector-index ValueVectorpush safety net: type-mismatched pushes now fall back toVectorData::Genericinstead of silently dropping dataderive_rdf_schemaremoved: replaced by concrete type propagation throughplan_operatorreturn valueseval_functionsplit: 1,687-line monolith refactored into a thin dispatcher and 9 focused category methods- Dedup macros and utilities:
impl_algorithm!forGraphAlgorithmboilerplate (17 of 23 implementations),map_common_keywords!for shared lexer keyword mapping,unescape_stringextracted to shared module,extract_and_mapgeneric for binding entity extraction
[0.5.30] - 2026-03-30¶
Async storage foundation and continued test coverage. Thanks to @maxwellflitton for the async storage adapter discussion that shaped this release.
Added¶
async-storagefeature flag: new opt-in feature for async WAL and storage operations, included inserverprofileAsyncTypedWal<R>: type-safe async WAL wrapper mirroring syncTypedWal<R>, with identical on-disk format for cross-recovery compatibilityAsyncLpgWal: type alias forAsyncTypedWal<WalRecord>, the async equivalent ofLpgWalAsyncWalManager::write_frame: extracted low-level frame writer enabling genericWalEntrytypes in async contextAsyncWalGraphStore: async decorator that logs mutations toAsyncLpgWalbefore applying toLpgStore, with named graph context tracking via tokio mutexGrafeoDB::async_wal_checkpoint(): async WAL checkpoint viaspawn_blocking, avoids blocking the tokio runtime during fsyncGrafeoDB::async_write_snapshot(): async snapshot write viaspawn_blockingfor.grafeosingle-file formatAsyncStorageBackendtrait: object-safe async trait for pluggable persistence backends (WAL batches, snapshots, sync), enabling community implementations for Postgres, S3, etc.AsyncLocalBackend: built-in local filesystem implementation wrappingAsyncLpgWalSnapshotMetadata: metadata type for snapshot listing in async backends- Node.js
walCheckpoint()andsave(): new sync methods for checkpoint and persistence in Node.js bindings
Fixed¶
- 86 stale spec test skips removed: path modes (TRAIL, SIMPLE, ACYCLIC, WALK), ALL SHORTEST search prefix, list slice syntax, SPARQL string/datetime/hash functions, RDF term construction, conditional functions, named graphs, property paths, GraphQL directive evaluation, and more
- SPARQL dateTime extraction functions: YEAR, MONTH, DAY, HOURS, MINUTES, SECONDS, TIMEZONE, TZ now correctly parse typed
xsd:dateTimeliterals with timezone offsets - SPARQL LANGMATCHES(): implemented RFC 4647 basic filtering with case-insensitive prefix matching and wildcard
"*"support - SPARQL LANG() companion columns: language tags are now tracked through triple scans and available to LANG()/LANGMATCHES() in FILTER
- SQL/PGQ parameters in WHERE:
$name,$min_ageparameter references now resolved in filter evaluation via gtest runner wiring - SQL/PGQ HAVING inline aggregates:
HAVING COUNT(*) > 0and other inline aggregates in HAVING clauses now correctly extracted and referenced - SQL/PGQ zero-length paths:
*0..Nvariable-length patterns now emit the source node as a 0-hop match - Cypher
collect(DISTINCT ...):size(collect(DISTINCT n.v))now correctly extracts the wrapped aggregate through non-aggregate function calls
[0.5.29] - 2026-03-29¶
Query engine correctness improvements and unified declarative test suite.
Added¶
- Turtle parser and serializer: zero-dependency W3C Turtle support (
load_turtle(),to_turtle()onRdfStore), with prefix detection, subject grouping, numeric/boolean shorthands,ashorthand, and line/column error positions - N-Quads serializer:
to_nquads()onRdfStorefor exporting default and named graphs in a single stream - Declarative
.gtestspec test framework: newgrafeo-spec-testscrate with a YAML-based test format, build.rs code generator, and runtime comparison library. 2500+ tests across all 7 language/model combinations (GQL, Cypher, Gremlin, GraphQL (LPG+RDF), SQL/PGQ, SPARQL and Rosetta cross-language) from a single source of truth, with runners for binding-level verification - EXISTS subquery in RETURN:
RETURN EXISTS { MATCH (n)-[:R]->(:Label) } AS flagnow works for single-hop correlated patterns, including label-filtered endpoints - Aggregate detection in GQL WITH:
WITH count(n) AS cnt, max(n.val) AS mxnow correctly produces an aggregate operator instead of treating aggregates as scalar expressions
Changed¶
- Adjacency list memory: replaced
SmallVec<8>withVec(struct 256 to ~144 bytes), added auto-compaction inadd_edge()to fix unbounded delta buffer growth
Fixed¶
- Integer arithmetic overflow:
9223372036854775807 + 1no longer panics; checked arithmetic returns NULL on overflow (SQL semantics) for all operations (+, -, *, /, %, unary negation) - Label intersection across MATCH clauses:
MATCH (n:A) MATCH (n:B)now correctly filters to nodes with both labels instead of ignoring the second label constraint - CASE WHEN with NULL aggregate:
WITH count(c) AS cc RETURN CASE WHEN cc = 0 THEN 0 ELSE ... ENDno longer returns NULL when the WHEN branch is true - EXISTS with property filters:
EXISTS { (n)-[:R]->(m) WHERE m.age > 30 }silently dropped the WHERE, matching all connected nodes - Keywords as property names:
{order: 3}andn.orderrejectedorderand other keywords in property contexts - Gremlin
hasLabelon edges:g.E().hasLabel('KNOWS')returned 0 rows because the translator used node labels instead of edge type - Gremlin parser: added
regex()predicate,$paramparameters, mid-traversalV()step, barelabel/idkeywords inby()modifiers - Gremlin
coalesce()semantics: now usesOtherwiseOpfor first-non-empty branch selection instead ofUnionwhich returned all branches - Gremlin
group().by()two-pass:group().by(key).by(value)now correctly sets grouping key and value projection, withMapCollectwrapping for single-map output - Gremlin
optional()step: rewrote translation to produce correct per-row semantics (navigation vs filter cases) instead of returning identity vertex - Gremlin
values()null filtering:values('nonexistent')now returns zero rows instead of a row with null, matching Gremlin semantics - Gremlin
addEwithas()labels:from('a')/to('a')now resolves step labels from theas()alias map instead of treating them as literal strings - Gremlin
or()three-valued logic:or(hasLabel('X'), has('prop', val))across different node types now correctly returns matches from both branches (NULL OR true = true) - SPARQL functions in SELECT projections: created
RdfProjectOperatorthat delegates toRdfExpressionPredicatefor full function support (STRLEN, UCASE, LCASE, IF, COALESCE, REPLACE, etc.) - SPARQL IN/NOT IN operators: added
FilterExpression::Listevaluation andBinaryFilterOp::Inhandling inRdfExpressionPredicate - SPARQL BOUND() with OPTIONAL: checks vector validity bitmap directly to distinguish unbound variables from null values after LEFT JOIN
- SQL/PGQ unbounded variable-length paths:
*1..no longer silently caps max_hops to 1 - SQL/PGQ COUNT(column) NULL skipping:
COUNT(expr)now usesCountNonNullto skip NULL values per SQL standard - SQL/PGQ CASE expressions: CASE WHEN in outer SELECT and WHERE clauses now evaluated by the translator
- SQL/PGQ outer SELECT projection: non-aggregate
SELECT col FROM GRAPH_TABLE(... COLUMNS(...))now projects the correct columns - SQL/PGQ ORDER BY on aggregate aliases: ORDER BY for aggregate queries now placed after the Aggregate operator so output aliases resolve correctly
- JSON Infinity/NaN lost through C FFI:
SUM()overflow returnednullin bindings because JSON cannot represent infinity; now encoded as string"Infinity" - C#/Dart temporal values: dates, times, and durations returned as locale-dependent native types instead of ISO strings
- Binding spec runners: replaced YAML library parsers (Go yaml.v3, C# YamlDotNet, Dart package:yaml) with line-based parsers matching Rust/Node.js/Python; fixed SPARQL dispatch, hash assertions, error test logic, WASM feature gating
[0.5.28] - 2026-03-27¶
Hotfix: single-file .grafeo storage was silently disabled in all bindings.
Fixed¶
- Single-file storage broken in bindings (#185):
grafeo-filefeature was missing from theembeddedprofile, causinggrafeo_open_single_fileand.grafeoauto-detection to silently fall back to WAL directory format. Addedgrafeo-fileto engine defaults,embeddedprofile, and all binding crates (C, Python, Node.js, facade)
[0.5.27] - 2026-03-27¶
C FFI overhaul, Dart expansion, binding-wide usability audit, grafeo-memory engine support.
Added¶
- C API overhaul (#185):
grafeo_open_single_file,_with_paramsfor all 5 languages, unifiedgrafeo_execute_language, type-safeGrafeoIsolationLevelenum - Dart bindings expansion:
openSingleFile,openReadOnly,executeLanguage,execute*WithParams, schema context, property/vector indexes,batchCreateNodes - Dart Flutter guide: native library bundling for Windows, macOS, Linux desktop
- Go bindings:
OpenSingleFile,ExecuteLanguage,Execute*WithParams,ExecuteParams(map[string]any) - Rust facade re-exports:
Error,Result,QueryResultnow at crate root batch_create_nodes_with_props: engine + Python method accepting list of property dicts with mixed types including vectors- Temporal property versioning API (
temporalfeature):get_node_property_at_epoch,get_node_property_history,get_all_node_property_history - Node.js user guide: 5 pages covering database, queries, CRUD, transactions, results
- C# P/Invoke completeness: 11 missing native declarations added,
Transaction.ExecuteLanguage()with async variant - Crash safety testing: new crash injection point, 6 new recovery/concurrency tests
- Python API docs: 45+ undocumented methods added to API reference (DataFrame, batch, search, algorithms, temporal, admin)
Fixed¶
labels(n)/type(r)in aggregation (#187): complex expressions in GROUP BY and ORDER BY failed with "Cannot resolve expression to column". Fixed in all 4 planner locations (LPG aggregate, LPG sort, RDF aggregate, RDF sort)- C# isolation level always failed: P/Invoke passed
stringwhereintexpected. AddedIsolationLevelenum - C#
DropVectorIndexthrew on success: now returnsbool - C# P/Invoke mismatches (3): wrong signatures for property indexes, create_vector_index, batch_create_nodes
- C# double-rollback after commit:
TransactionHandlenow skips rollback when committed - Go stale
grafeo.h: 40+ missing declarations prevented compilation - Go column order random: replaced map iteration with ordered JSON key parsing
- Go thread-local error race: added
runtime.LockOSThread()around all C calls (includingGetNodeLabels,HasPropertyIndex) - Node.js stale TypeScript definitions: 6 missing methods, improved
rows()type - Dart iOS loader: missing
Platform.isIOSbranch - Dart Duration decoding: returned raw ISO string instead of
Durationobject - Rust docs (8 errors): wrong method names, nonexistent APIs, incorrect fallibility
- SPARQL docs contradicted themselves: two pages said "not supported" while it works
- README missing
pip install grafeo: added as primary install command - WASM docs:
createVectorIndexwrongly listed as unavailable - Vector search filter optimization: operator filters ($gt, $lt, etc.) now scan only the narrowed allowlist instead of all nodes
- Single-file storage silent failure (#185): no file created when WAL disabled
- C API
grafeo_current_schemamemory leak: returned caller-owned pointer but docs said not to free; now uses thread-local storage - C API
out_countuninitialized on error:vector_search,mmr_search,batch_create_nodes, andfind_nodes_by_propertynow zero all output pointers (out_count,out_ids,out_distances) before the main operation - Windows read-only file ops failure: skipped
sync_all()on read-only handles in bothclose()andsync() - Adjacency inline capacity: raised
SmallVecfrom 4 to 8, balancing L1 cache residency with fewer heap allocations for typical node degrees - ORDER BY complex expressions leaked columns:
RETURN n.name ORDER BY labels(n)[0]included a synthetic__expr_column in results. Complex ORDER BY expressions are now computed inside the augmented Return and stripped after sorting - GROUP BY on list-valued keys:
GROUP BY labels(n)on multi-label nodes produced extra rows becauseGroupKeyPartlacked aListvariant. Added recursiveList(Vec<GroupKeyPart>)with proper Hash/Eq, and fixed push-based aggregatorhash_value()which mapped all lists to0u8 - SPARQL GROUP BY/ORDER BY with expressions:
GROUP BY (STR(?s))andORDER BY ASC(STR(?s))failed with "Store required for expression evaluation". RDF planner now passes aNullGraphStoretoProjectOperatorfor expression evaluation
[0.5.26] - 2026-03-25¶
GQL conformance validation, SQL/PGQ features, and a big batch of bug fixes.
Added¶
- GQL conformance (ISO/IEC 39075:2024): 234-query corpus cross-validated against GraphGlot. All 24 identified gaps closed: post-edge quantifiers (
->{1,3},->+,->*), path alternation (|,|+|), FILTER WHERE, SELECT...FROM...MATCH, brace-delimited graph types, and per-pattern path search prefixes - SQL/PGQ: WHERE inside GRAPH_TABLE, SELECT DISTINCT, GROUP BY / HAVING, and graph name references
- Cross-language correctness tests: SQL/PGQ queries validated against GQL equivalents, plus CALL block scope isolation tests
Fixed¶
- EXISTS/COUNT subquery bugs: target-side correlation (#173) now flips traversal direction instead of looking up the anonymous source, end-node labels are verified at runtime (were silently ignored), and complex EXISTS inside OR predicates works via split semi-join + filter
- WAL directory-format data loss (#174):
close()wrote checkpoint metadata that caused recovery to skip older WAL files, silently losing pre-rotation data - UNWIND variable in SET clause (#172): five mutation planner functions assigned
LogicalType::Nodeto pass-through columns, silently dropping Map values from UNWIND. All now useLogicalType::Any. Present since 0.5.14 - SET n:Label drops variable binding (#178, #182): label operators discarded input columns, breaking any subsequent clause referencing the same variable. Now preserves columns per-row
- Missing expression functions (#179, #180):
timestamp()returns epoch milliseconds (was null),startNode(r)/endNode(r)return node IDs (were unimplemented), zero-argument temporal functions now work in SET clauses - CREATE after MATCH creates phantom nodes (#181): planner now skips node creation when the variable is already bound from a prior MATCH
- SQL/PGQ GROUP BY silently dropped non-aggregate columns; C API typed entity access (#177) now returns explicit
element_type/id/labels/typefields in JSON
[0.5.25] - 2026-03-25¶
RDF change tracking, CRDT counters, and tracing goes opt-in.
Added¶
- RDF CDC bridge (
cdc+rdf): SPARQL INSERT/DELETE mutations now emitChangeEventrecords to the CDC log, carrying N-Triples-encoded terms. Surfaces RDF changes throughGET /changesandPOST /syncfor offline-first clients - CDC structural metadata: node Create events now carry
labels, edge Create events carryedge_type/src_id/dst_id, giving sync clients everything needed to replay creates remotely - CRDT counter values:
Value::GCounterandValue::OnCounteras first-class types with proper merge semantics (per-replica max). All bindings surface them as structured JSON objects
Changed¶
- Tracing is now opt-in (
tracingfeature): compiles to zero-cost no-ops when disabled. Included inserverprofile, excluded fromembedded/browser. Eliminates ~29% overhead on micro-benchmarks
Fixed¶
- Cypher target node property filter ignored:
MATCH ()-[r]->(o {name: 'X'})returned unfiltered results. Translator now applies target and edge property predicates after expand (Discussion #155) - Schema isolation for types: SHOW/CREATE/DROP/ALTER type commands now respect
SESSION SET SCHEMA.DROP SCHEMArejects non-empty schemas (#167) - CREATE GRAPH TYPED regression: type name resolution now works correctly with session schemas, including cross-schema references like
my_schema.type_name - Schema context in bindings: all bindings now expose
set_schema/reset_schema/current_schemamethods that persist acrossexecute()calls - Temporal feature overhead: optimized
VersionLog::at()with O(1) fast path for current-epoch reads, eliminated double HashMap lookups. Reduces overhead from ~16% to ~6%
[0.5.24] - 2026-03-24¶
Temporal properties, read-only mode, and snapshot format v4.
Added¶
- Index metadata in snapshots: property, vector, and text index definitions now persist in v4 snapshots and auto-rebuild on import/restore
- Read-only open mode:
GrafeoDB::open_read_only()uses shared file locks for concurrent reads; mutations rejected at the session level - Agent memory migration tests: Rust and Python integration tests for HNSW at scale, BYOV 384-dim vectors, persistence, concurrent reads, bulk import, and storage size (Discussion #155)
- Temporal properties (
temporalfeature): opt-in append-only property versioning withexecute_at_epoch(),get_node_at_epoch()/get_node_history()APIs, snapshot roundtrip, and transaction-safe rollback (Discussion #163)
Breaking¶
- Snapshot format v4: properties stored as version-history lists; not backward-compatible
Fixed¶
- MERGE + UNWIND creates only one node: planner evaluated MERGE property expressions as constants at plan time, dropping UNWIND variable references. Now uses per-row resolution
- MERGE with NULL node reference:
OPTIONAL MATCH (n:NonExistent) MERGE (n)-[:R]->(m)silently succeeded as a no-op. Now returns a clear type mismatch error
[0.5.23] - 2026-03-23¶
Prometheus metrics, tracing spans, and SQL/PGQ optional matching.
Added¶
- Prometheus metrics export (
metrics):MetricsRegistry::to_prometheus()renders counters, gauges, and histograms in Prometheus text format;GrafeoDB::metrics_prometheus()for one-call access; plan cache stats merged into snapshots - Tracing spans: structured spans on query and transaction lifecycle (
session::execute,query::parse/optimize/plan/execute,tx::begin/commit/rollback); zero-cost when no subscriber is registered - SQL/PGQ LEFT OUTER JOIN:
LEFT [OUTER] JOIN MATCHandOPTIONAL MATCHinsideGRAPH_TABLE(...), producing NULL-padded rows for unmatched patterns
Changed¶
- Read-only expand fast path: all expand operators skip versioned MVCC lookups for read-only queries, using cheaper epoch-only visibility checks
Fixed¶
- Questioned edge (
->?) row preservation: LeftJoin collapsed source rows instead of preserving them with NULLs - Negative numeric literals in property maps: unary negation (e.g.
{lat: -6.248}) now folds correctly at plan time for both GQL and Cypher (#160)
[0.5.22] - 2026-03-14¶
Pretty printing, observability, RDF performance overhaul, and GQL conformance tracking.
Added¶
- Pretty-printed query results:
QueryResultnow renders as an ASCII table viaDisplay, replacing the rawVec<Vec<Value>>output - Observability (
metrics): lock-freeMetricsRegistrywith atomic counters and fixed-bucket histograms, tracking queries, latency (p50/p99), errors, transactions, sessions, GC sweeps, and plan cache stats across all 6 query languages. Zero overhead when disabled - Edge visibility fast path:
is_edge_visible_at_epoch()skips full edge construction when only checking MVCC visibility - Plan cache bindings:
clear_plan_cache()in Python, Node.js, C, and WASM - RDF bulk load:
bulk_load()builds all indexes in a single pass;load_ntriples()parses N-Triples with full term support (IRIs, blank nodes, typed/language-tagged literals) - SPARQL EXPLAIN: returns the optimized logical plan tree without executing
- GQL conformance tracking:
// ISO:test annotations linking to ISO/IEC 39075:2024 feature IDs, withscripts/gql-conformance.pyfor coverage reports and a machine-readablegql-dialect.json(community feedback) - GQL binary set functions (GF11): 12 statistical aggregates (COVAR_SAMP/POP, CORR, REGR_SLOPE/INTERCEPT/R2/COUNT/SXX/SYY/SXY/AVGX/AVGY)
Changed¶
- RDF query performance: O(N*M) nested loop joins replaced with O(N+M) hash joins for all join types; composite indexes (SP, PO, OS) for O(1) lookup on 2-bound triple patterns; SPARQL optimizer uses RDF-specific statistics
- Unsafe code enforcement:
#![forbid(unsafe_code)]on pure-safe crates,#![deny(unsafe_code)]on crates with targeted unsafe - GroupKeyPart zero-alloc: uses
ArcStrinstead ofString, eliminating allocations during aggregation - RDF code consolidation: scattered
#[cfg]gates consolidated into dedicateddatabase/rdf_ops.rsandsession/rdf.rsmodules
[0.5.21] - 2026-03-13¶
First implementation of C# and Dart bindings, single file database completed, snapshot consolidation and test hardening
Added¶
- C# / .NET bindings (
crates/bindings/csharp): .NET 8 P/Invoke binding wrapping grafeo-c. Covers GQL + multi-language queries (sync/async), ACID transactions, CRUD, vector search (k-NN + MMR), parameterized queries with temporal types, and SafeHandle resource management. CI on Ubuntu, Windows and macOS - Dart bindings (
crates/bindings/dart): Dart FFI binding wrapping grafeo-c. Covers parameterized queries with temporal type encoding, ACID transactions, CRUD, vector search (MMR), NativeFinalizer for memory safety, and sealed exception hierarchy. CI on all three platforms. Based on community PR #138 by @CorvusYe - Single-file
.grafeodatabase format: stores the entire database in one file with a sidecar WAL during operation (DuckDB-style). Dual-header crash safety with CRC32 checksums, auto format detection by extension, and WAL checkpoint merging. UseGrafeoDB::open("mydb.grafeo")ordb.save("mydb.grafeo"). Realizes feature request #139 by @CorvusYe - Exclusive file locking for
.grafeofiles: prevents multiple processes from opening the same database file simultaneously. Lock is acquired on open and released on close/drop (usesfs2for cross-platform advisory locking). - DDL schema persistence in snapshots: CREATE NODE/EDGE/GRAPH TYPE, PROCEDURE and SCHEMA definitions survive close/reopen and export/import. Snapshot format consolidated to v3 with full schema metadata
- Crash injection testing (
testing-crash-injectionfeature):maybe_crash()instrumentation points inwrite_snapshotandcheckpoint_to_fileenable deterministic crash simulation for verifying sidecar WAL recovery - Introspection functions:
RETURN CURRENT_SCHEMA,RETURN CURRENT_GRAPH,RETURN info(),RETURN schema()for querying session state and database metadata from within GQL
Breaking¶
- Snapshot format v3:
export_snapshot()/import_snapshot()now produce/consume v3 format (includes schema metadata). Snapshots from previous versions are no longer readable. Re-export from a running database to migrate.
Testing¶
- Spec compliance seam tests: systematic coverage of ISO/IEC 39075 feature boundaries and negative paths (sessions, transactions, DML, patterns, aggregates, CASE, type coercion, cross-graph isolation). Uncovered 3 spec deviations
Fixed¶
- DDL in READ ONLY transactions (ISO 39075 §8): CREATE/DROP GRAPH now blocked inside READ ONLY transactions
- SUM on empty set (ISO 39075 §20.9): returns NULL instead of 0, matching AVG/MIN/MAX
- CASE WHEN with NULL conditions (ISO 39075 §21): NULL conditions now correctly fall through to ELSE
- SESSION SET SCHEMA / GRAPH separation (ISO 39075 §7.1-7.2): schema and graph are now independent session fields with independent reset targets, schema-scoped graph keys, and
SHOW SCHEMAS.DROP SCHEMAenforces "must be empty" per §12.3 - COUNT(*) parsing (ISO 39075 §20.9): correctly parsed as a zero-argument aggregate
[0.5.20] - 2026-03-11¶
Small release bringing new methods to WASM and adding SESSION SET validation
Added¶
- WASM
memoryUsage()andimportRows(): memory introspection and bulk row import (the DataFrame equivalent) now available in WebAssembly bindings - WASM vector search bindings:
createVectorIndex(),dropVectorIndex(),rebuildVectorIndex(),vectorSearch()andmmrSearch()now exposed in WebAssembly, enabling client-side k-NN and MMR search with HNSW indexes
Fixed¶
SESSION SET GRAPH/SESSION SET SCHEMAvalidation: now errors when the target graph does not exist, matching the behavior ofUSE GRAPH; previously it silently accepted any name and fell back to the default store
[0.5.19] - 2026-03-11¶
GQL translator refactor, new methods, GQL improvements and fixes
Added¶
- Graph type enforcement: full write-path schema enforcement with node type inheritance, edge endpoint validation, UNIQUE/NOT NULL/CHECK constraints, default value injection, closed graph type guards, MERGE validator support, pattern-form syntax, SHOW commands and Cypher
ALTER CURRENT GRAPH TYPE - LOAD DATA (multi-format import): generalized
LOAD DATA FROM 'path' FORMAT CSV|JSONL|PARQUET [WITH HEADERS] AS variablein GQL, with Cypher-compatibleLOAD CSVsyntax preserved; JSONL behindjsonl-importfeature, Parquet behindparquet-importfeature - Python
import_df(): bulk-import nodes or edges from a pandas or polars DataFrame viadb.import_df(df, 'nodes', label='Person')ordb.import_df(df, 'edges', edge_type='KNOWS') - Memory introspection:
db.memory_usage()returns a hierarchical breakdown of heap usage across store, indexes, MVCC chains, query caches, string pools and buffer manager regions - Named graph persistence: CREATE/DROP GRAPH and all mutations within named graphs are WAL-logged and recovered on restart. Snapshot v2 includes named graph data in all export/import/save paths; v1 snapshots remain backward-compatible
- SHOW GRAPHS:
SHOW GRAPHSlists all named graphs in the database, complementing existingSHOW NODE TYPES/SHOW EDGE TYPES - RDF persistence: SPARQL INSERT/DELETE/CLEAR/CREATE/DROP operations are now WAL-logged and recovered on restart; snapshot export/import includes RDF triples and RDF named graphs
- Cross-graph transactions:
USE GRAPHandSESSION SET GRAPHnow work within active transactions; commit/rollback/savepoint operations apply atomically across all touched graphs - GrafeoDB graph context: one-shot
db.execute()calls now persistUSE GRAPHcontext across calls;current_graph()andset_current_graph()public API for programmatic access - WASM batch import:
importLpg()andimportRdf()methods for bulk-loading structured LPG nodes/edges and RDF triples in a single call, with index-relative edge references and typed literal support
Fixed¶
- Named graph data isolation (#133): USE GRAPH / SESSION SET GRAPH now correctly route all queries to the selected graph; query cache keys include graph name; dropping the active graph resets session to default
- OPTIONAL MATCH WHERE pushdown: right-side predicates pushed into the join instead of filtering out NULL rows
- Cypher COUNT(expr) NULL skipping:
COUNT(expr)now skips NULLs (usingCountNonNull), matchingCOUNT(*)behavior - Vector validity bitmap: consecutive NULL pushes no longer silently drop null bits, fixing incorrect results in SPARQL OPTIONAL and RDF left joins
Improved¶
- GQL translator submodules: split
gql.rsintogql/mod.rs,expression.rs,pattern.rs,aggregate.rsfor maintainability - Wildcard imports lint: re-enabled
clippy::wildcard_importsas warning; replaceduse super::*in LPG planner submodules with explicit imports - Unwrap reduction: replaced production
.expect()calls withResult/?propagation in session initialization, persistence and WAL recovery paths
[0.5.18] - 2026-03-09¶
Query language compliance improvements, expanded test coverage and Deriva compatibility fixes
Added¶
- Extensive spec test suites: 8 Cypher + 12 GQL spec modules covering 1,300+ test cases, plus 67 Cypher exotic integration tests (NOT EXISTS, any()/reduce, list comprehensions, OPTIONAL MATCH, CASE, multi-label, etc.)
Fixed (Cypher)¶
- CALL subquery variable scope: inner RETURN columns now resolve in the outer query instead of returning NULL
- RETURN after DELETE: delete operators pass through input rows for downstream aggregation
- Inline MERGE with relationship SET: decomposes inline node patterns into chained MERGE operations
- WITH * wildcard: correctly passes all bound variables through
- DoubleDash edge patterns: undirected
--patterns now parsed alongside-[]-syntax
Fixed (GQL)¶
- CALL { subquery } recognized as query-level clause instead of procedure call
- WITH + LET bindings: LET clauses after WITH parsed and attached correctly
- String concatenation:
||(CONCAT) now supported in arithmetic expressions - Inline MERGE with relationship SET: same decomposition fix as Cypher
Fixed¶
- Multiple NOT EXISTS subqueries: two or more
NOT EXISTSpredicates no longer cause variable-not-found errors - Transaction rollback: SET property, SET/REMOVE label, and MERGE ON MATCH SET changes all correctly undone on ROLLBACK. Savepoint partial rollback preserves earlier changes
- NPM package missing native binaries (#128):
@grafeo-db/jsnow publishes per-platform packages asoptionalDependencies
[0.5.17] - 2026-03-09¶
Cypher query execution bug fixes for Deriva compatibility.
Fixed¶
- Correlated EXISTS subqueries:
NOT EXISTS { MATCH (a)-[r]->(b) WHERE type(r) = 'X' }now correctly plans via semi-join instead of failing with "Unsupported EXISTS subquery pattern" - CASE WHEN in aggregates:
sum(CASE WHEN ... THEN 1 ELSE 0 END)resolves correctly inside aggregate functions - any()/all()/none()/single() with IN list:
any(lbl IN labels(n) WHERE lbl IN ['A', 'B'])now evaluates the IN operator correctly in list predicate contexts - CASE WHEN in reduce():
reduce(acc = 0, x IN vals | CASE WHEN x > acc THEN x ELSE acc END)evaluates CASE expressions with both accumulator and item variable bindings
[0.5.16] - 2026-03-08¶
Performance enhancements, bug fixes and Rust examples
Added¶
- LOAD CSV:
LOAD CSV [WITH HEADERS] FROM 'path' AS row [FIELDTERMINATOR '\t']in Cypher, with inline CSV parser supporting quoted fields,file:///URIs and custom delimiters - Cypher schema DDL:
CREATE/DROP INDEX,CREATE/DROP CONSTRAINT,SHOW INDEXES,SHOW CONSTRAINTS - Relationship WHERE: inline predicates on relationship patterns (
-[r WHERE r.since > 2020]->) - Temporal map constructors:
date({year:2024, month:3}),time({hour:14}),datetime(...),duration({years:1, months:2, days:3}) - PROFILE statement:
PROFILE MATCH ... RETURN ...executes the query and returns per-operator metrics (rows, self-time, call counts) for GQL and Cypher - Rust examples: 7 runnable examples in
examples/rust/covering the core API (basic queries, transactions, parameterized queries, vector search, graph algorithms, WAL persistence, multi-language dispatch) - Plan cache invalidation: query plan cache is automatically cleared after DDL operations (CREATE/DROP INDEX, TYPE, CONSTRAINT, etc.), with manual
clear_plan_cache()API onGrafeoDBandSession - Cache invalidation counter:
CacheStats.invalidationstracks how often DDL clears the plan cache
Improved¶
- Cost model calibration: recursive plan costing, statistics-aware IO estimation, actual child cardinalities for joins, multi-edge-type expand costing
- Supply chain audit: replaced
cargo auditCI job withcargo-deny(licenses, advisories, bans, source verification) - Benchmark regression detection: PRs now run all three criterion suites (arena, index, query) and fail on >10% regression via
benchmark-action - Examples CI: added
cargo build -p grafeo-examplesto CI checks
Fixed¶
- GQL
-->shorthand: parser recognizes-->as a directed outgoing edge instead of splitting into--and> - EXISTS bare patterns:
EXISTS { (a)-[r]->(b) }without explicit MATCH keyword now works in GQL and Cypher - CASE WHEN in aggregates: expressions like
sum(CASE WHEN ... THEN 1 ELSE 0 END)resolve correctly in the LPG planner - SPARQL parameters:
execute_sparql_with_params()now substitutes$paramvalues instead of ignoring them
[0.5.15] - 2026-03-07¶
Full ecosystem feature profile rework and several graph database nice-to-haves
Added¶
- Ecosystem feature profiles:
embedded,browser,servernamed profiles across all crates.storageconvenience group (wal+spill+mmap) - WASM multi-variant builds: AI variant (531 KB gzip) and lite variant (513 KB gzip) via
build-wasm-all.sh, withregex-litefor smaller binaries - Savepoints and nested transactions:
SAVEPOINT/ROLLBACK TO/RELEASE, innerSTART TRANSACTIONauto-creates savepoints - Correlated subqueries:
EXISTS { ... },COUNT { ... },VALUE { ... }in WHERE/RETURN - Subpath variable binding:
(p = (a)-[e]->(b)){2,5}withlength(p),nodes(p),edges(p) - Type system extensions:
LIST<T>typed lists with coercion,IS TYPED RECORD/PATH/GRAPHpredicates,path()constructor - Graph DDL:
CREATE GRAPH g2 LIKE g1,AS COPY OF,CREATE GRAPH g ANY/OPEN - GQLSTATUS diagnostics: ISO sec 23 status codes and diagnostic records on all query results
- Catalog procedures:
CALL db.labels(),db.relationshipTypes(),db.propertyKeys()with YIELD - Python DataFrame bridge:
result.to_pandas(),result.to_polars(),db.nodes_df(),db.edges_df()for zero-friction data science integration
Fixed¶
- Temporal functions:
local_time(),local_datetime(),zoned_datetime()constructors,date_trunc()truncation - Aggregate separators:
LISTAGGandGROUP_CONCATwith custom separators and per-language defaults
Changed¶
- Default profile: facade crate default changed from
fulltoembedded. All binding crates follow - WASM: default changed to
browserprofile, binary size reduced from 1,001 KB to 513 KB gzipped (49%)
[0.5.14] - 2026-03-06¶
Moving crates and lots of small improvements and fixes
Added¶
- EXPLAIN statement:
EXPLAIN <query>in GQL and Cypher returns the optimized logical plan tree with pushdown hints ([index: prop],[range: prop],[label-first]) - WASM size optimization:
wasm-opt -Ozapplied during release builds - NetworkX bridge:
adjproperty andsubgraph(nodes)method - SPARQL built-in functions: date/time (NOW, YEAR, MONTH, ...), hash (MD5, SHA1, SHA256, SHA384, SHA512), RDF term (LANG, DATATYPE, IRI, BNODE, ...) and RAND
- GROUP_CONCAT / SAMPLE aggregates: proper implementations replacing the previous Collect stub
Fixed¶
- Auto-commit for mutations: single-shot
execute()calls with INSERT/DELETE/SET now auto-commit instead of silently discarding changes - WAL persistence for queries: mutations via GQL/Cypher now persist to WAL (previously only the CRUD API did)
- WAL property removal:
remove_node_propertyandremove_edge_propertynow log to WAL - Cypher count(*): parses correctly when
countis tokenized as a keyword - SPARQL unary plus: treated as identity instead of
NOT - CLI fixes:
data dump/data loadnow work (JSON Lines),compactperforms real compaction,index listshows per-index details, nonexistent databases error instead of being silently created - WASM test suite: fixed compilation and runtime panics on wasm32
Changed¶
- Node.js
nodeCount/edgeCount: changed from getter properties to methods (db.nodeCount()) - Arena allocator: returns
Result<T, AllocError>instead of panicking on allocation failure - Planner refactor: split into
planner/lpg/andplanner/rdf/with shared operator builders - Translator refactor: shared plan-builder functions extracted into
translators/common.rs, all 7 translators moved intoquery/translators/ - Dependency cleanup: removed unused deps, replaced ahash with foldhash, narrowed tokio features
[0.5.13] - 2026-03-04¶
Big language compliance push, schema DDL, time-travel and named graphs
Improved¶
- GQL: full compliance with ISO/IEC 39075:2024, covering all features practical for a graph database
- Cypher: improved openCypher v9 compliance, plus pattern comprehensions, CALL subqueries, FOREACH
- SPARQL: improved W3C SPARQL 1.1 compliance (no 1.2/SPARQL Star yet)
Infrastructure¶
- LPG named graphs: multi-graph support with per-graph storage, labels, indexes and MVCC versioning (
create_graph(),drop_graph(),list_graphs()) - Apply operator: correlated subquery execution for CALL, VALUE, NEXT and pattern comprehensions
- Temporal types:
Date,Time,Durationwith ISO 8601 parsing, arithmetic and component extraction. Python round-trips viadatetime.date/datetime.time
Schema / DDL¶
- Full schema DDL: CREATE/DROP/ALTER for NODE TYPE, EDGE TYPE, GRAPH TYPE, INDEX, CONSTRAINT and SCHEMA, with
OR REPLACE,IF NOT EXISTS/IF EXISTSand WAL persistence - Type definitions:
CREATE NODE TYPE Person (name STRING NOT NULL, age INT64)with nullability - Index DDL:
CREATE INDEX ... FOR (n:Label) ON (n.property) [USING TEXT|VECTOR|BTREE] - Constraint enforcement: UNIQUE, NOT NULL, NODE KEY, EXISTS validated on writes
Time-Travel¶
- Epoch-based time-travel:
execute_at_epoch(query, epoch)runs any query against a historical snapshot. Also available viaset_viewing_epoch()orSESSION SET PARAMETER viewing_epoch = <n> - Version history:
get_node_history(id)/get_edge_history(id)return all versions with creation/deletion epochs
GQL Spec Compliance (78% to ~97%)¶
- New syntax: LIKE, CAST to temporal, SET map operations (
= {map},+= {map}), NODETACH DELETE, RETURN */WITH *, list comprehensions, transaction characteristics, zoned temporals, ALTER DDL, CREATE GRAPH TYPED, stored procedures - List property storage:
reduce()and list operations work correctly after INSERT with list-valued properties
Fixed¶
- Time-travel scans: now use pure epoch-based visibility instead of transaction-aware checks
- LIKE parser: token existed but was never consumed as an infix operator
- RETURN * binder: was incorrectly rejected as an undefined variable
- List comprehensions: planner now handles these in RETURN projections
- Cypher fixes: standalone DELETE/SET/REMOVE error messages,
^power operator, anonymous variable name collisions - Temporal comparison: Date/Time/Timestamp comparisons no longer silently return false
Improved¶
- Test coverage: 80+ GQL parser tests (was 44), 137 Python compliance tests (was 100), new SPARQL and Cypher suites
[0.5.12] - 2026-03-02¶
Two-phase commit, snapshot restore, EXISTS subqueries.
Added¶
- PreparedCommit: two-phase commit via
session.prepare_commit(), inspect pending mutations and attach metadata before finalizing - Atomic snapshot restore:
db.restore_snapshot(data)replaces the database in place, with full pre-validation (store unchanged on error) - EXISTS subqueries (GQL, Cypher): complex inner patterns with multi-hop traversals, property filters and label constraints via semi-join rewrite
Fixed¶
- SET on edge variables: Cypher translator now correctly handles SET when targeting an edge variable
Improved¶
- Variable-length path traversal: BFS path tracking uses shared-prefix
Rcsegments instead of cloning full vectors, reducing per-edge cost from O(depth) to O(1)
[0.5.11] - 2026-03-02¶
Pluggable storage traits, query language compliance, UNION support.
Added¶
- Pluggable storage:
GraphStore/GraphStoreMuttraits decouple all query operators and algorithms fromLpgStore. UseGrafeoDB::with_store(Arc<dyn GraphStoreMut>, Config)to plug in any backend - Type-safe WAL:
WalEntrytrait andTypedWal<R>wrapper constrain WAL record types at compile time, preventing cross-model logging - Query language compliance tests: spec-level integration tests for all 6 query languages
- Cypher UNION / UNION ALL: combining query results with duplicate elimination or preservation
- GQL MERGE on relationships:
MERGE (a)-[r:TYPE]->(b)with idempotent edge creation - Gremlin traversal steps:
and(),or(),not(),where(),filter(),choose(),optional(),union(),coalesce()and more - SPARQL improvements: DISTINCT, HAVING, FILTER NOT EXISTS / EXISTS
[0.5.10] - 2026-02-29¶
Robustness: bidirectional shortest path, crash recovery tests, stress tests.
Added¶
- Skip index for adjacency chunks: compressed cold chunks maintain a zone-map skip index.
contains_edge(src, dst)provides O(log n) point lookups;edges_in_range(src, min, max)supports efficient range queries - Bidirectional BFS shortest path: meet-in-the-middle BFS expanding smaller frontier first, reducing search space from O(b^d) to O(b^(d/2))
Improved¶
- Crash recovery tests: 7 deterministic crash injection tests verifying WAL recovery at every crash point
- Concurrent stress tests: 6 multi-threaded tests covering concurrent writers, mixed read/write, transaction conflicts, epoch pressure and rapid session lifecycle
- Hardened panic messages: ~50 bare
unwrap()calls converted toexpect()with invariant descriptions; no behavioral change
[0.5.9] - 2026-02-28¶
Compact property storage, snapshot validation, crash injection framework.
Added¶
- Snapshot validation:
import_snapshot()pre-validates everything before inserting: rejects duplicate IDs and dangling edge references - Crash injection framework: feature-gated
maybe_crash()/with_crash_at()for deterministic recovery testing, with three WAL crash points. Zero overhead when disabled - Backward compatibility tests: pinned v1 snapshot fixture with 8 regression tests for format stability
Fixed¶
- WASM build with
getrandom0.4: addedwasm_jscrate feature for 0.4.x on wasm32 targets - WASM binary size regression: disabled transitive engine features in bindings-common, reducing WASM gzip from 974 KB to 744 KB
Improved¶
- Compact property storage: property maps switched from
BTreeMaptoSmallVec<4>, so nodes with 4 or fewer properties avoid heap allocation - Cost model per-type fanout: the optimizer now tracks per-edge-type average degree instead of a single global estimate
[0.5.8] - 2026-02-22¶
Shared bindings crate, unified query dispatch, Node.js/WASM API expansion.
Added¶
grafeo-bindings-commoncrate: shared entity extraction, error classification and JSON conversion for all four bindings (Python, Node.js, C, WASM)- Unified query dispatch:
execute_language(query, "gql"|"cypher"|"sparql"|...)replaces 18 per-language functions - Node.js API parity: property removal, label management,
info(),schema(),version()and transaction isolation levels now match the Python binding - WASM expansion: parameterized queries, per-language convenience methods, proper feature gating
- Batch edge creation:
batch_create_edges()with single lock acquisition for bulk imports
Improved¶
- Incremental statistics:
compute_statistics()reads atomic delta counters instead of scanning all entities, reducing refresh from O(n+m) to O(|labels|+|edge_types|) - Cost model uses real fanout: optimizer derives average edge fanout from actual graph statistics instead of a hardcoded 10.0
[0.5.7] - 2026-02-19¶
UNWIND property access fix, algos feature flag.
Fixed¶
- UNWIND mutation property access: map property access like
e.src,e.weightin CREATE/SET now resolves correctly. Previously only column references and constants worked, so map properties came back as NULL
Added¶
algosfeature flag: graph algorithms gated behindalgos(included infull). Reduces compile time and binary size when algorithms are not needed
[0.5.6] - 2026-02-18¶
UNWIND/FOR list expansion, embedding model config, zero unsafe in property storage.
Added¶
- UNWIND clause: expand lists into rows for batch processing. Works with literals, parameters (
UNWIND $items AS x) and vectors. Combine with MATCH + INSERT for bulk edge creation - FOR statement (GQL standard):
FOR x IN [1, 2, 3] RETURN x, withWITH ORDINALITY(1-based) andWITH OFFSET(0-based) index tracking - Text index auto-sync: text indexes update automatically on property changes, no manual rebuild needed. WASM bindings added too
- SPARQL COPY/MOVE/ADD: graph management operators with source-existence validation and SILENT support
- Embedding model config: 3 presets (MiniLM-L6-v2, MiniLM-L12-v2, BGE-small-en-v1.5) with HuggingFace auto-download. Exposed in Python and Node.js
- Native SSSP procedure:
CALL grafeo.sssp('node_name', 'weight')for LDBC Graphanalytics compatibility
Fixed¶
- UNWIND scoping: MATCH clauses after UNWIND now correctly receive UNWIND variables, scalar values no longer resolve as node IDs and
Value::Vectoris handled alongsideValue::List RETURN nreturns full entities:MATCH (n) RETURN nnow returns{_id, _labels, ...properties}instead of a bare integer ID- GQL lexer UTF-8 panic: multi-byte characters no longer cause boundary panics
- Scalar column tracking: Gremlin
.values(),.count()and GQLWITH expr AS aliasno longer return NULL - Vector index rebuild after drop: works without the old index, infers dimensions from data
Improved¶
- Zero unsafe in property storage: replaced final
transmute_copycalls with safeEntityIdconversions - Statistics access:
statistics()returnsArc<Statistics>instead of deep-cloning on every planner invocation - Entity resolution: moved from 6-site post-processing into the ProjectOperator pipeline for single-pass resolution
[0.5.5] - 2026-02-16¶
Filter pushdown, query error positions, transaction fixes.
Added¶
- Filter pushdown: equality predicates on labeled scans are pushed to the store level. Compound predicates like
WHERE n.name = 'Alix' AND n.age > 30correctly split: equality pushed down, range kept as post-filter - Query error positions: all six parsers now produce errors with line/column positions and source-caret display
Fixed¶
- Transaction edge type visibility: edges created within a transaction are now visible to subsequent queries in the same transaction
- SPARQL INSERT/DELETE DATA with GRAPH clause: triples now route to the named graph instead of the default graph
- Compound predicate correctness: filter pushdown no longer drops non-equality parts of compound predicates
[0.5.4] - 2026-02-15¶
Fixed¶
- Multi-pattern CREATE:
CREATE (:A {id: 'x'}), (:B {id: 'y'})now creates all nodes instead of only the first
[0.5.3] - 2026-02-13¶
Improved¶
- Query error quality: translator errors now produce
QueryErrorwith semantic error codes (GRAFEO-Q002) instead of generic internal errors. More actionable messages - GraphQL range filters: operator suffixes (
_gt,_lt, etc.) now work on direct query arguments, not justwhereclauses
Fixed¶
- SPARQL
FILTER NOT EXISTS: parser now recognizes NOT EXISTS/EXISTS, producing correct anti-join/semi-join plans - SPARQL
FILTER REGEX: REGEX evaluation was missing from the RDF planner (parser/translator already supported it)
[0.5.2] - 2026-02-13¶
Added¶
- CALL procedure support: invoke any of the 22 built-in graph algorithms from query strings:
CALL grafeo.<algorithm>() [YIELD columns]. Supported in GQL, Cypher and SQL/PGQ - Map literal arguments:
CALL grafeo.pagerank({damping: 0.85, max_iterations: 20}) - Procedure listing:
CALL grafeo.procedures()returns all available procedures
[0.5.1] - 2026-02-12¶
Hybrid search, built-in embeddings, change data capture. The features that make grafeo-memory work.
Added¶
- BM25 text search (
text-index): inverted indexes on string properties with BM25 scoring. Built-in tokenizer with Unicode word boundaries, lowercasing and stop word removal - Hybrid search (
hybrid-search): combine BM25 text + HNSW vector similarity via RRF or weighted fusion. Singlehybrid_search()call in Python and Node.js - Built-in embeddings (
embed, opt-in): in-process embedding generation via ONNX Runtime. Load any.onnxmodel, callembed_text(). Adds ~17MB, off by default - Change data capture (
cdc): track all mutations with before/after property snapshots. Query viahistory(),history_since(),changes_between(). Available in Python and Node.js
[0.5.0] - 2026-02-11¶
Error codes, query timeouts, auto-GC, ~50% memory savings for vector workloads.
Added¶
- Standardized error codes: all errors carry
GRAFEO-XXXXcodes (Q = query, T = transaction, S = storage, V = validation, X = internal) witherror_code()andis_retryable() - Query timeout:
Config::default().with_query_timeout(Duration::from_secs(30))stops long-running queries cleanly - MVCC auto-GC: version chains garbage-collected every N commits (default 100, configurable). Also
db.gc()for manual control
Improved¶
- Topology-only HNSW: vectors no longer duplicated inside the index; reads on-demand via
VectorAccessortrait. ~50% memory reduction for vector workloads
[0.4.4] - 2026-02-11¶
SQL/PGQ queries, MMR search for RAG, auto-syncing vector indexes, CLI overhaul.
Added¶
- SQL/PGQ support: query with SQL:2023 syntax,
SELECT ... FROM GRAPH_TABLE (MATCH ... COLUMNS ...). Includes path functions, DDL and all bindings - MMR search: diverse, relevant results for RAG pipelines via
mmr_search()with tunable relevance/diversity balance - Filtered vector search: property equality filters on
vector_search(),batch_vector_search()andmmr_search()using pre-computed allowlists for efficient HNSW traversal - Incremental vector indexing: indexes stay in sync automatically as nodes change
- CLI overhaul: interactive shell with transactions, meta-commands (
:schema,:info,:stats), persistent history, CSV output. Install viacargo install,pip installornpm install -g - Configurable cardinality estimation: tune 9 selectivity parameters via
SelectivityConfig - AdminService trait: unified introspection and maintenance:
info(),detailed_stats(),schema(),validate(),wal_status() - GQL
INoperator:WHERE n.name IN ['Alix', 'Gus'] - String escape sequences:
\',\",\\,\n,\r,\tin GQL, Cypher, SQL/PGQ
Fixed¶
- Node.js ID validation: rejects negative, NaN, Infinity and values above
MAX_SAFE_INTEGER
Changed¶
- Python CLI removed: replaced by the unified
grafeo-cliRust binary
[0.4.3] - 2026-02-08¶
Per-database graph model selection, snapshot export/import, expanded WASM APIs.
Added¶
- Database creation options: choose LPG or RDF per database, configure durability mode, toggle schema constraints
- Snapshot export/import: serialize to binary snapshots for backups or WASM persistence via IndexedDB
- WASM expansion:
executeWithLanguage(),exportSnapshot()/importSnapshot(),schema()
[0.4.2] - 2026-02-08¶
Grafeo now runs in the browser. WebAssembly bindings with TypeScript definitions at 660 KB gzipped.
Added¶
- WebAssembly bindings (
@grafeo-db/wasm):execute(),executeRaw(),nodeCount(),edgeCount(), full TypeScript definitions. 660 KB gzipped (target was <800 KB) - Feature-gated platform subsystems:
parallel,spill,mmap,walare opt-in, making wasm32 compilation straightforward
[0.4.1] - 2026-02-08¶
Go and C bindings. Grafeo now embeds in pretty much any language.
Added¶
- Go bindings (
github.com/GrafeoDB/grafeo): full CRUD, multi-language queries, ACID transactions, vector search, batch operations, admin APIs - C FFI layer (
grafeo-c): C-compatible ABI for embedding Grafeo in any language
[0.4.0] - 2026-02-07¶
Node.js/TypeScript bindings, Python vector search and transaction isolation.
Added¶
- Node.js/TypeScript bindings (
@grafeo-db/js): full CRUD, async queries across all 5 languages, transactions, native type mapping, TypeScript definitions - Python vector support: pass
list[float]directly,grafeo.vector(), distance functions in GQL, HNSW indexes, k-NN search - Python transaction isolation:
"read_committed","snapshot"or"serializable"per transaction - Batch vector APIs:
batch_create_nodes()andbatch_vector_search()for Python and Node.js
Fixed¶
- GQL INSERT with list or
vector()properties no longer silently drops values - Multi-hop MATCH queries (3+ hops) no longer return duplicate rows
- GQL multi-hop patterns now correctly filter intermediate nodes by label
- Transaction
execute()rejects queries after commit/rollback
Improved¶
- HNSW recall and speed: Vamana-style diversity pruning, pre-normalized cosine vectors, pre-allocated structures
- Query optimizer uses actual store statistics instead of hardcoded defaults
[0.3.4] - 2026-02-06¶
Query timing, "did you mean?" suggestions, Python pagination.
Added¶
- Query performance metrics: every result includes
execution_time_msandrows_scanned - "Did you mean?" suggestions: typo in a variable or label? Grafeo suggests the closest match
- Python pagination:
get_nodes_by_label()supportsoffsetfor paging
[0.3.3] - Unreleased¶
Added¶
- VectorJoin operator: graph traversal + vector similarity in a single query
- Vector zone maps: skips irrelevant data blocks during vector search
- Product quantization: 8-32x memory compression with ~90% recall retention
- Memory-mapped vector storage: disk-backed with LRU caching for large datasets
- Python quantization API:
ScalarQuantizer,ProductQuantizer,BinaryQuantizer
[0.3.2] - Unreleased¶
Added¶
- Selective property loading: fetch only the properties you need, much faster for wide nodes
- Parallel node scan: 3-8x speedup on large scans (10K+ nodes) across CPU cores
[0.3.1] - Unreleased¶
Added¶
- Vector quantization: f32 to u8 (scalar) or 1-bit (binary) compression with quantized HNSW search + exact rescoring
- SIMD acceleration: 4-8x faster distance computations; auto-selects AVX2/FMA, SSE or NEON
- Vector batch operations:
batch_insert()andbatch_search()for bulk loading - VectorScan operators: vector similarity integrated into the query execution engine
- Adaptive WAL flusher: self-tuning background flush based on actual disk speed
- Fingerprinted hash index: sharded with 48-bit fingerprints for near-instant miss detection
[0.3.0] - Unreleased¶
Vectors are a first-class type. Graph + vector hybrid queries let you do things no pure vector database can.
Added¶
- Vector type: native storage with dimension-aware schema validation
- Distance functions: cosine, euclidean, dot product, manhattan
- HNSW index: O(log n) approximate nearest neighbor with tunable presets (
high_recall(),fast()). Also brute-force k-NN with optional predicate filtering - GQL vector syntax:
vector([...])literals, distance functions,CREATE VECTOR INDEX - SPARQL vector functions:
COSINE_SIMILARITY(),EUCLIDEAN_DISTANCE(),DOT_PRODUCT(),MANHATTAN_DISTANCE() - Serializable snapshot isolation:
ReadCommitted,SnapshotIsolationorSerializableper transaction
[0.2.7] - 2026-02-05¶
Parallel execution primitives, second-chance LRU cache.
Added¶
- Second-chance LRU cache: lock-free access marking for concurrent workloads
- Parallel fold-reduce:
parallel_count,parallel_sum,parallel_stats,parallel_partitionand a composable collector trait
[0.2.6] - 2026-02-04¶
Zone map filtering, clustering coefficient, faster batch reads.
Added¶
- Local clustering coefficient: triangle counting with parallel execution
- Chunk-level zone map filtering: skip entire data chunks when predicates can't match
Improved¶
- Batch property retrieval acquires a single lock instead of one per entity
[0.2.5] - 2026-02-03¶
Full SPARQL functions, platform allocators, batch property APIs.
Added¶
- Full SPARQL function coverage: string, type, math functions and REGEX
- EXISTS/NOT EXISTS: semi-join and anti-join subqueries
- Platform allocators: optional jemalloc (Linux/macOS) or mimalloc (Windows) for 10-20% faster allocations
- Batch property APIs, compound predicate pushdown, range queries with zone map pruning
Improved¶
- Community detection now O(E) instead of O(V^2 E), roughly 100-500x faster on large graphs
[0.2.4b] - 2026-02-02¶
Fixed release workflow --exclude flag (requires --workspace).
[0.2.4] - 2026-02-02¶
Benchmark-driven optimizations: lock-free reads, direct lookups, faster filters.
Improved¶
- Lock-free concurrent reads: hash indexes use DashMap, 4-6x improvement under concurrency
- Direct lookup APIs: O(1) point reads bypassing query planning, 10-20x faster than MATCH
- Filter performance: 20-50x improvement for equality and range filters
[0.2.3] - Unreleased¶
Added¶
- Succinct data structures (
succinct-indexes): O(1) rank/select bitvectors, Elias-Fano, wavelet trees - Block-STM parallel execution (
block-stm): optimistic parallel transactions, 3-4x batch speedup - Ring index for RDF (
ring-index): compact triple storage via wavelet trees (~3x space reduction) - Query plan caching: repeated queries skip parsing and optimization, 5-10x speedup
[0.2.2] - Unreleased¶
Added¶
- Bidirectional edge indexing:
edges_to(),in_degree(),out_degree() - NUMA-aware scheduling: work-stealing prefers same-node to minimize cross-node memory access
- Leapfrog TrieJoin: worst-case optimal joins for cyclic patterns, O(N^1.5) vs O(N^2)
[0.2.1] - Unreleased¶
Added¶
- Tiered version index: hot/cold separation for memory-efficient MVCC
- Compressed epoch store: zone maps for predicate pushdown on archived data
- Epoch freeze: compress and archive old epochs to reclaim memory
[0.2.0] - 2026-02-01¶
Performance foundation: factorized execution to avoid Cartesian products in multi-hop queries.
Added¶
- Factorized execution: avoids Cartesian product materialization, inspired by Kuzu
Changed¶
- Switched from Python-based pre-commit to prek (Rust-native, faster)
[0.1.4] - 2026-01-31¶
Label removal, Python label APIs, all languages on by default.
Added¶
- REMOVE clause:
REMOVE n:LabelandREMOVE n.propertyin GQL - Label APIs:
add_node_label(),remove_node_label(),get_node_labels()in Python - RDF transactions: SPARQL now supports proper commit/rollback
Changed¶
- All query languages enabled by default, no feature flags needed
[0.1.3] - 2026-01-30¶
CLI, Python admin APIs, adaptive execution, property compression.
Added¶
- CLI (
grafeo-cli): inspect, backup, export, manage WAL, compact databases - Admin APIs: Python bindings for
info(),detailed_stats(),schema(),validate() - Adaptive execution: runtime re-optimization when cardinality estimates deviate 3x+ from actuals
- Property compression: dictionary, delta, RLE codecs with hot buffer pattern
Improved¶
- Query optimizer: projection pushdown, better join reordering, histogram-based cardinality estimation
[0.1.2] - 2026-01-29¶
Python test suite, documentation pass.
Added¶
- Comprehensive Python test suite covering LPG, RDF, all 5 query languages and plugins
- Docstring pass across all crates
[0.1.1] - Unreleased¶
Added¶
- GQL parser: full ISO/IEC 39075 support
- Multi-language: Cypher, Gremlin, GraphQL, SPARQL translators
- MVCC transactions: snapshot isolation
- Indexes: hash, B-tree, trie, adjacency
- Storage: in-memory and write-ahead log
- Python bindings: PyO3-based API
Changed¶
- Renamed from Graphos to Grafeo, reset version to 0.1.0
[0.1.0] - Unreleased¶
Added¶
- Core architecture: modular crate structure (common, core, adapters, engine, python)
- Graph models: LPG and RDF triple store
- In-memory storage: fast graph operations without persistence overhead
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.