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