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}