Skip to main content

lora_store/
graph.rs

1use crate::spatial::LoraPoint;
2use crate::temporal::{
3    LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraTime,
4};
5use crate::vector::LoraVector;
6use lora_ast::Direction;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9
10pub type NodeId = u64;
11pub type RelationshipId = u64;
12
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub enum PropertyValue {
15    Null,
16    Bool(bool),
17    Int(i64),
18    Float(f64),
19    String(String),
20    List(Vec<PropertyValue>),
21    Map(BTreeMap<String, PropertyValue>),
22    Date(LoraDate),
23    Time(LoraTime),
24    LocalTime(LoraLocalTime),
25    DateTime(LoraDateTime),
26    LocalDateTime(LoraLocalDateTime),
27    Duration(LoraDuration),
28    Point(LoraPoint),
29    Vector(LoraVector),
30}
31
32pub type Properties = BTreeMap<String, PropertyValue>;
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct NodeRecord {
36    pub id: NodeId,
37    pub labels: Vec<String>,
38    pub properties: Properties,
39}
40
41impl NodeRecord {
42    pub fn has_label(&self, label: &str) -> bool {
43        self.labels.iter().any(|l| l == label)
44    }
45
46    pub fn property(&self, key: &str) -> Option<&PropertyValue> {
47        self.properties.get(key)
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct RelationshipRecord {
53    pub id: RelationshipId,
54    pub src: NodeId,
55    pub dst: NodeId,
56    pub rel_type: String,
57    pub properties: Properties,
58}
59
60impl RelationshipRecord {
61    pub fn property(&self, key: &str) -> Option<&PropertyValue> {
62        self.properties.get(key)
63    }
64
65    pub fn other_node(&self, node_id: NodeId) -> Option<NodeId> {
66        if self.src == node_id {
67            Some(self.dst)
68        } else if self.dst == node_id {
69            Some(self.src)
70        } else {
71            None
72        }
73    }
74
75    pub fn matches_direction_from(&self, node_id: NodeId, direction: Direction) -> bool {
76        match direction {
77            Direction::Right => self.src == node_id,
78            Direction::Left => self.dst == node_id,
79            Direction::Undirected => self.src == node_id || self.dst == node_id,
80        }
81    }
82}
83
84#[derive(Debug, Clone, PartialEq)]
85pub struct ExpandedRelationship {
86    pub relationship: RelationshipRecord,
87    pub other_node: NodeRecord,
88}
89
90// ============================================================================
91// GraphStorage — the read-side storage contract
92//
93// The trait is intentionally layered into three groups: a small set of
94// backend-neutral required primitives, a pair of optional optimization hooks
95// (`with_node` / `with_relationship`), and a large cloud of defaulted helpers
96// that derive from the primitives.
97//
98// Adding a new backend means implementing the required primitives (roughly a
99// dozen methods) plus — optionally — overriding the hooks for zero-copy or the
100// record-scan helpers for bulk perf. Implementors SHOULD NOT need to rewrite
101// the catalog / traversal helper surface unless they can beat the default
102// composition.
103// ============================================================================
104
105pub trait GraphStorage {
106    // ---------- Required node primitives ----------
107
108    /// Cheap existence check. Should not clone or materialize the record.
109    fn contains_node(&self, id: NodeId) -> bool;
110
111    /// Point lookup returning an owned record. Backends that can hand out
112    /// borrows should also implement [`BorrowedGraphStorage::node_ref`] and
113    /// override [`with_node`] to avoid clones on the hot path.
114    fn node(&self, id: NodeId) -> Option<NodeRecord>;
115
116    /// Enumerate every node id. Should be O(nodes) without cloning records.
117    fn all_node_ids(&self) -> Vec<NodeId>;
118
119    /// Enumerate node ids carrying the given label. Implementations that keep
120    /// a label index should override this.
121    fn node_ids_by_label(&self, label: &str) -> Vec<NodeId>;
122
123    // ---------- Required relationship primitives ----------
124
125    fn contains_relationship(&self, id: RelationshipId) -> bool;
126
127    fn relationship(&self, id: RelationshipId) -> Option<RelationshipRecord>;
128
129    fn all_rel_ids(&self) -> Vec<RelationshipId>;
130
131    fn rel_ids_by_type(&self, rel_type: &str) -> Vec<RelationshipId>;
132
133    /// Endpoint pair `(src, dst)` for a relationship. Required because
134    /// traversal uses it on hot paths; a backend that stores endpoints
135    /// alongside the id index can answer this without fetching properties.
136    fn relationship_endpoints(&self, id: RelationshipId) -> Option<(NodeId, NodeId)>;
137
138    // ---------- Required traversal primitive ----------
139
140    /// Expand a node's incident relationships filtered by direction and
141    /// (optional) types. This is the single traversal primitive; variable-
142    /// length paths, degree, and adjacency helpers are all derived from it.
143    fn expand_ids(
144        &self,
145        node_id: NodeId,
146        direction: Direction,
147        types: &[String],
148    ) -> Vec<(RelationshipId, NodeId)>;
149
150    // ---------- Required catalog primitives ----------
151
152    fn all_labels(&self) -> Vec<String>;
153    fn all_relationship_types(&self) -> Vec<String>;
154
155    // ---------- Optional optimization hooks ----------
156    //
157    // Generic methods gated on `Self: Sized` so they don't affect object
158    // safety. Backends override these to supply borrow-based access on hot
159    // paths; defaults clone through `node` / `relationship`.
160
161    fn with_node<F, R>(&self, id: NodeId, f: F) -> Option<R>
162    where
163        F: FnOnce(&NodeRecord) -> R,
164        Self: Sized,
165    {
166        self.node(id).as_ref().map(f)
167    }
168
169    fn with_relationship<F, R>(&self, id: RelationshipId, f: F) -> Option<R>
170    where
171        F: FnOnce(&RelationshipRecord) -> R,
172        Self: Sized,
173    {
174        self.relationship(id).as_ref().map(f)
175    }
176
177    // ---------- Defaulted: counts / existence aliases ----------
178
179    fn has_node(&self, id: NodeId) -> bool {
180        self.contains_node(id)
181    }
182
183    fn has_relationship(&self, id: RelationshipId) -> bool {
184        self.contains_relationship(id)
185    }
186
187    fn node_count(&self) -> usize {
188        self.all_node_ids().len()
189    }
190
191    fn relationship_count(&self) -> usize {
192        self.all_rel_ids().len()
193    }
194
195    // ---------- Defaulted: record-returning scans ----------
196    //
197    // These synthesize full-record scans from id scans + point lookups. That
198    // is correct for any backend and fast enough for small graphs, but a
199    // backend that can scan records in one pass (in-memory via a BTreeMap
200    // `.values()`, a column store via a streaming read) should override.
201
202    fn all_nodes(&self) -> Vec<NodeRecord> {
203        self.all_node_ids()
204            .into_iter()
205            .filter_map(|id| self.node(id))
206            .collect()
207    }
208
209    fn nodes_by_label(&self, label: &str) -> Vec<NodeRecord> {
210        self.node_ids_by_label(label)
211            .into_iter()
212            .filter_map(|id| self.node(id))
213            .collect()
214    }
215
216    fn all_relationships(&self) -> Vec<RelationshipRecord> {
217        self.all_rel_ids()
218            .into_iter()
219            .filter_map(|id| self.relationship(id))
220            .collect()
221    }
222
223    fn relationships_by_type(&self, rel_type: &str) -> Vec<RelationshipRecord> {
224        self.rel_ids_by_type(rel_type)
225            .into_iter()
226            .filter_map(|id| self.relationship(id))
227            .collect()
228    }
229
230    // ---------- Defaulted: traversal helpers ----------
231
232    fn relationship_ids_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipId> {
233        self.expand_ids(node_id, direction, &[])
234            .into_iter()
235            .map(|(rel_id, _)| rel_id)
236            .collect()
237    }
238
239    fn outgoing_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
240        self.relationship_ids_of(node_id, Direction::Right)
241            .into_iter()
242            .filter_map(|id| self.relationship(id))
243            .collect()
244    }
245
246    fn incoming_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
247        self.relationship_ids_of(node_id, Direction::Left)
248            .into_iter()
249            .filter_map(|id| self.relationship(id))
250            .collect()
251    }
252
253    fn relationships_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipRecord> {
254        self.relationship_ids_of(node_id, direction)
255            .into_iter()
256            .filter_map(|id| self.relationship(id))
257            .collect()
258    }
259
260    fn degree(&self, node_id: NodeId, direction: Direction) -> usize {
261        self.expand_ids(node_id, direction, &[]).len()
262    }
263
264    fn is_isolated(&self, node_id: NodeId) -> bool {
265        self.degree(node_id, Direction::Undirected) == 0
266    }
267
268    fn expand(
269        &self,
270        node_id: NodeId,
271        direction: Direction,
272        types: &[String],
273    ) -> Vec<(RelationshipRecord, NodeRecord)> {
274        self.expand_ids(node_id, direction, types)
275            .into_iter()
276            .filter_map(|(rid, nid)| {
277                let rel = self.relationship(rid)?;
278                let node = self.node(nid)?;
279                Some((rel, node))
280            })
281            .collect()
282    }
283
284    fn expand_detailed(
285        &self,
286        node_id: NodeId,
287        direction: Direction,
288        types: &[String],
289    ) -> Vec<ExpandedRelationship> {
290        self.expand(node_id, direction, types)
291            .into_iter()
292            .map(|(relationship, other_node)| ExpandedRelationship {
293                relationship,
294                other_node,
295            })
296            .collect()
297    }
298
299    fn neighbors(
300        &self,
301        node_id: NodeId,
302        direction: Direction,
303        types: &[String],
304    ) -> Vec<NodeRecord> {
305        self.expand_ids(node_id, direction, types)
306            .into_iter()
307            .filter_map(|(_, nid)| self.node(nid))
308            .collect()
309    }
310
311    // ---------- Defaulted: narrow node accessors ----------
312
313    fn node_has_label(&self, node_id: NodeId, label: &str) -> bool
314    where
315        Self: Sized,
316    {
317        self.with_node(node_id, |n| n.labels.iter().any(|l| l == label))
318            .unwrap_or(false)
319    }
320
321    fn node_labels(&self, node_id: NodeId) -> Option<Vec<String>>
322    where
323        Self: Sized,
324    {
325        self.with_node(node_id, |n| n.labels.clone())
326    }
327
328    fn node_properties(&self, node_id: NodeId) -> Option<Properties>
329    where
330        Self: Sized,
331    {
332        self.with_node(node_id, |n| n.properties.clone())
333    }
334
335    fn node_property(&self, node_id: NodeId, key: &str) -> Option<PropertyValue>
336    where
337        Self: Sized,
338    {
339        self.with_node(node_id, |n| n.properties.get(key).cloned())
340            .flatten()
341    }
342
343    // ---------- Defaulted: narrow relationship accessors ----------
344
345    fn relationship_type(&self, rel_id: RelationshipId) -> Option<String>
346    where
347        Self: Sized,
348    {
349        self.with_relationship(rel_id, |r| r.rel_type.clone())
350    }
351
352    fn relationship_properties(&self, rel_id: RelationshipId) -> Option<Properties>
353    where
354        Self: Sized,
355    {
356        self.with_relationship(rel_id, |r| r.properties.clone())
357    }
358
359    fn relationship_property(&self, rel_id: RelationshipId, key: &str) -> Option<PropertyValue>
360    where
361        Self: Sized,
362    {
363        self.with_relationship(rel_id, |r| r.properties.get(key).cloned())
364            .flatten()
365    }
366
367    fn relationship_source(&self, rel_id: RelationshipId) -> Option<NodeId> {
368        self.relationship_endpoints(rel_id).map(|(s, _)| s)
369    }
370
371    fn relationship_target(&self, rel_id: RelationshipId) -> Option<NodeId> {
372        self.relationship_endpoints(rel_id).map(|(_, d)| d)
373    }
374
375    fn other_node(&self, rel_id: RelationshipId, node_id: NodeId) -> Option<NodeId> {
376        let (src, dst) = self.relationship_endpoints(rel_id)?;
377        if src == node_id {
378            Some(dst)
379        } else if dst == node_id {
380            Some(src)
381        } else {
382            None
383        }
384    }
385
386    // ---------- Defaulted: catalog helpers ----------
387
388    fn has_label_name(&self, label: &str) -> bool {
389        self.all_labels().iter().any(|l| l == label)
390    }
391
392    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
393        self.all_relationship_types().iter().any(|t| t == rel_type)
394    }
395
396    fn all_node_property_keys(&self) -> Vec<String>
397    where
398        Self: Sized,
399    {
400        let mut keys = BTreeSet::new();
401        for id in self.all_node_ids() {
402            self.with_node(id, |n| {
403                for key in n.properties.keys() {
404                    keys.insert(key.clone());
405                }
406            });
407        }
408        keys.into_iter().collect()
409    }
410
411    fn all_relationship_property_keys(&self) -> Vec<String>
412    where
413        Self: Sized,
414    {
415        let mut keys = BTreeSet::new();
416        for id in self.all_rel_ids() {
417            self.with_relationship(id, |r| {
418                for key in r.properties.keys() {
419                    keys.insert(key.clone());
420                }
421            });
422        }
423        keys.into_iter().collect()
424    }
425
426    fn all_property_keys(&self) -> Vec<String>
427    where
428        Self: Sized,
429    {
430        let mut keys = BTreeSet::new();
431        for key in self.all_node_property_keys() {
432            keys.insert(key);
433        }
434        for key in self.all_relationship_property_keys() {
435            keys.insert(key);
436        }
437        keys.into_iter().collect()
438    }
439
440    fn has_property_key(&self, key: &str) -> bool
441    where
442        Self: Sized,
443    {
444        self.all_node_property_keys().iter().any(|k| k == key)
445            || self
446                .all_relationship_property_keys()
447                .iter()
448                .any(|k| k == key)
449    }
450
451    fn label_property_keys(&self, label: &str) -> Vec<String>
452    where
453        Self: Sized,
454    {
455        let mut keys = BTreeSet::new();
456        for id in self.node_ids_by_label(label) {
457            self.with_node(id, |n| {
458                for key in n.properties.keys() {
459                    keys.insert(key.clone());
460                }
461            });
462        }
463        keys.into_iter().collect()
464    }
465
466    fn rel_type_property_keys(&self, rel_type: &str) -> Vec<String>
467    where
468        Self: Sized,
469    {
470        let mut keys = BTreeSet::new();
471        for id in self.rel_ids_by_type(rel_type) {
472            self.with_relationship(id, |r| {
473                for key in r.properties.keys() {
474                    keys.insert(key.clone());
475                }
476            });
477        }
478        keys.into_iter().collect()
479    }
480
481    fn label_has_property_key(&self, label: &str, key: &str) -> bool
482    where
483        Self: Sized,
484    {
485        self.node_ids_by_label(label).into_iter().any(|id| {
486            self.with_node(id, |n| n.properties.contains_key(key))
487                .unwrap_or(false)
488        })
489    }
490
491    fn rel_type_has_property_key(&self, rel_type: &str, key: &str) -> bool
492    where
493        Self: Sized,
494    {
495        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
496            self.with_relationship(id, |r| r.properties.contains_key(key))
497                .unwrap_or(false)
498        })
499    }
500
501    // ---------- Defaulted: property-filter lookups ----------
502
503    fn find_nodes_by_property(
504        &self,
505        label: Option<&str>,
506        key: &str,
507        value: &PropertyValue,
508    ) -> Vec<NodeRecord>
509    where
510        Self: Sized,
511    {
512        let ids = match label {
513            Some(label) => self.node_ids_by_label(label),
514            None => self.all_node_ids(),
515        };
516
517        ids.into_iter()
518            .filter_map(|id| {
519                let matches = self
520                    .with_node(id, |n| n.properties.get(key) == Some(value))
521                    .unwrap_or(false);
522                if matches {
523                    self.node(id)
524                } else {
525                    None
526                }
527            })
528            .collect()
529    }
530
531    fn find_relationships_by_property(
532        &self,
533        rel_type: Option<&str>,
534        key: &str,
535        value: &PropertyValue,
536    ) -> Vec<RelationshipRecord>
537    where
538        Self: Sized,
539    {
540        let ids = match rel_type {
541            Some(rel_type) => self.rel_ids_by_type(rel_type),
542            None => self.all_rel_ids(),
543        };
544
545        ids.into_iter()
546            .filter_map(|id| {
547                let matches = self
548                    .with_relationship(id, |r| r.properties.get(key) == Some(value))
549                    .unwrap_or(false);
550                if matches {
551                    self.relationship(id)
552                } else {
553                    None
554                }
555            })
556            .collect()
557    }
558
559    fn node_exists_with_label_and_property(
560        &self,
561        label: &str,
562        key: &str,
563        value: &PropertyValue,
564    ) -> bool
565    where
566        Self: Sized,
567    {
568        self.node_ids_by_label(label).into_iter().any(|id| {
569            self.with_node(id, |n| n.properties.get(key) == Some(value))
570                .unwrap_or(false)
571        })
572    }
573
574    fn relationship_exists_with_type_and_property(
575        &self,
576        rel_type: &str,
577        key: &str,
578        value: &PropertyValue,
579    ) -> bool
580    where
581        Self: Sized,
582    {
583        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
584            self.with_relationship(id, |r| r.properties.get(key) == Some(value))
585                .unwrap_or(false)
586        })
587    }
588}
589
590// ============================================================================
591// GraphCatalog — narrow schema-query slice used by the analyzer.
592//
593// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
594// `GraphCatalog` without every backend having to implement a second trait.
595// ============================================================================
596
597pub trait GraphCatalog {
598    fn node_count(&self) -> usize;
599    fn relationship_count(&self) -> usize;
600    fn has_label_name(&self, label: &str) -> bool;
601    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
602    fn has_property_key(&self, key: &str) -> bool;
603}
604
605impl<T: GraphStorage> GraphCatalog for T {
606    fn node_count(&self) -> usize {
607        GraphStorage::node_count(self)
608    }
609    fn relationship_count(&self) -> usize {
610        GraphStorage::relationship_count(self)
611    }
612    fn has_label_name(&self, label: &str) -> bool {
613        GraphStorage::has_label_name(self, label)
614    }
615    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
616        GraphStorage::has_relationship_type_name(self, rel_type)
617    }
618    fn has_property_key(&self, key: &str) -> bool {
619        GraphStorage::has_property_key(self, key)
620    }
621}
622
623// ============================================================================
624// BorrowedGraphStorage — optional capability for backends that can hand out
625// long-lived borrows into internal records.
626//
627// The executor prefers `with_node` / `with_relationship` on hot paths because
628// they work for both borrow-capable and owned-only backends. This trait is
629// available for callers that really do want a `&NodeRecord` outliving the
630// closure — mostly internal optimization paths and tests.
631// ============================================================================
632
633pub trait BorrowedGraphStorage: GraphStorage {
634    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord>;
635    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord>;
636}
637
638// ============================================================================
639// GraphStorageMut — write-side storage contract.
640//
641// A backend that implements `GraphStorage` can additionally implement
642// `GraphStorageMut` to support create / mutate / delete / admin operations.
643// Everything above the `Defaulted convenience helpers` block is a required
644// primitive; everything below is defaulted and can be overridden for perf.
645// ============================================================================
646
647pub trait GraphStorageMut: GraphStorage {
648    // ---------- Creation ----------
649
650    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord;
651
652    fn create_relationship(
653        &mut self,
654        src: NodeId,
655        dst: NodeId,
656        rel_type: &str,
657        properties: Properties,
658    ) -> Option<RelationshipRecord>;
659
660    // ---------- Node mutation ----------
661
662    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
663
664    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
665
666    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
667    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
668
669    // ---------- Relationship mutation ----------
670
671    fn set_relationship_property(
672        &mut self,
673        rel_id: RelationshipId,
674        key: String,
675        value: PropertyValue,
676    ) -> bool;
677
678    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
679
680    // ---------- Deletion ----------
681
682    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
683
684    /// Returns false if the node still has attached relationships.
685    fn delete_node(&mut self, node_id: NodeId) -> bool;
686
687    /// Deletes the node and all attached relationships.
688    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
689
690    // ---------- Admin / lifecycle ----------
691
692    /// Drop every node and every relationship, returning the store to an
693    /// empty state. Provided as a trait method so callers (bindings, admin
694    /// tools) can reset a graph without knowing the concrete backend.
695    ///
696    /// Future snapshot / WAL / restore entry points will also hang off the
697    /// `GraphStorageMut` surface — `clear` is the first of them.
698    fn clear(&mut self);
699
700    // ---------- Defaulted convenience helpers ----------
701
702    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
703    where
704        Self: Sized,
705    {
706        if !self.contains_node(node_id) {
707            return false;
708        }
709
710        let existing_keys = match self.node_properties(node_id) {
711            Some(props) => props.into_keys().collect::<Vec<_>>(),
712            None => return false,
713        };
714
715        for key in existing_keys {
716            self.remove_node_property(node_id, &key);
717        }
718
719        for (k, v) in properties {
720            self.set_node_property(node_id, k, v);
721        }
722
723        true
724    }
725
726    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
727        if !self.contains_node(node_id) {
728            return false;
729        }
730
731        for (k, v) in properties {
732            self.set_node_property(node_id, k, v);
733        }
734
735        true
736    }
737
738    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
739    where
740        Self: Sized,
741    {
742        if !self.contains_node(node_id) {
743            return false;
744        }
745
746        let current = match self.node_labels(node_id) {
747            Some(labels) => labels,
748            None => return false,
749        };
750
751        for label in &current {
752            self.remove_node_label(node_id, label);
753        }
754
755        for label in &labels {
756            self.add_node_label(node_id, label);
757        }
758
759        true
760    }
761
762    fn replace_relationship_properties(
763        &mut self,
764        rel_id: RelationshipId,
765        properties: Properties,
766    ) -> bool
767    where
768        Self: Sized,
769    {
770        if !self.contains_relationship(rel_id) {
771            return false;
772        }
773
774        let existing_keys = match self.relationship_properties(rel_id) {
775            Some(props) => props.into_keys().collect::<Vec<_>>(),
776            None => return false,
777        };
778
779        for key in existing_keys {
780            self.remove_relationship_property(rel_id, &key);
781        }
782
783        for (k, v) in properties {
784            self.set_relationship_property(rel_id, k, v);
785        }
786
787        true
788    }
789
790    fn merge_relationship_properties(
791        &mut self,
792        rel_id: RelationshipId,
793        properties: Properties,
794    ) -> bool {
795        if !self.contains_relationship(rel_id) {
796            return false;
797        }
798
799        for (k, v) in properties {
800            self.set_relationship_property(rel_id, k, v);
801        }
802
803        true
804    }
805
806    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
807        let rel_ids = self.relationship_ids_of(node_id, direction);
808
809        let mut deleted = 0;
810        for rel_id in rel_ids {
811            if self.delete_relationship(rel_id) {
812                deleted += 1;
813            }
814        }
815        deleted
816    }
817
818    fn get_or_create_node(
819        &mut self,
820        labels: Vec<String>,
821        match_key: &str,
822        match_value: &PropertyValue,
823        init_properties: Properties,
824    ) -> NodeRecord
825    where
826        Self: Sized,
827    {
828        for label in &labels {
829            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
830            if let Some(node) = matches.into_iter().next() {
831                return node;
832            }
833        }
834
835        self.create_node(labels, init_properties)
836    }
837}