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    /// Spatial-index candidates inside the closed `[ll, ur]` 2D
918    /// bounding box. The executor refilters every id with the precise
919    /// predicate, including the z-coordinate when the indexed point
920    /// is 3D.
921    fn node_point_within_bbox(
922        &self,
923        _label: &str,
924        _property: &str,
925        _ll: (f64, f64),
926        _ur: (f64, f64),
927    ) -> Option<Vec<NodeId>> {
928        None
929    }
930
931    /// Spatial-index candidates within `max_distance` of `(x, y)`. The
932    /// candidate set is conservative — the actual great-circle /
933    /// cartesian distance check is the executor's responsibility.
934    fn node_point_within_distance(
935        &self,
936        _label: &str,
937        _property: &str,
938        _center: (f64, f64),
939        _max_distance: f64,
940    ) -> Option<Vec<NodeId>> {
941        None
942    }
943
944    /// Trigram-index candidates for relationships of `rel_type` whose
945    /// `property` value matches `query` (substring/prefix/suffix). Mirror
946    /// of [`Self::node_text_candidates`] for relationship-target indexes.
947    fn relationship_text_candidates(
948        &self,
949        _rel_type: &str,
950        _property: &str,
951        _query: &str,
952    ) -> Option<Vec<RelationshipId>> {
953        None
954    }
955
956    /// Sorted-index candidates for relationships of `rel_type` on the
957    /// closed `[lo, hi]` range. Mirror of [`Self::node_range_candidates`].
958    fn relationship_range_candidates(
959        &self,
960        _rel_type: &str,
961        _property: &str,
962        _lo: Option<&PropertyValue>,
963        _hi: Option<&PropertyValue>,
964    ) -> Option<Vec<RelationshipId>> {
965        None
966    }
967
968    /// Mirror of [`Self::node_range_other_temporal_kind_ids`] for
969    /// relationships of `rel_type`.
970    fn relationship_range_other_temporal_kind_ids(
971        &self,
972        _rel_type: &str,
973        _property: &str,
974        _like: &PropertyValue,
975    ) -> Option<Vec<RelationshipId>> {
976        None
977    }
978
979    /// Spatial-index candidates inside the closed `[ll, ur]` 2D bounding
980    /// box, scoped to relationships of `rel_type`. Mirror of
981    /// [`Self::node_point_within_bbox`].
982    fn relationship_point_within_bbox(
983        &self,
984        _rel_type: &str,
985        _property: &str,
986        _ll: (f64, f64),
987        _ur: (f64, f64),
988    ) -> Option<Vec<RelationshipId>> {
989        None
990    }
991
992    /// Spatial-index candidates within `max_distance` of `(x, y)`,
993    /// scoped to relationships of `rel_type`. Mirror of
994    /// [`Self::node_point_within_distance`].
995    fn relationship_point_within_distance(
996        &self,
997        _rel_type: &str,
998        _property: &str,
999        _center: (f64, f64),
1000        _max_distance: f64,
1001    ) -> Option<Vec<RelationshipId>> {
1002        None
1003    }
1004}
1005
1006// ============================================================================
1007// GraphCatalog — narrow schema-query slice used by the analyzer.
1008//
1009// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
1010// `GraphCatalog` without every backend having to implement a second trait.
1011// ============================================================================
1012
1013pub trait GraphCatalog {
1014    fn node_count(&self) -> usize;
1015    fn relationship_count(&self) -> usize;
1016    fn has_label_name(&self, label: &str) -> bool;
1017    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
1018    fn has_property_key(&self, key: &str) -> bool;
1019}
1020
1021impl<T: GraphStorage> GraphCatalog for T {
1022    fn node_count(&self) -> usize {
1023        GraphStorage::node_count(self)
1024    }
1025    fn relationship_count(&self) -> usize {
1026        GraphStorage::relationship_count(self)
1027    }
1028    fn has_label_name(&self, label: &str) -> bool {
1029        GraphStorage::has_label_name(self, label)
1030    }
1031    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
1032        GraphStorage::has_relationship_type_name(self, rel_type)
1033    }
1034    fn has_property_key(&self, key: &str) -> bool {
1035        GraphStorage::has_property_key(self, key)
1036    }
1037}
1038
1039// ============================================================================
1040// GraphStorageMut — write-side storage contract.
1041//
1042// A backend that implements `GraphStorage` can additionally implement
1043// `GraphStorageMut` to support create / mutate / delete / admin operations.
1044// Everything above the `Defaulted convenience helpers` block is a required
1045// primitive; everything below is defaulted and can be overridden for perf.
1046// ============================================================================
1047
1048pub trait GraphStorageMut: GraphStorage {
1049    // ---------- Creation ----------
1050
1051    fn try_create_node(
1052        &mut self,
1053        labels: Vec<String>,
1054        properties: Properties,
1055    ) -> Option<NodeRecord>;
1056
1057    /// Compatibility helper for callers that historically used the
1058    /// infallible creation surface. Query and binding paths should prefer
1059    /// [`Self::try_create_node`] so allocation/id exhaustion can surface as
1060    /// an ordinary error instead of a process panic.
1061    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord
1062    where
1063        Self: Sized,
1064    {
1065        self.try_create_node(labels, properties)
1066            .unwrap_or_else(|| NodeRecord {
1067                id: NodeId::MAX,
1068                labels: Default::default(),
1069                properties: Properties::new(),
1070            })
1071    }
1072
1073    fn create_relationship(
1074        &mut self,
1075        src: NodeId,
1076        dst: NodeId,
1077        rel_type: &str,
1078        properties: Properties,
1079    ) -> Option<RelationshipRecord>;
1080
1081    // ---------- Node mutation ----------
1082
1083    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
1084
1085    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
1086
1087    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1088    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
1089
1090    // ---------- Relationship mutation ----------
1091
1092    fn set_relationship_property(
1093        &mut self,
1094        rel_id: RelationshipId,
1095        key: String,
1096        value: PropertyValue,
1097    ) -> bool;
1098
1099    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
1100
1101    // ---------- Deletion ----------
1102
1103    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
1104
1105    /// Returns false if the node still has attached relationships.
1106    fn delete_node(&mut self, node_id: NodeId) -> bool;
1107
1108    /// Deletes the node and all attached relationships.
1109    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
1110
1111    // ---------- Admin / lifecycle ----------
1112
1113    /// Drop every node and every relationship, returning the store to an
1114    /// empty state. Provided as a trait method so callers (bindings, admin
1115    /// tools) can reset a graph without knowing the concrete backend.
1116    ///
1117    /// Future snapshot / WAL / restore entry points will also hang off the
1118    /// `GraphStorageMut` surface — `clear` is the first of them.
1119    fn clear(&mut self);
1120
1121    /// Register an explicitly-declared index in the catalog. Backends that
1122    /// don't maintain a catalog return [`CreateIndexError::Unsupported`].
1123    ///
1124    /// `if_not_exists` collapses both name and schema-equivalence
1125    /// conflicts into [`CreateIndexOutcome::NoOpExists`] instead of
1126    /// surfacing them as errors.
1127    #[allow(clippy::result_large_err)]
1128    fn create_index(
1129        &mut self,
1130        _request: IndexRequest,
1131        _if_not_exists: bool,
1132    ) -> Result<CreateIndexOutcome, CreateIndexError> {
1133        Err(CreateIndexError::Unsupported(
1134            "this backend does not maintain an index catalog",
1135        ))
1136    }
1137
1138    /// Remove an explicitly-declared index from the catalog. Backends
1139    /// without catalog support return [`DropIndexError::Unsupported`].
1140    /// `if_exists` collapses missing-index errors into
1141    /// [`DropIndexOutcome::NoOpMissing`].
1142    fn drop_index(
1143        &mut self,
1144        _name: &str,
1145        _if_exists: bool,
1146    ) -> Result<DropIndexOutcome, DropIndexError> {
1147        Err(DropIndexError::Unsupported(
1148            "this backend does not maintain an index catalog",
1149        ))
1150    }
1151
1152    /// Register an explicitly-declared constraint. Backends without
1153    /// catalog support return [`CreateConstraintError::Unsupported`].
1154    /// Uniqueness/key kinds may transparently register a backing range
1155    /// index of the same name.
1156    fn create_constraint(
1157        &mut self,
1158        _request: ConstraintRequest,
1159        _if_not_exists: bool,
1160    ) -> Result<CreateConstraintOutcome, CreateConstraintError> {
1161        Err(CreateConstraintError::Unsupported(
1162            "this backend does not maintain a constraint catalog",
1163        ))
1164    }
1165
1166    /// Drop a named constraint. Cascades to the backing index if the
1167    /// constraint owned one.
1168    fn drop_constraint(
1169        &mut self,
1170        _name: &str,
1171        _if_exists: bool,
1172    ) -> Result<DropConstraintOutcome, DropConstraintError> {
1173        Err(DropConstraintError::Unsupported(
1174            "this backend does not maintain a constraint catalog",
1175        ))
1176    }
1177
1178    // ---------- Defaulted convenience helpers ----------
1179
1180    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
1181    where
1182        Self: Sized,
1183    {
1184        if !self.contains_node(node_id) {
1185            return false;
1186        }
1187
1188        let existing_keys = match self.node_properties(node_id) {
1189            Some(props) => props.into_keys().collect::<Vec<_>>(),
1190            None => return false,
1191        };
1192
1193        for key in existing_keys {
1194            self.remove_node_property(node_id, &key);
1195        }
1196
1197        for (k, v) in properties {
1198            self.set_node_property(node_id, k.to_string(), v);
1199        }
1200
1201        true
1202    }
1203
1204    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
1205        if !self.contains_node(node_id) {
1206            return false;
1207        }
1208
1209        for (k, v) in properties {
1210            self.set_node_property(node_id, k.to_string(), v);
1211        }
1212
1213        true
1214    }
1215
1216    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
1217    where
1218        Self: Sized,
1219    {
1220        if !self.contains_node(node_id) {
1221            return false;
1222        }
1223
1224        let current = match self.node_labels(node_id) {
1225            Some(labels) => labels,
1226            None => return false,
1227        };
1228
1229        for label in &current {
1230            self.remove_node_label(node_id, label);
1231        }
1232
1233        for label in &labels {
1234            self.add_node_label(node_id, label);
1235        }
1236
1237        true
1238    }
1239
1240    fn replace_relationship_properties(
1241        &mut self,
1242        rel_id: RelationshipId,
1243        properties: Properties,
1244    ) -> bool
1245    where
1246        Self: Sized,
1247    {
1248        if !self.contains_relationship(rel_id) {
1249            return false;
1250        }
1251
1252        let existing_keys = match self.relationship_properties(rel_id) {
1253            Some(props) => props.into_keys().collect::<Vec<_>>(),
1254            None => return false,
1255        };
1256
1257        for key in existing_keys {
1258            self.remove_relationship_property(rel_id, &key);
1259        }
1260
1261        for (k, v) in properties {
1262            self.set_relationship_property(rel_id, k.to_string(), v);
1263        }
1264
1265        true
1266    }
1267
1268    fn merge_relationship_properties(
1269        &mut self,
1270        rel_id: RelationshipId,
1271        properties: Properties,
1272    ) -> bool {
1273        if !self.contains_relationship(rel_id) {
1274            return false;
1275        }
1276
1277        for (k, v) in properties {
1278            self.set_relationship_property(rel_id, k.to_string(), v);
1279        }
1280
1281        true
1282    }
1283
1284    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
1285        let rel_ids = self.relationship_ids_of(node_id, direction);
1286
1287        let mut deleted = 0;
1288        for rel_id in rel_ids {
1289            if self.delete_relationship(rel_id) {
1290                deleted += 1;
1291            }
1292        }
1293        deleted
1294    }
1295
1296    fn get_or_create_node(
1297        &mut self,
1298        labels: Vec<String>,
1299        match_key: &str,
1300        match_value: &PropertyValue,
1301        init_properties: Properties,
1302    ) -> NodeRecord
1303    where
1304        Self: Sized,
1305    {
1306        for label in &labels {
1307            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
1308            if let Some(node) = matches.into_iter().next() {
1309                return node;
1310            }
1311        }
1312
1313        self.create_node(labels, init_properties)
1314    }
1315}