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