Skip to main content

lora_store/
traits.rs

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