grafeo_core/graph/traits.rs
1//! Storage traits for the graph engine.
2//!
3//! These traits capture the minimal surface that query operators need from
4//! the graph store. The split is intentional:
5//!
6//! - [`GraphStore`]: Read-only operations (scans, lookups, traversal, statistics)
7//! - [`GraphStoreMut`]: Write operations (create, delete, mutate)
8//!
9//! Admin operations (index management, MVCC internals, schema introspection,
10//! statistics recomputation, WAL recovery) stay on the concrete [`LpgStore`]
11//! and are not part of these traits.
12//!
13//! ## Design rationale
14//!
15//! The traits work with typed graph objects (`Node`, `Edge`, `Value`) rather
16//! than raw bytes. This preserves zero-overhead access for in-memory storage
17//! while allowing future backends (SpilloverStore, disk-backed) to implement
18//! the same interface with transparent serialization where needed.
19//!
20//! [`LpgStore`]: crate::graph::lpg::LpgStore
21
22use crate::graph::Direction;
23use crate::graph::lpg::CompareOp;
24use crate::graph::lpg::{Edge, Node};
25use crate::statistics::Statistics;
26use arcstr::ArcStr;
27use grafeo_common::types::{EdgeId, EpochId, NodeId, PropertyKey, TransactionId, Value};
28use grafeo_common::utils::hash::FxHashMap;
29use std::sync::Arc;
30
31/// Read-only graph operations used by the query engine.
32///
33/// This trait captures the minimal surface that scan, expand, filter,
34/// project, and shortest-path operators need. Implementations may serve
35/// data from memory, disk, or a hybrid of both.
36///
37/// # Object safety
38///
39/// This trait is object-safe: you can use `Arc<dyn GraphStore>` for dynamic
40/// dispatch. Traversal methods return `Vec` instead of `impl Iterator` to
41/// enable this.
42pub trait GraphStore: Send + Sync {
43 // --- Point lookups ---
44
45 /// Returns a node by ID (latest visible version at current epoch).
46 fn get_node(&self, id: NodeId) -> Option<Node>;
47
48 /// Returns an edge by ID (latest visible version at current epoch).
49 fn get_edge(&self, id: EdgeId) -> Option<Edge>;
50
51 /// Returns a node visible to a specific transaction.
52 fn get_node_versioned(
53 &self,
54 id: NodeId,
55 epoch: EpochId,
56 transaction_id: TransactionId,
57 ) -> Option<Node>;
58
59 /// Returns an edge visible to a specific transaction.
60 fn get_edge_versioned(
61 &self,
62 id: EdgeId,
63 epoch: EpochId,
64 transaction_id: TransactionId,
65 ) -> Option<Edge>;
66
67 /// Returns a node using pure epoch-based visibility (no transaction context).
68 ///
69 /// The node is visible if `created_epoch <= epoch` and not deleted at or
70 /// before `epoch`. Used for time-travel queries where transaction ownership
71 /// must not bypass the epoch check.
72 fn get_node_at_epoch(&self, id: NodeId, epoch: EpochId) -> Option<Node>;
73
74 /// Returns an edge using pure epoch-based visibility (no transaction context).
75 fn get_edge_at_epoch(&self, id: EdgeId, epoch: EpochId) -> Option<Edge>;
76
77 // --- Property access (fast path, avoids loading full entity) ---
78
79 /// Gets a single property from a node without loading all properties.
80 fn get_node_property(&self, id: NodeId, key: &PropertyKey) -> Option<Value>;
81
82 /// Gets a single property from an edge without loading all properties.
83 fn get_edge_property(&self, id: EdgeId, key: &PropertyKey) -> Option<Value>;
84
85 /// Gets a property for multiple nodes in a single batch operation.
86 fn get_node_property_batch(&self, ids: &[NodeId], key: &PropertyKey) -> Vec<Option<Value>>;
87
88 /// Gets all properties for multiple nodes in a single batch operation.
89 fn get_nodes_properties_batch(&self, ids: &[NodeId]) -> Vec<FxHashMap<PropertyKey, Value>>;
90
91 /// Gets selected properties for multiple nodes (projection pushdown).
92 fn get_nodes_properties_selective_batch(
93 &self,
94 ids: &[NodeId],
95 keys: &[PropertyKey],
96 ) -> Vec<FxHashMap<PropertyKey, Value>>;
97
98 /// Gets selected properties for multiple edges (projection pushdown).
99 fn get_edges_properties_selective_batch(
100 &self,
101 ids: &[EdgeId],
102 keys: &[PropertyKey],
103 ) -> Vec<FxHashMap<PropertyKey, Value>>;
104
105 // --- Traversal ---
106
107 /// Returns neighbor node IDs in the specified direction.
108 ///
109 /// Returns `Vec` instead of an iterator for object safety. The underlying
110 /// `ChunkedAdjacency` already produces a `Vec` internally.
111 fn neighbors(&self, node: NodeId, direction: Direction) -> Vec<NodeId>;
112
113 /// Returns (target_node, edge_id) pairs for edges from a node.
114 fn edges_from(&self, node: NodeId, direction: Direction) -> Vec<(NodeId, EdgeId)>;
115
116 /// Returns the out-degree of a node (number of outgoing edges).
117 fn out_degree(&self, node: NodeId) -> usize;
118
119 /// Returns the in-degree of a node (number of incoming edges).
120 fn in_degree(&self, node: NodeId) -> usize;
121
122 /// Whether backward adjacency is available for incoming edge queries.
123 fn has_backward_adjacency(&self) -> bool;
124
125 // --- Scans ---
126
127 /// Returns all non-deleted node IDs, sorted by ID.
128 fn node_ids(&self) -> Vec<NodeId>;
129
130 /// Returns all node IDs including uncommitted/PENDING versions.
131 ///
132 /// Unlike `node_ids()` which pre-filters by current epoch, this method
133 /// returns every node that has a version chain entry. Used by scan operators
134 /// that perform their own MVCC visibility filtering (e.g. with transaction context).
135 fn all_node_ids(&self) -> Vec<NodeId> {
136 // Default: fall back to node_ids() for stores without MVCC
137 self.node_ids()
138 }
139
140 /// Returns node IDs with a specific label.
141 fn nodes_by_label(&self, label: &str) -> Vec<NodeId>;
142
143 /// Returns the total number of non-deleted nodes.
144 fn node_count(&self) -> usize;
145
146 /// Returns the total number of non-deleted edges.
147 fn edge_count(&self) -> usize;
148
149 // --- Entity metadata ---
150
151 /// Returns the type string of an edge.
152 fn edge_type(&self, id: EdgeId) -> Option<ArcStr>;
153
154 /// Returns the type string of an edge visible to a specific transaction.
155 ///
156 /// Falls back to epoch-based `edge_type` if not overridden.
157 fn edge_type_versioned(
158 &self,
159 id: EdgeId,
160 epoch: EpochId,
161 transaction_id: TransactionId,
162 ) -> Option<ArcStr> {
163 let _ = (epoch, transaction_id);
164 self.edge_type(id)
165 }
166
167 // --- Index introspection ---
168
169 /// Returns `true` if a property index exists for the given property.
170 ///
171 /// The default returns `false`, which is correct for stores without indexes.
172 fn has_property_index(&self, _property: &str) -> bool {
173 false
174 }
175
176 // --- Filtered search ---
177
178 /// Finds all nodes with a specific property value. Uses indexes when available.
179 fn find_nodes_by_property(&self, property: &str, value: &Value) -> Vec<NodeId>;
180
181 /// Finds nodes matching multiple property equality conditions.
182 fn find_nodes_by_properties(&self, conditions: &[(&str, Value)]) -> Vec<NodeId>;
183
184 /// Finds nodes whose property value falls within a range.
185 fn find_nodes_in_range(
186 &self,
187 property: &str,
188 min: Option<&Value>,
189 max: Option<&Value>,
190 min_inclusive: bool,
191 max_inclusive: bool,
192 ) -> Vec<NodeId>;
193
194 // --- Zone maps (skip pruning) ---
195
196 /// Returns `true` if a node property predicate might match any nodes.
197 /// Uses zone maps for early filtering.
198 fn node_property_might_match(
199 &self,
200 property: &PropertyKey,
201 op: CompareOp,
202 value: &Value,
203 ) -> bool;
204
205 /// Returns `true` if an edge property predicate might match any edges.
206 fn edge_property_might_match(
207 &self,
208 property: &PropertyKey,
209 op: CompareOp,
210 value: &Value,
211 ) -> bool;
212
213 // --- Statistics (for cost-based optimizer) ---
214
215 /// Returns the current statistics snapshot (cheap Arc clone).
216 fn statistics(&self) -> Arc<Statistics>;
217
218 /// Estimates cardinality for a label scan.
219 fn estimate_label_cardinality(&self, label: &str) -> f64;
220
221 /// Estimates average degree for an edge type.
222 fn estimate_avg_degree(&self, edge_type: &str, outgoing: bool) -> f64;
223
224 // --- Epoch ---
225
226 /// Returns the current MVCC epoch.
227 fn current_epoch(&self) -> EpochId;
228
229 // --- Schema introspection ---
230
231 /// Returns all label names in the database.
232 fn all_labels(&self) -> Vec<String> {
233 Vec::new()
234 }
235
236 /// Returns all edge type names in the database.
237 fn all_edge_types(&self) -> Vec<String> {
238 Vec::new()
239 }
240
241 /// Returns all property key names used in the database.
242 fn all_property_keys(&self) -> Vec<String> {
243 Vec::new()
244 }
245
246 // --- Visibility checks (fast path, avoids building full entities) ---
247
248 /// Checks if a node is visible at the given epoch without building the full Node.
249 ///
250 /// More efficient than `get_node_at_epoch(...).is_some()` because it skips
251 /// label and property loading. Override in concrete stores for optimal
252 /// performance.
253 fn is_node_visible_at_epoch(&self, id: NodeId, epoch: EpochId) -> bool {
254 self.get_node_at_epoch(id, epoch).is_some()
255 }
256
257 /// Checks if a node is visible to a specific transaction without building
258 /// the full Node.
259 fn is_node_visible_versioned(
260 &self,
261 id: NodeId,
262 epoch: EpochId,
263 transaction_id: TransactionId,
264 ) -> bool {
265 self.get_node_versioned(id, epoch, transaction_id).is_some()
266 }
267
268 /// Checks if an edge is visible at the given epoch without building the full Edge.
269 ///
270 /// More efficient than `get_edge_at_epoch(...).is_some()` because it skips
271 /// type name resolution and property loading. Override in concrete stores
272 /// for optimal performance.
273 fn is_edge_visible_at_epoch(&self, id: EdgeId, epoch: EpochId) -> bool {
274 self.get_edge_at_epoch(id, epoch).is_some()
275 }
276
277 /// Checks if an edge is visible to a specific transaction without building
278 /// the full Edge.
279 fn is_edge_visible_versioned(
280 &self,
281 id: EdgeId,
282 epoch: EpochId,
283 transaction_id: TransactionId,
284 ) -> bool {
285 self.get_edge_versioned(id, epoch, transaction_id).is_some()
286 }
287
288 /// Filters node IDs to only those visible at the given epoch (batch).
289 ///
290 /// More efficient than per-node calls because implementations can hold
291 /// a single lock for the entire batch.
292 fn filter_visible_node_ids(&self, ids: &[NodeId], epoch: EpochId) -> Vec<NodeId> {
293 ids.iter()
294 .copied()
295 .filter(|id| self.is_node_visible_at_epoch(*id, epoch))
296 .collect()
297 }
298
299 /// Filters node IDs to only those visible to a transaction (batch).
300 fn filter_visible_node_ids_versioned(
301 &self,
302 ids: &[NodeId],
303 epoch: EpochId,
304 transaction_id: TransactionId,
305 ) -> Vec<NodeId> {
306 ids.iter()
307 .copied()
308 .filter(|id| self.is_node_visible_versioned(*id, epoch, transaction_id))
309 .collect()
310 }
311
312 // --- History ---
313
314 /// Returns all versions of a node with their creation/deletion epochs, newest first.
315 ///
316 /// Each entry is `(created_epoch, deleted_epoch, Node)`. Properties and labels
317 /// reflect the current state (they are not versioned per-epoch).
318 ///
319 /// Default returns empty (not all backends track version history).
320 fn get_node_history(&self, _id: NodeId) -> Vec<(EpochId, Option<EpochId>, Node)> {
321 Vec::new()
322 }
323
324 /// Returns all versions of an edge with their creation/deletion epochs, newest first.
325 ///
326 /// Each entry is `(created_epoch, deleted_epoch, Edge)`. Properties reflect
327 /// the current state (they are not versioned per-epoch).
328 ///
329 /// Default returns empty (not all backends track version history).
330 fn get_edge_history(&self, _id: EdgeId) -> Vec<(EpochId, Option<EpochId>, Edge)> {
331 Vec::new()
332 }
333}
334
335/// Write operations for graph mutation.
336///
337/// Separated from [`GraphStore`] so read-only wrappers (snapshots, read
338/// replicas) can implement only `GraphStore`. Any mutable store is also
339/// readable via the supertrait bound.
340pub trait GraphStoreMut: GraphStore {
341 // --- Node creation ---
342
343 /// Creates a new node with the given labels.
344 fn create_node(&self, labels: &[&str]) -> NodeId;
345
346 /// Creates a new node within a transaction context.
347 fn create_node_versioned(
348 &self,
349 labels: &[&str],
350 epoch: EpochId,
351 transaction_id: TransactionId,
352 ) -> NodeId;
353
354 // --- Edge creation ---
355
356 /// Creates a new edge between two nodes.
357 fn create_edge(&self, src: NodeId, dst: NodeId, edge_type: &str) -> EdgeId;
358
359 /// Creates a new edge within a transaction context.
360 fn create_edge_versioned(
361 &self,
362 src: NodeId,
363 dst: NodeId,
364 edge_type: &str,
365 epoch: EpochId,
366 transaction_id: TransactionId,
367 ) -> EdgeId;
368
369 /// Creates multiple edges in batch (single lock acquisition).
370 fn batch_create_edges(&self, edges: &[(NodeId, NodeId, &str)]) -> Vec<EdgeId>;
371
372 // --- Deletion ---
373
374 /// Deletes a node. Returns `true` if the node existed.
375 fn delete_node(&self, id: NodeId) -> bool;
376
377 /// Deletes a node within a transaction context. Returns `true` if the node existed.
378 fn delete_node_versioned(
379 &self,
380 id: NodeId,
381 epoch: EpochId,
382 transaction_id: TransactionId,
383 ) -> bool;
384
385 /// Deletes all edges connected to a node (DETACH DELETE).
386 fn delete_node_edges(&self, node_id: NodeId);
387
388 /// Deletes an edge. Returns `true` if the edge existed.
389 fn delete_edge(&self, id: EdgeId) -> bool;
390
391 /// Deletes an edge within a transaction context. Returns `true` if the edge existed.
392 fn delete_edge_versioned(
393 &self,
394 id: EdgeId,
395 epoch: EpochId,
396 transaction_id: TransactionId,
397 ) -> bool;
398
399 // --- Property mutation ---
400
401 /// Sets a property on a node.
402 fn set_node_property(&self, id: NodeId, key: &str, value: Value);
403
404 /// Sets a property on an edge.
405 fn set_edge_property(&self, id: EdgeId, key: &str, value: Value);
406
407 /// Sets a node property within a transaction, recording the previous value
408 /// so it can be restored on rollback.
409 ///
410 /// Default delegates to [`set_node_property`](Self::set_node_property).
411 fn set_node_property_versioned(
412 &self,
413 id: NodeId,
414 key: &str,
415 value: Value,
416 _transaction_id: TransactionId,
417 ) {
418 self.set_node_property(id, key, value);
419 }
420
421 /// Sets an edge property within a transaction, recording the previous value
422 /// so it can be restored on rollback.
423 ///
424 /// Default delegates to [`set_edge_property`](Self::set_edge_property).
425 fn set_edge_property_versioned(
426 &self,
427 id: EdgeId,
428 key: &str,
429 value: Value,
430 _transaction_id: TransactionId,
431 ) {
432 self.set_edge_property(id, key, value);
433 }
434
435 /// Removes a property from a node. Returns the previous value if it existed.
436 fn remove_node_property(&self, id: NodeId, key: &str) -> Option<Value>;
437
438 /// Removes a property from an edge. Returns the previous value if it existed.
439 fn remove_edge_property(&self, id: EdgeId, key: &str) -> Option<Value>;
440
441 /// Removes a node property within a transaction, recording the previous value
442 /// so it can be restored on rollback.
443 ///
444 /// Default delegates to [`remove_node_property`](Self::remove_node_property).
445 fn remove_node_property_versioned(
446 &self,
447 id: NodeId,
448 key: &str,
449 _transaction_id: TransactionId,
450 ) -> Option<Value> {
451 self.remove_node_property(id, key)
452 }
453
454 /// Removes an edge property within a transaction, recording the previous value
455 /// so it can be restored on rollback.
456 ///
457 /// Default delegates to [`remove_edge_property`](Self::remove_edge_property).
458 fn remove_edge_property_versioned(
459 &self,
460 id: EdgeId,
461 key: &str,
462 _transaction_id: TransactionId,
463 ) -> Option<Value> {
464 self.remove_edge_property(id, key)
465 }
466
467 // --- Label mutation ---
468
469 /// Adds a label to a node. Returns `true` if the label was new.
470 fn add_label(&self, node_id: NodeId, label: &str) -> bool;
471
472 /// Removes a label from a node. Returns `true` if the label existed.
473 fn remove_label(&self, node_id: NodeId, label: &str) -> bool;
474
475 /// Adds a label within a transaction, recording the change for rollback.
476 ///
477 /// Default delegates to [`add_label`](Self::add_label).
478 fn add_label_versioned(
479 &self,
480 node_id: NodeId,
481 label: &str,
482 _transaction_id: TransactionId,
483 ) -> bool {
484 self.add_label(node_id, label)
485 }
486
487 /// Removes a label within a transaction, recording the change for rollback.
488 ///
489 /// Default delegates to [`remove_label`](Self::remove_label).
490 fn remove_label_versioned(
491 &self,
492 node_id: NodeId,
493 label: &str,
494 _transaction_id: TransactionId,
495 ) -> bool {
496 self.remove_label(node_id, label)
497 }
498
499 // --- Convenience (with default implementations) ---
500
501 /// Creates a new node with labels and properties in one call.
502 ///
503 /// The default implementation calls [`create_node`](Self::create_node)
504 /// followed by [`set_node_property`](Self::set_node_property) for each
505 /// property. Implementations may override for atomicity or performance.
506 fn create_node_with_props(
507 &self,
508 labels: &[&str],
509 properties: &[(PropertyKey, Value)],
510 ) -> NodeId {
511 let id = self.create_node(labels);
512 for (key, value) in properties {
513 self.set_node_property(id, key.as_str(), value.clone());
514 }
515 id
516 }
517
518 /// Creates a new edge with properties in one call.
519 ///
520 /// The default implementation calls [`create_edge`](Self::create_edge)
521 /// followed by [`set_edge_property`](Self::set_edge_property) for each
522 /// property. Implementations may override for atomicity or performance.
523 fn create_edge_with_props(
524 &self,
525 src: NodeId,
526 dst: NodeId,
527 edge_type: &str,
528 properties: &[(PropertyKey, Value)],
529 ) -> EdgeId {
530 let id = self.create_edge(src, dst, edge_type);
531 for (key, value) in properties {
532 self.set_edge_property(id, key.as_str(), value.clone());
533 }
534 id
535 }
536}