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_node_ids_by_property(
532        &self,
533        label: Option<&str>,
534        key: &str,
535        value: &PropertyValue,
536    ) -> Vec<NodeId>
537    where
538        Self: Sized,
539    {
540        self.find_nodes_by_property(label, key, value)
541            .into_iter()
542            .map(|n| n.id)
543            .collect()
544    }
545
546    fn find_relationships_by_property(
547        &self,
548        rel_type: Option<&str>,
549        key: &str,
550        value: &PropertyValue,
551    ) -> Vec<RelationshipRecord>
552    where
553        Self: Sized,
554    {
555        let ids = match rel_type {
556            Some(rel_type) => self.rel_ids_by_type(rel_type),
557            None => self.all_rel_ids(),
558        };
559
560        ids.into_iter()
561            .filter_map(|id| {
562                let matches = self
563                    .with_relationship(id, |r| r.properties.get(key) == Some(value))
564                    .unwrap_or(false);
565                if matches {
566                    self.relationship(id)
567                } else {
568                    None
569                }
570            })
571            .collect()
572    }
573
574    fn find_relationship_ids_by_property(
575        &self,
576        rel_type: Option<&str>,
577        key: &str,
578        value: &PropertyValue,
579    ) -> Vec<RelationshipId>
580    where
581        Self: Sized,
582    {
583        self.find_relationships_by_property(rel_type, key, value)
584            .into_iter()
585            .map(|r| r.id)
586            .collect()
587    }
588
589    fn node_exists_with_label_and_property(
590        &self,
591        label: &str,
592        key: &str,
593        value: &PropertyValue,
594    ) -> bool
595    where
596        Self: Sized,
597    {
598        self.node_ids_by_label(label).into_iter().any(|id| {
599            self.with_node(id, |n| n.properties.get(key) == Some(value))
600                .unwrap_or(false)
601        })
602    }
603
604    fn relationship_exists_with_type_and_property(
605        &self,
606        rel_type: &str,
607        key: &str,
608        value: &PropertyValue,
609    ) -> bool
610    where
611        Self: Sized,
612    {
613        self.rel_ids_by_type(rel_type).into_iter().any(|id| {
614            self.with_relationship(id, |r| r.properties.get(key) == Some(value))
615                .unwrap_or(false)
616        })
617    }
618}
619
620// ============================================================================
621// GraphCatalog — narrow schema-query slice used by the analyzer.
622//
623// Blanket-implemented for every `GraphStorage`, so the analyzer can bound on
624// `GraphCatalog` without every backend having to implement a second trait.
625// ============================================================================
626
627pub trait GraphCatalog {
628    fn node_count(&self) -> usize;
629    fn relationship_count(&self) -> usize;
630    fn has_label_name(&self, label: &str) -> bool;
631    fn has_relationship_type_name(&self, rel_type: &str) -> bool;
632    fn has_property_key(&self, key: &str) -> bool;
633}
634
635impl<T: GraphStorage> GraphCatalog for T {
636    fn node_count(&self) -> usize {
637        GraphStorage::node_count(self)
638    }
639    fn relationship_count(&self) -> usize {
640        GraphStorage::relationship_count(self)
641    }
642    fn has_label_name(&self, label: &str) -> bool {
643        GraphStorage::has_label_name(self, label)
644    }
645    fn has_relationship_type_name(&self, rel_type: &str) -> bool {
646        GraphStorage::has_relationship_type_name(self, rel_type)
647    }
648    fn has_property_key(&self, key: &str) -> bool {
649        GraphStorage::has_property_key(self, key)
650    }
651}
652
653// ============================================================================
654// BorrowedGraphStorage — optional capability for backends that can hand out
655// long-lived borrows into internal records.
656//
657// The executor prefers `with_node` / `with_relationship` on hot paths because
658// they work for both borrow-capable and owned-only backends. This trait is
659// available for callers that really do want a `&NodeRecord` outliving the
660// closure — mostly internal optimization paths and tests.
661// ============================================================================
662
663pub trait BorrowedGraphStorage: GraphStorage {
664    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord>;
665    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord>;
666
667    fn node_refs(&self) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
668        Box::new(
669            self.all_node_ids()
670                .into_iter()
671                .filter_map(|id| self.node_ref(id)),
672        )
673    }
674
675    fn node_refs_by_label(&self, label: &str) -> Box<dyn Iterator<Item = &NodeRecord> + '_> {
676        Box::new(
677            self.node_ids_by_label(label)
678                .into_iter()
679                .filter_map(|id| self.node_ref(id)),
680        )
681    }
682
683    fn relationship_refs(&self) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
684        Box::new(
685            self.all_rel_ids()
686                .into_iter()
687                .filter_map(|id| self.relationship_ref(id)),
688        )
689    }
690
691    fn relationship_refs_by_type(
692        &self,
693        rel_type: &str,
694    ) -> Box<dyn Iterator<Item = &RelationshipRecord> + '_> {
695        Box::new(
696            self.rel_ids_by_type(rel_type)
697                .into_iter()
698                .filter_map(|id| self.relationship_ref(id)),
699        )
700    }
701}
702
703// ============================================================================
704// GraphStorageMut — write-side storage contract.
705//
706// A backend that implements `GraphStorage` can additionally implement
707// `GraphStorageMut` to support create / mutate / delete / admin operations.
708// Everything above the `Defaulted convenience helpers` block is a required
709// primitive; everything below is defaulted and can be overridden for perf.
710// ============================================================================
711
712pub trait GraphStorageMut: GraphStorage {
713    // ---------- Creation ----------
714
715    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord;
716
717    fn create_relationship(
718        &mut self,
719        src: NodeId,
720        dst: NodeId,
721        rel_type: &str,
722        properties: Properties,
723    ) -> Option<RelationshipRecord>;
724
725    // ---------- Node mutation ----------
726
727    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool;
728
729    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool;
730
731    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
732    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool;
733
734    // ---------- Relationship mutation ----------
735
736    fn set_relationship_property(
737        &mut self,
738        rel_id: RelationshipId,
739        key: String,
740        value: PropertyValue,
741    ) -> bool;
742
743    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool;
744
745    // ---------- Deletion ----------
746
747    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool;
748
749    /// Returns false if the node still has attached relationships.
750    fn delete_node(&mut self, node_id: NodeId) -> bool;
751
752    /// Deletes the node and all attached relationships.
753    fn detach_delete_node(&mut self, node_id: NodeId) -> bool;
754
755    // ---------- Admin / lifecycle ----------
756
757    /// Drop every node and every relationship, returning the store to an
758    /// empty state. Provided as a trait method so callers (bindings, admin
759    /// tools) can reset a graph without knowing the concrete backend.
760    ///
761    /// Future snapshot / WAL / restore entry points will also hang off the
762    /// `GraphStorageMut` surface — `clear` is the first of them.
763    fn clear(&mut self);
764
765    // ---------- Defaulted convenience helpers ----------
766
767    fn replace_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool
768    where
769        Self: Sized,
770    {
771        if !self.contains_node(node_id) {
772            return false;
773        }
774
775        let existing_keys = match self.node_properties(node_id) {
776            Some(props) => props.into_keys().collect::<Vec<_>>(),
777            None => return false,
778        };
779
780        for key in existing_keys {
781            self.remove_node_property(node_id, &key);
782        }
783
784        for (k, v) in properties {
785            self.set_node_property(node_id, k, v);
786        }
787
788        true
789    }
790
791    fn merge_node_properties(&mut self, node_id: NodeId, properties: Properties) -> bool {
792        if !self.contains_node(node_id) {
793            return false;
794        }
795
796        for (k, v) in properties {
797            self.set_node_property(node_id, k, v);
798        }
799
800        true
801    }
802
803    fn set_node_labels(&mut self, node_id: NodeId, labels: Vec<String>) -> bool
804    where
805        Self: Sized,
806    {
807        if !self.contains_node(node_id) {
808            return false;
809        }
810
811        let current = match self.node_labels(node_id) {
812            Some(labels) => labels,
813            None => return false,
814        };
815
816        for label in &current {
817            self.remove_node_label(node_id, label);
818        }
819
820        for label in &labels {
821            self.add_node_label(node_id, label);
822        }
823
824        true
825    }
826
827    fn replace_relationship_properties(
828        &mut self,
829        rel_id: RelationshipId,
830        properties: Properties,
831    ) -> bool
832    where
833        Self: Sized,
834    {
835        if !self.contains_relationship(rel_id) {
836            return false;
837        }
838
839        let existing_keys = match self.relationship_properties(rel_id) {
840            Some(props) => props.into_keys().collect::<Vec<_>>(),
841            None => return false,
842        };
843
844        for key in existing_keys {
845            self.remove_relationship_property(rel_id, &key);
846        }
847
848        for (k, v) in properties {
849            self.set_relationship_property(rel_id, k, v);
850        }
851
852        true
853    }
854
855    fn merge_relationship_properties(
856        &mut self,
857        rel_id: RelationshipId,
858        properties: Properties,
859    ) -> bool {
860        if !self.contains_relationship(rel_id) {
861            return false;
862        }
863
864        for (k, v) in properties {
865            self.set_relationship_property(rel_id, k, v);
866        }
867
868        true
869    }
870
871    fn delete_relationships_of(&mut self, node_id: NodeId, direction: Direction) -> usize {
872        let rel_ids = self.relationship_ids_of(node_id, direction);
873
874        let mut deleted = 0;
875        for rel_id in rel_ids {
876            if self.delete_relationship(rel_id) {
877                deleted += 1;
878            }
879        }
880        deleted
881    }
882
883    fn get_or_create_node(
884        &mut self,
885        labels: Vec<String>,
886        match_key: &str,
887        match_value: &PropertyValue,
888        init_properties: Properties,
889    ) -> NodeRecord
890    where
891        Self: Sized,
892    {
893        for label in &labels {
894            let matches = self.find_nodes_by_property(Some(label), match_key, match_value);
895            if let Some(node) = matches.into_iter().next() {
896                return node;
897            }
898        }
899
900        self.create_node(labels, init_properties)
901    }
902}