Skip to main content

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};
25#[cfg(feature = "vector-index")]
26use crate::index::vector::DistanceMetric;
27use crate::statistics::Statistics;
28use arcstr::ArcStr;
29use grafeo_common::types::{EdgeId, EpochId, NodeId, PropertyKey, TransactionId, Value};
30use grafeo_common::utils::hash::FxHashMap;
31use std::sync::Arc;
32
33/// Read-only graph operations used by the query engine.
34///
35/// This trait captures the minimal surface that scan, expand, filter,
36/// project, and shortest-path operators need. Implementations may serve
37/// data from memory, disk, or a hybrid of both.
38///
39/// # Object safety
40///
41/// This trait is object-safe: you can use `Arc<dyn GraphStoreSearch>` for dynamic
42/// dispatch. Traversal methods return `Vec` instead of `impl Iterator` to
43/// enable this.
44pub trait GraphStore: Send + Sync {
45    // --- Point lookups ---
46
47    /// Returns a node by ID (latest visible version at current epoch).
48    fn get_node(&self, id: NodeId) -> Option<Node>;
49
50    /// Returns an edge by ID (latest visible version at current epoch).
51    fn get_edge(&self, id: EdgeId) -> Option<Edge>;
52
53    /// Returns a node visible to a specific transaction.
54    fn get_node_versioned(
55        &self,
56        id: NodeId,
57        epoch: EpochId,
58        transaction_id: TransactionId,
59    ) -> Option<Node>;
60
61    /// Returns an edge visible to a specific transaction.
62    fn get_edge_versioned(
63        &self,
64        id: EdgeId,
65        epoch: EpochId,
66        transaction_id: TransactionId,
67    ) -> Option<Edge>;
68
69    /// Returns a node using pure epoch-based visibility (no transaction context).
70    ///
71    /// The node is visible if `created_epoch <= epoch` and not deleted at or
72    /// before `epoch`. Used for time-travel queries where transaction ownership
73    /// must not bypass the epoch check.
74    fn get_node_at_epoch(&self, id: NodeId, epoch: EpochId) -> Option<Node>;
75
76    /// Returns an edge using pure epoch-based visibility (no transaction context).
77    fn get_edge_at_epoch(&self, id: EdgeId, epoch: EpochId) -> Option<Edge>;
78
79    // --- Property access (fast path, avoids loading full entity) ---
80
81    /// Gets a single property from a node without loading all properties.
82    fn get_node_property(&self, id: NodeId, key: &PropertyKey) -> Option<Value>;
83
84    /// Gets a single property from an edge without loading all properties.
85    fn get_edge_property(&self, id: EdgeId, key: &PropertyKey) -> Option<Value>;
86
87    /// Gets a property for multiple nodes in a single batch operation.
88    fn get_node_property_batch(&self, ids: &[NodeId], key: &PropertyKey) -> Vec<Option<Value>>;
89
90    /// Gets all properties for multiple nodes in a single batch operation.
91    fn get_nodes_properties_batch(&self, ids: &[NodeId]) -> Vec<FxHashMap<PropertyKey, Value>>;
92
93    /// Gets selected properties for multiple nodes (projection pushdown).
94    fn get_nodes_properties_selective_batch(
95        &self,
96        ids: &[NodeId],
97        keys: &[PropertyKey],
98    ) -> Vec<FxHashMap<PropertyKey, Value>>;
99
100    /// Gets selected properties for multiple edges (projection pushdown).
101    fn get_edges_properties_selective_batch(
102        &self,
103        ids: &[EdgeId],
104        keys: &[PropertyKey],
105    ) -> Vec<FxHashMap<PropertyKey, Value>>;
106
107    // --- Traversal ---
108
109    /// Returns neighbor node IDs in the specified direction.
110    ///
111    /// Returns `Vec` instead of an iterator for object safety. The underlying
112    /// `ChunkedAdjacency` already produces a `Vec` internally.
113    fn neighbors(&self, node: NodeId, direction: Direction) -> Vec<NodeId>;
114
115    /// Returns (target_node, edge_id) pairs for edges from a node.
116    fn edges_from(&self, node: NodeId, direction: Direction) -> Vec<(NodeId, EdgeId)>;
117
118    /// Returns the out-degree of a node (number of outgoing edges).
119    fn out_degree(&self, node: NodeId) -> usize;
120
121    /// Returns the in-degree of a node (number of incoming edges).
122    fn in_degree(&self, node: NodeId) -> usize;
123
124    /// Whether backward adjacency is available for incoming edge queries.
125    fn has_backward_adjacency(&self) -> bool;
126
127    // --- Scans ---
128
129    /// Returns all non-deleted node IDs, sorted by ID.
130    fn node_ids(&self) -> Vec<NodeId>;
131
132    /// Returns all node IDs including uncommitted/PENDING versions.
133    ///
134    /// Unlike `node_ids()` which pre-filters by current epoch, this method
135    /// returns every node that has a version chain entry. Used by scan operators
136    /// that perform their own MVCC visibility filtering (e.g. with transaction context).
137    fn all_node_ids(&self) -> Vec<NodeId> {
138        // Default: fall back to node_ids() for stores without MVCC
139        self.node_ids()
140    }
141
142    /// Returns node IDs with a specific label.
143    fn nodes_by_label(&self, label: &str) -> Vec<NodeId>;
144
145    /// Returns the number of non-deleted nodes with a specific label.
146    ///
147    /// Default falls back to `self.nodes_by_label(label).len()`; stores that
148    /// maintain a per-label index should override for an O(1) count without
149    /// allocating the full ID list.
150    fn nodes_by_label_count(&self, label: &str) -> usize {
151        self.nodes_by_label(label).len()
152    }
153
154    /// Returns the total number of non-deleted nodes.
155    fn node_count(&self) -> usize;
156
157    /// Returns the total number of non-deleted edges.
158    fn edge_count(&self) -> usize;
159
160    // --- Entity metadata ---
161
162    /// Returns the type string of an edge.
163    fn edge_type(&self, id: EdgeId) -> Option<ArcStr>;
164
165    /// Returns the type string of an edge visible to a specific transaction.
166    ///
167    /// Falls back to epoch-based `edge_type` if not overridden.
168    fn edge_type_versioned(
169        &self,
170        id: EdgeId,
171        epoch: EpochId,
172        transaction_id: TransactionId,
173    ) -> Option<ArcStr> {
174        let _ = (epoch, transaction_id);
175        self.edge_type(id)
176    }
177
178    // --- Index introspection ---
179
180    /// Returns `true` if a property index exists for the given property.
181    ///
182    /// The default returns `false`, which is correct for stores without indexes.
183    fn has_property_index(&self, _property: &str) -> bool {
184        false
185    }
186
187    // --- Filtered search ---
188
189    /// Finds all nodes with a specific property value. Uses indexes when available.
190    fn find_nodes_by_property(&self, property: &str, value: &Value) -> Vec<NodeId>;
191
192    /// Finds nodes matching multiple property equality conditions.
193    fn find_nodes_by_properties(&self, conditions: &[(&str, Value)]) -> Vec<NodeId>;
194
195    /// Finds nodes whose property value falls within a range.
196    fn find_nodes_in_range(
197        &self,
198        property: &str,
199        min: Option<&Value>,
200        max: Option<&Value>,
201        min_inclusive: bool,
202        max_inclusive: bool,
203    ) -> Vec<NodeId>;
204
205    // --- Zone maps (skip pruning) ---
206
207    /// Returns `true` if a node property predicate might match any nodes.
208    /// Uses zone maps for early filtering.
209    fn node_property_might_match(
210        &self,
211        property: &PropertyKey,
212        op: CompareOp,
213        value: &Value,
214    ) -> bool;
215
216    /// Returns `true` if an edge property predicate might match any edges.
217    fn edge_property_might_match(
218        &self,
219        property: &PropertyKey,
220        op: CompareOp,
221        value: &Value,
222    ) -> bool;
223
224    // --- Statistics (for cost-based optimizer) ---
225
226    /// Returns the current statistics snapshot (cheap Arc clone).
227    fn statistics(&self) -> Arc<Statistics>;
228
229    /// Estimates cardinality for a label scan.
230    fn estimate_label_cardinality(&self, label: &str) -> f64;
231
232    /// Estimates average degree for an edge type.
233    fn estimate_avg_degree(&self, edge_type: &str, outgoing: bool) -> f64;
234
235    // --- Epoch ---
236
237    /// Returns the current MVCC epoch.
238    fn current_epoch(&self) -> EpochId;
239
240    // --- Schema introspection ---
241
242    /// Returns all label names in the database.
243    fn all_labels(&self) -> Vec<String> {
244        Vec::new()
245    }
246
247    /// Returns all edge type names in the database.
248    fn all_edge_types(&self) -> Vec<String> {
249        Vec::new()
250    }
251
252    /// Returns all property key names used in the database.
253    fn all_property_keys(&self) -> Vec<String> {
254        Vec::new()
255    }
256
257    // --- Visibility checks (fast path, avoids building full entities) ---
258
259    /// Checks if a node is visible at the given epoch without building the full Node.
260    ///
261    /// More efficient than `get_node_at_epoch(...).is_some()` because it skips
262    /// label and property loading. Override in concrete stores for optimal
263    /// performance.
264    fn is_node_visible_at_epoch(&self, id: NodeId, epoch: EpochId) -> bool {
265        self.get_node_at_epoch(id, epoch).is_some()
266    }
267
268    /// Checks if a node is visible to a specific transaction without building
269    /// the full Node.
270    fn is_node_visible_versioned(
271        &self,
272        id: NodeId,
273        epoch: EpochId,
274        transaction_id: TransactionId,
275    ) -> bool {
276        self.get_node_versioned(id, epoch, transaction_id).is_some()
277    }
278
279    /// Checks if an edge is visible at the given epoch without building the full Edge.
280    ///
281    /// More efficient than `get_edge_at_epoch(...).is_some()` because it skips
282    /// type name resolution and property loading. Override in concrete stores
283    /// for optimal performance.
284    fn is_edge_visible_at_epoch(&self, id: EdgeId, epoch: EpochId) -> bool {
285        self.get_edge_at_epoch(id, epoch).is_some()
286    }
287
288    /// Checks if an edge is visible to a specific transaction without building
289    /// the full Edge.
290    fn is_edge_visible_versioned(
291        &self,
292        id: EdgeId,
293        epoch: EpochId,
294        transaction_id: TransactionId,
295    ) -> bool {
296        self.get_edge_versioned(id, epoch, transaction_id).is_some()
297    }
298
299    /// Filters node IDs to only those visible at the given epoch (batch).
300    ///
301    /// More efficient than per-node calls because implementations can hold
302    /// a single lock for the entire batch.
303    fn filter_visible_node_ids(&self, ids: &[NodeId], epoch: EpochId) -> Vec<NodeId> {
304        ids.iter()
305            .copied()
306            .filter(|id| self.is_node_visible_at_epoch(*id, epoch))
307            .collect()
308    }
309
310    /// Filters node IDs to only those visible to a transaction (batch).
311    fn filter_visible_node_ids_versioned(
312        &self,
313        ids: &[NodeId],
314        epoch: EpochId,
315        transaction_id: TransactionId,
316    ) -> Vec<NodeId> {
317        ids.iter()
318            .copied()
319            .filter(|id| self.is_node_visible_versioned(*id, epoch, transaction_id))
320            .collect()
321    }
322
323    // --- History ---
324
325    /// Returns all versions of a node with their creation/deletion epochs, newest first.
326    ///
327    /// Each entry is `(created_epoch, deleted_epoch, Node)`. Properties and labels
328    /// reflect the current state (they are not versioned per-epoch).
329    ///
330    /// Default returns empty (not all backends track version history).
331    fn get_node_history(&self, _id: NodeId) -> Vec<(EpochId, Option<EpochId>, Node)> {
332        Vec::new()
333    }
334
335    /// Returns all versions of an edge with their creation/deletion epochs, newest first.
336    ///
337    /// Each entry is `(created_epoch, deleted_epoch, Edge)`. Properties reflect
338    /// the current state (they are not versioned per-epoch).
339    ///
340    /// Default returns empty (not all backends track version history).
341    fn get_edge_history(&self, _id: EdgeId) -> Vec<(EpochId, Option<EpochId>, Edge)> {
342        Vec::new()
343    }
344}
345
346/// Index-backed search capabilities that an LPG store may optionally provide.
347///
348/// Keeps the base [`GraphStore`] scoped to graph-structure operations. Stores
349/// that back text or vector indexes implement these methods with real search
350/// logic; stores that don't (columnar bases, projections, RDF adapters) accept
351/// the no-op defaults and the executor falls through to per-row evaluation.
352///
353/// # Symmetric text and vector APIs
354///
355/// `text_search` and `vector_search` are peer operations returning owned
356/// `Vec<(NodeId, f64)>`. The planner decides strategy based on `has_*_index`
357/// and `vector_index_metric` introspection; the store executes the chosen
358/// plan, falling back to brute-force internally when the request is valid
359/// but no matching index exists.
360pub trait GraphStoreSearch: GraphStore {
361    // --- Text search (BM25) ---
362
363    /// Returns true if a BM25 text index exists for the given label and property.
364    #[cfg(feature = "text-index")]
365    #[must_use]
366    fn has_text_index(&self, _label: &str, _property: &str) -> bool {
367        false
368    }
369
370    /// Scores a single document against a text query for per-row filter evaluation.
371    ///
372    /// Returns `None` when no text index exists for the (label, property) pair.
373    /// The planner calls this when pushdown is unavailable, for example when
374    /// the text predicate follows a traversal instead of a bare label scan.
375    #[cfg(feature = "text-index")]
376    fn score_text(
377        &self,
378        _node_id: NodeId,
379        _label: &str,
380        _property: &str,
381        _query: &str,
382    ) -> Option<f64> {
383        None
384    }
385
386    /// Returns the top-`k` documents by BM25 score for a text query.
387    ///
388    /// Results are sorted by score descending. Returns an empty vec when no
389    /// text index exists, so the caller can fall back to a slower path.
390    #[cfg(feature = "text-index")]
391    fn text_search(
392        &self,
393        _label: &str,
394        _property: &str,
395        _query: &str,
396        _k: usize,
397    ) -> Vec<(NodeId, f64)> {
398        Vec::new()
399    }
400
401    /// Returns every document whose BM25 score meets or exceeds a threshold.
402    #[cfg(feature = "text-index")]
403    fn text_search_with_threshold(
404        &self,
405        _label: &str,
406        _property: &str,
407        _query: &str,
408        _threshold: f64,
409    ) -> Vec<(NodeId, f64)> {
410        Vec::new()
411    }
412
413    // --- Vector search (HNSW or brute force) ---
414
415    /// Returns true if a vector index exists for the given label and property.
416    #[cfg(feature = "vector-index")]
417    #[must_use]
418    fn has_vector_index(&self, _label: &str, _property: &str) -> bool {
419        false
420    }
421
422    /// Returns the distance metric of the vector index at (label, property), if any.
423    ///
424    /// The planner uses this to decide between an HNSW-accelerated plan and a
425    /// brute-force fallback: an index whose metric does not match the query's
426    /// requested metric cannot serve the query directly, so the planner either
427    /// routes to brute force or skips pushdown entirely.
428    #[cfg(feature = "vector-index")]
429    fn vector_index_metric(&self, _label: &str, _property: &str) -> Option<DistanceMetric> {
430        None
431    }
432
433    /// Returns the top-`k` nearest neighbors for a vector similarity search.
434    ///
435    /// `label` is optional: `None` searches every node that has the named
436    /// property. The store uses HNSW when an index exists for (label, property)
437    /// whose metric matches `metric`; otherwise it falls back to brute force.
438    ///
439    /// Results are sorted by distance ascending (nearest first). Returns an
440    /// empty vec when neither an index nor any indexable property is found.
441    #[cfg(feature = "vector-index")]
442    fn vector_search(
443        &self,
444        _label: Option<&str>,
445        _property: &str,
446        _query: &[f32],
447        _k: usize,
448        _metric: DistanceMetric,
449    ) -> Vec<(NodeId, f64)> {
450        Vec::new()
451    }
452
453    /// Returns every node whose distance to the query vector is at or below a threshold.
454    #[cfg(feature = "vector-index")]
455    fn vector_search_with_threshold(
456        &self,
457        _label: Option<&str>,
458        _property: &str,
459        _query: &[f32],
460        _threshold: f64,
461        _metric: DistanceMetric,
462    ) -> Vec<(NodeId, f64)> {
463        Vec::new()
464    }
465}
466
467/// Write operations for graph mutation.
468///
469/// Separated from [`GraphStore`] so read-only wrappers (snapshots, read
470/// replicas) can implement only `GraphStore`. Any mutable store is also
471/// readable via the supertrait bound.
472pub trait GraphStoreMut: GraphStoreSearch {
473    // --- Node creation ---
474
475    /// Creates a new node with the given labels.
476    fn create_node(&self, labels: &[&str]) -> NodeId;
477
478    /// Creates a new node within a transaction context.
479    fn create_node_versioned(
480        &self,
481        labels: &[&str],
482        epoch: EpochId,
483        transaction_id: TransactionId,
484    ) -> NodeId;
485
486    // --- Edge creation ---
487
488    /// Creates a new edge between two nodes.
489    fn create_edge(&self, src: NodeId, dst: NodeId, edge_type: &str) -> EdgeId;
490
491    /// Creates a new edge within a transaction context.
492    fn create_edge_versioned(
493        &self,
494        src: NodeId,
495        dst: NodeId,
496        edge_type: &str,
497        epoch: EpochId,
498        transaction_id: TransactionId,
499    ) -> EdgeId;
500
501    /// Creates multiple edges in batch (single lock acquisition).
502    fn batch_create_edges(&self, edges: &[(NodeId, NodeId, &str)]) -> Vec<EdgeId>;
503
504    // --- Deletion ---
505
506    /// Deletes a node. Returns `true` if the node existed.
507    fn delete_node(&self, id: NodeId) -> bool;
508
509    /// Deletes a node within a transaction context. Returns `true` if the node existed.
510    fn delete_node_versioned(
511        &self,
512        id: NodeId,
513        epoch: EpochId,
514        transaction_id: TransactionId,
515    ) -> bool;
516
517    /// Deletes all edges connected to a node (DETACH DELETE).
518    fn delete_node_edges(&self, node_id: NodeId);
519
520    /// Deletes an edge. Returns `true` if the edge existed.
521    fn delete_edge(&self, id: EdgeId) -> bool;
522
523    /// Deletes an edge within a transaction context. Returns `true` if the edge existed.
524    fn delete_edge_versioned(
525        &self,
526        id: EdgeId,
527        epoch: EpochId,
528        transaction_id: TransactionId,
529    ) -> bool;
530
531    // --- Property mutation ---
532
533    /// Sets a property on a node.
534    fn set_node_property(&self, id: NodeId, key: &str, value: Value);
535
536    /// Sets a property on an edge.
537    fn set_edge_property(&self, id: EdgeId, key: &str, value: Value);
538
539    /// Sets a node property within a transaction, recording the previous value
540    /// so it can be restored on rollback.
541    ///
542    /// Default delegates to [`set_node_property`](Self::set_node_property).
543    fn set_node_property_versioned(
544        &self,
545        id: NodeId,
546        key: &str,
547        value: Value,
548        _transaction_id: TransactionId,
549    ) {
550        self.set_node_property(id, key, value);
551    }
552
553    /// Sets an edge property within a transaction, recording the previous value
554    /// so it can be restored on rollback.
555    ///
556    /// Default delegates to [`set_edge_property`](Self::set_edge_property).
557    fn set_edge_property_versioned(
558        &self,
559        id: EdgeId,
560        key: &str,
561        value: Value,
562        _transaction_id: TransactionId,
563    ) {
564        self.set_edge_property(id, key, value);
565    }
566
567    /// Removes a property from a node. Returns the previous value if it existed.
568    fn remove_node_property(&self, id: NodeId, key: &str) -> Option<Value>;
569
570    /// Removes a property from an edge. Returns the previous value if it existed.
571    fn remove_edge_property(&self, id: EdgeId, key: &str) -> Option<Value>;
572
573    /// Removes a node property within a transaction, recording the previous value
574    /// so it can be restored on rollback.
575    ///
576    /// Default delegates to [`remove_node_property`](Self::remove_node_property).
577    fn remove_node_property_versioned(
578        &self,
579        id: NodeId,
580        key: &str,
581        _transaction_id: TransactionId,
582    ) -> Option<Value> {
583        self.remove_node_property(id, key)
584    }
585
586    /// Removes an edge property within a transaction, recording the previous value
587    /// so it can be restored on rollback.
588    ///
589    /// Default delegates to [`remove_edge_property`](Self::remove_edge_property).
590    fn remove_edge_property_versioned(
591        &self,
592        id: EdgeId,
593        key: &str,
594        _transaction_id: TransactionId,
595    ) -> Option<Value> {
596        self.remove_edge_property(id, key)
597    }
598
599    // --- Label mutation ---
600
601    /// Adds a label to a node. Returns `true` if the label was new.
602    fn add_label(&self, node_id: NodeId, label: &str) -> bool;
603
604    /// Removes a label from a node. Returns `true` if the label existed.
605    fn remove_label(&self, node_id: NodeId, label: &str) -> bool;
606
607    /// Adds a label within a transaction, recording the change for rollback.
608    ///
609    /// Default delegates to [`add_label`](Self::add_label).
610    fn add_label_versioned(
611        &self,
612        node_id: NodeId,
613        label: &str,
614        _transaction_id: TransactionId,
615    ) -> bool {
616        self.add_label(node_id, label)
617    }
618
619    /// Removes a label within a transaction, recording the change for rollback.
620    ///
621    /// Default delegates to [`remove_label`](Self::remove_label).
622    fn remove_label_versioned(
623        &self,
624        node_id: NodeId,
625        label: &str,
626        _transaction_id: TransactionId,
627    ) -> bool {
628        self.remove_label(node_id, label)
629    }
630
631    // --- Convenience (with default implementations) ---
632
633    /// Creates a new node with labels and properties in one call.
634    ///
635    /// The default implementation calls [`create_node`](Self::create_node)
636    /// followed by [`set_node_property`](Self::set_node_property) for each
637    /// property. Implementations may override for atomicity or performance.
638    fn create_node_with_props(
639        &self,
640        labels: &[&str],
641        properties: &[(PropertyKey, Value)],
642    ) -> NodeId {
643        let id = self.create_node(labels);
644        for (key, value) in properties {
645            self.set_node_property(id, key.as_str(), value.clone());
646        }
647        id
648    }
649
650    /// Creates a new edge with properties in one call.
651    ///
652    /// The default implementation calls [`create_edge`](Self::create_edge)
653    /// followed by [`set_edge_property`](Self::set_edge_property) for each
654    /// property. Implementations may override for atomicity or performance.
655    fn create_edge_with_props(
656        &self,
657        src: NodeId,
658        dst: NodeId,
659        edge_type: &str,
660        properties: &[(PropertyKey, Value)],
661    ) -> EdgeId {
662        let id = self.create_edge(src, dst, edge_type);
663        for (key, value) in properties {
664            self.set_edge_property(id, key.as_str(), value.clone());
665        }
666        id
667    }
668}
669
670/// A no-op [`GraphStore`] that returns empty results for all queries.
671///
672/// Used by the RDF planner to satisfy the expression evaluator's store
673/// requirement. SPARQL expression functions (STR, LANG, DATATYPE, etc.)
674/// operate on already-materialized values in DataChunk columns and never
675/// call store methods.
676pub struct NullGraphStore;
677
678impl GraphStore for NullGraphStore {
679    fn get_node(&self, _: NodeId) -> Option<Node> {
680        None
681    }
682    fn get_edge(&self, _: EdgeId) -> Option<Edge> {
683        None
684    }
685    fn get_node_versioned(&self, _: NodeId, _: EpochId, _: TransactionId) -> Option<Node> {
686        None
687    }
688    fn get_edge_versioned(&self, _: EdgeId, _: EpochId, _: TransactionId) -> Option<Edge> {
689        None
690    }
691    fn get_node_at_epoch(&self, _: NodeId, _: EpochId) -> Option<Node> {
692        None
693    }
694    fn get_edge_at_epoch(&self, _: EdgeId, _: EpochId) -> Option<Edge> {
695        None
696    }
697    fn get_node_property(&self, _: NodeId, _: &PropertyKey) -> Option<Value> {
698        None
699    }
700    fn get_edge_property(&self, _: EdgeId, _: &PropertyKey) -> Option<Value> {
701        None
702    }
703    fn get_node_property_batch(&self, ids: &[NodeId], _: &PropertyKey) -> Vec<Option<Value>> {
704        vec![None; ids.len()]
705    }
706    fn get_nodes_properties_batch(&self, ids: &[NodeId]) -> Vec<FxHashMap<PropertyKey, Value>> {
707        vec![FxHashMap::default(); ids.len()]
708    }
709    fn get_nodes_properties_selective_batch(
710        &self,
711        ids: &[NodeId],
712        _: &[PropertyKey],
713    ) -> Vec<FxHashMap<PropertyKey, Value>> {
714        vec![FxHashMap::default(); ids.len()]
715    }
716    fn get_edges_properties_selective_batch(
717        &self,
718        ids: &[EdgeId],
719        _: &[PropertyKey],
720    ) -> Vec<FxHashMap<PropertyKey, Value>> {
721        vec![FxHashMap::default(); ids.len()]
722    }
723    fn neighbors(&self, _: NodeId, _: Direction) -> Vec<NodeId> {
724        Vec::new()
725    }
726    fn edges_from(&self, _: NodeId, _: Direction) -> Vec<(NodeId, EdgeId)> {
727        Vec::new()
728    }
729    fn out_degree(&self, _: NodeId) -> usize {
730        0
731    }
732    fn in_degree(&self, _: NodeId) -> usize {
733        0
734    }
735    fn has_backward_adjacency(&self) -> bool {
736        false
737    }
738    fn node_ids(&self) -> Vec<NodeId> {
739        Vec::new()
740    }
741    fn nodes_by_label(&self, _: &str) -> Vec<NodeId> {
742        Vec::new()
743    }
744    fn node_count(&self) -> usize {
745        0
746    }
747    fn edge_count(&self) -> usize {
748        0
749    }
750    fn edge_type(&self, _: EdgeId) -> Option<ArcStr> {
751        None
752    }
753    fn find_nodes_by_property(&self, _: &str, _: &Value) -> Vec<NodeId> {
754        Vec::new()
755    }
756    fn find_nodes_by_properties(&self, _: &[(&str, Value)]) -> Vec<NodeId> {
757        Vec::new()
758    }
759    fn find_nodes_in_range(
760        &self,
761        _: &str,
762        _: Option<&Value>,
763        _: Option<&Value>,
764        _: bool,
765        _: bool,
766    ) -> Vec<NodeId> {
767        Vec::new()
768    }
769    fn node_property_might_match(&self, _: &PropertyKey, _: CompareOp, _: &Value) -> bool {
770        false
771    }
772    fn edge_property_might_match(&self, _: &PropertyKey, _: CompareOp, _: &Value) -> bool {
773        false
774    }
775    fn statistics(&self) -> Arc<Statistics> {
776        Arc::new(Statistics::default())
777    }
778    fn estimate_label_cardinality(&self, _: &str) -> f64 {
779        0.0
780    }
781    fn estimate_avg_degree(&self, _: &str, _: bool) -> f64 {
782        0.0
783    }
784    fn current_epoch(&self) -> EpochId {
785        EpochId(0)
786    }
787}
788
789impl GraphStoreSearch for NullGraphStore {}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794    use std::sync::Mutex;
795
796    #[test]
797    fn null_graph_store_point_lookups() {
798        let store = NullGraphStore;
799        let nid = NodeId(1);
800        let eid = EdgeId(1);
801        let epoch = EpochId(0);
802        let txn = TransactionId(1);
803
804        assert!(store.get_node(nid).is_none());
805        assert!(store.get_edge(eid).is_none());
806        assert!(store.get_node_versioned(nid, epoch, txn).is_none());
807        assert!(store.get_edge_versioned(eid, epoch, txn).is_none());
808        assert!(store.get_node_at_epoch(nid, epoch).is_none());
809        assert!(store.get_edge_at_epoch(eid, epoch).is_none());
810    }
811
812    #[test]
813    fn null_graph_store_property_access() {
814        let store = NullGraphStore;
815        let nid = NodeId(1);
816        let eid = EdgeId(1);
817        let key = PropertyKey::from("name");
818
819        assert!(store.get_node_property(nid, &key).is_none());
820        assert!(store.get_edge_property(eid, &key).is_none());
821        assert_eq!(
822            store.get_node_property_batch(&[nid, NodeId(2)], &key),
823            vec![None, None]
824        );
825
826        let node_props = store.get_nodes_properties_batch(&[nid]);
827        assert_eq!(node_props.len(), 1);
828        assert!(node_props[0].is_empty());
829
830        let selective =
831            store.get_nodes_properties_selective_batch(&[nid], std::slice::from_ref(&key));
832        assert_eq!(selective.len(), 1);
833        assert!(selective[0].is_empty());
834
835        let edge_selective = store.get_edges_properties_selective_batch(&[eid], &[key]);
836        assert_eq!(edge_selective.len(), 1);
837        assert!(edge_selective[0].is_empty());
838    }
839
840    #[test]
841    fn null_graph_store_traversal() {
842        let store = NullGraphStore;
843        let nid = NodeId(1);
844
845        assert!(store.neighbors(nid, Direction::Outgoing).is_empty());
846        assert!(store.edges_from(nid, Direction::Incoming).is_empty());
847        assert_eq!(store.out_degree(nid), 0);
848        assert_eq!(store.in_degree(nid), 0);
849        assert!(!store.has_backward_adjacency());
850    }
851
852    #[test]
853    fn null_graph_store_scans_and_counts() {
854        let store = NullGraphStore;
855
856        assert!(store.node_ids().is_empty());
857        assert!(store.all_node_ids().is_empty());
858        assert!(store.nodes_by_label("Person").is_empty());
859        assert_eq!(store.node_count(), 0);
860        assert_eq!(store.edge_count(), 0);
861    }
862
863    #[test]
864    fn null_graph_store_metadata_and_schema() {
865        let store = NullGraphStore;
866        let eid = EdgeId(1);
867        let epoch = EpochId(0);
868        let txn = TransactionId(1);
869
870        assert!(store.edge_type(eid).is_none());
871        assert!(store.edge_type_versioned(eid, epoch, txn).is_none());
872        assert!(!store.has_property_index("name"));
873        assert!(store.all_labels().is_empty());
874        assert!(store.all_edge_types().is_empty());
875        assert!(store.all_property_keys().is_empty());
876    }
877
878    #[test]
879    fn null_graph_store_search() {
880        let store = NullGraphStore;
881        let key = PropertyKey::from("age");
882        let val = Value::Int64(30);
883
884        assert!(store.find_nodes_by_property("age", &val).is_empty());
885        assert!(
886            store
887                .find_nodes_by_properties(&[("age", val.clone())])
888                .is_empty()
889        );
890        assert!(
891            store
892                .find_nodes_in_range("age", Some(&val), None, true, false)
893                .is_empty()
894        );
895        assert!(!store.node_property_might_match(&key, CompareOp::Eq, &val));
896        assert!(!store.edge_property_might_match(&key, CompareOp::Eq, &val));
897    }
898
899    #[test]
900    fn null_graph_store_statistics() {
901        let store = NullGraphStore;
902
903        let _stats = store.statistics();
904        assert_eq!(store.estimate_label_cardinality("Person"), 0.0);
905        assert_eq!(store.estimate_avg_degree("KNOWS", true), 0.0);
906        assert_eq!(store.current_epoch(), EpochId(0));
907    }
908
909    #[test]
910    fn null_graph_store_visibility() {
911        let store = NullGraphStore;
912        let nid = NodeId(1);
913        let eid = EdgeId(1);
914        let epoch = EpochId(0);
915        let txn = TransactionId(1);
916
917        assert!(!store.is_node_visible_at_epoch(nid, epoch));
918        assert!(!store.is_node_visible_versioned(nid, epoch, txn));
919        assert!(!store.is_edge_visible_at_epoch(eid, epoch));
920        assert!(!store.is_edge_visible_versioned(eid, epoch, txn));
921
922        assert!(
923            store
924                .filter_visible_node_ids(&[nid, NodeId(2)], epoch)
925                .is_empty()
926        );
927        assert!(
928            store
929                .filter_visible_node_ids_versioned(&[nid], epoch, txn)
930                .is_empty()
931        );
932    }
933
934    #[test]
935    fn null_graph_store_history() {
936        let store = NullGraphStore;
937
938        assert!(store.get_node_history(NodeId(1)).is_empty());
939        assert!(store.get_edge_history(EdgeId(1)).is_empty());
940    }
941
942    /// Minimal in-memory store used to exercise the default method bodies on
943    /// `GraphStoreMut`. Concrete production stores override every default, so
944    /// without this harness those default bodies would stay uncovered.
945    #[derive(Default)]
946    struct TestMutStore {
947        inner: Mutex<TestMutInner>,
948    }
949
950    #[derive(Default)]
951    struct TestMutInner {
952        next_node: u64,
953        next_edge: u64,
954        nodes: Vec<Node>,
955        edges: Vec<Edge>,
956    }
957
958    impl TestMutStore {
959        fn new() -> Self {
960            Self::default()
961        }
962
963        fn find_node(&self, id: NodeId) -> Option<Node> {
964            self.inner
965                .lock()
966                .unwrap()
967                .nodes
968                .iter()
969                .find(|n| n.id == id)
970                .cloned()
971        }
972
973        fn find_edge(&self, id: EdgeId) -> Option<Edge> {
974            self.inner
975                .lock()
976                .unwrap()
977                .edges
978                .iter()
979                .find(|e| e.id == id)
980                .cloned()
981        }
982    }
983
984    impl GraphStore for TestMutStore {
985        fn get_node(&self, id: NodeId) -> Option<Node> {
986            self.find_node(id)
987        }
988        fn get_edge(&self, id: EdgeId) -> Option<Edge> {
989            self.find_edge(id)
990        }
991        fn get_node_versioned(&self, id: NodeId, _: EpochId, _: TransactionId) -> Option<Node> {
992            self.find_node(id)
993        }
994        fn get_edge_versioned(&self, id: EdgeId, _: EpochId, _: TransactionId) -> Option<Edge> {
995            self.find_edge(id)
996        }
997        fn get_node_at_epoch(&self, id: NodeId, _: EpochId) -> Option<Node> {
998            self.find_node(id)
999        }
1000        fn get_edge_at_epoch(&self, id: EdgeId, _: EpochId) -> Option<Edge> {
1001            self.find_edge(id)
1002        }
1003        fn get_node_property(&self, id: NodeId, key: &PropertyKey) -> Option<Value> {
1004            self.find_node(id)
1005                .and_then(|n| n.properties.get(key).cloned())
1006        }
1007        fn get_edge_property(&self, id: EdgeId, key: &PropertyKey) -> Option<Value> {
1008            self.find_edge(id)
1009                .and_then(|e| e.properties.get(key).cloned())
1010        }
1011        fn get_node_property_batch(&self, ids: &[NodeId], key: &PropertyKey) -> Vec<Option<Value>> {
1012            ids.iter()
1013                .map(|id| self.get_node_property(*id, key))
1014                .collect()
1015        }
1016        fn get_nodes_properties_batch(&self, ids: &[NodeId]) -> Vec<FxHashMap<PropertyKey, Value>> {
1017            ids.iter()
1018                .map(|id| {
1019                    let mut map = FxHashMap::default();
1020                    if let Some(n) = self.find_node(*id) {
1021                        for (k, v) in n.properties.iter() {
1022                            map.insert(k.clone(), v.clone());
1023                        }
1024                    }
1025                    map
1026                })
1027                .collect()
1028        }
1029        fn get_nodes_properties_selective_batch(
1030            &self,
1031            ids: &[NodeId],
1032            _: &[PropertyKey],
1033        ) -> Vec<FxHashMap<PropertyKey, Value>> {
1034            vec![FxHashMap::default(); ids.len()]
1035        }
1036        fn get_edges_properties_selective_batch(
1037            &self,
1038            ids: &[EdgeId],
1039            _: &[PropertyKey],
1040        ) -> Vec<FxHashMap<PropertyKey, Value>> {
1041            vec![FxHashMap::default(); ids.len()]
1042        }
1043        fn neighbors(&self, _: NodeId, _: Direction) -> Vec<NodeId> {
1044            Vec::new()
1045        }
1046        fn edges_from(&self, _: NodeId, _: Direction) -> Vec<(NodeId, EdgeId)> {
1047            Vec::new()
1048        }
1049        fn out_degree(&self, _: NodeId) -> usize {
1050            0
1051        }
1052        fn in_degree(&self, _: NodeId) -> usize {
1053            0
1054        }
1055        fn has_backward_adjacency(&self) -> bool {
1056            false
1057        }
1058        fn node_ids(&self) -> Vec<NodeId> {
1059            self.inner
1060                .lock()
1061                .unwrap()
1062                .nodes
1063                .iter()
1064                .map(|n| n.id)
1065                .collect()
1066        }
1067        fn nodes_by_label(&self, _: &str) -> Vec<NodeId> {
1068            Vec::new()
1069        }
1070        fn node_count(&self) -> usize {
1071            self.inner.lock().unwrap().nodes.len()
1072        }
1073        fn edge_count(&self) -> usize {
1074            self.inner.lock().unwrap().edges.len()
1075        }
1076        fn edge_type(&self, id: EdgeId) -> Option<ArcStr> {
1077            self.find_edge(id).map(|e| e.edge_type)
1078        }
1079        fn find_nodes_by_property(&self, _: &str, _: &Value) -> Vec<NodeId> {
1080            Vec::new()
1081        }
1082        fn find_nodes_by_properties(&self, _: &[(&str, Value)]) -> Vec<NodeId> {
1083            Vec::new()
1084        }
1085        fn find_nodes_in_range(
1086            &self,
1087            _: &str,
1088            _: Option<&Value>,
1089            _: Option<&Value>,
1090            _: bool,
1091            _: bool,
1092        ) -> Vec<NodeId> {
1093            Vec::new()
1094        }
1095        fn node_property_might_match(&self, _: &PropertyKey, _: CompareOp, _: &Value) -> bool {
1096            true
1097        }
1098        fn edge_property_might_match(&self, _: &PropertyKey, _: CompareOp, _: &Value) -> bool {
1099            true
1100        }
1101        fn statistics(&self) -> Arc<Statistics> {
1102            Arc::new(Statistics::default())
1103        }
1104        fn estimate_label_cardinality(&self, _: &str) -> f64 {
1105            0.0
1106        }
1107        fn estimate_avg_degree(&self, _: &str, _: bool) -> f64 {
1108            0.0
1109        }
1110        fn current_epoch(&self) -> EpochId {
1111            EpochId(0)
1112        }
1113    }
1114
1115    impl GraphStoreSearch for TestMutStore {}
1116
1117    impl GraphStoreMut for TestMutStore {
1118        fn create_node(&self, labels: &[&str]) -> NodeId {
1119            let mut inner = self.inner.lock().unwrap();
1120            inner.next_node += 1;
1121            let id = NodeId(inner.next_node);
1122            let mut node = Node::new(id);
1123            for label in labels {
1124                node.add_label(*label);
1125            }
1126            inner.nodes.push(node);
1127            id
1128        }
1129        fn create_node_versioned(&self, labels: &[&str], _: EpochId, _: TransactionId) -> NodeId {
1130            self.create_node(labels)
1131        }
1132        fn create_edge(&self, src: NodeId, dst: NodeId, edge_type: &str) -> EdgeId {
1133            let mut inner = self.inner.lock().unwrap();
1134            inner.next_edge += 1;
1135            let id = EdgeId(inner.next_edge);
1136            inner.edges.push(Edge::new(id, src, dst, edge_type));
1137            id
1138        }
1139        fn create_edge_versioned(
1140            &self,
1141            src: NodeId,
1142            dst: NodeId,
1143            edge_type: &str,
1144            _: EpochId,
1145            _: TransactionId,
1146        ) -> EdgeId {
1147            self.create_edge(src, dst, edge_type)
1148        }
1149        fn batch_create_edges(&self, edges: &[(NodeId, NodeId, &str)]) -> Vec<EdgeId> {
1150            edges
1151                .iter()
1152                .map(|(s, d, t)| self.create_edge(*s, *d, t))
1153                .collect()
1154        }
1155        fn delete_node(&self, id: NodeId) -> bool {
1156            let mut inner = self.inner.lock().unwrap();
1157            if let Some(pos) = inner.nodes.iter().position(|n| n.id == id) {
1158                inner.nodes.remove(pos);
1159                true
1160            } else {
1161                false
1162            }
1163        }
1164        fn delete_node_versioned(&self, id: NodeId, _: EpochId, _: TransactionId) -> bool {
1165            self.delete_node(id)
1166        }
1167        fn delete_node_edges(&self, node_id: NodeId) {
1168            let mut inner = self.inner.lock().unwrap();
1169            inner.edges.retain(|e| e.src != node_id && e.dst != node_id);
1170        }
1171        fn delete_edge(&self, id: EdgeId) -> bool {
1172            let mut inner = self.inner.lock().unwrap();
1173            if let Some(pos) = inner.edges.iter().position(|e| e.id == id) {
1174                inner.edges.remove(pos);
1175                true
1176            } else {
1177                false
1178            }
1179        }
1180        fn delete_edge_versioned(&self, id: EdgeId, _: EpochId, _: TransactionId) -> bool {
1181            self.delete_edge(id)
1182        }
1183        fn set_node_property(&self, id: NodeId, key: &str, value: Value) {
1184            let mut inner = self.inner.lock().unwrap();
1185            if let Some(node) = inner.nodes.iter_mut().find(|n| n.id == id) {
1186                node.set_property(key, value);
1187            }
1188        }
1189        fn set_edge_property(&self, id: EdgeId, key: &str, value: Value) {
1190            let mut inner = self.inner.lock().unwrap();
1191            if let Some(edge) = inner.edges.iter_mut().find(|e| e.id == id) {
1192                edge.set_property(key, value);
1193            }
1194        }
1195        fn remove_node_property(&self, id: NodeId, key: &str) -> Option<Value> {
1196            let mut inner = self.inner.lock().unwrap();
1197            inner
1198                .nodes
1199                .iter_mut()
1200                .find(|n| n.id == id)
1201                .and_then(|n| n.remove_property(key))
1202        }
1203        fn remove_edge_property(&self, id: EdgeId, key: &str) -> Option<Value> {
1204            let mut inner = self.inner.lock().unwrap();
1205            inner
1206                .edges
1207                .iter_mut()
1208                .find(|e| e.id == id)
1209                .and_then(|e| e.remove_property(key))
1210        }
1211        fn add_label(&self, node_id: NodeId, label: &str) -> bool {
1212            let mut inner = self.inner.lock().unwrap();
1213            if let Some(node) = inner.nodes.iter_mut().find(|n| n.id == node_id) {
1214                if node.has_label(label) {
1215                    false
1216                } else {
1217                    node.add_label(label);
1218                    true
1219                }
1220            } else {
1221                false
1222            }
1223        }
1224        fn remove_label(&self, node_id: NodeId, label: &str) -> bool {
1225            let mut inner = self.inner.lock().unwrap();
1226            inner
1227                .nodes
1228                .iter_mut()
1229                .find(|n| n.id == node_id)
1230                .is_some_and(|n| n.remove_label(label))
1231        }
1232    }
1233
1234    #[test]
1235    fn test_mut_store_default_set_versioned_property_delegates() {
1236        let store = TestMutStore::new();
1237        let id = store.create_node(&["Person"]);
1238        let key = PropertyKey::from("name");
1239        let txn = TransactionId(7);
1240
1241        // Default impl of set_node_property_versioned calls set_node_property.
1242        store.set_node_property_versioned(id, "name", Value::from("Vincent"), txn);
1243        assert_eq!(
1244            store.get_node_property(id, &key),
1245            Some(Value::from("Vincent"))
1246        );
1247
1248        let edge_id = {
1249            let src = store.create_node(&["Person"]);
1250            let dst = store.create_node(&["City"]);
1251            store.create_edge(src, dst, "LIVES_IN")
1252        };
1253        let since = PropertyKey::from("since");
1254        store.set_edge_property_versioned(edge_id, "since", Value::Int64(1994), txn);
1255        assert_eq!(
1256            store.get_edge_property(edge_id, &since),
1257            Some(Value::Int64(1994))
1258        );
1259    }
1260
1261    #[test]
1262    fn test_mut_store_default_remove_versioned_property_delegates() {
1263        let store = TestMutStore::new();
1264        let txn = TransactionId(11);
1265
1266        let node_id = store.create_node(&["Person"]);
1267        store.set_node_property(node_id, "city", Value::from("Amsterdam"));
1268        let removed = store.remove_node_property_versioned(node_id, "city", txn);
1269        assert_eq!(removed, Some(Value::from("Amsterdam")));
1270        assert!(
1271            store
1272                .get_node_property(node_id, &PropertyKey::from("city"))
1273                .is_none()
1274        );
1275
1276        let missing = store.remove_node_property_versioned(node_id, "absent", txn);
1277        assert!(missing.is_none());
1278
1279        let src = store.create_node(&["Person"]);
1280        let dst = store.create_node(&["Person"]);
1281        let edge_id = store.create_edge(src, dst, "KNOWS");
1282        store.set_edge_property(edge_id, "weight", Value::Int64(42));
1283        let removed_edge = store.remove_edge_property_versioned(edge_id, "weight", txn);
1284        assert_eq!(removed_edge, Some(Value::Int64(42)));
1285        let removed_again = store.remove_edge_property_versioned(edge_id, "weight", txn);
1286        assert!(removed_again.is_none());
1287    }
1288
1289    #[test]
1290    fn test_mut_store_default_label_versioned_delegates() {
1291        let store = TestMutStore::new();
1292        let txn = TransactionId(3);
1293        let id = store.create_node(&["Person"]);
1294
1295        // Adding a new label returns true, re-adding the same returns false.
1296        assert!(store.add_label_versioned(id, "Director", txn));
1297        assert!(!store.add_label_versioned(id, "Director", txn));
1298
1299        // Removing an existing label returns true, removing absent returns false.
1300        assert!(store.remove_label_versioned(id, "Director", txn));
1301        assert!(!store.remove_label_versioned(id, "Director", txn));
1302
1303        // Unknown node id yields false on both add and remove paths.
1304        let unknown = NodeId(9999);
1305        assert!(!store.add_label_versioned(unknown, "Ghost", txn));
1306        assert!(!store.remove_label_versioned(unknown, "Ghost", txn));
1307    }
1308
1309    #[test]
1310    fn test_mut_store_default_create_node_with_props() {
1311        let store = TestMutStore::new();
1312        let props = vec![
1313            (PropertyKey::from("name"), Value::from("Jules")),
1314            (PropertyKey::from("city"), Value::from("Paris")),
1315        ];
1316
1317        let id = store.create_node_with_props(&["Person"], &props);
1318        let node = store.get_node(id).expect("node should exist");
1319        assert!(node.has_label("Person"));
1320        assert_eq!(
1321            node.properties.get(&PropertyKey::from("name")),
1322            Some(&Value::from("Jules"))
1323        );
1324        assert_eq!(
1325            node.properties.get(&PropertyKey::from("city")),
1326            Some(&Value::from("Paris"))
1327        );
1328
1329        // Empty properties slice still produces a valid node.
1330        let bare = store.create_node_with_props(&["Person"], &[]);
1331        let bare_node = store.get_node(bare).expect("bare node should exist");
1332        assert!(bare_node.properties.is_empty());
1333    }
1334
1335    #[test]
1336    fn test_mut_store_default_create_edge_with_props() {
1337        let store = TestMutStore::new();
1338        let src = store.create_node_with_props(
1339            &["Person"],
1340            &[(PropertyKey::from("name"), Value::from("Mia"))],
1341        );
1342        let dst = store.create_node_with_props(
1343            &["City"],
1344            &[(PropertyKey::from("name"), Value::from("Berlin"))],
1345        );
1346        let props = vec![
1347            (PropertyKey::from("since"), Value::Int64(2021)),
1348            (PropertyKey::from("role"), Value::from("resident")),
1349        ];
1350
1351        let edge_id = store.create_edge_with_props(src, dst, "LIVES_IN", &props);
1352        let edge = store.get_edge(edge_id).expect("edge should exist");
1353        assert_eq!(edge.src, src);
1354        assert_eq!(edge.dst, dst);
1355        assert_eq!(edge.edge_type.as_str(), "LIVES_IN");
1356        assert_eq!(
1357            edge.properties.get(&PropertyKey::from("since")),
1358            Some(&Value::Int64(2021))
1359        );
1360        assert_eq!(
1361            edge.properties.get(&PropertyKey::from("role")),
1362            Some(&Value::from("resident"))
1363        );
1364
1365        // Confirm the edge type is also reachable through the read trait.
1366        assert_eq!(
1367            store
1368                .edge_type(edge_id)
1369                .as_ref()
1370                .map(arcstr::ArcStr::as_str),
1371            Some("LIVES_IN")
1372        );
1373
1374        // With no properties, default still produces an edge.
1375        let bare = store.create_edge_with_props(src, dst, "VISITED", &[]);
1376        let bare_edge = store.get_edge(bare).expect("bare edge should exist");
1377        assert!(bare_edge.properties.is_empty());
1378    }
1379
1380    #[test]
1381    fn test_mut_store_object_safe_dyn_dispatch() {
1382        // Exercise the object-safe contract: GraphStore methods through `dyn`.
1383        let store: Arc<dyn GraphStoreSearch> = Arc::new(TestMutStore::new());
1384        assert_eq!(store.node_count(), 0);
1385        assert_eq!(store.edge_count(), 0);
1386        assert!(store.node_ids().is_empty());
1387        assert!(store.get_node(NodeId(1)).is_none());
1388        assert_eq!(store.current_epoch(), EpochId(0));
1389    }
1390}