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, NodeId, NodeRecord, Properties, PropertyValue, RelationshipId,
19    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    // ---------- Defaulted: record-returning scans ----------
148    //
149    // These synthesize full-record scans from id scans + point lookups. That
150    // is correct for any backend and fast enough for small graphs, but a
151    // backend that can scan records in one pass (in-memory via a BTreeMap
152    // `.values()`, a column store via a streaming read) should override.
153
154    fn all_nodes(&self) -> Vec<NodeRecord> {
155        self.all_node_ids()
156            .into_iter()
157            .filter_map(|id| self.node(id))
158            .collect()
159    }
160
161    fn nodes_by_label(&self, label: &str) -> Vec<NodeRecord> {
162        self.node_ids_by_label(label)
163            .into_iter()
164            .filter_map(|id| self.node(id))
165            .collect()
166    }
167
168    fn all_relationships(&self) -> Vec<RelationshipRecord> {
169        self.all_rel_ids()
170            .into_iter()
171            .filter_map(|id| self.relationship(id))
172            .collect()
173    }
174
175    fn relationships_by_type(&self, rel_type: &str) -> Vec<RelationshipRecord> {
176        self.rel_ids_by_type(rel_type)
177            .into_iter()
178            .filter_map(|id| self.relationship(id))
179            .collect()
180    }
181
182    // ---------- Defaulted: traversal helpers ----------
183
184    fn relationship_ids_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipId> {
185        self.expand_ids(node_id, direction, &[])
186            .into_iter()
187            .map(|(rel_id, _)| rel_id)
188            .collect()
189    }
190
191    fn outgoing_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
192        self.relationship_ids_of(node_id, Direction::Right)
193            .into_iter()
194            .filter_map(|id| self.relationship(id))
195            .collect()
196    }
197
198    fn incoming_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
199        self.relationship_ids_of(node_id, Direction::Left)
200            .into_iter()
201            .filter_map(|id| self.relationship(id))
202            .collect()
203    }
204
205    fn relationships_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipRecord> {
206        self.relationship_ids_of(node_id, direction)
207            .into_iter()
208            .filter_map(|id| self.relationship(id))
209            .collect()
210    }
211
212    fn degree(&self, node_id: NodeId, direction: Direction) -> usize {
213        self.expand_ids(node_id, direction, &[]).len()
214    }
215
216    fn is_isolated(&self, node_id: NodeId) -> bool {
217        self.degree(node_id, Direction::Undirected) == 0
218    }
219
220    fn expand(
221        &self,
222        node_id: NodeId,
223        direction: Direction,
224        types: &[String],
225    ) -> Vec<(RelationshipRecord, NodeRecord)> {
226        self.expand_ids(node_id, direction, types)
227            .into_iter()
228            .filter_map(|(rid, nid)| {
229                let rel = self.relationship(rid)?;
230                let node = self.node(nid)?;
231                Some((rel, node))
232            })
233            .collect()
234    }
235
236    fn expand_detailed(
237        &self,
238        node_id: NodeId,
239        direction: Direction,
240        types: &[String],
241    ) -> Vec<ExpandedRelationship> {
242        self.expand(node_id, direction, types)
243            .into_iter()
244            .map(|(relationship, other_node)| ExpandedRelationship {
245                relationship,
246                other_node,
247            })
248            .collect()
249    }
250
251    fn neighbors(
252        &self,
253        node_id: NodeId,
254        direction: Direction,
255        types: &[String],
256    ) -> Vec<NodeRecord> {
257        self.expand_ids(node_id, direction, types)
258            .into_iter()
259            .filter_map(|(_, nid)| self.node(nid))
260            .collect()
261    }
262
263    // ---------- Defaulted: narrow node accessors ----------
264
265    fn node_has_label(&self, node_id: NodeId, label: &str) -> bool
266    where
267        Self: Sized,
268    {
269        self.with_node(node_id, |n| n.labels.iter().any(|l| l == label))
270            .unwrap_or(false)
271    }
272
273    fn node_labels(&self, node_id: NodeId) -> Option<Vec<String>>
274    where
275        Self: Sized,
276    {
277        self.with_node(node_id, |n| n.labels.clone())
278    }
279
280    fn node_properties(&self, node_id: NodeId) -> Option<Properties>
281    where
282        Self: Sized,
283    {
284        self.with_node(node_id, |n| n.properties.clone())
285    }
286
287    fn node_property(&self, node_id: NodeId, key: &str) -> Option<PropertyValue>
288    where
289        Self: Sized,
290    {
291        self.with_node(node_id, |n| n.properties.get(key).cloned())
292            .flatten()
293    }
294
295    // ---------- Defaulted: narrow relationship accessors ----------
296
297    fn relationship_type(&self, rel_id: RelationshipId) -> Option<String>
298    where
299        Self: Sized,
300    {
301        self.with_relationship(rel_id, |r| r.rel_type.clone())
302    }
303
304    fn relationship_properties(&self, rel_id: RelationshipId) -> Option<Properties>
305    where
306        Self: Sized,
307    {
308        self.with_relationship(rel_id, |r| r.properties.clone())
309    }
310
311    fn relationship_property(&self, rel_id: RelationshipId, key: &str) -> Option<PropertyValue>
312    where
313        Self: Sized,
314    {
315        self.with_relationship(rel_id, |r| r.properties.get(key).cloned())
316            .flatten()
317    }
318
319    fn relationship_source(&self, rel_id: RelationshipId) -> Option<NodeId> {
320        self.relationship_endpoints(rel_id).map(|(s, _)| s)
321    }
322
323    fn relationship_target(&self, rel_id: RelationshipId) -> Option<NodeId> {
324        self.relationship_endpoints(rel_id).map(|(_, d)| d)
325    }
326
327    fn other_node(&self, rel_id: RelationshipId, node_id: NodeId) -> Option<NodeId> {
328        let (src, dst) = self.relationship_endpoints(rel_id)?;
329        if src == node_id {
330            Some(dst)
331        } else if dst == node_id {
332            Some(src)
333        } else {
334            None
335        }
336    }
337
338    // ---------- Defaulted: catalog helpers ----------
339
340    fn has_label_name(&self, label: &str) -> bool {
341        self.all_labels().iter().any(|l| l == label)
342    }
343
344    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
345        self.all_relationship_types().iter().any(|t| t == rel_type)
346    }
347
348    fn all_node_property_keys(&self) -> Vec<String>
349    where
350        Self: Sized,
351    {
352        let mut keys = BTreeSet::new();
353        for id in self.all_node_ids() {
354            self.with_node(id, |n| {
355                for key in n.properties.keys() {
356                    keys.insert(key.clone());
357                }
358            });
359        }
360        keys.into_iter().collect()
361    }
362
363    fn all_relationship_property_keys(&self) -> Vec<String>
364    where
365        Self: Sized,
366    {
367        let mut keys = BTreeSet::new();
368        for id in self.all_rel_ids() {
369            self.with_relationship(id, |r| {
370                for key in r.properties.keys() {
371                    keys.insert(key.clone());
372                }
373            });
374        }
375        keys.into_iter().collect()
376    }
377
378    fn all_property_keys(&self) -> Vec<String>
379    where
380        Self: Sized,
381    {
382        let mut keys = BTreeSet::new();
383        for key in self.all_node_property_keys() {
384            keys.insert(key);
385        }
386        for key in self.all_relationship_property_keys() {
387            keys.insert(key);
388        }
389        keys.into_iter().collect()
390    }
391
392    fn has_property_key(&self, key: &str) -> bool
393    where
394        Self: Sized,
395    {
396        self.all_node_property_keys().iter().any(|k| k == key)
397            || self
398                .all_relationship_property_keys()
399                .iter()
400                .any(|k| k == key)
401    }
402
403    fn label_property_keys(&self, label: &str) -> Vec<String>
404    where
405        Self: Sized,
406    {
407        let mut keys = BTreeSet::new();
408        for id in self.node_ids_by_label(label) {
409            self.with_node(id, |n| {
410                for key in n.properties.keys() {
411                    keys.insert(key.clone());
412                }
413            });
414        }
415        keys.into_iter().collect()
416    }
417
418    fn rel_type_property_keys(&self, rel_type: &str) -> Vec<String>
419    where
420        Self: Sized,
421    {
422        let mut keys = BTreeSet::new();
423        for id in self.rel_ids_by_type(rel_type) {
424            self.with_relationship(id, |r| {
425                for key in r.properties.keys() {
426                    keys.insert(key.clone());
427                }
428            });
429        }
430        keys.into_iter().collect()
431    }
432
433    fn label_has_property_key(&self, label: &str, key: &str) -> bool
434    where
435        Self: Sized,
436    {
437        self.node_ids_by_label(label).into_iter().any(|id| {
438            self.with_node(id, |n| n.properties.contains_key(key))
439                .unwrap_or(false)
440        })
441    }
442
443    fn rel_type_has_property_key(&self, rel_type: &str, key: &str) -> bool
444    where
445        Self: Sized,
446    {
447        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
448            self.with_relationship(id, |r| r.properties.contains_key(key))
449                .unwrap_or(false)
450        })
451    }
452
453    // ---------- Defaulted: property-filter lookups ----------
454
455    fn find_nodes_by_property(
456        &self,
457        label: Option<&str>,
458        key: &str,
459        value: &PropertyValue,
460    ) -> Vec<NodeRecord>
461    where
462        Self: Sized,
463    {
464        let ids = match label {
465            Some(label) => self.node_ids_by_label(label),
466            None => self.all_node_ids(),
467        };
468
469        ids.into_iter()
470            .filter_map(|id| {
471                let matches = self
472                    .with_node(id, |n| n.properties.get(key) == Some(value))
473                    .unwrap_or(false);
474                if matches {
475                    self.node(id)
476                } else {
477                    None
478                }
479            })
480            .collect()
481    }
482
483    fn find_node_ids_by_property(
484        &self,
485        label: Option<&str>,
486        key: &str,
487        value: &PropertyValue,
488    ) -> Vec<NodeId>
489    where
490        Self: Sized,
491    {
492        self.find_nodes_by_property(label, key, value)
493            .into_iter()
494            .map(|n| n.id)
495            .collect()
496    }
497
498    fn find_relationships_by_property(
499        &self,
500        rel_type: Option<&str>,
501        key: &str,
502        value: &PropertyValue,
503    ) -> Vec<RelationshipRecord>
504    where
505        Self: Sized,
506    {
507        let ids = match rel_type {
508            Some(rel_type) => self.rel_ids_by_type(rel_type),
509            None => self.all_rel_ids(),
510        };
511
512        ids.into_iter()
513            .filter_map(|id| {
514                let matches = self
515                    .with_relationship(id, |r| r.properties.get(key) == Some(value))
516                    .unwrap_or(false);
517                if matches {
518                    self.relationship(id)
519                } else {
520                    None
521                }
522            })
523            .collect()
524    }
525
526    fn find_relationship_ids_by_property(
527        &self,
528        rel_type: Option<&str>,
529        key: &str,
530        value: &PropertyValue,
531    ) -> Vec<RelationshipId>
532    where
533        Self: Sized,
534    {
535        self.find_relationships_by_property(rel_type, key, value)
536            .into_iter()
537            .map(|r| r.id)
538            .collect()
539    }
540
541    fn node_exists_with_label_and_property(
542        &self,
543        label: &str,
544        key: &str,
545        value: &PropertyValue,
546    ) -> bool
547    where
548        Self: Sized,
549    {
550        self.node_ids_by_label(label).into_iter().any(|id| {
551            self.with_node(id, |n| n.properties.get(key) == Some(value))
552                .unwrap_or(false)
553        })
554    }
555
556    fn relationship_exists_with_type_and_property(
557        &self,
558        rel_type: &str,
559        key: &str,
560        value: &PropertyValue,
561    ) -> bool
562    where
563        Self: Sized,
564    {
565        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
566            self.with_relationship(id, |r| r.properties.get(key) == Some(value))
567                .unwrap_or(false)
568        })
569    }
570
571    // ---------- Defaulted: index catalog ----------
572    //
573    // Backends that maintain an index catalog (currently the in-memory
574    // backend) override these. Backends without catalog support keep
575    // the no-op defaults so callers can list / look up safely.
576
577    fn list_indexes(&self) -> Vec<IndexDefinition> {
578        Vec::new()
579    }
580
581    fn get_index(&self, _name: &str) -> Option<IndexDefinition> {
582        None
583    }
584
585    /// Run a FULLTEXT index query against the named index. Returns
586    /// `(entity_id, score)` pairs sorted descending by score. Backends
587    /// without fulltext support return an empty vector; the caller is
588    /// expected to have validated that the index exists via the
589    /// catalog first.
590    fn fulltext_search(&self, _name: &str, _query: &str) -> Vec<(u64, f64)> {
591        Vec::new()
592    }
593
594    /// List explicitly-declared constraints. Backends without a
595    /// constraint catalog return the empty vector.
596    fn list_constraints(&self) -> Vec<ConstraintDefinition> {
597        Vec::new()
598    }
599
600    fn get_constraint(&self, _name: &str) -> Option<ConstraintDefinition> {
601        None
602    }
603
604    /// Mutation-time pre-check: would creating a node with these
605    /// `labels` and `properties` violate any registered constraint?
606    /// Default returns `Ok(())` so backends without a constraint
607    /// catalog pay nothing. The in-memory backend overrides this and
608    /// the call is virtually free when the catalog is empty.
609    fn check_node_create_against_constraints(
610        &self,
611        _labels: &[String],
612        _properties: &Properties,
613    ) -> Result<(), String> {
614        Ok(())
615    }
616
617    /// Mutation-time pre-check for `CREATE ()-[r:TYPE { ... }]->()`.
618    fn check_relationship_create_against_constraints(
619        &self,
620        _rel_type: &str,
621        _properties: &Properties,
622    ) -> Result<(), String> {
623        Ok(())
624    }
625
626    /// Mutation-time pre-check: would setting `key = value` on this
627    /// node violate any registered constraint? Default `Ok(())`.
628    fn check_node_set_property_against_constraints(
629        &self,
630        _node_id: NodeId,
631        _key: &str,
632        _value: &PropertyValue,
633    ) -> Result<(), String> {
634        Ok(())
635    }
636
637    /// Mutation-time pre-check: would removing `key` on this node
638    /// violate an existence / key constraint? Default `Ok(())`.
639    fn check_node_remove_property_against_constraints(
640        &self,
641        _node_id: NodeId,
642        _key: &str,
643    ) -> Result<(), String> {
644        Ok(())
645    }
646
647    /// Mutation-time pre-check: would replacing all properties on this
648    /// node leave it in violation of any registered constraint? Default
649    /// `Ok(())`.
650    fn check_node_replace_properties_against_constraints(
651        &self,
652        _node_id: NodeId,
653        _properties: &Properties,
654    ) -> Result<(), String> {
655        Ok(())
656    }
657
658    /// Mutation-time pre-check: equivalent for relationship
659    /// property writes.
660    fn check_relationship_set_property_against_constraints(
661        &self,
662        _rel_id: RelationshipId,
663        _key: &str,
664        _value: &PropertyValue,
665    ) -> Result<(), String> {
666        Ok(())
667    }
668
669    fn check_relationship_remove_property_against_constraints(
670        &self,
671        _rel_id: RelationshipId,
672        _key: &str,
673    ) -> Result<(), String> {
674        Ok(())
675    }
676
677    /// Mutation-time pre-check: would replacing all properties on this
678    /// relationship leave it in violation of any registered constraint?
679    /// Default `Ok(())`.
680    fn check_relationship_replace_properties_against_constraints(
681        &self,
682        _rel_id: RelationshipId,
683        _properties: &Properties,
684    ) -> Result<(), String> {
685        Ok(())
686    }
687
688    /// Mutation-time pre-check: would adding `label` to this node
689    /// activate a constraint the node currently violates?
690    fn check_node_add_label_against_constraints(
691        &self,
692        _node_id: NodeId,
693        _label: &str,
694    ) -> Result<(), String> {
695        Ok(())
696    }
697
698    /// Cardinality snapshot used by the cost model. Backends without
699    /// per-label / per-type indexes return [`GraphStats::default()`],
700    /// which the planner treats as "no information available".
701    fn graph_stats(&self) -> GraphStats {
702        GraphStats::default()
703    }
704
705    /// Trigram-index candidates for `query` on `label.property`.
706    ///
707    /// Semantics:
708    /// * `Some(ids)` → these node ids *might* match (refilter required).
709    /// * `None` → no trigram scope for `(label, property)`; caller must
710    ///   fall back to a full scan.
711    ///
712    /// Backends without text-index support always return `None`.
713    fn node_text_candidates(
714        &self,
715        _label: &str,
716        _property: &str,
717        _query: &str,
718    ) -> Option<Vec<NodeId>> {
719        None
720    }
721
722    /// Sorted-index candidates for a `[lo, hi]` range on `label.property`.
723    /// Both bounds are inclusive at this layer; the caller refilters with
724    /// the precise predicate inclusivity (`>` vs `>=`, `<` vs `<=`).
725    ///
726    /// Returns `None` when no scope exists — caller falls back to scan.
727    fn node_range_candidates(
728        &self,
729        _label: &str,
730        _property: &str,
731        _lo: Option<&PropertyValue>,
732        _hi: Option<&PropertyValue>,
733    ) -> Option<Vec<NodeId>> {
734        None
735    }
736
737    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
738    /// bounding box. The executor refilters every id with the precise
739    /// predicate, including the z-coordinate when the indexed point
740    /// is 3D.
741    fn node_point_within_bbox(
742        &self,
743        _label: &str,
744        _property: &str,
745        _ll: (f64, f64),
746        _ur: (f64, f64),
747    ) -> Option<Vec<NodeId>> {
748        None
749    }
750
751    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
752    /// candidate set is conservative — the actual great-circle /
753    /// cartesian distance check is the executor's responsibility.
754    fn node_point_within_distance(
755        &self,
756        _label: &str,
757        _property: &str,
758        _center: (f64, f64),
759        _max_distance: f64,
760    ) -> Option<Vec<NodeId>> {
761        None
762    }
763
764    /// Trigram-index candidates for relationships of `rel_type` whose
765    /// `property` value matches `query` (substring/prefix/suffix). Mirror
766    /// of [`Self::node_text_candidates`] for relationship-target indexes.
767    fn relationship_text_candidates(
768        &self,
769        _rel_type: &str,
770        _property: &str,
771        _query: &str,
772    ) -> Option<Vec<RelationshipId>> {
773        None
774    }
775
776    /// Sorted-index candidates for relationships of `rel_type` on the
777    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
778    fn relationship_range_candidates(
779        &self,
780        _rel_type: &str,
781        _property: &str,
782        _lo: Option<&PropertyValue>,
783        _hi: Option<&PropertyValue>,
784    ) -> Option<Vec<RelationshipId>> {
785        None
786    }
787
788    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
789    /// box, scoped to relationships of `rel_type`. Mirror of
790    /// [`Self::node_point_within_bbox`].
791    fn relationship_point_within_bbox(
792        &self,
793        _rel_type: &str,
794        _property: &str,
795        _ll: (f64, f64),
796        _ur: (f64, f64),
797    ) -> Option<Vec<RelationshipId>> {
798        None
799    }
800
801    /// Spatial-index candidates within `max_distance` of `(x, y)`,
802    /// scoped to relationships of `rel_type`. Mirror of
803    /// [`Self::node_point_within_distance`].
804    fn relationship_point_within_distance(
805        &self,
806        _rel_type: &str,
807        _property: &str,
808        _center: (f64, f64),
809        _max_distance: f64,
810    ) -> Option<Vec<RelationshipId>> {
811        None
812    }
813}
814
815// ============================================================================
816// GraphCatalog — narrow schema-query slice used by the analyzer.
817//
818// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
819// `GraphCatalog` without every backend having to implement a second trait.
820// ============================================================================
821
822pub trait GraphCatalog {
823    fn node_count(&self) -> usize;
824    fn relationship_count(&self) -> usize;
825    fn has_label_name(&self, label: &str) -> bool;
826    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
827    fn has_property_key(&self, key: &str) -> bool;
828}
829
830impl<T: GraphStorage> GraphCatalog for T {
831    fn node_count(&self) -> usize {
832        GraphStorage::node_count(self)
833    }
834    fn relationship_count(&self) -> usize {
835        GraphStorage::relationship_count(self)
836    }
837    fn has_label_name(&self, label: &str) -> bool {
838        GraphStorage::has_label_name(self, label)
839    }
840    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
841        GraphStorage::has_relationship_type_name(self, rel_type)
842    }
843    fn has_property_key(&self, key: &str) -> bool {
844        GraphStorage::has_property_key(self, key)
845    }
846}
847
848// ============================================================================
849// BorrowedGraphStorage — optional capability for backends that can hand out
850// long-lived borrows into internal records.
851//
852// The executor prefers `with_node` / `with_relationship` on hot paths because
853// they work for both borrow-capable and owned-only backends. This trait is
854// available for callers that really do want a `&NodeRecord` outliving the
855// closure — mostly internal optimization paths and tests.
856// ============================================================================
857
858pub trait BorrowedGraphStorage: GraphStorage {
859    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord>;
860    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord>;
861
862    fn node_refs(&self) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
863        Box::new(
864            self.all_node_ids()
865                .into_iter()
866                .filter_map(|id| self.node_ref(id)),
867        )
868    }
869
870    fn node_refs_by_label(&self, label: &str) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
871        Box::new(
872            self.node_ids_by_label(label)
873                .into_iter()
874                .filter_map(|id| self.node_ref(id)),
875        )
876    }
877
878    fn relationship_refs(&self) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
879        Box::new(
880            self.all_rel_ids()
881                .into_iter()
882                .filter_map(|id| self.relationship_ref(id)),
883        )
884    }
885
886    fn relationship_refs_by_type(
887        &self,
888        rel_type: &str,
889    ) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
890        Box::new(
891            self.rel_ids_by_type(rel_type)
892                .into_iter()
893                .filter_map(|id| self.relationship_ref(id)),
894        )
895    }
896}
897
898// ============================================================================
899// GraphStorageMut — write-side storage contract.
900//
901// A backend that implements `GraphStorage` can additionally implement
902// `GraphStorageMut` to support create / mutate / delete / admin operations.
903// Everything above the `Defaulted convenience helpers` block is a required
904// primitive; everything below is defaulted and can be overridden for perf.
905// ============================================================================
906
907pub trait GraphStorageMut: GraphStorage {
908    // ---------- Creation ----------
909
910    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord;
911
912    fn create_relationship(
913        &mut self,
914        src: NodeId,
915        dst: NodeId,
916        rel_type: &str,
917        properties: Properties,
918    ) -> Option<RelationshipRecord>;
919
920    // ---------- Node mutation ----------
921
922    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
923
924    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
925
926    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
927    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
928
929    // ---------- Relationship mutation ----------
930
931    fn set_relationship_property(
932        &mut self,
933        rel_id: RelationshipId,
934        key: String,
935        value: PropertyValue,
936    ) -> bool;
937
938    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
939
940    // ---------- Deletion ----------
941
942    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
943
944    /// Returns false if the node still has attached relationships.
945    fn delete_node(&mut self, node_id: NodeId) -> bool;
946
947    /// Deletes the node and all attached relationships.
948    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
949
950    // ---------- Admin / lifecycle ----------
951
952    /// Drop every node and every relationship, returning the store to an
953    /// empty state. Provided as a trait method so callers (bindings, admin
954    /// tools) can reset a graph without knowing the concrete backend.
955    ///
956    /// Future snapshot / WAL / restore entry points will also hang off the
957    /// `GraphStorageMut` surface — `clear` is the first of them.
958    fn clear(&mut self);
959
960    /// Register an explicitly-declared index in the catalog. Backends that
961    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
962    ///
963    /// `if_not_exists` collapses both name and schema-equivalence
964    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
965    /// surfacing them as errors.
966    #[allow(clippy::result_large_err)]
967    fn create_index(
968        &mut self,
969        _request: IndexRequest,
970        _if_not_exists: bool,
971    ) -> Result<CreateIndexOutcome, CreateIndexError> {
972        Err(CreateIndexError::Unsupported(
973            "this backend does not maintain an index catalog",
974        ))
975    }
976
977    /// Remove an explicitly-declared index from the catalog. Backends
978    /// without catalog support return [`DropIndexError::Unsupported`].
979    /// `if_exists` collapses missing-index errors into
980    /// [`DropIndexOutcome::NoOpMissing`].
981    fn drop_index(
982        &mut self,
983        _name: &str,
984        _if_exists: bool,
985    ) -> Result<DropIndexOutcome, DropIndexError> {
986        Err(DropIndexError::Unsupported(
987            "this backend does not maintain an index catalog",
988        ))
989    }
990
991    /// Register an explicitly-declared constraint. Backends without
992    /// catalog support return [`CreateConstraintError::Unsupported`].
993    /// Uniqueness/key kinds may transparently register a backing range
994    /// index of the same name.
995    fn create_constraint(
996        &mut self,
997        _request: ConstraintRequest,
998        _if_not_exists: bool,
999    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1000        Err(CreateConstraintError::Unsupported(
1001            "this backend does not maintain a constraint catalog",
1002        ))
1003    }
1004
1005    /// Drop a named constraint. Cascades to the backing index if the
1006    /// constraint owned one.
1007    fn drop_constraint(
1008        &mut self,
1009        _name: &str,
1010        _if_exists: bool,
1011    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1012        Err(DropConstraintError::Unsupported(
1013            "this backend does not maintain a constraint catalog",
1014        ))
1015    }
1016
1017    // ---------- Defaulted convenience helpers ----------
1018
1019    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1020    where
1021        Self: Sized,
1022    {
1023        if !self.contains_node(node_id) {
1024            return false;
1025        }
1026
1027        let existing_keys = match self.node_properties(node_id) {
1028            Some(props) => props.into_keys().collect::<Vec<_>>(),
1029            None => return false,
1030        };
1031
1032        for key in existing_keys {
1033            self.remove_node_property(node_id, &key);
1034        }
1035
1036        for (k, v) in properties {
1037            self.set_node_property(node_id, k, v);
1038        }
1039
1040        true
1041    }
1042
1043    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1044        if !self.contains_node(node_id) {
1045            return false;
1046        }
1047
1048        for (k, v) in properties {
1049            self.set_node_property(node_id, k, v);
1050        }
1051
1052        true
1053    }
1054
1055    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1056    where
1057        Self: Sized,
1058    {
1059        if !self.contains_node(node_id) {
1060            return false;
1061        }
1062
1063        let current = match self.node_labels(node_id) {
1064            Some(labels) => labels,
1065            None => return false,
1066        };
1067
1068        for label in &current {
1069            self.remove_node_label(node_id, label);
1070        }
1071
1072        for label in &labels {
1073            self.add_node_label(node_id, label);
1074        }
1075
1076        true
1077    }
1078
1079    fn replace_relationship_properties(
1080        &mut self,
1081        rel_id: RelationshipId,
1082        properties: Properties,
1083    ) -> bool
1084    where
1085        Self: Sized,
1086    {
1087        if !self.contains_relationship(rel_id) {
1088            return false;
1089        }
1090
1091        let existing_keys = match self.relationship_properties(rel_id) {
1092            Some(props) => props.into_keys().collect::<Vec<_>>(),
1093            None => return false,
1094        };
1095
1096        for key in existing_keys {
1097            self.remove_relationship_property(rel_id, &key);
1098        }
1099
1100        for (k, v) in properties {
1101            self.set_relationship_property(rel_id, k, v);
1102        }
1103
1104        true
1105    }
1106
1107    fn merge_relationship_properties(
1108        &mut self,
1109        rel_id: RelationshipId,
1110        properties: Properties,
1111    ) -> bool {
1112        if !self.contains_relationship(rel_id) {
1113            return false;
1114        }
1115
1116        for (k, v) in properties {
1117            self.set_relationship_property(rel_id, k, v);
1118        }
1119
1120        true
1121    }
1122
1123    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1124        let rel_ids = self.relationship_ids_of(node_id, direction);
1125
1126        let mut deleted = 0;
1127        for rel_id in rel_ids {
1128            if self.delete_relationship(rel_id) {
1129                deleted += 1;
1130            }
1131        }
1132        deleted
1133    }
1134
1135    fn get_or_create_node(
1136        &mut self,
1137        labels: Vec<String>,
1138        match_key: &str,
1139        match_value: &PropertyValue,
1140        init_properties: Properties,
1141    ) -> NodeRecord
1142    where
1143        Self: Sized,
1144    {
1145        for label in &labels {
1146            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1147            if let Some(node) = matches.into_iter().next() {
1148                return node;
1149            }
1150        }
1151
1152        self.create_node(labels, init_properties)
1153    }
1154}