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    // ---------- 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.to_string());
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.to_string());
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.to_string());
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.to_string());
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    /// Run a VECTOR index query against the named index. Returns
595    /// unsorted `(entity_id, score)` pairs; the caller sorts by score
596    /// descending and truncates to top-k. Backends without vector
597    /// support return an empty vector. The caller has already
598    /// validated that the index exists via the catalog and that the
599    /// query vector matches the configured dimensions.
600    ///
601    /// `k` is a hint: future ANN backends can use it to prune work;
602    /// the flat backend ignores it because exhaustive scoring is the
603    /// cheapest correctness contract.
604    ///
605    /// `restrict_to`, when `Some`, hard-filters the result set to
606    /// those entity ids. The backend may still traverse other
607    /// entities internally (HNSW uses them as routing hops), but
608    /// returned tuples are guaranteed to live in the set.
609    fn vector_search(
610        &self,
611        _name: &str,
612        _query: &LoraVector,
613        _k: usize,
614        _restrict_to: Option<&std::collections::BTreeSet<u64>>,
615    ) -> Vec<(u64, f64)> {
616        Vec::new()
617    }
618
619    /// List explicitly-declared constraints. Backends without a
620    /// constraint catalog return the empty vector.
621    fn list_constraints(&self) -> Vec<ConstraintDefinition> {
622        Vec::new()
623    }
624
625    fn get_constraint(&self, _name: &str) -> Option<ConstraintDefinition> {
626        None
627    }
628
629    /// Mutation-time pre-check: would creating a node with these
630    /// `labels` and `properties` violate any registered constraint?
631    /// Default returns `Ok(())` so backends without a constraint
632    /// catalog pay nothing. The in-memory backend overrides this and
633    /// the call is virtually free when the catalog is empty.
634    fn check_node_create_against_constraints(
635        &self,
636        _labels: &[String],
637        _properties: &Properties,
638    ) -> Result<(), String> {
639        Ok(())
640    }
641
642    /// Mutation-time pre-check for `CREATE ()-[r:TYPE { ... }]->()`.
643    fn check_relationship_create_against_constraints(
644        &self,
645        _rel_type: &str,
646        _properties: &Properties,
647    ) -> Result<(), String> {
648        Ok(())
649    }
650
651    /// Mutation-time pre-check: would setting `key = value` on this
652    /// node violate any registered constraint? Default `Ok(())`.
653    fn check_node_set_property_against_constraints(
654        &self,
655        _node_id: NodeId,
656        _key: &str,
657        _value: &PropertyValue,
658    ) -> Result<(), String> {
659        Ok(())
660    }
661
662    /// Mutation-time pre-check: would removing `key` on this node
663    /// violate an existence / key constraint? Default `Ok(())`.
664    fn check_node_remove_property_against_constraints(
665        &self,
666        _node_id: NodeId,
667        _key: &str,
668    ) -> Result<(), String> {
669        Ok(())
670    }
671
672    /// Mutation-time pre-check: would replacing all properties on this
673    /// node leave it in violation of any registered constraint? Default
674    /// `Ok(())`.
675    fn check_node_replace_properties_against_constraints(
676        &self,
677        _node_id: NodeId,
678        _properties: &Properties,
679    ) -> Result<(), String> {
680        Ok(())
681    }
682
683    /// Mutation-time pre-check: equivalent for relationship
684    /// property writes.
685    fn check_relationship_set_property_against_constraints(
686        &self,
687        _rel_id: RelationshipId,
688        _key: &str,
689        _value: &PropertyValue,
690    ) -> Result<(), String> {
691        Ok(())
692    }
693
694    fn check_relationship_remove_property_against_constraints(
695        &self,
696        _rel_id: RelationshipId,
697        _key: &str,
698    ) -> Result<(), String> {
699        Ok(())
700    }
701
702    /// Mutation-time pre-check: would replacing all properties on this
703    /// relationship leave it in violation of any registered constraint?
704    /// Default `Ok(())`.
705    fn check_relationship_replace_properties_against_constraints(
706        &self,
707        _rel_id: RelationshipId,
708        _properties: &Properties,
709    ) -> Result<(), String> {
710        Ok(())
711    }
712
713    /// Mutation-time pre-check: would adding `label` to this node
714    /// activate a constraint the node currently violates?
715    fn check_node_add_label_against_constraints(
716        &self,
717        _node_id: NodeId,
718        _label: &str,
719    ) -> Result<(), String> {
720        Ok(())
721    }
722
723    /// Cardinality snapshot used by the cost model. Backends without
724    /// per-label / per-type indexes return [`GraphStats::default()`],
725    /// which the planner treats as "no information available".
726    fn graph_stats(&self) -> GraphStats {
727        GraphStats::default()
728    }
729
730    /// Trigram-index candidates for `query` on `label.property`.
731    ///
732    /// Semantics:
733    /// * `Some(ids)` → these node ids *might* match (refilter required).
734    /// * `None` → no trigram scope for `(label, property)`; caller must
735    ///   fall back to a full scan.
736    ///
737    /// Backends without text-index support always return `None`.
738    fn node_text_candidates(
739        &self,
740        _label: &str,
741        _property: &str,
742        _query: &str,
743    ) -> Option<Vec<NodeId>> {
744        None
745    }
746
747    /// Sorted-index candidates for a `[lo, hi]` range on `label.property`.
748    /// Both bounds are inclusive at this layer; the caller refilters with
749    /// the precise predicate inclusivity (`>` vs `>=`, `<` vs `<=`).
750    ///
751    /// Returns `None` when no scope exists — caller falls back to scan.
752    fn node_range_candidates(
753        &self,
754        _label: &str,
755        _property: &str,
756        _lo: Option<&PropertyValue>,
757        _hi: Option<&PropertyValue>,
758    ) -> Option<Vec<NodeId>> {
759        None
760    }
761
762    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
763    /// bounding box. The executor refilters every id with the precise
764    /// predicate, including the z-coordinate when the indexed point
765    /// is 3D.
766    fn node_point_within_bbox(
767        &self,
768        _label: &str,
769        _property: &str,
770        _ll: (f64, f64),
771        _ur: (f64, f64),
772    ) -> Option<Vec<NodeId>> {
773        None
774    }
775
776    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
777    /// candidate set is conservative — the actual great-circle /
778    /// cartesian distance check is the executor's responsibility.
779    fn node_point_within_distance(
780        &self,
781        _label: &str,
782        _property: &str,
783        _center: (f64, f64),
784        _max_distance: f64,
785    ) -> Option<Vec<NodeId>> {
786        None
787    }
788
789    /// Trigram-index candidates for relationships of `rel_type` whose
790    /// `property` value matches `query` (substring/prefix/suffix). Mirror
791    /// of [`Self::node_text_candidates`] for relationship-target indexes.
792    fn relationship_text_candidates(
793        &self,
794        _rel_type: &str,
795        _property: &str,
796        _query: &str,
797    ) -> Option<Vec<RelationshipId>> {
798        None
799    }
800
801    /// Sorted-index candidates for relationships of `rel_type` on the
802    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
803    fn relationship_range_candidates(
804        &self,
805        _rel_type: &str,
806        _property: &str,
807        _lo: Option<&PropertyValue>,
808        _hi: Option<&PropertyValue>,
809    ) -> Option<Vec<RelationshipId>> {
810        None
811    }
812
813    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
814    /// box, scoped to relationships of `rel_type`. Mirror of
815    /// [`Self::node_point_within_bbox`].
816    fn relationship_point_within_bbox(
817        &self,
818        _rel_type: &str,
819        _property: &str,
820        _ll: (f64, f64),
821        _ur: (f64, f64),
822    ) -> Option<Vec<RelationshipId>> {
823        None
824    }
825
826    /// Spatial-index candidates within `max_distance` of `(x, y)`,
827    /// scoped to relationships of `rel_type`. Mirror of
828    /// [`Self::node_point_within_distance`].
829    fn relationship_point_within_distance(
830        &self,
831        _rel_type: &str,
832        _property: &str,
833        _center: (f64, f64),
834        _max_distance: f64,
835    ) -> Option<Vec<RelationshipId>> {
836        None
837    }
838}
839
840// ============================================================================
841// GraphCatalog — narrow schema-query slice used by the analyzer.
842//
843// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
844// `GraphCatalog` without every backend having to implement a second trait.
845// ============================================================================
846
847pub trait GraphCatalog {
848    fn node_count(&self) -> usize;
849    fn relationship_count(&self) -> usize;
850    fn has_label_name(&self, label: &str) -> bool;
851    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
852    fn has_property_key(&self, key: &str) -> bool;
853}
854
855impl<T: GraphStorage> GraphCatalog for T {
856    fn node_count(&self) -> usize {
857        GraphStorage::node_count(self)
858    }
859    fn relationship_count(&self) -> usize {
860        GraphStorage::relationship_count(self)
861    }
862    fn has_label_name(&self, label: &str) -> bool {
863        GraphStorage::has_label_name(self, label)
864    }
865    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
866        GraphStorage::has_relationship_type_name(self, rel_type)
867    }
868    fn has_property_key(&self, key: &str) -> bool {
869        GraphStorage::has_property_key(self, key)
870    }
871}
872
873// ============================================================================
874// BorrowedGraphStorage — optional capability for backends that can hand out
875// long-lived borrows into internal records.
876//
877// The executor prefers `with_node` / `with_relationship` on hot paths because
878// they work for both borrow-capable and owned-only backends. This trait is
879// available for callers that really do want a `&NodeRecord` outliving the
880// closure — mostly internal optimization paths and tests.
881// ============================================================================
882
883pub trait BorrowedGraphStorage: GraphStorage {
884    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord>;
885    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord>;
886
887    fn node_refs(&self) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
888        Box::new(
889            self.all_node_ids()
890                .into_iter()
891                .filter_map(|id| self.node_ref(id)),
892        )
893    }
894
895    fn node_refs_by_label(&self, label: &str) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
896        Box::new(
897            self.node_ids_by_label(label)
898                .into_iter()
899                .filter_map(|id| self.node_ref(id)),
900        )
901    }
902
903    fn relationship_refs(&self) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
904        Box::new(
905            self.all_rel_ids()
906                .into_iter()
907                .filter_map(|id| self.relationship_ref(id)),
908        )
909    }
910
911    fn relationship_refs_by_type(
912        &self,
913        rel_type: &str,
914    ) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
915        Box::new(
916            self.rel_ids_by_type(rel_type)
917                .into_iter()
918                .filter_map(|id| self.relationship_ref(id)),
919        )
920    }
921}
922
923// ============================================================================
924// GraphStorageMut — write-side storage contract.
925//
926// A backend that implements `GraphStorage` can additionally implement
927// `GraphStorageMut` to support create / mutate / delete / admin operations.
928// Everything above the `Defaulted convenience helpers` block is a required
929// primitive; everything below is defaulted and can be overridden for perf.
930// ============================================================================
931
932pub trait GraphStorageMut: GraphStorage {
933    // ---------- Creation ----------
934
935    fn try_create_node(
936        &mut self,
937        labels: Vec<String>,
938        properties: Properties,
939    ) -> Option<NodeRecord>;
940
941    /// Compatibility helper for callers that historically used the
942    /// infallible creation surface. Query and binding paths should prefer
943    /// [`Self::try_create_node`] so allocation/id exhaustion can surface as
944    /// an ordinary error instead of a process panic.
945    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord
946    where
947        Self: Sized,
948    {
949        self.try_create_node(labels, properties)
950            .unwrap_or_else(|| NodeRecord {
951                id: NodeId::MAX,
952                labels: Vec::new(),
953                properties: Properties::new(),
954            })
955    }
956
957    fn create_relationship(
958        &mut self,
959        src: NodeId,
960        dst: NodeId,
961        rel_type: &str,
962        properties: Properties,
963    ) -> Option<RelationshipRecord>;
964
965    // ---------- Node mutation ----------
966
967    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
968
969    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
970
971    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
972    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
973
974    // ---------- Relationship mutation ----------
975
976    fn set_relationship_property(
977        &mut self,
978        rel_id: RelationshipId,
979        key: String,
980        value: PropertyValue,
981    ) -> bool;
982
983    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
984
985    // ---------- Deletion ----------
986
987    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
988
989    /// Returns false if the node still has attached relationships.
990    fn delete_node(&mut self, node_id: NodeId) -> bool;
991
992    /// Deletes the node and all attached relationships.
993    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
994
995    // ---------- Admin / lifecycle ----------
996
997    /// Drop every node and every relationship, returning the store to an
998    /// empty state. Provided as a trait method so callers (bindings, admin
999    /// tools) can reset a graph without knowing the concrete backend.
1000    ///
1001    /// Future snapshot / WAL / restore entry points will also hang off the
1002    /// `GraphStorageMut` surface — `clear` is the first of them.
1003    fn clear(&mut self);
1004
1005    /// Register an explicitly-declared index in the catalog. Backends that
1006    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
1007    ///
1008    /// `if_not_exists` collapses both name and schema-equivalence
1009    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
1010    /// surfacing them as errors.
1011    #[allow(clippy::result_large_err)]
1012    fn create_index(
1013        &mut self,
1014        _request: IndexRequest,
1015        _if_not_exists: bool,
1016    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1017        Err(CreateIndexError::Unsupported(
1018            "this backend does not maintain an index catalog",
1019        ))
1020    }
1021
1022    /// Remove an explicitly-declared index from the catalog. Backends
1023    /// without catalog support return [`DropIndexError::Unsupported`].
1024    /// `if_exists` collapses missing-index errors into
1025    /// [`DropIndexOutcome::NoOpMissing`].
1026    fn drop_index(
1027        &mut self,
1028        _name: &str,
1029        _if_exists: bool,
1030    ) -> Result<DropIndexOutcome, DropIndexError> {
1031        Err(DropIndexError::Unsupported(
1032            "this backend does not maintain an index catalog",
1033        ))
1034    }
1035
1036    /// Register an explicitly-declared constraint. Backends without
1037    /// catalog support return [`CreateConstraintError::Unsupported`].
1038    /// Uniqueness/key kinds may transparently register a backing range
1039    /// index of the same name.
1040    fn create_constraint(
1041        &mut self,
1042        _request: ConstraintRequest,
1043        _if_not_exists: bool,
1044    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1045        Err(CreateConstraintError::Unsupported(
1046            "this backend does not maintain a constraint catalog",
1047        ))
1048    }
1049
1050    /// Drop a named constraint. Cascades to the backing index if the
1051    /// constraint owned one.
1052    fn drop_constraint(
1053        &mut self,
1054        _name: &str,
1055        _if_exists: bool,
1056    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1057        Err(DropConstraintError::Unsupported(
1058            "this backend does not maintain a constraint catalog",
1059        ))
1060    }
1061
1062    // ---------- Defaulted convenience helpers ----------
1063
1064    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1065    where
1066        Self: Sized,
1067    {
1068        if !self.contains_node(node_id) {
1069            return false;
1070        }
1071
1072        let existing_keys = match self.node_properties(node_id) {
1073            Some(props) => props.into_keys().collect::<Vec<_>>(),
1074            None => return false,
1075        };
1076
1077        for key in existing_keys {
1078            self.remove_node_property(node_id, &key);
1079        }
1080
1081        for (k, v) in properties {
1082            self.set_node_property(node_id, k.to_string(), v);
1083        }
1084
1085        true
1086    }
1087
1088    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1089        if !self.contains_node(node_id) {
1090            return false;
1091        }
1092
1093        for (k, v) in properties {
1094            self.set_node_property(node_id, k.to_string(), v);
1095        }
1096
1097        true
1098    }
1099
1100    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1101    where
1102        Self: Sized,
1103    {
1104        if !self.contains_node(node_id) {
1105            return false;
1106        }
1107
1108        let current = match self.node_labels(node_id) {
1109            Some(labels) => labels,
1110            None => return false,
1111        };
1112
1113        for label in &current {
1114            self.remove_node_label(node_id, label);
1115        }
1116
1117        for label in &labels {
1118            self.add_node_label(node_id, label);
1119        }
1120
1121        true
1122    }
1123
1124    fn replace_relationship_properties(
1125        &mut self,
1126        rel_id: RelationshipId,
1127        properties: Properties,
1128    ) -> bool
1129    where
1130        Self: Sized,
1131    {
1132        if !self.contains_relationship(rel_id) {
1133            return false;
1134        }
1135
1136        let existing_keys = match self.relationship_properties(rel_id) {
1137            Some(props) => props.into_keys().collect::<Vec<_>>(),
1138            None => return false,
1139        };
1140
1141        for key in existing_keys {
1142            self.remove_relationship_property(rel_id, &key);
1143        }
1144
1145        for (k, v) in properties {
1146            self.set_relationship_property(rel_id, k.to_string(), v);
1147        }
1148
1149        true
1150    }
1151
1152    fn merge_relationship_properties(
1153        &mut self,
1154        rel_id: RelationshipId,
1155        properties: Properties,
1156    ) -> bool {
1157        if !self.contains_relationship(rel_id) {
1158            return false;
1159        }
1160
1161        for (k, v) in properties {
1162            self.set_relationship_property(rel_id, k.to_string(), v);
1163        }
1164
1165        true
1166    }
1167
1168    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1169        let rel_ids = self.relationship_ids_of(node_id, direction);
1170
1171        let mut deleted = 0;
1172        for rel_id in rel_ids {
1173            if self.delete_relationship(rel_id) {
1174                deleted += 1;
1175            }
1176        }
1177        deleted
1178    }
1179
1180    fn get_or_create_node(
1181        &mut self,
1182        labels: Vec<String>,
1183        match_key: &str,
1184        match_value: &PropertyValue,
1185        init_properties: Properties,
1186    ) -> NodeRecord
1187    where
1188        Self: Sized,
1189    {
1190        for label in &labels {
1191            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1192            if let Some(node) = matches.into_iter().next() {
1193                return node;
1194            }
1195        }
1196
1197        self.create_node(labels, init_properties)
1198    }
1199}