Skip to main content

lora_store/
traits.rs

1//! Storage trait surface: read, borrow, and mutate contracts.
2//!
3//! Backends speak the value types defined in [`crate::types`] and surface
4//! them through the traits here. The split keeps the hot loop of "what
5//! shape does a record have" (types) separate from "what can a backend
6//! do with one" (traits).
7
8use std::collections::BTreeSet;
9
10use lora_ast::Direction;
11
12use crate::memory::{
13    ConstraintDefinition, ConstraintRequest, CreateConstraintError, CreateConstraintOutcome,
14    CreateIndexError, CreateIndexOutcome, DropConstraintError, DropConstraintOutcome,
15    DropIndexError, DropIndexOutcome, GraphStats, IndexDefinition, IndexRequest,
16};
17use crate::types::{
18    ExpandedRelationship, LoraVector, NodeId, NodeRecord, Properties, PropertyValue,
19    RelationshipId, RelationshipRecord,
20};
21
22// ============================================================================
23// GraphStorage — the read-side storage contract
24//
25// The trait is intentionally layered into three groups: a small set of
26// backend-neutral required primitives, a pair of optional optimization hooks
27// (`with_node` / `with_relationship`), and a large cloud of defaulted helpers
28// that derive from the primitives.
29//
30// Adding a new backend means implementing the required primitives (roughly a
31// dozen methods) plus — optionally — overriding the hooks for zero-copy or the
32// record-scan helpers for bulk perf. Implementors SHOULD NOT need to rewrite
33// the catalog / traversal helper surface unless they can beat the default
34// composition.
35// ============================================================================
36
37pub trait GraphStorage {
38    // ---------- Required node primitives ----------
39
40    /// Cheap existence check. Should not clone or materialize the record.
41    fn contains_node(&self, id: NodeId) -> bool;
42
43    /// Point lookup returning an owned record. Backends that can hand out
44    /// borrows should also implement [`BorrowedGraphStorage::node_ref`] and
45    /// override [`with_node`] to avoid clones on the hot path.
46    fn node(&self, id: NodeId) -> Option<NodeRecord>;
47
48    /// Enumerate every node id. Should be O(nodes) without cloning records.
49    fn all_node_ids(&self) -> Vec<NodeId>;
50
51    /// Enumerate node ids carrying the given label. Implementations that keep
52    /// a label index should override this.
53    fn node_ids_by_label(&self, label: &str) -> Vec<NodeId>;
54
55    // ---------- Required relationship primitives ----------
56
57    fn contains_relationship(&self, id: RelationshipId) -> bool;
58
59    fn relationship(&self, id: RelationshipId) -> Option<RelationshipRecord>;
60
61    fn all_rel_ids(&self) -> Vec<RelationshipId>;
62
63    fn rel_ids_by_type(&self, rel_type: &str) -> Vec<RelationshipId>;
64
65    /// Endpoint pair `(src, dst)` for a relationship. Required because
66    /// traversal uses it on hot paths; a backend that stores endpoints
67    /// alongside the id index can answer this without fetching properties.
68    fn relationship_endpoints(&self, id: RelationshipId) -> Option<(NodeId, NodeId)>;
69
70    // ---------- Required traversal primitive ----------
71
72    /// Expand a node's incident relationships filtered by direction and
73    /// (optional) types. This is the single traversal primitive; variable-
74    /// length paths, degree, and adjacency helpers are all derived from it.
75    fn expand_ids(
76        &self,
77        node_id: NodeId,
78        direction: Direction,
79        types: &[String],
80    ) -> Vec<(RelationshipId, NodeId)>;
81
82    /// Visit expanded `(relationship_id, other_node_id)` pairs without
83    /// forcing backends to allocate an intermediate Vec. The default keeps the
84    /// trait easy to implement; hot backends can override it.
85    fn try_for_each_expand_id<F, E>(
86        &self,
87        node_id: NodeId,
88        direction: Direction,
89        types: &[String],
90        mut visit: F,
91    ) -> Result<(), E>
92    where
93        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
94        Self: Sized,
95    {
96        for (rel_id, other_id) in self.expand_ids(node_id, direction, types) {
97            visit(rel_id, other_id)?;
98        }
99        Ok(())
100    }
101
102    // ---------- Required catalog primitives ----------
103
104    fn all_labels(&self) -> Vec<String>;
105    fn all_relationship_types(&self) -> Vec<String>;
106
107    // ---------- Optional optimization hooks ----------
108    //
109    // Generic methods gated on `Self: Sized` so they don't affect object
110    // safety. Backends override these to supply borrow-based access on hot
111    // paths; defaults clone through `node` / `relationship`.
112
113    fn with_node<F, R>(&self, id: NodeId, f: F) -> Option<R>
114    where
115        F: FnOnce(&NodeRecord) -> R,
116        Self: Sized,
117    {
118        self.node(id).as_ref().map(f)
119    }
120
121    fn with_relationship<F, R>(&self, id: RelationshipId, f: F) -> Option<R>
122    where
123        F: FnOnce(&RelationshipRecord) -> R,
124        Self: Sized,
125    {
126        self.relationship(id).as_ref().map(f)
127    }
128
129    // ---------- Defaulted: counts / existence aliases ----------
130
131    fn has_node(&self, id: NodeId) -> bool {
132        self.contains_node(id)
133    }
134
135    fn has_relationship(&self, id: RelationshipId) -> bool {
136        self.contains_relationship(id)
137    }
138
139    fn node_count(&self) -> usize {
140        self.all_node_ids().len()
141    }
142
143    fn relationship_count(&self) -> usize {
144        self.all_rel_ids().len()
145    }
146
147    /// Number of nodes carrying `label`: exactly the rows a scan of
148    /// [`Self::node_ids_by_label`] yields. Backends with a label index
149    /// should answer this without materializing the ids.
150    fn node_count_by_label(&self, label: &str) -> usize {
151        self.node_ids_by_label(label).len()
152    }
153
154    // ---------- Defaulted: record-returning scans ----------
155    //
156    // These synthesize full-record scans from id scans + point lookups. That
157    // is correct for any backend and fast enough for small graphs, but a
158    // backend that can scan records in one pass (in-memory via a BTreeMap
159    // `.values()`, a column store via a streaming read) should override.
160
161    fn all_nodes(&self) -> Vec<NodeRecord> {
162        self.all_node_ids()
163            .into_iter()
164            .filter_map(|id| self.node(id))
165            .collect()
166    }
167
168    fn nodes_by_label(&self, label: &str) -> Vec<NodeRecord> {
169        self.node_ids_by_label(label)
170            .into_iter()
171            .filter_map(|id| self.node(id))
172            .collect()
173    }
174
175    fn all_relationships(&self) -> Vec<RelationshipRecord> {
176        self.all_rel_ids()
177            .into_iter()
178            .filter_map(|id| self.relationship(id))
179            .collect()
180    }
181
182    fn relationships_by_type(&self, rel_type: &str) -> Vec<RelationshipRecord> {
183        self.rel_ids_by_type(rel_type)
184            .into_iter()
185            .filter_map(|id| self.relationship(id))
186            .collect()
187    }
188
189    // ---------- Defaulted: traversal helpers ----------
190
191    fn relationship_ids_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipId> {
192        self.expand_ids(node_id, direction, &[])
193            .into_iter()
194            .map(|(rel_id, _)| rel_id)
195            .collect()
196    }
197
198    fn outgoing_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
199        self.relationship_ids_of(node_id, Direction::Right)
200            .into_iter()
201            .filter_map(|id| self.relationship(id))
202            .collect()
203    }
204
205    fn incoming_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
206        self.relationship_ids_of(node_id, Direction::Left)
207            .into_iter()
208            .filter_map(|id| self.relationship(id))
209            .collect()
210    }
211
212    fn relationships_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipRecord> {
213        self.relationship_ids_of(node_id, direction)
214            .into_iter()
215            .filter_map(|id| self.relationship(id))
216            .collect()
217    }
218
219    fn degree(&self, node_id: NodeId, direction: Direction) -> usize {
220        self.expand_ids(node_id, direction, &[]).len()
221    }
222
223    fn is_isolated(&self, node_id: NodeId) -> bool {
224        self.degree(node_id, Direction::Undirected) == 0
225    }
226
227    fn expand(
228        &self,
229        node_id: NodeId,
230        direction: Direction,
231        types: &[String],
232    ) -> Vec<(RelationshipRecord, NodeRecord)> {
233        self.expand_ids(node_id, direction, types)
234            .into_iter()
235            .filter_map(|(rid, nid)| {
236                let rel = self.relationship(rid)?;
237                let node = self.node(nid)?;
238                Some((rel, node))
239            })
240            .collect()
241    }
242
243    fn expand_detailed(
244        &self,
245        node_id: NodeId,
246        direction: Direction,
247        types: &[String],
248    ) -> Vec<ExpandedRelationship> {
249        self.expand(node_id, direction, types)
250            .into_iter()
251            .map(|(relationship, other_node)| ExpandedRelationship {
252                relationship,
253                other_node,
254            })
255            .collect()
256    }
257
258    fn neighbors(
259        &self,
260        node_id: NodeId,
261        direction: Direction,
262        types: &[String],
263    ) -> Vec<NodeRecord> {
264        self.expand_ids(node_id, direction, types)
265            .into_iter()
266            .filter_map(|(_, nid)| self.node(nid))
267            .collect()
268    }
269
270    // ---------- Defaulted: narrow node accessors ----------
271
272    fn node_has_label(&self, node_id: NodeId, label: &str) -> bool
273    where
274        Self: Sized,
275    {
276        self.with_node(node_id, |n| n.labels.iter().any(|l| l == label))
277            .unwrap_or(false)
278    }
279
280    fn node_labels(&self, node_id: NodeId) -> Option<Vec<String>>
281    where
282        Self: Sized,
283    {
284        self.with_node(node_id, |n| n.labels.clone())
285    }
286
287    fn node_properties(&self, node_id: NodeId) -> Option<Properties>
288    where
289        Self: Sized,
290    {
291        self.with_node(node_id, |n| n.properties.clone())
292    }
293
294    fn node_property(&self, node_id: NodeId, key: &str) -> Option<PropertyValue>
295    where
296        Self: Sized,
297    {
298        self.with_node(node_id, |n| n.properties.get(key).cloned())
299            .flatten()
300    }
301
302    // ---------- Defaulted: narrow relationship accessors ----------
303
304    fn relationship_type(&self, rel_id: RelationshipId) -> Option<String>
305    where
306        Self: Sized,
307    {
308        self.with_relationship(rel_id, |r| r.rel_type.clone())
309    }
310
311    fn relationship_properties(&self, rel_id: RelationshipId) -> Option<Properties>
312    where
313        Self: Sized,
314    {
315        self.with_relationship(rel_id, |r| r.properties.clone())
316    }
317
318    fn relationship_property(&self, rel_id: RelationshipId, key: &str) -> Option<PropertyValue>
319    where
320        Self: Sized,
321    {
322        self.with_relationship(rel_id, |r| r.properties.get(key).cloned())
323            .flatten()
324    }
325
326    fn relationship_source(&self, rel_id: RelationshipId) -> Option<NodeId> {
327        self.relationship_endpoints(rel_id).map(|(s, _)| s)
328    }
329
330    fn relationship_target(&self, rel_id: RelationshipId) -> Option<NodeId> {
331        self.relationship_endpoints(rel_id).map(|(_, d)| d)
332    }
333
334    fn other_node(&self, rel_id: RelationshipId, node_id: NodeId) -> Option<NodeId> {
335        let (src, dst) = self.relationship_endpoints(rel_id)?;
336        if src == node_id {
337            Some(dst)
338        } else if dst == node_id {
339            Some(src)
340        } else {
341            None
342        }
343    }
344
345    // ---------- Defaulted: catalog helpers ----------
346
347    fn has_label_name(&self, label: &str) -> bool {
348        self.all_labels().iter().any(|l| l == label)
349    }
350
351    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
352        self.all_relationship_types().iter().any(|t| t == rel_type)
353    }
354
355    fn all_node_property_keys(&self) -> Vec<String>
356    where
357        Self: Sized,
358    {
359        let mut keys = BTreeSet::new();
360        for id in self.all_node_ids() {
361            self.with_node(id, |n| {
362                for key in n.properties.keys() {
363                    keys.insert(key.to_string());
364                }
365            });
366        }
367        keys.into_iter().collect()
368    }
369
370    fn all_relationship_property_keys(&self) -> Vec<String>
371    where
372        Self: Sized,
373    {
374        let mut keys = BTreeSet::new();
375        for id in self.all_rel_ids() {
376            self.with_relationship(id, |r| {
377                for key in r.properties.keys() {
378                    keys.insert(key.to_string());
379                }
380            });
381        }
382        keys.into_iter().collect()
383    }
384
385    fn all_property_keys(&self) -> Vec<String>
386    where
387        Self: Sized,
388    {
389        let mut keys = BTreeSet::new();
390        for key in self.all_node_property_keys() {
391            keys.insert(key);
392        }
393        for key in self.all_relationship_property_keys() {
394            keys.insert(key);
395        }
396        keys.into_iter().collect()
397    }
398
399    fn has_property_key(&self, key: &str) -> bool
400    where
401        Self: Sized,
402    {
403        self.all_node_property_keys().iter().any(|k| k == key)
404            || self
405                .all_relationship_property_keys()
406                .iter()
407                .any(|k| k == key)
408    }
409
410    fn label_property_keys(&self, label: &str) -> Vec<String>
411    where
412        Self: Sized,
413    {
414        let mut keys = BTreeSet::new();
415        for id in self.node_ids_by_label(label) {
416            self.with_node(id, |n| {
417                for key in n.properties.keys() {
418                    keys.insert(key.to_string());
419                }
420            });
421        }
422        keys.into_iter().collect()
423    }
424
425    fn rel_type_property_keys(&self, rel_type: &str) -> Vec<String>
426    where
427        Self: Sized,
428    {
429        let mut keys = BTreeSet::new();
430        for id in self.rel_ids_by_type(rel_type) {
431            self.with_relationship(id, |r| {
432                for key in r.properties.keys() {
433                    keys.insert(key.to_string());
434                }
435            });
436        }
437        keys.into_iter().collect()
438    }
439
440    fn label_has_property_key(&self, label: &str, key: &str) -> bool
441    where
442        Self: Sized,
443    {
444        self.node_ids_by_label(label).into_iter().any(|id| {
445            self.with_node(id, |n| n.properties.contains_key(key))
446                .unwrap_or(false)
447        })
448    }
449
450    fn rel_type_has_property_key(&self, rel_type: &str, key: &str) -> bool
451    where
452        Self: Sized,
453    {
454        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
455            self.with_relationship(id, |r| r.properties.contains_key(key))
456                .unwrap_or(false)
457        })
458    }
459
460    // ---------- Defaulted: property-filter lookups ----------
461
462    fn find_nodes_by_property(
463        &self,
464        label: Option<&str>,
465        key: &str,
466        value: &PropertyValue,
467    ) -> Vec<NodeRecord>
468    where
469        Self: Sized,
470    {
471        let ids = match label {
472            Some(label) => self.node_ids_by_label(label),
473            None => self.all_node_ids(),
474        };
475
476        ids.into_iter()
477            .filter_map(|id| {
478                let matches = self
479                    .with_node(id, |n| n.properties.get(key) == Some(value))
480                    .unwrap_or(false);
481                if matches {
482                    self.node(id)
483                } else {
484                    None
485                }
486            })
487            .collect()
488    }
489
490    fn find_node_ids_by_property(
491        &self,
492        label: Option<&str>,
493        key: &str,
494        value: &PropertyValue,
495    ) -> Vec<NodeId>
496    where
497        Self: Sized,
498    {
499        self.find_nodes_by_property(label, key, value)
500            .into_iter()
501            .map(|n| n.id)
502            .collect()
503    }
504
505    fn find_relationships_by_property(
506        &self,
507        rel_type: Option<&str>,
508        key: &str,
509        value: &PropertyValue,
510    ) -> Vec<RelationshipRecord>
511    where
512        Self: Sized,
513    {
514        let ids = match rel_type {
515            Some(rel_type) => self.rel_ids_by_type(rel_type),
516            None => self.all_rel_ids(),
517        };
518
519        ids.into_iter()
520            .filter_map(|id| {
521                let matches = self
522                    .with_relationship(id, |r| r.properties.get(key) == Some(value))
523                    .unwrap_or(false);
524                if matches {
525                    self.relationship(id)
526                } else {
527                    None
528                }
529            })
530            .collect()
531    }
532
533    fn find_relationship_ids_by_property(
534        &self,
535        rel_type: Option<&str>,
536        key: &str,
537        value: &PropertyValue,
538    ) -> Vec<RelationshipId>
539    where
540        Self: Sized,
541    {
542        self.find_relationships_by_property(rel_type, key, value)
543            .into_iter()
544            .map(|r| r.id)
545            .collect()
546    }
547
548    fn node_exists_with_label_and_property(
549        &self,
550        label: &str,
551        key: &str,
552        value: &PropertyValue,
553    ) -> bool
554    where
555        Self: Sized,
556    {
557        self.node_ids_by_label(label).into_iter().any(|id| {
558            self.with_node(id, |n| n.properties.get(key) == Some(value))
559                .unwrap_or(false)
560        })
561    }
562
563    fn relationship_exists_with_type_and_property(
564        &self,
565        rel_type: &str,
566        key: &str,
567        value: &PropertyValue,
568    ) -> bool
569    where
570        Self: Sized,
571    {
572        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
573            self.with_relationship(id, |r| r.properties.get(key) == Some(value))
574                .unwrap_or(false)
575        })
576    }
577
578    // ---------- Defaulted: index catalog ----------
579    //
580    // Backends that maintain an index catalog (currently the in-memory
581    // backend) override these. Backends without catalog support keep
582    // the no-op defaults so callers can list / look up safely.
583
584    fn list_indexes(&self) -> Vec<IndexDefinition> {
585        Vec::new()
586    }
587
588    fn get_index(&self, _name: &str) -> Option<IndexDefinition> {
589        None
590    }
591
592    /// Run a FULLTEXT index query against the named index. Returns
593    /// `(entity_id, score)` pairs sorted descending by score. Backends
594    /// without fulltext support return an empty vector; the caller is
595    /// expected to have validated that the index exists via the
596    /// catalog first.
597    fn fulltext_search(&self, _name: &str, _query: &str) -> Vec<(u64, f64)> {
598        Vec::new()
599    }
600
601    /// Run a VECTOR index query against the named index. Returns
602    /// unsorted `(entity_id, score)` pairs; the caller sorts by score
603    /// descending and truncates to top-k. Backends without vector
604    /// support return an empty vector. The caller has already
605    /// validated that the index exists via the catalog and that the
606    /// query vector matches the configured dimensions.
607    ///
608    /// `k` is a hint: future ANN backends can use it to prune work;
609    /// the flat backend ignores it because exhaustive scoring is the
610    /// cheapest correctness contract.
611    ///
612    /// `restrict_to`, when `Some`, hard-filters the result set to
613    /// those entity ids. The backend may still traverse other
614    /// entities internally (HNSW uses them as routing hops), but
615    /// returned tuples are guaranteed to live in the set.
616    fn vector_search(
617        &self,
618        _name: &str,
619        _query: &LoraVector,
620        _k: usize,
621        _restrict_to: Option<&std::collections::BTreeSet<u64>>,
622    ) -> Vec<(u64, f64)> {
623        Vec::new()
624    }
625
626    /// List explicitly-declared constraints. Backends without a
627    /// constraint catalog return the empty vector.
628    fn list_constraints(&self) -> Vec<ConstraintDefinition> {
629        Vec::new()
630    }
631
632    fn get_constraint(&self, _name: &str) -> Option<ConstraintDefinition> {
633        None
634    }
635
636    /// Mutation-time pre-check: would creating a node with these
637    /// `labels` and `properties` violate any registered constraint?
638    /// Default returns `Ok(())` so backends without a constraint
639    /// catalog pay nothing. The in-memory backend overrides this and
640    /// the call is virtually free when the catalog is empty.
641    fn check_node_create_against_constraints(
642        &self,
643        _labels: &[String],
644        _properties: &Properties,
645    ) -> Result<(), String> {
646        Ok(())
647    }
648
649    /// Mutation-time pre-check for `CREATE ()-[r:TYPE { ... }]->()`.
650    fn check_relationship_create_against_constraints(
651        &self,
652        _rel_type: &str,
653        _properties: &Properties,
654    ) -> Result<(), String> {
655        Ok(())
656    }
657
658    /// [`Self::check_node_create_against_constraints`] minus existence
659    /// checks, for a statement that runs
660    /// [`Self::check_node_existence_constraints`] once it finishes (a later
661    /// `SET` may still supply the property). Defaults to the full check.
662    fn check_node_create_deferring_existence(
663        &self,
664        labels: &[String],
665        properties: &Properties,
666    ) -> Result<(), String> {
667        self.check_node_create_against_constraints(labels, properties)
668    }
669
670    /// Relationship counterpart of
671    /// [`Self::check_node_create_deferring_existence`].
672    fn check_relationship_create_deferring_existence(
673        &self,
674        rel_type: &str,
675        properties: &Properties,
676    ) -> Result<(), String> {
677        self.check_relationship_create_against_constraints(rel_type, properties)
678    }
679
680    /// Existence (and key) constraints on a node as it stands now; a
681    /// deleted node passes. Default `Ok(())`.
682    fn check_node_existence_constraints(&self, _node_id: NodeId) -> Result<(), String> {
683        Ok(())
684    }
685
686    /// Relationship counterpart of [`Self::check_node_existence_constraints`].
687    fn check_relationship_existence_constraints(
688        &self,
689        _rel_id: RelationshipId,
690    ) -> Result<(), String> {
691        Ok(())
692    }
693
694    /// Mutation-time pre-check: would setting `key = value` on this
695    /// node violate any registered constraint? Default `Ok(())`.
696    fn check_node_set_property_against_constraints(
697        &self,
698        _node_id: NodeId,
699        _key: &str,
700        _value: &PropertyValue,
701    ) -> Result<(), String> {
702        Ok(())
703    }
704
705    /// Mutation-time pre-check: would removing `key` on this node
706    /// violate an existence / key constraint? Default `Ok(())`.
707    fn check_node_remove_property_against_constraints(
708        &self,
709        _node_id: NodeId,
710        _key: &str,
711    ) -> Result<(), String> {
712        Ok(())
713    }
714
715    /// Mutation-time pre-check: would replacing all properties on this
716    /// node leave it in violation of any registered constraint? Default
717    /// `Ok(())`.
718    fn check_node_replace_properties_against_constraints(
719        &self,
720        _node_id: NodeId,
721        _properties: &Properties,
722    ) -> Result<(), String> {
723        Ok(())
724    }
725
726    /// Mutation-time pre-check: equivalent for relationship
727    /// property writes.
728    fn check_relationship_set_property_against_constraints(
729        &self,
730        _rel_id: RelationshipId,
731        _key: &str,
732        _value: &PropertyValue,
733    ) -> Result<(), String> {
734        Ok(())
735    }
736
737    fn check_relationship_remove_property_against_constraints(
738        &self,
739        _rel_id: RelationshipId,
740        _key: &str,
741    ) -> Result<(), String> {
742        Ok(())
743    }
744
745    /// Mutation-time pre-check: would replacing all properties on this
746    /// relationship leave it in violation of any registered constraint?
747    /// Default `Ok(())`.
748    fn check_relationship_replace_properties_against_constraints(
749        &self,
750        _rel_id: RelationshipId,
751        _properties: &Properties,
752    ) -> Result<(), String> {
753        Ok(())
754    }
755
756    /// Mutation-time pre-check: would adding `label` to this node
757    /// activate a constraint the node currently violates?
758    fn check_node_add_label_against_constraints(
759        &self,
760        _node_id: NodeId,
761        _label: &str,
762    ) -> Result<(), String> {
763        Ok(())
764    }
765
766    /// Cardinality snapshot used by the cost model. Backends without
767    /// per-label / per-type indexes return [`GraphStats::default()`],
768    /// which the planner treats as "no information available".
769    fn graph_stats(&self) -> GraphStats {
770        GraphStats::default()
771    }
772
773    /// Trigram-index candidates for `query` on `label.property`.
774    ///
775    /// Semantics:
776    /// * `Some(ids)` → these node ids *might* match (refilter required).
777    /// * `None` → no trigram scope for `(label, property)`; caller must
778    ///   fall back to a full scan.
779    ///
780    /// Backends without text-index support always return `None`.
781    fn node_text_candidates(
782        &self,
783        _label: &str,
784        _property: &str,
785        _query: &str,
786    ) -> Option<Vec<NodeId>> {
787        None
788    }
789
790    /// Sorted-index candidates for a `[lo, hi]` range on `label.property`.
791    /// Both bounds are inclusive at this layer; the caller refilters with
792    /// the precise predicate inclusivity (`>` vs `>=`, `<` vs `<=`).
793    ///
794    /// Returns `None` when no scope exists — caller falls back to scan.
795    fn node_range_candidates(
796        &self,
797        _label: &str,
798        _property: &str,
799        _lo: Option<&PropertyValue>,
800        _hi: Option<&PropertyValue>,
801    ) -> Option<Vec<NodeId>> {
802        None
803    }
804
805    /// Whether the range index on `label.property` holds a temporal value
806    /// of another kind than `like` (a DATE when `like` is a DATETIME).
807    /// Lets a range scan on a temporal bound fail on a kind mismatch the
808    /// index would otherwise skip without a word. `None` when no index
809    /// covers `(label, property)`.
810    fn node_range_holds_other_temporal_kind(
811        &self,
812        _label: &str,
813        _property: &str,
814        _like: &PropertyValue,
815    ) -> Option<bool> {
816        None
817    }
818
819    /// Ids of `label` nodes with `property` in `[lo, hi]`, in index order
820    /// (value, then id), strictly after the `(value, id)` cursor `after`,
821    /// at most `max` of them; `descending` reverses the order. Bounds are
822    /// inclusive here; the executor refilters exact ones. `None` means no
823    /// ordered index covers `(label, property)`.
824    #[allow(clippy::too_many_arguments)]
825    fn node_range_ordered_chunk(
826        &self,
827        _label: &str,
828        _property: &str,
829        _lo: Option<&PropertyValue>,
830        _hi: Option<&PropertyValue>,
831        _descending: bool,
832        _after: Option<(&PropertyValue, NodeId)>,
833        _max: usize,
834    ) -> Option<Vec<NodeId>> {
835        None
836    }
837
838    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
839    /// bounding box. The executor refilters every id with the precise
840    /// predicate, including the z-coordinate when the indexed point
841    /// is 3D.
842    fn node_point_within_bbox(
843        &self,
844        _label: &str,
845        _property: &str,
846        _ll: (f64, f64),
847        _ur: (f64, f64),
848    ) -> Option<Vec<NodeId>> {
849        None
850    }
851
852    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
853    /// candidate set is conservative — the actual great-circle /
854    /// cartesian distance check is the executor's responsibility.
855    fn node_point_within_distance(
856        &self,
857        _label: &str,
858        _property: &str,
859        _center: (f64, f64),
860        _max_distance: f64,
861    ) -> Option<Vec<NodeId>> {
862        None
863    }
864
865    /// Trigram-index candidates for relationships of `rel_type` whose
866    /// `property` value matches `query` (substring/prefix/suffix). Mirror
867    /// of [`Self::node_text_candidates`] for relationship-target indexes.
868    fn relationship_text_candidates(
869        &self,
870        _rel_type: &str,
871        _property: &str,
872        _query: &str,
873    ) -> Option<Vec<RelationshipId>> {
874        None
875    }
876
877    /// Sorted-index candidates for relationships of `rel_type` on the
878    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
879    fn relationship_range_candidates(
880        &self,
881        _rel_type: &str,
882        _property: &str,
883        _lo: Option<&PropertyValue>,
884        _hi: Option<&PropertyValue>,
885    ) -> Option<Vec<RelationshipId>> {
886        None
887    }
888
889    /// Mirror of [`Self::node_range_holds_other_temporal_kind`] for
890    /// relationships of `rel_type`.
891    fn relationship_range_holds_other_temporal_kind(
892        &self,
893        _rel_type: &str,
894        _property: &str,
895        _like: &PropertyValue,
896    ) -> Option<bool> {
897        None
898    }
899
900    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
901    /// box, scoped to relationships of `rel_type`. Mirror of
902    /// [`Self::node_point_within_bbox`].
903    fn relationship_point_within_bbox(
904        &self,
905        _rel_type: &str,
906        _property: &str,
907        _ll: (f64, f64),
908        _ur: (f64, f64),
909    ) -> Option<Vec<RelationshipId>> {
910        None
911    }
912
913    /// Spatial-index candidates within `max_distance` of `(x, y)`,
914    /// scoped to relationships of `rel_type`. Mirror of
915    /// [`Self::node_point_within_distance`].
916    fn relationship_point_within_distance(
917        &self,
918        _rel_type: &str,
919        _property: &str,
920        _center: (f64, f64),
921        _max_distance: f64,
922    ) -> Option<Vec<RelationshipId>> {
923        None
924    }
925}
926
927// ============================================================================
928// GraphCatalog — narrow schema-query slice used by the analyzer.
929//
930// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
931// `GraphCatalog` without every backend having to implement a second trait.
932// ============================================================================
933
934pub trait GraphCatalog {
935    fn node_count(&self) -> usize;
936    fn relationship_count(&self) -> usize;
937    fn has_label_name(&self, label: &str) -> bool;
938    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
939    fn has_property_key(&self, key: &str) -> bool;
940}
941
942impl<T: GraphStorage> GraphCatalog for T {
943    fn node_count(&self) -> usize {
944        GraphStorage::node_count(self)
945    }
946    fn relationship_count(&self) -> usize {
947        GraphStorage::relationship_count(self)
948    }
949    fn has_label_name(&self, label: &str) -> bool {
950        GraphStorage::has_label_name(self, label)
951    }
952    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
953        GraphStorage::has_relationship_type_name(self, rel_type)
954    }
955    fn has_property_key(&self, key: &str) -> bool {
956        GraphStorage::has_property_key(self, key)
957    }
958}
959
960// ============================================================================
961// BorrowedGraphStorage — optional capability for backends that can hand out
962// long-lived borrows into internal records.
963//
964// The executor prefers `with_node` / `with_relationship` on hot paths because
965// they work for both borrow-capable and owned-only backends. This trait is
966// available for callers that really do want a `&NodeRecord` outliving the
967// closure — mostly internal optimization paths and tests.
968// ============================================================================
969
970pub trait BorrowedGraphStorage: GraphStorage {
971    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord>;
972    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord>;
973
974    fn node_refs(&self) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
975        Box::new(
976            self.all_node_ids()
977                .into_iter()
978                .filter_map(|id| self.node_ref(id)),
979        )
980    }
981
982    fn node_refs_by_label(&self, label: &str) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
983        Box::new(
984            self.node_ids_by_label(label)
985                .into_iter()
986                .filter_map(|id| self.node_ref(id)),
987        )
988    }
989
990    fn relationship_refs(&self) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
991        Box::new(
992            self.all_rel_ids()
993                .into_iter()
994                .filter_map(|id| self.relationship_ref(id)),
995        )
996    }
997
998    fn relationship_refs_by_type(
999        &self,
1000        rel_type: &str,
1001    ) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
1002        Box::new(
1003            self.rel_ids_by_type(rel_type)
1004                .into_iter()
1005                .filter_map(|id| self.relationship_ref(id)),
1006        )
1007    }
1008}
1009
1010// ============================================================================
1011// GraphStorageMut — write-side storage contract.
1012//
1013// A backend that implements `GraphStorage` can additionally implement
1014// `GraphStorageMut` to support create / mutate / delete / admin operations.
1015// Everything above the `Defaulted convenience helpers` block is a required
1016// primitive; everything below is defaulted and can be overridden for perf.
1017// ============================================================================
1018
1019pub trait GraphStorageMut: GraphStorage {
1020    // ---------- Creation ----------
1021
1022    fn try_create_node(
1023        &mut self,
1024        labels: Vec<String>,
1025        properties: Properties,
1026    ) -> Option<NodeRecord>;
1027
1028    /// Compatibility helper for callers that historically used the
1029    /// infallible creation surface. Query and binding paths should prefer
1030    /// [`Self::try_create_node`] so allocation/id exhaustion can surface as
1031    /// an ordinary error instead of a process panic.
1032    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord
1033    where
1034        Self: Sized,
1035    {
1036        self.try_create_node(labels, properties)
1037            .unwrap_or_else(|| NodeRecord {
1038                id: NodeId::MAX,
1039                labels: Vec::new(),
1040                properties: Properties::new(),
1041            })
1042    }
1043
1044    fn create_relationship(
1045        &mut self,
1046        src: NodeId,
1047        dst: NodeId,
1048        rel_type: &str,
1049        properties: Properties,
1050    ) -> Option<RelationshipRecord>;
1051
1052    // ---------- Node mutation ----------
1053
1054    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
1055
1056    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
1057
1058    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1059    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1060
1061    // ---------- Relationship mutation ----------
1062
1063    fn set_relationship_property(
1064        &mut self,
1065        rel_id: RelationshipId,
1066        key: String,
1067        value: PropertyValue,
1068    ) -> bool;
1069
1070    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
1071
1072    // ---------- Deletion ----------
1073
1074    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
1075
1076    /// Returns false if the node still has attached relationships.
1077    fn delete_node(&mut self, node_id: NodeId) -> bool;
1078
1079    /// Deletes the node and all attached relationships.
1080    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
1081
1082    // ---------- Admin / lifecycle ----------
1083
1084    /// Drop every node and every relationship, returning the store to an
1085    /// empty state. Provided as a trait method so callers (bindings, admin
1086    /// tools) can reset a graph without knowing the concrete backend.
1087    ///
1088    /// Future snapshot / WAL / restore entry points will also hang off the
1089    /// `GraphStorageMut` surface — `clear` is the first of them.
1090    fn clear(&mut self);
1091
1092    /// Register an explicitly-declared index in the catalog. Backends that
1093    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
1094    ///
1095    /// `if_not_exists` collapses both name and schema-equivalence
1096    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
1097    /// surfacing them as errors.
1098    #[allow(clippy::result_large_err)]
1099    fn create_index(
1100        &mut self,
1101        _request: IndexRequest,
1102        _if_not_exists: bool,
1103    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1104        Err(CreateIndexError::Unsupported(
1105            "this backend does not maintain an index catalog",
1106        ))
1107    }
1108
1109    /// Remove an explicitly-declared index from the catalog. Backends
1110    /// without catalog support return [`DropIndexError::Unsupported`].
1111    /// `if_exists` collapses missing-index errors into
1112    /// [`DropIndexOutcome::NoOpMissing`].
1113    fn drop_index(
1114        &mut self,
1115        _name: &str,
1116        _if_exists: bool,
1117    ) -> Result<DropIndexOutcome, DropIndexError> {
1118        Err(DropIndexError::Unsupported(
1119            "this backend does not maintain an index catalog",
1120        ))
1121    }
1122
1123    /// Register an explicitly-declared constraint. Backends without
1124    /// catalog support return [`CreateConstraintError::Unsupported`].
1125    /// Uniqueness/key kinds may transparently register a backing range
1126    /// index of the same name.
1127    fn create_constraint(
1128        &mut self,
1129        _request: ConstraintRequest,
1130        _if_not_exists: bool,
1131    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1132        Err(CreateConstraintError::Unsupported(
1133            "this backend does not maintain a constraint catalog",
1134        ))
1135    }
1136
1137    /// Drop a named constraint. Cascades to the backing index if the
1138    /// constraint owned one.
1139    fn drop_constraint(
1140        &mut self,
1141        _name: &str,
1142        _if_exists: bool,
1143    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1144        Err(DropConstraintError::Unsupported(
1145            "this backend does not maintain a constraint catalog",
1146        ))
1147    }
1148
1149    // ---------- Defaulted convenience helpers ----------
1150
1151    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1152    where
1153        Self: Sized,
1154    {
1155        if !self.contains_node(node_id) {
1156            return false;
1157        }
1158
1159        let existing_keys = match self.node_properties(node_id) {
1160            Some(props) => props.into_keys().collect::<Vec<_>>(),
1161            None => return false,
1162        };
1163
1164        for key in existing_keys {
1165            self.remove_node_property(node_id, &key);
1166        }
1167
1168        for (k, v) in properties {
1169            self.set_node_property(node_id, k.to_string(), v);
1170        }
1171
1172        true
1173    }
1174
1175    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1176        if !self.contains_node(node_id) {
1177            return false;
1178        }
1179
1180        for (k, v) in properties {
1181            self.set_node_property(node_id, k.to_string(), v);
1182        }
1183
1184        true
1185    }
1186
1187    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1188    where
1189        Self: Sized,
1190    {
1191        if !self.contains_node(node_id) {
1192            return false;
1193        }
1194
1195        let current = match self.node_labels(node_id) {
1196            Some(labels) => labels,
1197            None => return false,
1198        };
1199
1200        for label in &current {
1201            self.remove_node_label(node_id, label);
1202        }
1203
1204        for label in &labels {
1205            self.add_node_label(node_id, label);
1206        }
1207
1208        true
1209    }
1210
1211    fn replace_relationship_properties(
1212        &mut self,
1213        rel_id: RelationshipId,
1214        properties: Properties,
1215    ) -> bool
1216    where
1217        Self: Sized,
1218    {
1219        if !self.contains_relationship(rel_id) {
1220            return false;
1221        }
1222
1223        let existing_keys = match self.relationship_properties(rel_id) {
1224            Some(props) => props.into_keys().collect::<Vec<_>>(),
1225            None => return false,
1226        };
1227
1228        for key in existing_keys {
1229            self.remove_relationship_property(rel_id, &key);
1230        }
1231
1232        for (k, v) in properties {
1233            self.set_relationship_property(rel_id, k.to_string(), v);
1234        }
1235
1236        true
1237    }
1238
1239    fn merge_relationship_properties(
1240        &mut self,
1241        rel_id: RelationshipId,
1242        properties: Properties,
1243    ) -> bool {
1244        if !self.contains_relationship(rel_id) {
1245            return false;
1246        }
1247
1248        for (k, v) in properties {
1249            self.set_relationship_property(rel_id, k.to_string(), v);
1250        }
1251
1252        true
1253    }
1254
1255    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1256        let rel_ids = self.relationship_ids_of(node_id, direction);
1257
1258        let mut deleted = 0;
1259        for rel_id in rel_ids {
1260            if self.delete_relationship(rel_id) {
1261                deleted += 1;
1262            }
1263        }
1264        deleted
1265    }
1266
1267    fn get_or_create_node(
1268        &mut self,
1269        labels: Vec<String>,
1270        match_key: &str,
1271        match_value: &PropertyValue,
1272        init_properties: Properties,
1273    ) -> NodeRecord
1274    where
1275        Self: Sized,
1276    {
1277        for label in &labels {
1278            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1279            if let Some(node) = matches.into_iter().next() {
1280                return node;
1281            }
1282        }
1283
1284        self.create_node(labels, init_properties)
1285    }
1286}