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