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    /// [`GraphStats::plan_signature`] of [`Self::graph_stats`]: changes
842    /// whenever a plan compiled against the stats could come out
843    /// different. Plan caches key on it, so it must change when an
844    /// online index appears or goes, a label / type / counted key
845    /// appears or goes, or a count crosses a power of two. Backends
846    /// override it to skip building the stats.
847    fn plan_signature(&self) -> u64 {
848        self.graph_stats().plan_signature()
849    }
850
851    /// Trigram-index candidates for `query` on `label.property`.
852    ///
853    /// Semantics:
854    /// * `Some(ids)` → these node ids *might* match (refilter required).
855    /// * `None` → no trigram scope for `(label, property)`; caller must
856    ///   fall back to a full scan.
857    ///
858    /// Backends without text-index support always return `None`.
859    fn node_text_candidates(
860        &self,
861        _label: &str,
862        _property: &str,
863        _query: &str,
864    ) -> Option<Vec<NodeId>> {
865        None
866    }
867
868    /// Sorted-index candidates for a `[lo, hi]` range on `label.property`.
869    /// Both bounds are inclusive at this layer; the caller refilters with
870    /// the precise predicate inclusivity (`>` vs `>=`, `<` vs `<=`).
871    ///
872    /// Returns `None` when no scope exists — caller falls back to scan.
873    fn node_range_candidates(
874        &self,
875        _label: &str,
876        _property: &str,
877        _lo: Option<&PropertyValue>,
878        _hi: Option<&PropertyValue>,
879    ) -> Option<Vec<NodeId>> {
880        None
881    }
882
883    /// Ids of `label` nodes whose `property` holds a temporal value of
884    /// another kind than `like` (a DATE when `like` is a DATETIME). A range
885    /// scan on a temporal bound emits them too, so the comparison above it
886    /// sees them as an unindexed scan would, instead of the index skipping
887    /// them without a word. `None` when no index covers
888    /// `(label, property)`.
889    fn node_range_other_temporal_kind_ids(
890        &self,
891        _label: &str,
892        _property: &str,
893        _like: &PropertyValue,
894    ) -> Option<Vec<NodeId>> {
895        None
896    }
897
898    /// Ids of `label` nodes with `property` in `[lo, hi]`, in index order
899    /// (value, then id), strictly after the `(value, id)` cursor `after`,
900    /// at most `max` of them; `descending` reverses the order. Bounds are
901    /// inclusive here; the executor refilters exact ones. `None` means no
902    /// ordered index covers `(label, property)`.
903    #[allow(clippy::too_many_arguments)]
904    fn node_range_ordered_chunk(
905        &self,
906        _label: &str,
907        _property: &str,
908        _lo: Option<&PropertyValue>,
909        _hi: Option<&PropertyValue>,
910        _descending: bool,
911        _after: Option<(&PropertyValue, NodeId)>,
912        _max: usize,
913    ) -> Option<Vec<NodeId>> {
914        None
915    }
916
917    /// Ids of `label` nodes whose `property` is a list holding an element
918    /// equal to `element` (`element IN n.property`), from the RANGE index
919    /// on `(label, property)`, ascending. Numbers match across integer and
920    /// float (`1` finds `1.0`); an integer beyond 2^53 also finds the
921    /// integers that share its `f64` image, so the caller refilters. Empty
922    /// for a null `element`. `None` when no RANGE index covers the pair or
923    /// `element` cannot be sought (a duration, point or vector): the
924    /// caller scans.
925    fn node_list_element_candidates(
926        &self,
927        _label: &str,
928        _property: &str,
929        _element: &PropertyValue,
930    ) -> Option<Vec<NodeId>> {
931        None
932    }
933
934    /// Mirror of [`Self::node_list_element_candidates`] for relationships
935    /// of `rel_type`.
936    fn relationship_list_element_candidates(
937        &self,
938        _rel_type: &str,
939        _property: &str,
940        _element: &PropertyValue,
941    ) -> Option<Vec<RelationshipId>> {
942        None
943    }
944
945    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
946    /// bounding box. The executor refilters every id with the precise
947    /// predicate, including the z-coordinate when the indexed point
948    /// is 3D.
949    fn node_point_within_bbox(
950        &self,
951        _label: &str,
952        _property: &str,
953        _ll: (f64, f64),
954        _ur: (f64, f64),
955    ) -> Option<Vec<NodeId>> {
956        None
957    }
958
959    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
960    /// candidate set is conservative — the actual great-circle /
961    /// cartesian distance check is the executor's responsibility.
962    fn node_point_within_distance(
963        &self,
964        _label: &str,
965        _property: &str,
966        _center: (f64, f64),
967        _max_distance: f64,
968    ) -> Option<Vec<NodeId>> {
969        None
970    }
971
972    /// Trigram-index candidates for relationships of `rel_type` whose
973    /// `property` value matches `query` (substring/prefix/suffix). Mirror
974    /// of [`Self::node_text_candidates`] for relationship-target indexes.
975    fn relationship_text_candidates(
976        &self,
977        _rel_type: &str,
978        _property: &str,
979        _query: &str,
980    ) -> Option<Vec<RelationshipId>> {
981        None
982    }
983
984    /// Sorted-index candidates for relationships of `rel_type` on the
985    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
986    fn relationship_range_candidates(
987        &self,
988        _rel_type: &str,
989        _property: &str,
990        _lo: Option<&PropertyValue>,
991        _hi: Option<&PropertyValue>,
992    ) -> Option<Vec<RelationshipId>> {
993        None
994    }
995
996    /// Mirror of [`Self::node_range_other_temporal_kind_ids`] for
997    /// relationships of `rel_type`.
998    fn relationship_range_other_temporal_kind_ids(
999        &self,
1000        _rel_type: &str,
1001        _property: &str,
1002        _like: &PropertyValue,
1003    ) -> Option<Vec<RelationshipId>> {
1004        None
1005    }
1006
1007    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
1008    /// box, scoped to relationships of `rel_type`. Mirror of
1009    /// [`Self::node_point_within_bbox`].
1010    fn relationship_point_within_bbox(
1011        &self,
1012        _rel_type: &str,
1013        _property: &str,
1014        _ll: (f64, f64),
1015        _ur: (f64, f64),
1016    ) -> Option<Vec<RelationshipId>> {
1017        None
1018    }
1019
1020    /// Spatial-index candidates within `max_distance` of `(x, y)`,
1021    /// scoped to relationships of `rel_type`. Mirror of
1022    /// [`Self::node_point_within_distance`].
1023    fn relationship_point_within_distance(
1024        &self,
1025        _rel_type: &str,
1026        _property: &str,
1027        _center: (f64, f64),
1028        _max_distance: f64,
1029    ) -> Option<Vec<RelationshipId>> {
1030        None
1031    }
1032}
1033
1034// ============================================================================
1035// GraphCatalog — narrow schema-query slice used by the analyzer.
1036//
1037// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
1038// `GraphCatalog` without every backend having to implement a second trait.
1039// ============================================================================
1040
1041pub trait GraphCatalog {
1042    fn node_count(&self) -> usize;
1043    fn relationship_count(&self) -> usize;
1044    fn has_label_name(&self, label: &str) -> bool;
1045    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
1046    fn has_property_key(&self, key: &str) -> bool;
1047}
1048
1049impl<T: GraphStorage> GraphCatalog for T {
1050    fn node_count(&self) -> usize {
1051        GraphStorage::node_count(self)
1052    }
1053    fn relationship_count(&self) -> usize {
1054        GraphStorage::relationship_count(self)
1055    }
1056    fn has_label_name(&self, label: &str) -> bool {
1057        GraphStorage::has_label_name(self, label)
1058    }
1059    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
1060        GraphStorage::has_relationship_type_name(self, rel_type)
1061    }
1062    fn has_property_key(&self, key: &str) -> bool {
1063        GraphStorage::has_property_key(self, key)
1064    }
1065}
1066
1067// ============================================================================
1068// GraphStorageMut — write-side storage contract.
1069//
1070// A backend that implements `GraphStorage` can additionally implement
1071// `GraphStorageMut` to support create / mutate / delete / admin operations.
1072// Everything above the `Defaulted convenience helpers` block is a required
1073// primitive; everything below is defaulted and can be overridden for perf.
1074// ============================================================================
1075
1076pub trait GraphStorageMut: GraphStorage {
1077    // ---------- Creation ----------
1078
1079    fn try_create_node(
1080        &mut self,
1081        labels: Vec<String>,
1082        properties: Properties,
1083    ) -> Option<NodeRecord>;
1084
1085    /// Compatibility helper for callers that historically used the
1086    /// infallible creation surface. Query and binding paths should prefer
1087    /// [`Self::try_create_node`] so allocation/id exhaustion can surface as
1088    /// an ordinary error instead of a process panic.
1089    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord
1090    where
1091        Self: Sized,
1092    {
1093        self.try_create_node(labels, properties)
1094            .unwrap_or_else(|| NodeRecord {
1095                id: NodeId::MAX,
1096                labels: Default::default(),
1097                properties: Properties::new(),
1098            })
1099    }
1100
1101    fn create_relationship(
1102        &mut self,
1103        src: NodeId,
1104        dst: NodeId,
1105        rel_type: &str,
1106        properties: Properties,
1107    ) -> Option<RelationshipRecord>;
1108
1109    // ---------- Node mutation ----------
1110
1111    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
1112
1113    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
1114
1115    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1116    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1117
1118    // ---------- Relationship mutation ----------
1119
1120    fn set_relationship_property(
1121        &mut self,
1122        rel_id: RelationshipId,
1123        key: String,
1124        value: PropertyValue,
1125    ) -> bool;
1126
1127    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
1128
1129    // ---------- Deletion ----------
1130
1131    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
1132
1133    /// Returns false if the node still has attached relationships.
1134    fn delete_node(&mut self, node_id: NodeId) -> bool;
1135
1136    /// Deletes the node and all attached relationships.
1137    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
1138
1139    // ---------- Admin / lifecycle ----------
1140
1141    /// Drop every node and every relationship, returning the store to an
1142    /// empty state. Provided as a trait method so callers (bindings, admin
1143    /// tools) can reset a graph without knowing the concrete backend.
1144    ///
1145    /// Future snapshot / WAL / restore entry points will also hang off the
1146    /// `GraphStorageMut` surface — `clear` is the first of them.
1147    fn clear(&mut self);
1148
1149    /// Register an explicitly-declared index in the catalog. Backends that
1150    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
1151    ///
1152    /// `if_not_exists` collapses both name and schema-equivalence
1153    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
1154    /// surfacing them as errors.
1155    #[allow(clippy::result_large_err)]
1156    fn create_index(
1157        &mut self,
1158        _request: IndexRequest,
1159        _if_not_exists: bool,
1160    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1161        Err(CreateIndexError::Unsupported(
1162            "this backend does not maintain an index catalog",
1163        ))
1164    }
1165
1166    /// Remove an explicitly-declared index from the catalog. Backends
1167    /// without catalog support return [`DropIndexError::Unsupported`].
1168    /// `if_exists` collapses missing-index errors into
1169    /// [`DropIndexOutcome::NoOpMissing`].
1170    fn drop_index(
1171        &mut self,
1172        _name: &str,
1173        _if_exists: bool,
1174    ) -> Result<DropIndexOutcome, DropIndexError> {
1175        Err(DropIndexError::Unsupported(
1176            "this backend does not maintain an index catalog",
1177        ))
1178    }
1179
1180    /// Register an explicitly-declared constraint. Backends without
1181    /// catalog support return [`CreateConstraintError::Unsupported`].
1182    /// Uniqueness/key kinds may transparently register a backing range
1183    /// index of the same name.
1184    fn create_constraint(
1185        &mut self,
1186        _request: ConstraintRequest,
1187        _if_not_exists: bool,
1188    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1189        Err(CreateConstraintError::Unsupported(
1190            "this backend does not maintain a constraint catalog",
1191        ))
1192    }
1193
1194    /// Drop a named constraint. Cascades to the backing index if the
1195    /// constraint owned one.
1196    fn drop_constraint(
1197        &mut self,
1198        _name: &str,
1199        _if_exists: bool,
1200    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1201        Err(DropConstraintError::Unsupported(
1202            "this backend does not maintain a constraint catalog",
1203        ))
1204    }
1205
1206    // ---------- Defaulted convenience helpers ----------
1207
1208    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1209    where
1210        Self: Sized,
1211    {
1212        if !self.contains_node(node_id) {
1213            return false;
1214        }
1215
1216        let existing_keys = match self.node_properties(node_id) {
1217            Some(props) => props.into_keys().collect::<Vec<_>>(),
1218            None => return false,
1219        };
1220
1221        for key in existing_keys {
1222            self.remove_node_property(node_id, &key);
1223        }
1224
1225        for (k, v) in properties {
1226            self.set_node_property(node_id, k.to_string(), v);
1227        }
1228
1229        true
1230    }
1231
1232    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1233        if !self.contains_node(node_id) {
1234            return false;
1235        }
1236
1237        for (k, v) in properties {
1238            self.set_node_property(node_id, k.to_string(), v);
1239        }
1240
1241        true
1242    }
1243
1244    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1245    where
1246        Self: Sized,
1247    {
1248        if !self.contains_node(node_id) {
1249            return false;
1250        }
1251
1252        let current = match self.node_labels(node_id) {
1253            Some(labels) => labels,
1254            None => return false,
1255        };
1256
1257        for label in &current {
1258            self.remove_node_label(node_id, label);
1259        }
1260
1261        for label in &labels {
1262            self.add_node_label(node_id, label);
1263        }
1264
1265        true
1266    }
1267
1268    fn replace_relationship_properties(
1269        &mut self,
1270        rel_id: RelationshipId,
1271        properties: Properties,
1272    ) -> bool
1273    where
1274        Self: Sized,
1275    {
1276        if !self.contains_relationship(rel_id) {
1277            return false;
1278        }
1279
1280        let existing_keys = match self.relationship_properties(rel_id) {
1281            Some(props) => props.into_keys().collect::<Vec<_>>(),
1282            None => return false,
1283        };
1284
1285        for key in existing_keys {
1286            self.remove_relationship_property(rel_id, &key);
1287        }
1288
1289        for (k, v) in properties {
1290            self.set_relationship_property(rel_id, k.to_string(), v);
1291        }
1292
1293        true
1294    }
1295
1296    fn merge_relationship_properties(
1297        &mut self,
1298        rel_id: RelationshipId,
1299        properties: Properties,
1300    ) -> bool {
1301        if !self.contains_relationship(rel_id) {
1302            return false;
1303        }
1304
1305        for (k, v) in properties {
1306            self.set_relationship_property(rel_id, k.to_string(), v);
1307        }
1308
1309        true
1310    }
1311
1312    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1313        let rel_ids = self.relationship_ids_of(node_id, direction);
1314        self.delete_relationships(&rel_ids)
1315    }
1316
1317    /// Delete each relationship in `rel_ids` that exists, in order, and
1318    /// return how many went. Same effect and the same mutation events as
1319    /// [`Self::delete_relationship`] on each id; a backend may batch the
1320    /// work (the in-memory graph edits each adjacency list once).
1321    fn delete_relationships(&mut self, rel_ids: &[RelationshipId]) -> usize {
1322        rel_ids
1323            .iter()
1324            .filter(|&&rel_id| self.delete_relationship(rel_id))
1325            .count()
1326    }
1327
1328    /// [`Self::detach_delete_node`] each node in `node_ids`, in order, and
1329    /// return how many were deleted. The mutation events are the same as
1330    /// those calls emit; a backend may batch the work.
1331    fn detach_delete_nodes(&mut self, node_ids: &[NodeId]) -> usize {
1332        node_ids
1333            .iter()
1334            .filter(|&&node_id| self.detach_delete_node(node_id))
1335            .count()
1336    }
1337
1338    fn get_or_create_node(
1339        &mut self,
1340        labels: Vec<String>,
1341        match_key: &str,
1342        match_value: &PropertyValue,
1343        init_properties: Properties,
1344    ) -> NodeRecord
1345    where
1346        Self: Sized,
1347    {
1348        for label in &labels {
1349            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1350            if let Some(node) = matches.into_iter().next() {
1351                return node;
1352            }
1353        }
1354
1355        self.create_node(labels, init_properties)
1356    }
1357}