Skip to main content

astraea_core/
traits.rs

1use std::path::Path;
2
3use crate::error::Result;
4use crate::types::*;
5
6/// Low-level storage engine trait for persisting and retrieving nodes and edges.
7///
8/// Implementations handle the page-based storage, buffer pool, and disk I/O.
9/// This trait intentionally does NOT handle transactions — that is layered on top.
10pub trait StorageEngine: Send + Sync {
11    /// Store a node. Overwrites if the node ID already exists.
12    fn put_node(&self, node: &Node) -> Result<()>;
13
14    /// Retrieve a node by ID.
15    fn get_node(&self, id: NodeId) -> Result<Option<Node>>;
16
17    /// Delete a node by ID. Returns true if the node existed.
18    fn delete_node(&self, id: NodeId) -> Result<bool>;
19
20    /// Store an edge. Overwrites if the edge ID already exists.
21    fn put_edge(&self, edge: &Edge) -> Result<()>;
22
23    /// Retrieve an edge by ID.
24    fn get_edge(&self, id: EdgeId) -> Result<Option<Edge>>;
25
26    /// Delete an edge by ID. Returns true if the edge existed.
27    fn delete_edge(&self, id: EdgeId) -> Result<bool>;
28
29    /// Get all edges connected to a node in the given direction.
30    fn get_edges(&self, node_id: NodeId, direction: Direction) -> Result<Vec<Edge>>;
31
32    /// Flush all dirty data to disk.
33    fn flush(&self) -> Result<()>;
34
35    /// Find all node IDs that carry the given label.
36    ///
37    /// The default implementation returns an empty vector. Storage engines
38    /// that maintain a label index should override this for O(1) lookups.
39    fn find_nodes_by_label(&self, _label: &str) -> Result<Vec<NodeId>> {
40        Ok(Vec::new())
41    }
42
43    /// Find all edges whose `edge_type` matches the given string.
44    ///
45    /// Returns a list of `(EdgeId, source NodeId, target NodeId)` triples.
46    /// The default implementation returns an empty vector. Storage engines
47    /// that maintain an edge index should override this.
48    fn find_edges_by_type(&self, _edge_type: &str) -> Result<Vec<(EdgeId, NodeId, NodeId)>> {
49        Ok(Vec::new())
50    }
51
52    /// List all node IDs currently stored in the engine.
53    ///
54    /// The default implementation returns an empty vector. Storage engines
55    /// that maintain a node index should override this to return every stored
56    /// node ID. Used by `Graph::rebuild_vector_index` to repopulate an
57    /// in-memory HNSW index after a WAL replay / restart.
58    fn list_all_nodes(&self) -> Result<Vec<NodeId>> {
59        Ok(Vec::new())
60    }
61}
62
63/// Extension trait for transactional storage operations.
64///
65/// Provides MVCC-based transactional access to the storage engine.
66/// Writes are buffered in the transaction and applied atomically on commit.
67pub trait TransactionalEngine: StorageEngine {
68    /// Begin a new transaction. Returns the assigned transaction ID.
69    fn begin_transaction(&self) -> Result<TransactionId>;
70
71    /// Commit a transaction, atomically applying all buffered writes.
72    fn commit_transaction(&self, txn_id: TransactionId) -> Result<()>;
73
74    /// Abort a transaction, discarding all buffered writes.
75    fn abort_transaction(&self, txn_id: TransactionId) -> Result<()>;
76
77    /// Buffer a node write within the given transaction.
78    fn put_node_tx(&self, node: &Node, txn_id: TransactionId) -> Result<()>;
79
80    /// Buffer a node deletion within the given transaction.
81    /// Returns false if the node was not found (but does not error).
82    fn delete_node_tx(&self, id: NodeId, txn_id: TransactionId) -> Result<bool>;
83
84    /// Buffer an edge write within the given transaction.
85    fn put_edge_tx(&self, edge: &Edge, txn_id: TransactionId) -> Result<()>;
86
87    /// Buffer an edge deletion within the given transaction.
88    /// Returns false if the edge was not found (but does not error).
89    fn delete_edge_tx(&self, id: EdgeId, txn_id: TransactionId) -> Result<bool>;
90}
91
92/// Graph-level operations: CRUD and traversals over the property graph.
93pub trait GraphOps: Send + Sync {
94    /// Create a new node with the given labels and properties.
95    /// Returns the assigned NodeId.
96    fn create_node(
97        &self,
98        labels: Vec<String>,
99        properties: serde_json::Value,
100        embedding: Option<Vec<f32>>,
101    ) -> Result<NodeId>;
102
103    /// Create a node at a caller-supplied id. Used by import paths
104    /// (Flight `do_put`, JSON `import`) that need to preserve client-side
105    /// identifiers across an export/import roundtrip.
106    /// astraeadb-issues.md #14.
107    ///
108    /// Implementations must (a) fail with [`AstraeaError::DuplicateNode`]
109    /// if `id` is already in use, and (b) advance the id allocator past
110    /// `id` so subsequent [`create_node`] calls don't collide.
111    ///
112    /// The default falls back to auto-assignment via [`create_node`] —
113    /// override if your implementation can actually honor the id.
114    fn create_node_with_id(
115        &self,
116        _id: NodeId,
117        labels: Vec<String>,
118        properties: serde_json::Value,
119        embedding: Option<Vec<f32>>,
120    ) -> Result<NodeId> {
121        self.create_node(labels, properties, embedding)
122    }
123
124    /// Create a new edge between two nodes.
125    /// Returns the assigned EdgeId.
126    /// `valid_from` and `valid_to` are optional epoch-millisecond bounds for temporal validity.
127    #[allow(clippy::too_many_arguments)]
128    fn create_edge(
129        &self,
130        source: NodeId,
131        target: NodeId,
132        edge_type: String,
133        properties: serde_json::Value,
134        weight: f64,
135        valid_from: Option<i64>,
136        valid_to: Option<i64>,
137    ) -> Result<EdgeId>;
138
139    /// Get a node by ID.
140    fn get_node(&self, id: NodeId) -> Result<Option<Node>>;
141
142    /// Get an edge by ID.
143    fn get_edge(&self, id: EdgeId) -> Result<Option<Edge>>;
144
145    /// Update a node's properties (merge semantics).
146    fn update_node(&self, id: NodeId, properties: serde_json::Value) -> Result<()>;
147
148    /// Update an edge's properties (merge semantics).
149    fn update_edge(&self, id: EdgeId, properties: serde_json::Value) -> Result<()>;
150
151    /// Delete a node and all its connected edges.
152    fn delete_node(&self, id: NodeId) -> Result<()>;
153
154    /// Delete an edge.
155    fn delete_edge(&self, id: EdgeId) -> Result<()>;
156
157    /// Get neighbor node IDs reachable from the given node in the given direction.
158    fn neighbors(&self, node_id: NodeId, direction: Direction) -> Result<Vec<(EdgeId, NodeId)>>;
159
160    /// Get neighbor node IDs filtered by edge type.
161    fn neighbors_filtered(
162        &self,
163        node_id: NodeId,
164        direction: Direction,
165        edge_type: &str,
166    ) -> Result<Vec<(EdgeId, NodeId)>>;
167
168    /// Breadth-first search from a starting node up to a maximum depth.
169    /// Returns all discovered nodes with their depth.
170    fn bfs(&self, start: NodeId, max_depth: usize) -> Result<Vec<(NodeId, usize)>>;
171
172    /// Depth-first search from a starting node up to a maximum depth.
173    /// Returns all discovered nodes.
174    fn dfs(&self, start: NodeId, max_depth: usize) -> Result<Vec<NodeId>>;
175
176    /// Find the shortest path between two nodes (unweighted).
177    fn shortest_path(&self, from: NodeId, to: NodeId) -> Result<Option<GraphPath>>;
178
179    /// Find the shortest path between two nodes using edge weights (Dijkstra).
180    fn shortest_path_weighted(&self, from: NodeId, to: NodeId) -> Result<Option<(GraphPath, f64)>>;
181
182    /// Find all nodes matching a label.
183    fn find_by_label(&self, label: &str) -> Result<Vec<NodeId>>;
184
185    /// List every node ID in the graph, regardless of label.
186    ///
187    /// This is the dedicated "full scan" primitive for callers that need
188    /// every node -- most notably query engines seeding the candidate set
189    /// for an unlabeled node pattern (e.g. `astraea-query`'s MATCH executor,
190    /// which needs this for the leading/anchor node of a pattern: unlike
191    /// later nodes in a chain, the anchor has no incoming edge to filter
192    /// through, so an empty label list must mean "every node," not "no
193    /// nodes"). Do not try to emulate this via `find_by_label("")` --
194    /// backends are not required to (and in practice do not) index the
195    /// empty string as a label meaning "all nodes."
196    ///
197    /// The default implementation returns an error; backends that can
198    /// enumerate their nodes (typically by delegating to
199    /// [`StorageEngine::list_all_nodes`]) should override this.
200    fn list_all_nodes(&self) -> Result<Vec<NodeId>> {
201        Err(crate::error::AstraeaError::QueryExecution(
202            "listing all nodes not supported by this implementation".into(),
203        ))
204    }
205
206    /// Find all edges whose `edge_type` matches the given string.
207    ///
208    /// Returns `(EdgeId, source NodeId, target NodeId)` triples.
209    /// The default implementation returns an empty vector.
210    /// astraeadb-issues.md #3.
211    fn find_edges_by_type(&self, _edge_type: &str) -> Result<Vec<(EdgeId, NodeId, NodeId)>> {
212        Ok(Vec::new())
213    }
214
215    /// Flush any in-memory dirty pages to disk.
216    ///
217    /// Disk-backed implementations (e.g. those wrapping a [`StorageEngine`])
218    /// should override this to propagate the call to the underlying engine so
219    /// that buffer-pool pages are persisted on clean shutdown. The default
220    /// implementation is a no-op, which is correct for in-memory / test
221    /// backends.
222    ///
223    /// This method is called by the server process on receipt of SIGTERM or
224    /// SIGINT so that the buffer pool is written to disk even when WAL replay
225    /// would otherwise recover the data on the next startup.
226    ///
227    /// astraeadb-issues.md #1.
228    fn flush(&self) -> Result<()> {
229        Ok(())
230    }
231
232    /// Hybrid search combining graph proximity and vector similarity.
233    ///
234    /// 1. BFS from `anchor` up to `max_hops` to collect candidate nodes
235    /// 2. For each candidate with an embedding, compute vector distance to `query_embedding`
236    /// 3. Blend: `final_score = alpha * vector_score + (1 - alpha) * graph_score`
237    /// 4. Sort ascending (lower = better), return top-k
238    ///
239    /// `alpha`: 0.0 = pure graph proximity, 1.0 = pure vector similarity.
240    fn hybrid_search(
241        &self,
242        _anchor: NodeId,
243        _query_embedding: &[f32],
244        _max_hops: usize,
245        _k: usize,
246        _alpha: f32,
247    ) -> Result<Vec<(NodeId, f32)>> {
248        Err(crate::error::AstraeaError::QueryExecution(
249            "hybrid search not supported by this implementation".into(),
250        ))
251    }
252
253    /// Rank neighbors of a node by semantic similarity to a concept embedding.
254    ///
255    /// Returns up to `k` neighbors sorted by ascending distance (most similar first).
256    /// Neighbors without embeddings are excluded.
257    fn semantic_neighbors(
258        &self,
259        _node_id: NodeId,
260        _concept_embedding: &[f32],
261        _direction: Direction,
262        _k: usize,
263    ) -> Result<Vec<(NodeId, f32)>> {
264        Err(crate::error::AstraeaError::QueryExecution(
265            "semantic neighbors not supported by this implementation".into(),
266        ))
267    }
268
269    /// Greedy multi-hop walk toward a semantic concept.
270    ///
271    /// At each hop, moves to the unvisited neighbor most similar to `concept_embedding`.
272    /// Returns the full path of (NodeId, distance) pairs including the start node.
273    /// Stops when `max_hops` is reached or no unvisited neighbors with embeddings exist.
274    fn semantic_walk(
275        &self,
276        _start: NodeId,
277        _concept_embedding: &[f32],
278        _max_hops: usize,
279    ) -> Result<Vec<(NodeId, f32)>> {
280        Err(crate::error::AstraeaError::QueryExecution(
281            "semantic walk not supported by this implementation".into(),
282        ))
283    }
284
285    // ---- Temporal query methods ----
286
287    /// Get neighbors of a node, only including edges valid at the given timestamp.
288    fn neighbors_at(
289        &self,
290        _node_id: NodeId,
291        _direction: Direction,
292        _timestamp: i64,
293    ) -> Result<Vec<(EdgeId, NodeId)>> {
294        Err(crate::error::AstraeaError::QueryExecution(
295            "temporal neighbors not supported by this implementation".into(),
296        ))
297    }
298
299    /// Breadth-first search from a starting node, only traversing edges valid at the given timestamp.
300    fn bfs_at(
301        &self,
302        _start: NodeId,
303        _max_depth: usize,
304        _timestamp: i64,
305    ) -> Result<Vec<(NodeId, usize)>> {
306        Err(crate::error::AstraeaError::QueryExecution(
307            "temporal BFS not supported by this implementation".into(),
308        ))
309    }
310
311    /// Find the shortest path between two nodes, only traversing edges valid at the given timestamp.
312    fn shortest_path_at(
313        &self,
314        _from: NodeId,
315        _to: NodeId,
316        _timestamp: i64,
317    ) -> Result<Option<GraphPath>> {
318        Err(crate::error::AstraeaError::QueryExecution(
319            "temporal shortest path not supported by this implementation".into(),
320        ))
321    }
322
323    /// Find the weighted shortest path between two nodes at a specific timestamp.
324    fn shortest_path_weighted_at(
325        &self,
326        _from: NodeId,
327        _to: NodeId,
328        _timestamp: i64,
329    ) -> Result<Option<(GraphPath, f64)>> {
330        Err(crate::error::AstraeaError::QueryExecution(
331            "temporal weighted shortest path not supported by this implementation".into(),
332        ))
333    }
334}
335
336/// Vector index trait for approximate nearest neighbor search.
337pub trait VectorIndex: Send + Sync {
338    /// Insert a vector for a node. Dimension must match the index's configured dimension.
339    fn insert(&self, node_id: NodeId, embedding: &[f32]) -> Result<()>;
340
341    /// Remove a vector for a node.
342    fn remove(&self, node_id: NodeId) -> Result<bool>;
343
344    /// Search for the k nearest neighbors of the query vector.
345    fn search(&self, query: &[f32], k: usize) -> Result<Vec<SimilarityResult>>;
346
347    /// The dimensionality of vectors in this index.
348    fn dimension(&self) -> usize;
349
350    /// The distance metric used by this index.
351    fn metric(&self) -> DistanceMetric;
352
353    /// Number of vectors currently in the index.
354    fn len(&self) -> usize;
355
356    /// Whether the index is empty.
357    fn is_empty(&self) -> bool {
358        self.len() == 0
359    }
360
361    /// Return all node IDs currently held by the index.
362    ///
363    /// The default implementation returns an empty `Vec`. Implementors that
364    /// support snapshot reconciliation (e.g. `HnswVectorIndex`) should override
365    /// this to return the exact set of stored ids so the Graph layer can
366    /// diff them against storage (§11.3 of the issue-26 design).
367    fn node_ids(&self) -> Vec<NodeId> {
368        Vec::new()
369    }
370
371    /// Persist the full index state to the given file path.
372    ///
373    /// The default implementation returns an error so that callers can detect
374    /// when an index implementation does not support persistence and fall back
375    /// appropriately (e.g. skip the save or log a warning). Implementations
376    /// that wrap a serialisable index (e.g. `HnswVectorIndex`) should override
377    /// this to write a versioned binary snapshot.
378    fn save_to_path(&self, _path: &Path) -> Result<()> {
379        Err(crate::error::AstraeaError::QueryExecution(
380            "save_to_path not supported by this VectorIndex implementation".into(),
381        ))
382    }
383}