pub trait NodeDb: Send + Sync {
Show 16 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, 'async_trait>(
&'life0 self,
start: &'life1 NodeId,
depth: u8,
edge_filter: Option<&'life2 EdgeFilter>,
) -> Pin<Box<dyn Future<Output = NodeDbResult<SubGraph>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait;
fn graph_insert_edge<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
from: &'life1 NodeId,
to: &'life2 NodeId,
edge_type: &'life3 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;
fn graph_delete_edge<'life0, 'life1, 'async_trait>(
&'life0 self,
edge_id: &'life1 EdgeId,
) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: '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: '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: '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, 'async_trait>(
&'life0 self,
from: &'life1 NodeId,
to: &'life2 NodeId,
max_depth: u8,
edge_filter: Option<&'life3 EdgeFilter>,
) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Vec<NodeId>>>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait { ... }
fn text_search<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
collection: &'life1 str,
query: &'life2 str,
top_k: usize,
params: TextSearchParams,
) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: '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: '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, 'async_trait>(
&'life0 self,
edges: &'life1 [(&'life2 str, &'life3 str, &'life4 str)],
) -> 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,
'life4: '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§
Sourcefn 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_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,
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.
Sourcefn 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_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 (...).
Sourcefn 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 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.
Sourcefn graph_traverse<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
start: &'life1 NodeId,
depth: u8,
edge_filter: Option<&'life2 EdgeFilter>,
) -> Pin<Box<dyn Future<Output = NodeDbResult<SubGraph>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn graph_traverse<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
start: &'life1 NodeId,
depth: u8,
edge_filter: Option<&'life2 EdgeFilter>,
) -> Pin<Box<dyn Future<Output = NodeDbResult<SubGraph>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Traverse the graph from start up to depth hops.
Returns the discovered subgraph (nodes + edges). Optional edge filter constrains which edges are followed during traversal.
On Lite: direct CSR pointer-chasing in contiguous memory. Microseconds.
On Remote: SELECT * FROM graph_traverse($1, $2, $3).
Sourcefn graph_insert_edge<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
from: &'life1 NodeId,
to: &'life2 NodeId,
edge_type: &'life3 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,
fn graph_insert_edge<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
from: &'life1 NodeId,
to: &'life2 NodeId,
edge_type: &'life3 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,
Insert a directed edge from from to to with the given label.
Returns the generated edge ID.
On Lite: appends to mutable adjacency buffer + CRDT delta + SQLite.
On Remote: INSERT INTO edges (src, dst, label, properties) VALUES (...).
Sourcefn graph_delete_edge<'life0, 'life1, 'async_trait>(
&'life0 self,
edge_id: &'life1 EdgeId,
) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn graph_delete_edge<'life0, 'life1, 'async_trait>(
&'life0 self,
edge_id: &'life1 EdgeId,
) -> Pin<Box<dyn Future<Output = NodeDbResult<()>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
Delete a graph edge by ID.
On Lite: marks deleted + CRDT tombstone.
On Remote: DELETE FROM edges WHERE id = $1.
Sourcefn 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_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.
Sourcefn 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_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 ....
Sourcefn 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 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.
Sourcefn 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,
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§
Sourcefn 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: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
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: '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: delegates to vector_insert() ignoring field_name.
Sourcefn 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: '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: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Search a named vector field.
Default: delegates to vector_search() ignoring field_name.
Sourcefn graph_shortest_path<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
from: &'life1 NodeId,
to: &'life2 NodeId,
max_depth: u8,
edge_filter: Option<&'life3 EdgeFilter>,
) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Vec<NodeId>>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
fn graph_shortest_path<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
from: &'life1 NodeId,
to: &'life2 NodeId,
max_depth: u8,
edge_filter: Option<&'life3 EdgeFilter>,
) -> Pin<Box<dyn Future<Output = NodeDbResult<Option<Vec<NodeId>>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Find the shortest path between two nodes.
Returns the path as a list of node IDs, or None if no path exists
within max_depth hops. Uses bidirectional BFS.
Sourcefn text_search<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
collection: &'life1 str,
query: &'life2 str,
top_k: usize,
params: TextSearchParams,
) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn text_search<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
collection: &'life1 str,
query: &'life2 str,
top_k: usize,
params: TextSearchParams,
) -> Pin<Box<dyn Future<Output = NodeDbResult<Vec<SearchResult>>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Full-text search with BM25 scoring.
Returns document IDs with relevance scores, ordered by descending score.
Pass TextSearchParams::default() for standard OR-mode non-fuzzy search.
Sourcefn 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: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: '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: '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.
Sourcefn batch_graph_insert_edges<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
edges: &'life1 [(&'life2 str, &'life3 str, &'life4 str)],
) -> 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,
'life4: 'async_trait,
fn batch_graph_insert_edges<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
edges: &'life1 [(&'life2 str, &'life3 str, &'life4 str)],
) -> 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,
'life4: 'async_trait,
Batch insert graph edges — amortizes CRDT delta export to O(1) per batch.