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    /// Ids of `label` nodes whose `property` holds a temporal value of
806    /// another kind than `like` (a DATE when `like` is a DATETIME). A range
807    /// scan on a temporal bound emits them too, so the comparison above it
808    /// sees them as an unindexed scan would, instead of the index skipping
809    /// them without a word. `None` when no index covers
810    /// `(label, property)`.
811    fn node_range_other_temporal_kind_ids(
812        &self,
813        _label: &str,
814        _property: &str,
815        _like: &PropertyValue,
816    ) -> Option<Vec<NodeId>> {
817        None
818    }
819
820    /// Ids of `label` nodes with `property` in `[lo, hi]`, in index order
821    /// (value, then id), strictly after the `(value, id)` cursor `after`,
822    /// at most `max` of them; `descending` reverses the order. Bounds are
823    /// inclusive here; the executor refilters exact ones. `None` means no
824    /// ordered index covers `(label, property)`.
825    #[allow(clippy::too_many_arguments)]
826    fn node_range_ordered_chunk(
827        &self,
828        _label: &str,
829        _property: &str,
830        _lo: Option<&PropertyValue>,
831        _hi: Option<&PropertyValue>,
832        _descending: bool,
833        _after: Option<(&PropertyValue, NodeId)>,
834        _max: usize,
835    ) -> Option<Vec<NodeId>> {
836        None
837    }
838
839    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
840    /// bounding box. The executor refilters every id with the precise
841    /// predicate, including the z-coordinate when the indexed point
842    /// is 3D.
843    fn node_point_within_bbox(
844        &self,
845        _label: &str,
846        _property: &str,
847        _ll: (f64, f64),
848        _ur: (f64, f64),
849    ) -> Option<Vec<NodeId>> {
850        None
851    }
852
853    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
854    /// candidate set is conservative — the actual great-circle /
855    /// cartesian distance check is the executor's responsibility.
856    fn node_point_within_distance(
857        &self,
858        _label: &str,
859        _property: &str,
860        _center: (f64, f64),
861        _max_distance: f64,
862    ) -> Option<Vec<NodeId>> {
863        None
864    }
865
866    /// Trigram-index candidates for relationships of `rel_type` whose
867    /// `property` value matches `query` (substring/prefix/suffix). Mirror
868    /// of [`Self::node_text_candidates`] for relationship-target indexes.
869    fn relationship_text_candidates(
870        &self,
871        _rel_type: &str,
872        _property: &str,
873        _query: &str,
874    ) -> Option<Vec<RelationshipId>> {
875        None
876    }
877
878    /// Sorted-index candidates for relationships of `rel_type` on the
879    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
880    fn relationship_range_candidates(
881        &self,
882        _rel_type: &str,
883        _property: &str,
884        _lo: Option<&PropertyValue>,
885        _hi: Option<&PropertyValue>,
886    ) -> Option<Vec<RelationshipId>> {
887        None
888    }
889
890    /// Mirror of [`Self::node_range_other_temporal_kind_ids`] for
891    /// relationships of `rel_type`.
892    fn relationship_range_other_temporal_kind_ids(
893        &self,
894        _rel_type: &str,
895        _property: &str,
896        _like: &PropertyValue,
897    ) -> Option<Vec<RelationshipId>> {
898        None
899    }
900
901    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
902    /// box, scoped to relationships of `rel_type`. Mirror of
903    /// [`Self::node_point_within_bbox`].
904    fn relationship_point_within_bbox(
905        &self,
906        _rel_type: &str,
907        _property: &str,
908        _ll: (f64, f64),
909        _ur: (f64, f64),
910    ) -> Option<Vec<RelationshipId>> {
911        None
912    }
913
914    /// Spatial-index candidates within `max_distance` of `(x, y)`,
915    /// scoped to relationships of `rel_type`. Mirror of
916    /// [`Self::node_point_within_distance`].
917    fn relationship_point_within_distance(
918        &self,
919        _rel_type: &str,
920        _property: &str,
921        _center: (f64, f64),
922        _max_distance: f64,
923    ) -> Option<Vec<RelationshipId>> {
924        None
925    }
926}
927
928// ============================================================================
929// GraphCatalog — narrow schema-query slice used by the analyzer.
930//
931// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
932// `GraphCatalog` without every backend having to implement a second trait.
933// ============================================================================
934
935pub trait GraphCatalog {
936    fn node_count(&self) -> usize;
937    fn relationship_count(&self) -> usize;
938    fn has_label_name(&self, label: &str) -> bool;
939    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
940    fn has_property_key(&self, key: &str) -> bool;
941}
942
943impl<T: GraphStorage> GraphCatalog for T {
944    fn node_count(&self) -> usize {
945        GraphStorage::node_count(self)
946    }
947    fn relationship_count(&self) -> usize {
948        GraphStorage::relationship_count(self)
949    }
950    fn has_label_name(&self, label: &str) -> bool {
951        GraphStorage::has_label_name(self, label)
952    }
953    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
954        GraphStorage::has_relationship_type_name(self, rel_type)
955    }
956    fn has_property_key(&self, key: &str) -> bool {
957        GraphStorage::has_property_key(self, key)
958    }
959}
960
961// ============================================================================
962// BorrowedGraphStorage — optional capability for backends that can hand out
963// long-lived borrows into internal records.
964//
965// The executor prefers `with_node` / `with_relationship` on hot paths because
966// they work for both borrow-capable and owned-only backends. This trait is
967// available for callers that really do want a `&NodeRecord` outliving the
968// closure — mostly internal optimization paths and tests.
969// ============================================================================
970
971pub trait BorrowedGraphStorage: GraphStorage {
972    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord>;
973    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord>;
974
975    fn node_refs(&self) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
976        Box::new(
977            self.all_node_ids()
978                .into_iter()
979                .filter_map(|id| self.node_ref(id)),
980        )
981    }
982
983    fn node_refs_by_label(&self, label: &str) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
984        Box::new(
985            self.node_ids_by_label(label)
986                .into_iter()
987                .filter_map(|id| self.node_ref(id)),
988        )
989    }
990
991    fn relationship_refs(&self) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
992        Box::new(
993            self.all_rel_ids()
994                .into_iter()
995                .filter_map(|id| self.relationship_ref(id)),
996        )
997    }
998
999    fn relationship_refs_by_type(
1000        &self,
1001        rel_type: &str,
1002    ) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
1003        Box::new(
1004            self.rel_ids_by_type(rel_type)
1005                .into_iter()
1006                .filter_map(|id| self.relationship_ref(id)),
1007        )
1008    }
1009}
1010
1011// ============================================================================
1012// GraphStorageMut — write-side storage contract.
1013//
1014// A backend that implements `GraphStorage` can additionally implement
1015// `GraphStorageMut` to support create / mutate / delete / admin operations.
1016// Everything above the `Defaulted convenience helpers` block is a required
1017// primitive; everything below is defaulted and can be overridden for perf.
1018// ============================================================================
1019
1020pub trait GraphStorageMut: GraphStorage {
1021    // ---------- Creation ----------
1022
1023    fn try_create_node(
1024        &mut self,
1025        labels: Vec<String>,
1026        properties: Properties,
1027    ) -> Option<NodeRecord>;
1028
1029    /// Compatibility helper for callers that historically used the
1030    /// infallible creation surface. Query and binding paths should prefer
1031    /// [`Self::try_create_node`] so allocation/id exhaustion can surface as
1032    /// an ordinary error instead of a process panic.
1033    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord
1034    where
1035        Self: Sized,
1036    {
1037        self.try_create_node(labels, properties)
1038            .unwrap_or_else(|| NodeRecord {
1039                id: NodeId::MAX,
1040                labels: Vec::new(),
1041                properties: Properties::new(),
1042            })
1043    }
1044
1045    fn create_relationship(
1046        &mut self,
1047        src: NodeId,
1048        dst: NodeId,
1049        rel_type: &str,
1050        properties: Properties,
1051    ) -> Option<RelationshipRecord>;
1052
1053    // ---------- Node mutation ----------
1054
1055    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
1056
1057    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
1058
1059    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1060    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1061
1062    // ---------- Relationship mutation ----------
1063
1064    fn set_relationship_property(
1065        &mut self,
1066        rel_id: RelationshipId,
1067        key: String,
1068        value: PropertyValue,
1069    ) -> bool;
1070
1071    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
1072
1073    // ---------- Deletion ----------
1074
1075    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
1076
1077    /// Returns false if the node still has attached relationships.
1078    fn delete_node(&mut self, node_id: NodeId) -> bool;
1079
1080    /// Deletes the node and all attached relationships.
1081    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
1082
1083    // ---------- Admin / lifecycle ----------
1084
1085    /// Drop every node and every relationship, returning the store to an
1086    /// empty state. Provided as a trait method so callers (bindings, admin
1087    /// tools) can reset a graph without knowing the concrete backend.
1088    ///
1089    /// Future snapshot / WAL / restore entry points will also hang off the
1090    /// `GraphStorageMut` surface — `clear` is the first of them.
1091    fn clear(&mut self);
1092
1093    /// Register an explicitly-declared index in the catalog. Backends that
1094    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
1095    ///
1096    /// `if_not_exists` collapses both name and schema-equivalence
1097    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
1098    /// surfacing them as errors.
1099    #[allow(clippy::result_large_err)]
1100    fn create_index(
1101        &mut self,
1102        _request: IndexRequest,
1103        _if_not_exists: bool,
1104    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1105        Err(CreateIndexError::Unsupported(
1106            "this backend does not maintain an index catalog",
1107        ))
1108    }
1109
1110    /// Remove an explicitly-declared index from the catalog. Backends
1111    /// without catalog support return [`DropIndexError::Unsupported`].
1112    /// `if_exists` collapses missing-index errors into
1113    /// [`DropIndexOutcome::NoOpMissing`].
1114    fn drop_index(
1115        &mut self,
1116        _name: &str,
1117        _if_exists: bool,
1118    ) -> Result<DropIndexOutcome, DropIndexError> {
1119        Err(DropIndexError::Unsupported(
1120            "this backend does not maintain an index catalog",
1121        ))
1122    }
1123
1124    /// Register an explicitly-declared constraint. Backends without
1125    /// catalog support return [`CreateConstraintError::Unsupported`].
1126    /// Uniqueness/key kinds may transparently register a backing range
1127    /// index of the same name.
1128    fn create_constraint(
1129        &mut self,
1130        _request: ConstraintRequest,
1131        _if_not_exists: bool,
1132    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1133        Err(CreateConstraintError::Unsupported(
1134            "this backend does not maintain a constraint catalog",
1135        ))
1136    }
1137
1138    /// Drop a named constraint. Cascades to the backing index if the
1139    /// constraint owned one.
1140    fn drop_constraint(
1141        &mut self,
1142        _name: &str,
1143        _if_exists: bool,
1144    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1145        Err(DropConstraintError::Unsupported(
1146            "this backend does not maintain a constraint catalog",
1147        ))
1148    }
1149
1150    // ---------- Defaulted convenience helpers ----------
1151
1152    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1153    where
1154        Self: Sized,
1155    {
1156        if !self.contains_node(node_id) {
1157            return false;
1158        }
1159
1160        let existing_keys = match self.node_properties(node_id) {
1161            Some(props) => props.into_keys().collect::<Vec<_>>(),
1162            None => return false,
1163        };
1164
1165        for key in existing_keys {
1166            self.remove_node_property(node_id, &key);
1167        }
1168
1169        for (k, v) in properties {
1170            self.set_node_property(node_id, k.to_string(), v);
1171        }
1172
1173        true
1174    }
1175
1176    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1177        if !self.contains_node(node_id) {
1178            return false;
1179        }
1180
1181        for (k, v) in properties {
1182            self.set_node_property(node_id, k.to_string(), v);
1183        }
1184
1185        true
1186    }
1187
1188    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1189    where
1190        Self: Sized,
1191    {
1192        if !self.contains_node(node_id) {
1193            return false;
1194        }
1195
1196        let current = match self.node_labels(node_id) {
1197            Some(labels) => labels,
1198            None => return false,
1199        };
1200
1201        for label in &current {
1202            self.remove_node_label(node_id, label);
1203        }
1204
1205        for label in &labels {
1206            self.add_node_label(node_id, label);
1207        }
1208
1209        true
1210    }
1211
1212    fn replace_relationship_properties(
1213        &mut self,
1214        rel_id: RelationshipId,
1215        properties: Properties,
1216    ) -> bool
1217    where
1218        Self: Sized,
1219    {
1220        if !self.contains_relationship(rel_id) {
1221            return false;
1222        }
1223
1224        let existing_keys = match self.relationship_properties(rel_id) {
1225            Some(props) => props.into_keys().collect::<Vec<_>>(),
1226            None => return false,
1227        };
1228
1229        for key in existing_keys {
1230            self.remove_relationship_property(rel_id, &key);
1231        }
1232
1233        for (k, v) in properties {
1234            self.set_relationship_property(rel_id, k.to_string(), v);
1235        }
1236
1237        true
1238    }
1239
1240    fn merge_relationship_properties(
1241        &mut self,
1242        rel_id: RelationshipId,
1243        properties: Properties,
1244    ) -> bool {
1245        if !self.contains_relationship(rel_id) {
1246            return false;
1247        }
1248
1249        for (k, v) in properties {
1250            self.set_relationship_property(rel_id, k.to_string(), v);
1251        }
1252
1253        true
1254    }
1255
1256    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1257        let rel_ids = self.relationship_ids_of(node_id, direction);
1258
1259        let mut deleted = 0;
1260        for rel_id in rel_ids {
1261            if self.delete_relationship(rel_id) {
1262                deleted += 1;
1263            }
1264        }
1265        deleted
1266    }
1267
1268    fn get_or_create_node(
1269        &mut self,
1270        labels: Vec<String>,
1271        match_key: &str,
1272        match_value: &PropertyValue,
1273        init_properties: Properties,
1274    ) -> NodeRecord
1275    where
1276        Self: Sized,
1277    {
1278        for label in &labels {
1279            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1280            if let Some(node) = matches.into_iter().next() {
1281                return node;
1282            }
1283        }
1284
1285        self.create_node(labels, init_properties)
1286    }
1287}