Skip to main content

NodeDb

Trait NodeDb 

Source
pub trait NodeDb: NodeDbMarker {
Show 24 methods // Required methods fn vector_search<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, collection: &'life1 str, query: &'life2 [f32], k: usize, filter: Option<&'life3 MetadataFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait; fn vector_insert<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, embedding: &'life3 [f32], metadata: Option<Document>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait; fn vector_delete<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; fn graph_traverse<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, collection: &'life1 str, start: &'life2 NodeId, depth: u8, edge_filter: Option<&'life3 EdgeFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<SubGraph>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait; fn graph_insert_edge<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, from: &'life2 NodeId, to: &'life3 NodeId, edge_type: &'life4 str, properties: Option<Document>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<EdgeId>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait; fn graph_delete_edge<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, edge_id: &'life2 EdgeId, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; fn document_get<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Document>>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; fn document_put<'life0, 'life1, 'async_trait>( &'life0 self, collection: &'life1 str, doc: Document, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait; fn document_delete<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; fn execute_sql<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, query: &'life1 str, params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = NodeDbResult<QueryResult>> + Send + 'async_trait>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait; // Provided methods fn vector_insert_field<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, field_name: &'life2 str, id: &'life3 str, embedding: &'life4 [f32], metadata: Option<Document>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait { ... } fn vector_search_field<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, field_name: &'life2 str, query: &'life3 [f32], k: usize, filter: Option<&'life4 MetadataFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait { ... } fn graph_shortest_path<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, from: &'life2 NodeId, to: &'life3 NodeId, max_depth: u8, edge_filter: Option<&'life4 EdgeFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Vec<NodeId>>>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait { ... } fn text_search<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, collection: &'life1 str, field: &'life2 str, query: &'life3 str, top_k: usize, params: TextSearchParams, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait { ... } fn batch_vector_insert<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, vectors: &'life2 [(&'life3 str, &'life4 [f32])], ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait { ... } fn batch_graph_insert_edges<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, collection: &'life1 str, edges: &'life2 [(&'life3 str, &'life4 str, &'life5 str)], ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait { ... } fn proto_version(&self) -> u16 { ... } fn capabilities(&self) -> u64 { ... } fn server_version(&self) -> String { ... } fn limits(&self) -> Limits { ... } fn undrop_collection<'life0, 'life1, 'async_trait>( &'life0 self, name: &'life1 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait { ... } fn drop_collection_purge<'life0, 'life1, 'async_trait>( &'life0 self, name: &'life1 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait { ... } fn list_dropped_collections<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<DroppedCollection>>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait { ... } fn on_collection_purged<'life0, 'async_trait>( &'life0 self, _handler: CollectionPurgedHandler, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>> where Self: Sync + 'async_trait, 'life0: 'async_trait { ... }
}
Expand description

Unified database interface for NodeDB.

Two implementations:

  • NodeDbLite: executes queries against in-memory HNSW/CSR/Loro engines on the edge device. Writes produce CRDT deltas synced to Origin in background.
  • NodeDbRemote: translates trait calls into parameterized SQL and sends them over pgwire to the Origin cluster.

The developer writes agent logic once. Switching between local and cloud is a one-line configuration change.

Required Methods§

Search for the k nearest vectors to query in collection.

Returns results ordered by ascending distance. Optional metadata filter constrains which vectors are considered.

On Lite: direct in-memory HNSW search. Sub-millisecond. On Remote: translated to SELECT ... ORDER BY embedding <-> $1 LIMIT $2.

Source

fn vector_insert<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, embedding: &'life3 [f32], metadata: Option<Document>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait,

Insert a vector with optional metadata into collection.

On Lite: inserts into in-memory HNSW + emits CRDT delta + persists to SQLite. On Remote: translated to INSERT INTO collection (id, embedding, metadata) VALUES (...).

Source

fn vector_delete<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Delete a vector by ID from collection.

On Lite: marks deleted in HNSW + emits CRDT tombstone. On Remote: DELETE FROM collection WHERE id = $1.

Source

fn graph_traverse<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, collection: &'life1 str, start: &'life2 NodeId, depth: u8, edge_filter: Option<&'life3 EdgeFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<SubGraph>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait,

Traverse the graph from start up to depth hops within collection.

collection names the graph collection holding the adjacency data. NodeDB’s graph overlay scopes edges per collection, so the caller picks which graph to walk. Returns the discovered subgraph (nodes + edges). Optional edge filter constrains which edges are followed.

On Lite: direct CSR pointer-chasing in contiguous memory. Microseconds. On Remote: GRAPH TRAVERSE FROM '<start>' DEPTH <n> [LABEL '<l>'].

Source

fn graph_insert_edge<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, from: &'life2 NodeId, to: &'life3 NodeId, edge_type: &'life4 str, properties: Option<Document>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<EdgeId>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Insert a directed edge from from to to with the given label into collection.

Returns the generated edge ID.

On Lite: appends to mutable adjacency buffer + CRDT delta + SQLite. On Remote: GRAPH INSERT EDGE IN '<collection>' FROM '<from>' TO '<to>' TYPE '<label>'.

Source

fn graph_delete_edge<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, edge_id: &'life2 EdgeId, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Delete a graph edge by ID from collection.

On Lite: marks deleted + CRDT tombstone. On Remote: GRAPH DELETE EDGE IN '<collection>' FROM '<src>' TO '<dst>' TYPE '<label>'.

Source

fn document_get<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Document>>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Get a document by ID from collection.

On Lite: direct Loro state read. Sub-millisecond. On Remote: SELECT * FROM collection WHERE id = $1.

Source

fn document_put<'life0, 'life1, 'async_trait>( &'life0 self, collection: &'life1 str, doc: Document, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Put (insert or update) a document into collection.

The document’s id field determines the key. If a document with that ID already exists, it is overwritten (last-writer-wins locally; CRDT merge on sync).

On Lite: Loro apply + CRDT delta + SQLite persist. On Remote: INSERT ... ON CONFLICT (id) DO UPDATE SET ....

Source

fn document_delete<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, collection: &'life1 str, id: &'life2 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Delete a document by ID from collection.

On Lite: Loro delete + CRDT tombstone. On Remote: DELETE FROM collection WHERE id = $1.

Source

fn execute_sql<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, query: &'life1 str, params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = NodeDbResult<QueryResult>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Execute a raw SQL query with parameters.

On Lite: requires the sql feature flag (compiles in DataFusion parser). Returns NodeDbError::SqlNotEnabled if the feature is not compiled in. On Remote: pass-through to Origin via pgwire.

For most AI agent workloads, the typed methods above are sufficient and faster. Use this for BI tools, existing ORMs, or ad-hoc queries.

Provided Methods§

Source

fn vector_insert_field<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, field_name: &'life2 str, id: &'life3 str, embedding: &'life4 [f32], metadata: Option<Document>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Insert a vector into a named field within a collection.

Enables multiple embeddings per collection (e.g., “title_embedding”, “body_embedding”) with independent HNSW indexes.

Default returns Err — silently delegating to vector_insert and dropping field_name would land the vector in the wrong field. Implementations that route through to a server with field-aware support must override.

Source

fn vector_search_field<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, field_name: &'life2 str, query: &'life3 [f32], k: usize, filter: Option<&'life4 MetadataFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Search a named vector field.

Default returns Err — silently delegating to vector_search and dropping field_name would search the wrong field. Implementations that route through to a server with field-aware support must override.

Source

fn graph_shortest_path<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, from: &'life2 NodeId, to: &'life3 NodeId, max_depth: u8, edge_filter: Option<&'life4 EdgeFilter>, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Vec<NodeId>>>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Find the shortest path between two nodes.

Returns the path as a list of node IDs (from first, to last), or None if no path exists within max_depth hops.

Default: forward breadth-first search built on graph_traverse. Each frontier expansion calls graph_traverse(node, 1, edge_filter) to discover outgoing neighbors. Inherits the underlying impl’s edge direction semantics. Implementations with a server-side shortest-path operator (e.g. NodeDB’s GRAPH PATH FROM <src> TO <dst> DSL) should override for performance — round-tripping per-hop is O(path_length) wire hops.

Full-text search with BM25 scoring against the FTS-indexed field on collection.

NodeDB’s FTS is per-field — every BM25 index is scoped to one declared field, so the caller names which field to search. Returns document IDs with relevance scores, ordered by descending score. Pass TextSearchParams::default() for standard OR-mode non-fuzzy search.

Default returns ErrOk(Vec::new()) is indistinguishable from a real “no matches” answer and would silently mask the missing implementation. Implementations must override (e.g., a SEARCH IN '<collection>' FIELD '<field>' QUERY '<q>' round-trip via execute_sql).

Source

fn batch_vector_insert<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, collection: &'life1 str, vectors: &'life2 [(&'life3 str, &'life4 [f32])], ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait,

Batch insert vectors — amortizes CRDT delta export to O(1) per batch.

Source

fn batch_graph_insert_edges<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, collection: &'life1 str, edges: &'life2 [(&'life3 str, &'life4 str, &'life5 str)], ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait,

Batch insert graph edges into collection — amortizes CRDT delta export to O(1) per batch.

Source

fn proto_version(&self) -> u16

The protocol version negotiated during the connection handshake.

Returns 0 for implementations that do not maintain a persistent connection and therefore never perform a handshake.

Source

fn capabilities(&self) -> u64

The raw capability bitfield advertised by the server.

Returns 0 when no handshake was performed. Use Capabilities::from_raw(self.capabilities()) for named predicates.

Source

fn server_version(&self) -> String

The server version string from HelloAckFrame (e.g. "0.1.0-dev").

Returns an empty string when no handshake was performed.

Source

fn limits(&self) -> Limits

Per-operation limits announced by the server.

All fields are None when no handshake was performed — the caller should treat None as “no server-side cap” for that dimension.

Source

fn undrop_collection<'life0, 'life1, 'async_trait>( &'life0 self, name: &'life1 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Restore a soft-deleted collection within its retention window.

Equivalent to UNDROP COLLECTION <name>. Fails with 42P01 if the retention window has elapsed and the row is gone, or with 42501 if the caller is neither preserved owner nor admin.

Default impl routes through execute_sql so any implementation that can execute SQL inherits the correct behavior for free.

Source

fn drop_collection_purge<'life0, 'life1, 'async_trait>( &'life0 self, name: &'life1 str, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Hard-delete a collection, skipping soft-delete and retention.

Equivalent to DROP COLLECTION <name> PURGE. Admin-only on the server; the server rejects non-admin callers with 42501. Bypasses the retention safety net — data is unrecoverable.

Source

fn list_dropped_collections<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<DroppedCollection>>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait,

List every soft-deleted collection in the current tenant that is still within its retention window.

Equivalent to SELECT tenant_id, name, owner, deactivated_at_ns, retention_expires_at_ns FROM _system.dropped_collections. Returns Vec<DroppedCollection> — empty if no soft-deleted rows exist for the caller’s tenant.

Source

fn on_collection_purged<'life0, 'async_trait>( &'life0 self, _handler: CollectionPurgedHandler, ) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: Sync + 'async_trait, 'life0: 'async_trait,

Register a handler fired when a collection the caller has synced is purged on Origin and the local copy is removed.

Default impl returns NodeDbError::storage with a "not supported" detail — implementations that maintain a sync client (Lite, any future push-capable remote client) override with registration into their internal handler list. Stateless clients (pgwire-only NodeDbRemote) have nothing to push, so the default rejection is the correct behavior.

Implementors§