Skip to main content

lora_store/
traits.rs

1//! Storage trait surface: read 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, NodeRef, Properties, PropertyValue,
19    RelRef, 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 read a
44    /// node without building a record should override [`Self::with_node`],
45    /// which is what hot paths call.
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    /// Streaming node-id scan. Appends the next ids of the scan to `out`
56    /// (every node, or those carrying `label`), in the order
57    /// [`Self::all_node_ids`] / [`Self::node_ids_by_label`] return them,
58    /// and advances `cursor`. A scan starts with `cursor == 0`; the cursor
59    /// is otherwise opaque and only valid while the store is unchanged.
60    /// Returns `false` once the scan is exhausted.
61    ///
62    /// `max` is a hint for how many ids to append per call. The default
63    /// ignores it and hands over the whole list at once; backends with
64    /// positional access should override it so a scan holds one page of
65    /// ids, not all of them.
66    fn scan_node_ids(
67        &self,
68        label: Option<&str>,
69        cursor: &mut u64,
70        max: usize,
71        out: &mut Vec<NodeId>,
72    ) -> bool {
73        let _ = max;
74        if *cursor == 0 {
75            *cursor = u64::MAX;
76            out.extend(match label {
77                Some(label) => self.node_ids_by_label(label),
78                None => self.all_node_ids(),
79            });
80        }
81        false
82    }
83
84    // ---------- Required relationship primitives ----------
85
86    fn contains_relationship(&self, id: RelationshipId) -> bool;
87
88    fn relationship(&self, id: RelationshipId) -> Option<RelationshipRecord>;
89
90    fn all_rel_ids(&self) -> Vec<RelationshipId>;
91
92    fn rel_ids_by_type(&self, rel_type: &str) -> Vec<RelationshipId>;
93
94    /// Endpoint pair `(src, dst)` for a relationship. Required because
95    /// traversal uses it on hot paths; a backend that stores endpoints
96    /// alongside the id index can answer this without fetching properties.
97    fn relationship_endpoints(&self, id: RelationshipId) -> Option<(NodeId, NodeId)>;
98
99    // ---------- Required traversal primitive ----------
100
101    /// Expand a node's incident relationships filtered by direction and
102    /// (optional) types. This is the single traversal primitive; variable-
103    /// length paths, degree, and adjacency helpers are all derived from it.
104    fn expand_ids(
105        &self,
106        node_id: NodeId,
107        direction: Direction,
108        types: &[String],
109    ) -> Vec<(RelationshipId, NodeId)>;
110
111    /// Visit expanded `(relationship_id, other_node_id)` pairs without
112    /// forcing backends to allocate an intermediate Vec. The default keeps the
113    /// trait easy to implement; hot backends can override it.
114    fn try_for_each_expand_id<F, E>(
115        &self,
116        node_id: NodeId,
117        direction: Direction,
118        types: &[String],
119        mut visit: F,
120    ) -> Result<(), E>
121    where
122        F: FnMut(RelationshipId, NodeId) -> Result<(), E>,
123        Self: Sized,
124    {
125        for (rel_id, other_id) in self.expand_ids(node_id, direction, types) {
126            visit(rel_id, other_id)?;
127        }
128        Ok(())
129    }
130
131    // ---------- Required catalog primitives ----------
132
133    fn all_labels(&self) -> Vec<String>;
134    fn all_relationship_types(&self) -> Vec<String>;
135
136    // ---------- Optional optimization hooks ----------
137    //
138    // Generic methods gated on `Self: Sized` so they don't affect object
139    // safety. A reader gets a view (`NodeRef` / `RelRef`) for the length of
140    // the closure. Backends override these to read in place; the defaults
141    // fetch an owned record through `node` / `relationship` and wrap it.
142
143    fn with_node<F, R>(&self, id: NodeId, f: F) -> Option<R>
144    where
145        F: FnOnce(NodeRef<'_>) -> R,
146        Self: Sized,
147    {
148        self.node(id).as_ref().map(|node| f(NodeRef::from(node)))
149    }
150
151    fn with_relationship<F, R>(&self, id: RelationshipId, f: F) -> Option<R>
152    where
153        F: FnOnce(RelRef<'_>) -> R,
154        Self: Sized,
155    {
156        self.relationship(id)
157            .as_ref()
158            .map(|rel| f(RelRef::from(rel)))
159    }
160
161    // ---------- Defaulted: counts / existence aliases ----------
162
163    fn has_node(&self, id: NodeId) -> bool {
164        self.contains_node(id)
165    }
166
167    fn has_relationship(&self, id: RelationshipId) -> bool {
168        self.contains_relationship(id)
169    }
170
171    fn node_count(&self) -> usize {
172        self.all_node_ids().len()
173    }
174
175    fn relationship_count(&self) -> usize {
176        self.all_rel_ids().len()
177    }
178
179    /// Number of nodes carrying `label`: exactly the rows a scan of
180    /// [`Self::node_ids_by_label`] yields. Backends with a label index
181    /// should answer this without materializing the ids.
182    fn node_count_by_label(&self, label: &str) -> usize {
183        self.node_ids_by_label(label).len()
184    }
185
186    // ---------- Defaulted: record-returning scans ----------
187    //
188    // These synthesize full-record scans from id scans + point lookups. That
189    // is correct for any backend and fast enough for small graphs, but a
190    // backend that can scan records in one pass (in-memory via a BTreeMap
191    // `.values()`, a column store via a streaming read) should override.
192
193    fn all_nodes(&self) -> Vec<NodeRecord> {
194        self.all_node_ids()
195            .into_iter()
196            .filter_map(|id| self.node(id))
197            .collect()
198    }
199
200    fn nodes_by_label(&self, label: &str) -> Vec<NodeRecord> {
201        self.node_ids_by_label(label)
202            .into_iter()
203            .filter_map(|id| self.node(id))
204            .collect()
205    }
206
207    fn all_relationships(&self) -> Vec<RelationshipRecord> {
208        self.all_rel_ids()
209            .into_iter()
210            .filter_map(|id| self.relationship(id))
211            .collect()
212    }
213
214    fn relationships_by_type(&self, rel_type: &str) -> Vec<RelationshipRecord> {
215        self.rel_ids_by_type(rel_type)
216            .into_iter()
217            .filter_map(|id| self.relationship(id))
218            .collect()
219    }
220
221    // ---------- Defaulted: traversal helpers ----------
222
223    fn relationship_ids_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipId> {
224        self.expand_ids(node_id, direction, &[])
225            .into_iter()
226            .map(|(rel_id, _)| rel_id)
227            .collect()
228    }
229
230    fn outgoing_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
231        self.relationship_ids_of(node_id, Direction::Right)
232            .into_iter()
233            .filter_map(|id| self.relationship(id))
234            .collect()
235    }
236
237    fn incoming_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
238        self.relationship_ids_of(node_id, Direction::Left)
239            .into_iter()
240            .filter_map(|id| self.relationship(id))
241            .collect()
242    }
243
244    fn relationships_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipRecord> {
245        self.relationship_ids_of(node_id, direction)
246            .into_iter()
247            .filter_map(|id| self.relationship(id))
248            .collect()
249    }
250
251    fn degree(&self, node_id: NodeId, direction: Direction) -> usize {
252        self.expand_ids(node_id, direction, &[]).len()
253    }
254
255    fn is_isolated(&self, node_id: NodeId) -> bool {
256        self.degree(node_id, Direction::Undirected) == 0
257    }
258
259    fn expand(
260        &self,
261        node_id: NodeId,
262        direction: Direction,
263        types: &[String],
264    ) -> Vec<(RelationshipRecord, NodeRecord)> {
265        self.expand_ids(node_id, direction, types)
266            .into_iter()
267            .filter_map(|(rid, nid)| {
268                let rel = self.relationship(rid)?;
269                let node = self.node(nid)?;
270                Some((rel, node))
271            })
272            .collect()
273    }
274
275    fn expand_detailed(
276        &self,
277        node_id: NodeId,
278        direction: Direction,
279        types: &[String],
280    ) -> Vec<ExpandedRelationship> {
281        self.expand(node_id, direction, types)
282            .into_iter()
283            .map(|(relationship, other_node)| ExpandedRelationship {
284                relationship,
285                other_node,
286            })
287            .collect()
288    }
289
290    fn neighbors(
291        &self,
292        node_id: NodeId,
293        direction: Direction,
294        types: &[String],
295    ) -> Vec<NodeRecord> {
296        self.expand_ids(node_id, direction, types)
297            .into_iter()
298            .filter_map(|(_, nid)| self.node(nid))
299            .collect()
300    }
301
302    // ---------- Defaulted: narrow node accessors ----------
303
304    fn node_has_label(&self, node_id: NodeId, label: &str) -> bool
305    where
306        Self: Sized,
307    {
308        self.with_node(node_id, |n| n.has_label(label))
309            .unwrap_or(false)
310    }
311
312    fn node_labels(&self, node_id: NodeId) -> Option<Vec<String>>
313    where
314        Self: Sized,
315    {
316        self.with_node(node_id, |n| n.labels().to_strings())
317    }
318
319    fn node_properties(&self, node_id: NodeId) -> Option<Properties>
320    where
321        Self: Sized,
322    {
323        self.with_node(node_id, |n| n.properties().to_owned())
324    }
325
326    fn node_property(&self, node_id: NodeId, key: &str) -> Option<PropertyValue>
327    where
328        Self: Sized,
329    {
330        self.with_node(node_id, |n| n.properties().get(key).map(|v| v.to_owned()))
331            .flatten()
332    }
333
334    // ---------- Defaulted: narrow relationship accessors ----------
335
336    fn relationship_type(&self, rel_id: RelationshipId) -> Option<String>
337    where
338        Self: Sized,
339    {
340        self.with_relationship(rel_id, |r| r.rel_type().to_string())
341    }
342
343    fn relationship_properties(&self, rel_id: RelationshipId) -> Option<Properties>
344    where
345        Self: Sized,
346    {
347        self.with_relationship(rel_id, |r| r.properties().to_owned())
348    }
349
350    fn relationship_property(&self, rel_id: RelationshipId, key: &str) -> Option<PropertyValue>
351    where
352        Self: Sized,
353    {
354        self.with_relationship(rel_id, |r| r.properties().get(key).map(|v| v.to_owned()))
355            .flatten()
356    }
357
358    fn relationship_source(&self, rel_id: RelationshipId) -> Option<NodeId> {
359        self.relationship_endpoints(rel_id).map(|(s, _)| s)
360    }
361
362    fn relationship_target(&self, rel_id: RelationshipId) -> Option<NodeId> {
363        self.relationship_endpoints(rel_id).map(|(_, d)| d)
364    }
365
366    fn other_node(&self, rel_id: RelationshipId, node_id: NodeId) -> Option<NodeId> {
367        let (src, dst) = self.relationship_endpoints(rel_id)?;
368        if src == node_id {
369            Some(dst)
370        } else if dst == node_id {
371            Some(src)
372        } else {
373            None
374        }
375    }
376
377    // ---------- Defaulted: catalog helpers ----------
378
379    fn has_label_name(&self, label: &str) -> bool {
380        self.all_labels().iter().any(|l| l == label)
381    }
382
383    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
384        self.all_relationship_types().iter().any(|t| t == rel_type)
385    }
386
387    fn all_node_property_keys(&self) -> Vec<String>
388    where
389        Self: Sized,
390    {
391        let mut keys = BTreeSet::new();
392        for id in self.all_node_ids() {
393            self.with_node(id, |n| {
394                for key in n.properties().keys() {
395                    keys.insert(key.to_string());
396                }
397            });
398        }
399        keys.into_iter().collect()
400    }
401
402    fn all_relationship_property_keys(&self) -> Vec<String>
403    where
404        Self: Sized,
405    {
406        let mut keys = BTreeSet::new();
407        for id in self.all_rel_ids() {
408            self.with_relationship(id, |r| {
409                for key in r.properties().keys() {
410                    keys.insert(key.to_string());
411                }
412            });
413        }
414        keys.into_iter().collect()
415    }
416
417    fn all_property_keys(&self) -> Vec<String>
418    where
419        Self: Sized,
420    {
421        let mut keys = BTreeSet::new();
422        for key in self.all_node_property_keys() {
423            keys.insert(key);
424        }
425        for key in self.all_relationship_property_keys() {
426            keys.insert(key);
427        }
428        keys.into_iter().collect()
429    }
430
431    fn has_property_key(&self, key: &str) -> bool
432    where
433        Self: Sized,
434    {
435        self.all_node_property_keys().iter().any(|k| k == key)
436            || self
437                .all_relationship_property_keys()
438                .iter()
439                .any(|k| k == key)
440    }
441
442    fn label_property_keys(&self, label: &str) -> Vec<String>
443    where
444        Self: Sized,
445    {
446        let mut keys = BTreeSet::new();
447        for id in self.node_ids_by_label(label) {
448            self.with_node(id, |n| {
449                for key in n.properties().keys() {
450                    keys.insert(key.to_string());
451                }
452            });
453        }
454        keys.into_iter().collect()
455    }
456
457    fn rel_type_property_keys(&self, rel_type: &str) -> Vec<String>
458    where
459        Self: Sized,
460    {
461        let mut keys = BTreeSet::new();
462        for id in self.rel_ids_by_type(rel_type) {
463            self.with_relationship(id, |r| {
464                for key in r.properties().keys() {
465                    keys.insert(key.to_string());
466                }
467            });
468        }
469        keys.into_iter().collect()
470    }
471
472    fn label_has_property_key(&self, label: &str, key: &str) -> bool
473    where
474        Self: Sized,
475    {
476        self.node_ids_by_label(label).into_iter().any(|id| {
477            self.with_node(id, |n| n.properties().contains_key(key))
478                .unwrap_or(false)
479        })
480    }
481
482    fn rel_type_has_property_key(&self, rel_type: &str, key: &str) -> bool
483    where
484        Self: Sized,
485    {
486        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
487            self.with_relationship(id, |r| r.properties().contains_key(key))
488                .unwrap_or(false)
489        })
490    }
491
492    // ---------- Defaulted: property-filter lookups ----------
493
494    fn find_nodes_by_property(
495        &self,
496        label: Option<&str>,
497        key: &str,
498        value: &PropertyValue,
499    ) -> Vec<NodeRecord>
500    where
501        Self: Sized,
502    {
503        let ids = match label {
504            Some(label) => self.node_ids_by_label(label),
505            None => self.all_node_ids(),
506        };
507
508        ids.into_iter()
509            .filter_map(|id| {
510                let matches = self
511                    .with_node(id, |n| n.properties().get(key).is_some_and(|v| v == *value))
512                    .unwrap_or(false);
513                if matches {
514                    self.node(id)
515                } else {
516                    None
517                }
518            })
519            .collect()
520    }
521
522    fn find_node_ids_by_property(
523        &self,
524        label: Option<&str>,
525        key: &str,
526        value: &PropertyValue,
527    ) -> Vec<NodeId>
528    where
529        Self: Sized,
530    {
531        self.find_nodes_by_property(label, key, value)
532            .into_iter()
533            .map(|n| n.id)
534            .collect()
535    }
536
537    fn find_relationships_by_property(
538        &self,
539        rel_type: Option<&str>,
540        key: &str,
541        value: &PropertyValue,
542    ) -> Vec<RelationshipRecord>
543    where
544        Self: Sized,
545    {
546        let ids = match rel_type {
547            Some(rel_type) => self.rel_ids_by_type(rel_type),
548            None => self.all_rel_ids(),
549        };
550
551        ids.into_iter()
552            .filter_map(|id| {
553                let matches = self
554                    .with_relationship(id, |r| r.properties().get(key).is_some_and(|v| v == *value))
555                    .unwrap_or(false);
556                if matches {
557                    self.relationship(id)
558                } else {
559                    None
560                }
561            })
562            .collect()
563    }
564
565    fn find_relationship_ids_by_property(
566        &self,
567        rel_type: Option<&str>,
568        key: &str,
569        value: &PropertyValue,
570    ) -> Vec<RelationshipId>
571    where
572        Self: Sized,
573    {
574        self.find_relationships_by_property(rel_type, key, value)
575            .into_iter()
576            .map(|r| r.id)
577            .collect()
578    }
579
580    fn node_exists_with_label_and_property(
581        &self,
582        label: &str,
583        key: &str,
584        value: &PropertyValue,
585    ) -> bool
586    where
587        Self: Sized,
588    {
589        self.node_ids_by_label(label).into_iter().any(|id| {
590            self.with_node(id, |n| n.properties().get(key).is_some_and(|v| v == *value))
591                .unwrap_or(false)
592        })
593    }
594
595    fn relationship_exists_with_type_and_property(
596        &self,
597        rel_type: &str,
598        key: &str,
599        value: &PropertyValue,
600    ) -> bool
601    where
602        Self: Sized,
603    {
604        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
605            self.with_relationship(id, |r| r.properties().get(key).is_some_and(|v| v == *value))
606                .unwrap_or(false)
607        })
608    }
609
610    // ---------- Defaulted: index catalog ----------
611    //
612    // Backends that maintain an index catalog (currently the in-memory
613    // backend) override these. Backends without catalog support keep
614    // the no-op defaults so callers can list / look up safely.
615
616    fn list_indexes(&self) -> Vec<IndexDefinition> {
617        Vec::new()
618    }
619
620    fn get_index(&self, _name: &str) -> Option<IndexDefinition> {
621        None
622    }
623
624    /// Run a FULLTEXT index query against the named index. Returns
625    /// `(entity_id, score)` pairs sorted descending by score. Backends
626    /// without fulltext support return an empty vector; the caller is
627    /// expected to have validated that the index exists via the
628    /// catalog first.
629    fn fulltext_search(&self, _name: &str, _query: &str) -> Vec<(u64, f64)> {
630        Vec::new()
631    }
632
633    /// Run a VECTOR index query against the named index. Returns
634    /// unsorted `(entity_id, score)` pairs; the caller sorts by score
635    /// descending and truncates to top-k. Backends without vector
636    /// support return an empty vector. The caller has already
637    /// validated that the index exists via the catalog and that the
638    /// query vector matches the configured dimensions.
639    ///
640    /// `k` is a hint: future ANN backends can use it to prune work;
641    /// the flat backend ignores it because exhaustive scoring is the
642    /// cheapest correctness contract.
643    ///
644    /// `restrict_to`, when `Some`, hard-filters the result set to
645    /// those entity ids. The backend may still traverse other
646    /// entities internally (HNSW uses them as routing hops), but
647    /// returned tuples are guaranteed to live in the set.
648    fn vector_search(
649        &self,
650        _name: &str,
651        _query: &LoraVector,
652        _k: usize,
653        _restrict_to: Option<&std::collections::BTreeSet<u64>>,
654    ) -> Vec<(u64, f64)> {
655        Vec::new()
656    }
657
658    /// List explicitly-declared constraints. Backends without a
659    /// constraint catalog return the empty vector.
660    fn list_constraints(&self) -> Vec<ConstraintDefinition> {
661        Vec::new()
662    }
663
664    fn get_constraint(&self, _name: &str) -> Option<ConstraintDefinition> {
665        None
666    }
667
668    /// Mutation-time pre-check: would creating a node with these
669    /// `labels` and `properties` violate any registered constraint?
670    /// Default returns `Ok(())` so backends without a constraint
671    /// catalog pay nothing. The in-memory backend overrides this and
672    /// the call is virtually free when the catalog is empty.
673    fn check_node_create_against_constraints(
674        &self,
675        _labels: &[String],
676        _properties: &Properties,
677    ) -> Result<(), String> {
678        Ok(())
679    }
680
681    /// Mutation-time pre-check for `CREATE ()-[r:TYPE { ... }]->()`.
682    fn check_relationship_create_against_constraints(
683        &self,
684        _rel_type: &str,
685        _properties: &Properties,
686    ) -> Result<(), String> {
687        Ok(())
688    }
689
690    /// [`Self::check_node_create_against_constraints`] minus existence
691    /// checks, for a statement that runs
692    /// [`Self::check_node_existence_constraints`] once it finishes (a later
693    /// `SET` may still supply the property). Defaults to the full check.
694    fn check_node_create_deferring_existence(
695        &self,
696        labels: &[String],
697        properties: &Properties,
698    ) -> Result<(), String> {
699        self.check_node_create_against_constraints(labels, properties)
700    }
701
702    /// Relationship counterpart of
703    /// [`Self::check_node_create_deferring_existence`].
704    fn check_relationship_create_deferring_existence(
705        &self,
706        rel_type: &str,
707        properties: &Properties,
708    ) -> Result<(), String> {
709        self.check_relationship_create_against_constraints(rel_type, properties)
710    }
711
712    /// Existence (and key) constraints on a node as it stands now; a
713    /// deleted node passes. Default `Ok(())`.
714    fn check_node_existence_constraints(&self, _node_id: NodeId) -> Result<(), String> {
715        Ok(())
716    }
717
718    /// Relationship counterpart of [`Self::check_node_existence_constraints`].
719    fn check_relationship_existence_constraints(
720        &self,
721        _rel_id: RelationshipId,
722    ) -> Result<(), String> {
723        Ok(())
724    }
725
726    /// Mutation-time pre-check: would setting `key = value` on this
727    /// node violate any registered constraint? Default `Ok(())`.
728    fn check_node_set_property_against_constraints(
729        &self,
730        _node_id: NodeId,
731        _key: &str,
732        _value: &PropertyValue,
733    ) -> Result<(), String> {
734        Ok(())
735    }
736
737    /// Mutation-time pre-check: would removing `key` on this node
738    /// violate an existence / key constraint? Default `Ok(())`.
739    fn check_node_remove_property_against_constraints(
740        &self,
741        _node_id: NodeId,
742        _key: &str,
743    ) -> Result<(), String> {
744        Ok(())
745    }
746
747    /// Mutation-time pre-check: would replacing all properties on this
748    /// node leave it in violation of any registered constraint? Default
749    /// `Ok(())`.
750    fn check_node_replace_properties_against_constraints(
751        &self,
752        _node_id: NodeId,
753        _properties: &Properties,
754    ) -> Result<(), String> {
755        Ok(())
756    }
757
758    /// Mutation-time pre-check: equivalent for relationship
759    /// property writes.
760    fn check_relationship_set_property_against_constraints(
761        &self,
762        _rel_id: RelationshipId,
763        _key: &str,
764        _value: &PropertyValue,
765    ) -> Result<(), String> {
766        Ok(())
767    }
768
769    fn check_relationship_remove_property_against_constraints(
770        &self,
771        _rel_id: RelationshipId,
772        _key: &str,
773    ) -> Result<(), String> {
774        Ok(())
775    }
776
777    /// Mutation-time pre-check: would replacing all properties on this
778    /// relationship leave it in violation of any registered constraint?
779    /// Default `Ok(())`.
780    fn check_relationship_replace_properties_against_constraints(
781        &self,
782        _rel_id: RelationshipId,
783        _properties: &Properties,
784    ) -> Result<(), String> {
785        Ok(())
786    }
787
788    /// Mutation-time pre-check: would adding `label` to this node
789    /// activate a constraint the node currently violates?
790    fn check_node_add_label_against_constraints(
791        &self,
792        _node_id: NodeId,
793        _label: &str,
794    ) -> Result<(), String> {
795        Ok(())
796    }
797
798    /// [`Self::check_node_replace_properties_against_constraints`] minus
799    /// existence checks, for a statement that runs
800    /// [`Self::check_node_existence_constraints`] once it finishes (`SET
801    /// n = {…}, n.required = …`). Defaults to the full check.
802    fn check_node_replace_properties_deferring_existence(
803        &self,
804        node_id: NodeId,
805        properties: &Properties,
806    ) -> Result<(), String> {
807        self.check_node_replace_properties_against_constraints(node_id, properties)
808    }
809
810    /// Relationship counterpart of
811    /// [`Self::check_node_replace_properties_deferring_existence`].
812    fn check_relationship_replace_properties_deferring_existence(
813        &self,
814        rel_id: RelationshipId,
815        properties: &Properties,
816    ) -> Result<(), String> {
817        self.check_relationship_replace_properties_against_constraints(rel_id, properties)
818    }
819
820    /// [`Self::check_node_add_label_against_constraints`] minus existence
821    /// checks, for a statement that runs
822    /// [`Self::check_node_existence_constraints`] once it finishes (`SET
823    /// n:Label, n.required = …` supplies the property after the label).
824    /// Defaults to the full check.
825    fn check_node_add_label_deferring_existence(
826        &self,
827        node_id: NodeId,
828        label: &str,
829    ) -> Result<(), String> {
830        self.check_node_add_label_against_constraints(node_id, label)
831    }
832
833    /// Cardinality snapshot used by the cost model. Backends without
834    /// per-label / per-type indexes return [`GraphStats::default()`],
835    /// which the planner treats as "no information available".
836    fn graph_stats(&self) -> GraphStats {
837        GraphStats::default()
838    }
839
840    /// Trigram-index candidates for `query` on `label.property`.
841    ///
842    /// Semantics:
843    /// * `Some(ids)` → these node ids *might* match (refilter required).
844    /// * `None` → no trigram scope for `(label, property)`; caller must
845    ///   fall back to a full scan.
846    ///
847    /// Backends without text-index support always return `None`.
848    fn node_text_candidates(
849        &self,
850        _label: &str,
851        _property: &str,
852        _query: &str,
853    ) -> Option<Vec<NodeId>> {
854        None
855    }
856
857    /// Sorted-index candidates for a `[lo, hi]` range on `label.property`.
858    /// Both bounds are inclusive at this layer; the caller refilters with
859    /// the precise predicate inclusivity (`>` vs `>=`, `<` vs `<=`).
860    ///
861    /// Returns `None` when no scope exists — caller falls back to scan.
862    fn node_range_candidates(
863        &self,
864        _label: &str,
865        _property: &str,
866        _lo: Option<&PropertyValue>,
867        _hi: Option<&PropertyValue>,
868    ) -> Option<Vec<NodeId>> {
869        None
870    }
871
872    /// Ids of `label` nodes whose `property` holds a temporal value of
873    /// another kind than `like` (a DATE when `like` is a DATETIME). A range
874    /// scan on a temporal bound emits them too, so the comparison above it
875    /// sees them as an unindexed scan would, instead of the index skipping
876    /// them without a word. `None` when no index covers
877    /// `(label, property)`.
878    fn node_range_other_temporal_kind_ids(
879        &self,
880        _label: &str,
881        _property: &str,
882        _like: &PropertyValue,
883    ) -> Option<Vec<NodeId>> {
884        None
885    }
886
887    /// Ids of `label` nodes with `property` in `[lo, hi]`, in index order
888    /// (value, then id), strictly after the `(value, id)` cursor `after`,
889    /// at most `max` of them; `descending` reverses the order. Bounds are
890    /// inclusive here; the executor refilters exact ones. `None` means no
891    /// ordered index covers `(label, property)`.
892    #[allow(clippy::too_many_arguments)]
893    fn node_range_ordered_chunk(
894        &self,
895        _label: &str,
896        _property: &str,
897        _lo: Option<&PropertyValue>,
898        _hi: Option<&PropertyValue>,
899        _descending: bool,
900        _after: Option<(&PropertyValue, NodeId)>,
901        _max: usize,
902    ) -> Option<Vec<NodeId>> {
903        None
904    }
905
906    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
907    /// bounding box. The executor refilters every id with the precise
908    /// predicate, including the z-coordinate when the indexed point
909    /// is 3D.
910    fn node_point_within_bbox(
911        &self,
912        _label: &str,
913        _property: &str,
914        _ll: (f64, f64),
915        _ur: (f64, f64),
916    ) -> Option<Vec<NodeId>> {
917        None
918    }
919
920    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
921    /// candidate set is conservative — the actual great-circle /
922    /// cartesian distance check is the executor's responsibility.
923    fn node_point_within_distance(
924        &self,
925        _label: &str,
926        _property: &str,
927        _center: (f64, f64),
928        _max_distance: f64,
929    ) -> Option<Vec<NodeId>> {
930        None
931    }
932
933    /// Trigram-index candidates for relationships of `rel_type` whose
934    /// `property` value matches `query` (substring/prefix/suffix). Mirror
935    /// of [`Self::node_text_candidates`] for relationship-target indexes.
936    fn relationship_text_candidates(
937        &self,
938        _rel_type: &str,
939        _property: &str,
940        _query: &str,
941    ) -> Option<Vec<RelationshipId>> {
942        None
943    }
944
945    /// Sorted-index candidates for relationships of `rel_type` on the
946    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
947    fn relationship_range_candidates(
948        &self,
949        _rel_type: &str,
950        _property: &str,
951        _lo: Option<&PropertyValue>,
952        _hi: Option<&PropertyValue>,
953    ) -> Option<Vec<RelationshipId>> {
954        None
955    }
956
957    /// Mirror of [`Self::node_range_other_temporal_kind_ids`] for
958    /// relationships of `rel_type`.
959    fn relationship_range_other_temporal_kind_ids(
960        &self,
961        _rel_type: &str,
962        _property: &str,
963        _like: &PropertyValue,
964    ) -> Option<Vec<RelationshipId>> {
965        None
966    }
967
968    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
969    /// box, scoped to relationships of `rel_type`. Mirror of
970    /// [`Self::node_point_within_bbox`].
971    fn relationship_point_within_bbox(
972        &self,
973        _rel_type: &str,
974        _property: &str,
975        _ll: (f64, f64),
976        _ur: (f64, f64),
977    ) -> Option<Vec<RelationshipId>> {
978        None
979    }
980
981    /// Spatial-index candidates within `max_distance` of `(x, y)`,
982    /// scoped to relationships of `rel_type`. Mirror of
983    /// [`Self::node_point_within_distance`].
984    fn relationship_point_within_distance(
985        &self,
986        _rel_type: &str,
987        _property: &str,
988        _center: (f64, f64),
989        _max_distance: f64,
990    ) -> Option<Vec<RelationshipId>> {
991        None
992    }
993}
994
995// ============================================================================
996// GraphCatalog — narrow schema-query slice used by the analyzer.
997//
998// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
999// `GraphCatalog` without every backend having to implement a second trait.
1000// ============================================================================
1001
1002pub trait GraphCatalog {
1003    fn node_count(&self) -> usize;
1004    fn relationship_count(&self) -> usize;
1005    fn has_label_name(&self, label: &str) -> bool;
1006    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
1007    fn has_property_key(&self, key: &str) -> bool;
1008}
1009
1010impl<T: GraphStorage> GraphCatalog for T {
1011    fn node_count(&self) -> usize {
1012        GraphStorage::node_count(self)
1013    }
1014    fn relationship_count(&self) -> usize {
1015        GraphStorage::relationship_count(self)
1016    }
1017    fn has_label_name(&self, label: &str) -> bool {
1018        GraphStorage::has_label_name(self, label)
1019    }
1020    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
1021        GraphStorage::has_relationship_type_name(self, rel_type)
1022    }
1023    fn has_property_key(&self, key: &str) -> bool {
1024        GraphStorage::has_property_key(self, key)
1025    }
1026}
1027
1028// ============================================================================
1029// GraphStorageMut — write-side storage contract.
1030//
1031// A backend that implements `GraphStorage` can additionally implement
1032// `GraphStorageMut` to support create / mutate / delete / admin operations.
1033// Everything above the `Defaulted convenience helpers` block is a required
1034// primitive; everything below is defaulted and can be overridden for perf.
1035// ============================================================================
1036
1037pub trait GraphStorageMut: GraphStorage {
1038    // ---------- Creation ----------
1039
1040    fn try_create_node(
1041        &mut self,
1042        labels: Vec<String>,
1043        properties: Properties,
1044    ) -> Option<NodeRecord>;
1045
1046    /// Compatibility helper for callers that historically used the
1047    /// infallible creation surface. Query and binding paths should prefer
1048    /// [`Self::try_create_node`] so allocation/id exhaustion can surface as
1049    /// an ordinary error instead of a process panic.
1050    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord
1051    where
1052        Self: Sized,
1053    {
1054        self.try_create_node(labels, properties)
1055            .unwrap_or_else(|| NodeRecord {
1056                id: NodeId::MAX,
1057                labels: Default::default(),
1058                properties: Properties::new(),
1059            })
1060    }
1061
1062    fn create_relationship(
1063        &mut self,
1064        src: NodeId,
1065        dst: NodeId,
1066        rel_type: &str,
1067        properties: Properties,
1068    ) -> Option<RelationshipRecord>;
1069
1070    // ---------- Node mutation ----------
1071
1072    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
1073
1074    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
1075
1076    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1077    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1078
1079    // ---------- Relationship mutation ----------
1080
1081    fn set_relationship_property(
1082        &mut self,
1083        rel_id: RelationshipId,
1084        key: String,
1085        value: PropertyValue,
1086    ) -> bool;
1087
1088    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
1089
1090    // ---------- Deletion ----------
1091
1092    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
1093
1094    /// Returns false if the node still has attached relationships.
1095    fn delete_node(&mut self, node_id: NodeId) -> bool;
1096
1097    /// Deletes the node and all attached relationships.
1098    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
1099
1100    // ---------- Admin / lifecycle ----------
1101
1102    /// Drop every node and every relationship, returning the store to an
1103    /// empty state. Provided as a trait method so callers (bindings, admin
1104    /// tools) can reset a graph without knowing the concrete backend.
1105    ///
1106    /// Future snapshot / WAL / restore entry points will also hang off the
1107    /// `GraphStorageMut` surface — `clear` is the first of them.
1108    fn clear(&mut self);
1109
1110    /// Register an explicitly-declared index in the catalog. Backends that
1111    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
1112    ///
1113    /// `if_not_exists` collapses both name and schema-equivalence
1114    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
1115    /// surfacing them as errors.
1116    #[allow(clippy::result_large_err)]
1117    fn create_index(
1118        &mut self,
1119        _request: IndexRequest,
1120        _if_not_exists: bool,
1121    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1122        Err(CreateIndexError::Unsupported(
1123            "this backend does not maintain an index catalog",
1124        ))
1125    }
1126
1127    /// Remove an explicitly-declared index from the catalog. Backends
1128    /// without catalog support return [`DropIndexError::Unsupported`].
1129    /// `if_exists` collapses missing-index errors into
1130    /// [`DropIndexOutcome::NoOpMissing`].
1131    fn drop_index(
1132        &mut self,
1133        _name: &str,
1134        _if_exists: bool,
1135    ) -> Result<DropIndexOutcome, DropIndexError> {
1136        Err(DropIndexError::Unsupported(
1137            "this backend does not maintain an index catalog",
1138        ))
1139    }
1140
1141    /// Register an explicitly-declared constraint. Backends without
1142    /// catalog support return [`CreateConstraintError::Unsupported`].
1143    /// Uniqueness/key kinds may transparently register a backing range
1144    /// index of the same name.
1145    fn create_constraint(
1146        &mut self,
1147        _request: ConstraintRequest,
1148        _if_not_exists: bool,
1149    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1150        Err(CreateConstraintError::Unsupported(
1151            "this backend does not maintain a constraint catalog",
1152        ))
1153    }
1154
1155    /// Drop a named constraint. Cascades to the backing index if the
1156    /// constraint owned one.
1157    fn drop_constraint(
1158        &mut self,
1159        _name: &str,
1160        _if_exists: bool,
1161    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1162        Err(DropConstraintError::Unsupported(
1163            "this backend does not maintain a constraint catalog",
1164        ))
1165    }
1166
1167    // ---------- Defaulted convenience helpers ----------
1168
1169    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1170    where
1171        Self: Sized,
1172    {
1173        if !self.contains_node(node_id) {
1174            return false;
1175        }
1176
1177        let existing_keys = match self.node_properties(node_id) {
1178            Some(props) => props.into_keys().collect::<Vec<_>>(),
1179            None => return false,
1180        };
1181
1182        for key in existing_keys {
1183            self.remove_node_property(node_id, &key);
1184        }
1185
1186        for (k, v) in properties {
1187            self.set_node_property(node_id, k.to_string(), v);
1188        }
1189
1190        true
1191    }
1192
1193    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1194        if !self.contains_node(node_id) {
1195            return false;
1196        }
1197
1198        for (k, v) in properties {
1199            self.set_node_property(node_id, k.to_string(), v);
1200        }
1201
1202        true
1203    }
1204
1205    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1206    where
1207        Self: Sized,
1208    {
1209        if !self.contains_node(node_id) {
1210            return false;
1211        }
1212
1213        let current = match self.node_labels(node_id) {
1214            Some(labels) => labels,
1215            None => return false,
1216        };
1217
1218        for label in &current {
1219            self.remove_node_label(node_id, label);
1220        }
1221
1222        for label in &labels {
1223            self.add_node_label(node_id, label);
1224        }
1225
1226        true
1227    }
1228
1229    fn replace_relationship_properties(
1230        &mut self,
1231        rel_id: RelationshipId,
1232        properties: Properties,
1233    ) -> bool
1234    where
1235        Self: Sized,
1236    {
1237        if !self.contains_relationship(rel_id) {
1238            return false;
1239        }
1240
1241        let existing_keys = match self.relationship_properties(rel_id) {
1242            Some(props) => props.into_keys().collect::<Vec<_>>(),
1243            None => return false,
1244        };
1245
1246        for key in existing_keys {
1247            self.remove_relationship_property(rel_id, &key);
1248        }
1249
1250        for (k, v) in properties {
1251            self.set_relationship_property(rel_id, k.to_string(), v);
1252        }
1253
1254        true
1255    }
1256
1257    fn merge_relationship_properties(
1258        &mut self,
1259        rel_id: RelationshipId,
1260        properties: Properties,
1261    ) -> bool {
1262        if !self.contains_relationship(rel_id) {
1263            return false;
1264        }
1265
1266        for (k, v) in properties {
1267            self.set_relationship_property(rel_id, k.to_string(), v);
1268        }
1269
1270        true
1271    }
1272
1273    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1274        let rel_ids = self.relationship_ids_of(node_id, direction);
1275
1276        let mut deleted = 0;
1277        for rel_id in rel_ids {
1278            if self.delete_relationship(rel_id) {
1279                deleted += 1;
1280            }
1281        }
1282        deleted
1283    }
1284
1285    fn get_or_create_node(
1286        &mut self,
1287        labels: Vec<String>,
1288        match_key: &str,
1289        match_value: &PropertyValue,
1290        init_properties: Properties,
1291    ) -> NodeRecord
1292    where
1293        Self: Sized,
1294    {
1295        for label in &labels {
1296            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1297            if let Some(node) = matches.into_iter().next() {
1298                return node;
1299            }
1300        }
1301
1302        self.create_node(labels, init_properties)
1303    }
1304}