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