Skip to content

@ruvector/graph-node: storagePath data silently lost on restart (open() never hydrates, edges never persisted) #809

Description

@Bolivar1234

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:

  1. 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.
  2. createEdge / createHyperedge / batchInsert edges never reach storage. Only nodes are persisted; every edge lives solely in the in-memory hypergraph.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions