Summary
@ruvector/graph-node (v2.0.4) accepts a storagePath and writes node records to redb, but the NAPI binding never wires the rest of persistence, so any graph silently loses edges — and on reopen, everything — after a process restart:
GraphDatabase.open(path) never hydrates. It opens the redb file but returns an empty graph (stats() → {totalNodes: 0, totalEdges: 0}) even when the file contains data. The crate already has GraphDB::with_storage() / load_from_storage(); the binding just never calls them.
createEdge / createHyperedge / batchInsert edges never reach storage. Only nodes are persisted; every edge lives solely in the in-memory hypergraph.
deleteNode doesn't cascade through storage, leaving dangling persisted edges.
Repro
const { GraphDatabase } = require('@ruvector/graph-node');
const Z = new Float32Array(4);
// process 1
const db = new GraphDatabase({ distanceMetric: 'Cosine', dimensions: 4, storagePath: './t.db' });
await db.createNode({ id: 'a', labels: ['T'], properties: {}, embedding: Z });
await db.createNode({ id: 'b', labels: ['T'], properties: {}, embedding: Z });
await db.createEdge({ from: 'a', to: 'b', description: 'CALLS', confidence: 1, embedding: Z });
console.log(await db.stats()); // { totalNodes: 2, totalEdges: 1 }
// process 2 (restart)
const db2 = GraphDatabase.open('./t.db');
console.log(await db2.stats()); // { totalNodes: 0, totalEdges: 0 } <-- data gone
Root cause
In crates/ruvector-graph-node/src/lib.rs:
new()/open() construct GraphDB::new() and never replay storage into the hypergraph or property graph.
create_edge/create_hyperedge/batch_insert only call hypergraph.add_hyperedge(...); there is no storage.insert_edge/insert_hyperedge write-through.
delete_node deletes only the node record from storage.
The storage layer itself (ruvector-graph's redb backend, all_node_ids/all_edge_ids/all_hyperedge_ids, insert_edge, insert_hyperedge) works correctly — it's purely a binding gap.
Fix
I have a working patch (PR to follow, referencing this issue) that:
- hydrates hypergraph + property graph in
new()/open() (nodes, binary edges, multi-node hyperedges; embeddings/confidence round-tripped as __embedding/__confidence FloatArray properties)
- writes edges/hyperedges through to storage in
createEdge/createHyperedge/batchInsert
- makes
deleteNode cascade through persisted edges/hyperedges and deleteEdge clean the traversal index
- makes hydration tolerant of dangling records (skip + report) rather than failing
open()
Verified with a 12-gate suite (create / reopen in fresh process / delete persistence / batch / tx / Cypher / kHop / in-memory mode / single-writer lock) and a real workload: a 154,711-node / 353,496-edge code graph that now survives restart with exact counts (hydrate ~15s).
Happy to adjust the approach if you'd prefer hydration to live in the crate rather than the binding.
Summary
@ruvector/graph-node(v2.0.4) accepts astoragePathand writes node records to redb, but the NAPI binding never wires the rest of persistence, so any graph silently loses edges — and on reopen, everything — after a process restart:GraphDatabase.open(path)never hydrates. It opens the redb file but returns an empty graph (stats()→{totalNodes: 0, totalEdges: 0}) even when the file contains data. The crate already hasGraphDB::with_storage()/load_from_storage(); the binding just never calls them.createEdge/createHyperedge/batchInsertedges never reach storage. Only nodes are persisted; every edge lives solely in the in-memory hypergraph.deleteNodedoesn't cascade through storage, leaving dangling persisted edges.Repro
Root cause
In
crates/ruvector-graph-node/src/lib.rs:new()/open()constructGraphDB::new()and never replay storage into the hypergraph or property graph.create_edge/create_hyperedge/batch_insertonly callhypergraph.add_hyperedge(...); there is nostorage.insert_edge/insert_hyperedgewrite-through.delete_nodedeletes only the node record from storage.The storage layer itself (
ruvector-graph's redb backend,all_node_ids/all_edge_ids/all_hyperedge_ids,insert_edge,insert_hyperedge) works correctly — it's purely a binding gap.Fix
I have a working patch (PR to follow, referencing this issue) that:
new()/open()(nodes, binary edges, multi-node hyperedges; embeddings/confidence round-tripped as__embedding/__confidenceFloatArray properties)createEdge/createHyperedge/batchInsertdeleteNodecascade through persisted edges/hyperedges anddeleteEdgeclean the traversal indexopen()Verified with a 12-gate suite (create / reopen in fresh process / delete persistence / batch / tx / Cypher / kHop / in-memory mode / single-writer lock) and a real workload: a 154,711-node / 353,496-edge code graph that now survives restart with exact counts (hydrate ~15s).
Happy to adjust the approach if you'd prefer hydration to live in the crate rather than the binding.