Skip to main content

lora_store/
memory.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use lora_ast::Direction;
4
5use crate::{
6    GraphStorage, GraphStorageMut, NodeId, NodeRecord, Properties, PropertyValue, RelationshipId,
7    RelationshipRecord,
8};
9
10#[derive(Debug, Clone, Default)]
11pub struct InMemoryGraph {
12    next_node_id: NodeId,
13    next_rel_id: RelationshipId,
14
15    nodes: BTreeMap<NodeId, NodeRecord>,
16    relationships: BTreeMap<RelationshipId, RelationshipRecord>,
17
18    // adjacency
19    outgoing: BTreeMap<NodeId, BTreeSet<RelationshipId>>,
20    incoming: BTreeMap<NodeId, BTreeSet<RelationshipId>>,
21
22    // secondary indexes
23    nodes_by_label: BTreeMap<String, BTreeSet<NodeId>>,
24    relationships_by_type: BTreeMap<String, BTreeSet<RelationshipId>>,
25}
26
27impl InMemoryGraph {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn with_capacity_hint(_nodes: usize, _relationships: usize) -> Self {
33        // BTreeMap/BTreeSet do not support capacity reservation.
34        Self::default()
35    }
36
37    pub fn clear(&mut self) {
38        *self = Self::default();
39    }
40
41    pub fn contains_node(&self, node_id: NodeId) -> bool {
42        self.nodes.contains_key(&node_id)
43    }
44
45    pub fn contains_relationship(&self, rel_id: RelationshipId) -> bool {
46        self.relationships.contains_key(&rel_id)
47    }
48
49    fn alloc_node_id(&mut self) -> NodeId {
50        let id = self.next_node_id;
51        self.next_node_id += 1;
52        id
53    }
54
55    fn alloc_rel_id(&mut self) -> RelationshipId {
56        let id = self.next_rel_id;
57        self.next_rel_id += 1;
58        id
59    }
60
61    fn normalize_labels(labels: Vec<String>) -> Vec<String> {
62        let mut seen = BTreeSet::new();
63
64        labels
65            .into_iter()
66            .map(|s| s.trim().to_string())
67            .filter(|s| !s.is_empty())
68            .filter(|s| seen.insert(s.clone()))
69            .collect()
70    }
71
72    fn insert_node_label_index(&mut self, node_id: NodeId, label: &str) {
73        self.nodes_by_label
74            .entry(label.to_string())
75            .or_default()
76            .insert(node_id);
77    }
78
79    fn remove_node_label_index(&mut self, node_id: NodeId, label: &str) {
80        if let Some(ids) = self.nodes_by_label.get_mut(label) {
81            ids.remove(&node_id);
82            if ids.is_empty() {
83                self.nodes_by_label.remove(label);
84            }
85        }
86    }
87
88    fn insert_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
89        self.relationships_by_type
90            .entry(rel_type.to_string())
91            .or_default()
92            .insert(rel_id);
93    }
94
95    fn remove_relationship_type_index(&mut self, rel_id: RelationshipId, rel_type: &str) {
96        if let Some(ids) = self.relationships_by_type.get_mut(rel_type) {
97            ids.remove(&rel_id);
98            if ids.is_empty() {
99                self.relationships_by_type.remove(rel_type);
100            }
101        }
102    }
103
104    fn attach_relationship(&mut self, rel: &RelationshipRecord) {
105        self.outgoing.entry(rel.src).or_default().insert(rel.id);
106        self.incoming.entry(rel.dst).or_default().insert(rel.id);
107        self.insert_relationship_type_index(rel.id, &rel.rel_type);
108    }
109
110    fn detach_relationship_indexes(&mut self, rel: &RelationshipRecord) {
111        if let Some(ids) = self.outgoing.get_mut(&rel.src) {
112            ids.remove(&rel.id);
113            if ids.is_empty() {
114                self.outgoing.remove(&rel.src);
115            }
116        }
117
118        if let Some(ids) = self.incoming.get_mut(&rel.dst) {
119            ids.remove(&rel.id);
120            if ids.is_empty() {
121                self.incoming.remove(&rel.dst);
122            }
123        }
124
125        self.remove_relationship_type_index(rel.id, &rel.rel_type);
126    }
127
128    fn relationship_ids_for_direction(
129        &self,
130        node_id: NodeId,
131        direction: Direction,
132    ) -> Vec<RelationshipId> {
133        match direction {
134            Direction::Left => self
135                .incoming
136                .get(&node_id)
137                .map(|ids| ids.iter().copied().collect())
138                .unwrap_or_default(),
139
140            Direction::Right => self
141                .outgoing
142                .get(&node_id)
143                .map(|ids| ids.iter().copied().collect())
144                .unwrap_or_default(),
145
146            Direction::Undirected => {
147                let mut ids = BTreeSet::new();
148
149                if let Some(out) = self.outgoing.get(&node_id) {
150                    ids.extend(out.iter().copied());
151                }
152                if let Some(inc) = self.incoming.get(&node_id) {
153                    ids.extend(inc.iter().copied());
154                }
155
156                ids.into_iter().collect()
157            }
158        }
159    }
160
161    fn other_endpoint(rel: &RelationshipRecord, node_id: NodeId) -> Option<NodeId> {
162        if rel.src == node_id {
163            Some(rel.dst)
164        } else if rel.dst == node_id {
165            Some(rel.src)
166        } else {
167            None
168        }
169    }
170
171    fn has_incident_relationships(&self, node_id: NodeId) -> bool {
172        self.outgoing
173            .get(&node_id)
174            .map(|ids| !ids.is_empty())
175            .unwrap_or(false)
176            || self
177                .incoming
178                .get(&node_id)
179                .map(|ids| !ids.is_empty())
180                .unwrap_or(false)
181    }
182
183    fn incident_relationship_ids(&self, node_id: NodeId) -> BTreeSet<RelationshipId> {
184        let mut rel_ids = BTreeSet::new();
185
186        if let Some(ids) = self.outgoing.get(&node_id) {
187            rel_ids.extend(ids.iter().copied());
188        }
189        if let Some(ids) = self.incoming.get(&node_id) {
190            rel_ids.extend(ids.iter().copied());
191        }
192
193        rel_ids
194    }
195}
196
197impl GraphStorage for InMemoryGraph {
198    fn all_nodes(&self) -> Vec<NodeRecord> {
199        self.nodes.values().cloned().collect()
200    }
201
202    fn nodes_by_label(&self, label: &str) -> Vec<NodeRecord> {
203        self.nodes_by_label
204            .get(label)
205            .into_iter()
206            .flat_map(|ids| ids.iter())
207            .filter_map(|id| self.nodes.get(id).cloned())
208            .collect()
209    }
210
211    fn node_ref(&self, id: NodeId) -> Option<&NodeRecord> {
212        self.nodes.get(&id)
213    }
214
215    fn all_node_ids(&self) -> Vec<NodeId> {
216        self.nodes.keys().copied().collect()
217    }
218
219    fn node_ids_by_label(&self, label: &str) -> Vec<NodeId> {
220        match self.nodes_by_label.get(label) {
221            Some(ids) => ids.iter().copied().collect(),
222            None => Vec::new(),
223        }
224    }
225
226    fn node_count(&self) -> usize {
227        self.nodes.len()
228    }
229
230    fn has_node(&self, id: NodeId) -> bool {
231        self.nodes.contains_key(&id)
232    }
233
234    fn all_relationships(&self) -> Vec<RelationshipRecord> {
235        self.relationships.values().cloned().collect()
236    }
237
238    fn relationships_by_type(&self, rel_type: &str) -> Vec<RelationshipRecord> {
239        self.relationships_by_type
240            .get(rel_type)
241            .into_iter()
242            .flat_map(|ids| ids.iter())
243            .filter_map(|id| self.relationships.get(id).cloned())
244            .collect()
245    }
246
247    fn relationship_ref(&self, id: RelationshipId) -> Option<&RelationshipRecord> {
248        self.relationships.get(&id)
249    }
250
251    fn all_rel_ids(&self) -> Vec<RelationshipId> {
252        self.relationships.keys().copied().collect()
253    }
254
255    fn rel_ids_by_type(&self, rel_type: &str) -> Vec<RelationshipId> {
256        match self.relationships_by_type.get(rel_type) {
257            Some(ids) => ids.iter().copied().collect(),
258            None => Vec::new(),
259        }
260    }
261
262    fn relationship_count(&self) -> usize {
263        self.relationships.len()
264    }
265
266    fn has_relationship(&self, id: RelationshipId) -> bool {
267        self.relationships.contains_key(&id)
268    }
269
270    fn all_labels(&self) -> Vec<String> {
271        self.nodes_by_label.keys().cloned().collect()
272    }
273
274    fn all_relationship_types(&self) -> Vec<String> {
275        self.relationships_by_type.keys().cloned().collect()
276    }
277
278    fn all_node_property_keys(&self) -> Vec<String> {
279        let mut keys = BTreeSet::new();
280        for node in self.nodes.values() {
281            for key in node.properties.keys() {
282                keys.insert(key.clone());
283            }
284        }
285        keys.into_iter().collect()
286    }
287
288    fn all_relationship_property_keys(&self) -> Vec<String> {
289        let mut keys = BTreeSet::new();
290        for rel in self.relationships.values() {
291            for key in rel.properties.keys() {
292                keys.insert(key.clone());
293            }
294        }
295        keys.into_iter().collect()
296    }
297
298    fn label_property_keys(&self, label: &str) -> Vec<String> {
299        let mut keys = BTreeSet::new();
300
301        if let Some(ids) = self.nodes_by_label.get(label) {
302            for id in ids {
303                if let Some(node) = self.nodes.get(id) {
304                    for key in node.properties.keys() {
305                        keys.insert(key.clone());
306                    }
307                }
308            }
309        }
310
311        keys.into_iter().collect()
312    }
313
314    fn rel_type_property_keys(&self, rel_type: &str) -> Vec<String> {
315        let mut keys = BTreeSet::new();
316
317        if let Some(ids) = self.relationships_by_type.get(rel_type) {
318            for id in ids {
319                if let Some(rel) = self.relationships.get(id) {
320                    for key in rel.properties.keys() {
321                        keys.insert(key.clone());
322                    }
323                }
324            }
325        }
326
327        keys.into_iter().collect()
328    }
329
330    fn node_has_label(&self, node_id: NodeId, label: &str) -> bool {
331        self.nodes
332            .get(&node_id)
333            .map(|n| n.labels.iter().any(|l| l == label))
334            .unwrap_or(false)
335    }
336
337    fn node_property(&self, node_id: NodeId, key: &str) -> Option<PropertyValue> {
338        self.nodes
339            .get(&node_id)
340            .and_then(|n| n.properties.get(key).cloned())
341    }
342
343    fn relationship_property(&self, rel_id: RelationshipId, key: &str) -> Option<PropertyValue> {
344        self.relationships
345            .get(&rel_id)
346            .and_then(|r| r.properties.get(key).cloned())
347    }
348
349    fn expand(
350        &self,
351        node_id: NodeId,
352        direction: Direction,
353        types: &[String],
354    ) -> Vec<(RelationshipRecord, NodeRecord)> {
355        if !self.nodes.contains_key(&node_id) {
356            return Vec::new();
357        }
358
359        let type_filter: Option<BTreeSet<&str>> = if types.is_empty() {
360            None
361        } else {
362            Some(types.iter().map(String::as_str).collect())
363        };
364
365        self.relationship_ids_for_direction(node_id, direction)
366            .into_iter()
367            .filter_map(|rel_id| self.relationships.get(&rel_id))
368            .filter(|rel| {
369                type_filter
370                    .as_ref()
371                    .map(|allowed| allowed.contains(rel.rel_type.as_str()))
372                    .unwrap_or(true)
373            })
374            .filter_map(|rel| {
375                let other_id = Self::other_endpoint(rel, node_id)?;
376                let other = self.nodes.get(&other_id)?;
377                Some((rel.clone(), other.clone()))
378            })
379            .collect()
380    }
381
382    fn expand_ids(
383        &self,
384        node_id: NodeId,
385        direction: Direction,
386        types: &[String],
387    ) -> Vec<(RelationshipId, NodeId)> {
388        if !self.nodes.contains_key(&node_id) {
389            return Vec::new();
390        }
391
392        // Fast path: no type filter — just join adjacency + rel endpoints.
393        if types.is_empty() {
394            return self
395                .relationship_ids_for_direction(node_id, direction)
396                .into_iter()
397                .filter_map(|rel_id| {
398                    let rel = self.relationships.get(&rel_id)?;
399                    let other_id = Self::other_endpoint(rel, node_id)?;
400                    Some((rel_id, other_id))
401                })
402                .collect();
403        }
404
405        // Type-filtered: borrow rel_type straight from the stored record.
406        // For small type lists (the common case) the linear scan beats a
407        // BTreeSet; we keep it allocation-free.
408        self.relationship_ids_for_direction(node_id, direction)
409            .into_iter()
410            .filter_map(|rel_id| {
411                let rel = self.relationships.get(&rel_id)?;
412                if !types.iter().any(|t| t == &rel.rel_type) {
413                    return None;
414                }
415                let other_id = Self::other_endpoint(rel, node_id)?;
416                Some((rel_id, other_id))
417            })
418            .collect()
419    }
420
421    fn outgoing_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
422        self.outgoing
423            .get(&node_id)
424            .into_iter()
425            .flat_map(|ids| ids.iter())
426            .filter_map(|id| self.relationships.get(id).cloned())
427            .collect()
428    }
429
430    fn incoming_relationships(&self, node_id: NodeId) -> Vec<RelationshipRecord> {
431        self.incoming
432            .get(&node_id)
433            .into_iter()
434            .flat_map(|ids| ids.iter())
435            .filter_map(|id| self.relationships.get(id).cloned())
436            .collect()
437    }
438
439    fn relationship_ids_of(&self, node_id: NodeId, direction: Direction) -> Vec<RelationshipId> {
440        self.relationship_ids_for_direction(node_id, direction)
441    }
442
443    fn degree(&self, node_id: NodeId, direction: Direction) -> usize {
444        match direction {
445            Direction::Right => self.outgoing.get(&node_id).map(|s| s.len()).unwrap_or(0),
446            Direction::Left => self.incoming.get(&node_id).map(|s| s.len()).unwrap_or(0),
447            Direction::Undirected => {
448                self.outgoing.get(&node_id).map(|s| s.len()).unwrap_or(0)
449                    + self.incoming.get(&node_id).map(|s| s.len()).unwrap_or(0)
450            }
451        }
452    }
453}
454
455impl GraphStorageMut for InMemoryGraph {
456    fn create_node(&mut self, labels: Vec<String>, properties: Properties) -> NodeRecord {
457        let id = self.alloc_node_id();
458        let labels = Self::normalize_labels(labels);
459
460        let node = NodeRecord {
461            id,
462            labels: labels.clone(),
463            properties,
464        };
465
466        self.nodes.insert(id, node.clone());
467
468        for label in &labels {
469            self.insert_node_label_index(id, label);
470        }
471
472        self.outgoing.entry(id).or_default();
473        self.incoming.entry(id).or_default();
474
475        node
476    }
477
478    fn create_relationship(
479        &mut self,
480        src: NodeId,
481        dst: NodeId,
482        rel_type: &str,
483        properties: Properties,
484    ) -> Option<RelationshipRecord> {
485        if !self.nodes.contains_key(&src) || !self.nodes.contains_key(&dst) {
486            return None;
487        }
488
489        let trimmed = rel_type.trim();
490        if trimmed.is_empty() {
491            return None;
492        }
493
494        let id = self.alloc_rel_id();
495        let rel = RelationshipRecord {
496            id,
497            src,
498            dst,
499            rel_type: trimmed.to_string(),
500            properties,
501        };
502
503        self.attach_relationship(&rel);
504        self.relationships.insert(id, rel.clone());
505
506        Some(rel)
507    }
508
509    fn set_node_property(&mut self, node_id: NodeId, key: String, value: PropertyValue) -> bool {
510        match self.nodes.get_mut(&node_id) {
511            Some(node) => {
512                node.properties.insert(key, value);
513                true
514            }
515            None => false,
516        }
517    }
518
519    fn remove_node_property(&mut self, node_id: NodeId, key: &str) -> bool {
520        match self.nodes.get_mut(&node_id) {
521            Some(node) => node.properties.remove(key).is_some(),
522            None => false,
523        }
524    }
525
526    fn add_node_label(&mut self, node_id: NodeId, label: &str) -> bool {
527        let label = label.trim();
528        if label.is_empty() {
529            return false;
530        }
531
532        match self.nodes.get_mut(&node_id) {
533            Some(node) => {
534                if node.labels.iter().any(|l| l == label) {
535                    return false;
536                }
537
538                node.labels.push(label.to_string());
539                self.insert_node_label_index(node_id, label);
540                true
541            }
542            None => false,
543        }
544    }
545
546    fn remove_node_label(&mut self, node_id: NodeId, label: &str) -> bool {
547        match self.nodes.get_mut(&node_id) {
548            Some(node) => {
549                let original_len = node.labels.len();
550                node.labels.retain(|l| l != label);
551
552                if node.labels.len() != original_len {
553                    self.remove_node_label_index(node_id, label);
554                    true
555                } else {
556                    false
557                }
558            }
559            None => false,
560        }
561    }
562
563    fn set_relationship_property(
564        &mut self,
565        rel_id: RelationshipId,
566        key: String,
567        value: PropertyValue,
568    ) -> bool {
569        match self.relationships.get_mut(&rel_id) {
570            Some(rel) => {
571                rel.properties.insert(key, value);
572                true
573            }
574            None => false,
575        }
576    }
577
578    fn remove_relationship_property(&mut self, rel_id: RelationshipId, key: &str) -> bool {
579        match self.relationships.get_mut(&rel_id) {
580            Some(rel) => rel.properties.remove(key).is_some(),
581            None => false,
582        }
583    }
584
585    fn delete_relationship(&mut self, rel_id: RelationshipId) -> bool {
586        match self.relationships.remove(&rel_id) {
587            Some(rel) => {
588                self.detach_relationship_indexes(&rel);
589                true
590            }
591            None => false,
592        }
593    }
594
595    fn delete_node(&mut self, node_id: NodeId) -> bool {
596        if !self.nodes.contains_key(&node_id) {
597            return false;
598        }
599
600        if self.has_incident_relationships(node_id) {
601            return false;
602        }
603
604        let node = match self.nodes.remove(&node_id) {
605            Some(node) => node,
606            None => return false,
607        };
608
609        for label in &node.labels {
610            self.remove_node_label_index(node_id, label);
611        }
612
613        self.outgoing.remove(&node_id);
614        self.incoming.remove(&node_id);
615
616        true
617    }
618
619    fn detach_delete_node(&mut self, node_id: NodeId) -> bool {
620        if !self.nodes.contains_key(&node_id) {
621            return false;
622        }
623
624        let rel_ids: Vec<_> = self
625            .incident_relationship_ids(node_id)
626            .into_iter()
627            .collect();
628
629        for rel_id in rel_ids {
630            let _ = self.delete_relationship(rel_id);
631        }
632
633        self.delete_node(node_id)
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640
641    fn props(pairs: &[(&str, PropertyValue)]) -> Properties {
642        pairs
643            .iter()
644            .map(|(k, v)| ((*k).to_string(), v.clone()))
645            .collect()
646    }
647
648    #[test]
649    fn create_and_lookup_nodes() {
650        let mut g = InMemoryGraph::new();
651
652        let a = g.create_node(
653            vec!["Person".into(), "Employee".into()],
654            props(&[("name", PropertyValue::String("Alice".into()))]),
655        );
656        let b = g.create_node(
657            vec!["Person".into()],
658            props(&[("name", PropertyValue::String("Bob".into()))]),
659        );
660
661        assert_eq!(a.id, 0);
662        assert_eq!(b.id, 1);
663
664        assert_eq!(g.all_nodes().len(), 2);
665        assert_eq!(g.nodes_by_label("Person").len(), 2);
666        assert_eq!(g.nodes_by_label("Employee").len(), 1);
667        assert!(g.node_has_label(a.id, "Person"));
668        assert_eq!(
669            g.node_property(a.id, "name"),
670            Some(PropertyValue::String("Alice".into()))
671        );
672    }
673
674    #[test]
675    fn create_and_expand_relationships() {
676        let mut g = InMemoryGraph::new();
677
678        let a = g.create_node(vec!["Person".into()], Properties::new());
679        let b = g.create_node(vec!["Person".into()], Properties::new());
680        let c = g.create_node(vec!["Company".into()], Properties::new());
681
682        let r1 = g
683            .create_relationship(a.id, b.id, "KNOWS", Properties::new())
684            .unwrap();
685        let r2 = g
686            .create_relationship(a.id, c.id, "WORKS_AT", Properties::new())
687            .unwrap();
688
689        assert_eq!(g.all_relationships().len(), 2);
690        assert_eq!(g.relationships_by_type("KNOWS").len(), 1);
691        assert_eq!(g.outgoing_relationships(a.id).len(), 2);
692        assert_eq!(g.incoming_relationships(b.id).len(), 1);
693
694        let knows = g.expand(a.id, Direction::Right, &[String::from("KNOWS")]);
695        assert_eq!(knows.len(), 1);
696        assert_eq!(knows[0].0.id, r1.id);
697        assert_eq!(knows[0].1.id, b.id);
698
699        let undirected = g.expand(a.id, Direction::Undirected, &[]);
700        assert_eq!(undirected.len(), 2);
701
702        assert_eq!(g.relationship(r2.id).unwrap().dst, c.id);
703    }
704
705    #[test]
706    fn incoming_and_outgoing_are_distinct() {
707        let mut g = InMemoryGraph::new();
708
709        let a = g.create_node(vec!["Person".into()], Properties::new());
710        let b = g.create_node(vec!["Person".into()], Properties::new());
711        let c = g.create_node(vec!["Person".into()], Properties::new());
712
713        g.create_relationship(a.id, b.id, "KNOWS", Properties::new())
714            .unwrap();
715        g.create_relationship(c.id, a.id, "LIKES", Properties::new())
716            .unwrap();
717
718        let outgoing = g.expand(a.id, Direction::Right, &[]);
719        let incoming = g.expand(a.id, Direction::Left, &[]);
720
721        assert_eq!(outgoing.len(), 1);
722        assert_eq!(incoming.len(), 1);
723        assert_eq!(outgoing[0].1.id, b.id);
724        assert_eq!(incoming[0].1.id, c.id);
725    }
726
727    #[test]
728    fn set_and_remove_properties() {
729        let mut g = InMemoryGraph::new();
730
731        let n = g.create_node(vec!["Person".into()], Properties::new());
732        assert!(g.set_node_property(n.id, "age".into(), PropertyValue::Int(42)));
733        assert_eq!(g.node_property(n.id, "age"), Some(PropertyValue::Int(42)));
734        assert!(g.remove_node_property(n.id, "age"));
735        assert_eq!(g.node_property(n.id, "age"), None);
736
737        let m = g.create_node(vec!["Person".into()], Properties::new());
738        let r = g
739            .create_relationship(n.id, m.id, "KNOWS", Properties::new())
740            .unwrap();
741
742        assert!(g.set_relationship_property(r.id, "since".into(), PropertyValue::Int(2020)));
743        assert_eq!(
744            g.relationship_property(r.id, "since"),
745            Some(PropertyValue::Int(2020))
746        );
747        assert!(g.remove_relationship_property(r.id, "since"));
748        assert_eq!(g.relationship_property(r.id, "since"), None);
749    }
750
751    #[test]
752    fn delete_requires_detach() {
753        let mut g = InMemoryGraph::new();
754
755        let a = g.create_node(vec!["Person".into()], Properties::new());
756        let b = g.create_node(vec!["Person".into()], Properties::new());
757        let r = g
758            .create_relationship(a.id, b.id, "KNOWS", Properties::new())
759            .unwrap();
760
761        assert!(!g.delete_node(a.id));
762        assert!(g.delete_relationship(r.id));
763        assert!(g.delete_node(a.id));
764        assert!(g.node(a.id).is_none());
765    }
766
767    #[test]
768    fn detach_delete_removes_incident_relationships() {
769        let mut g = InMemoryGraph::new();
770
771        let a = g.create_node(vec!["Person".into()], Properties::new());
772        let b = g.create_node(vec!["Person".into()], Properties::new());
773        let c = g.create_node(vec!["Person".into()], Properties::new());
774
775        let r1 = g
776            .create_relationship(a.id, b.id, "KNOWS", Properties::new())
777            .unwrap();
778        let r2 = g
779            .create_relationship(c.id, a.id, "LIKES", Properties::new())
780            .unwrap();
781
782        assert!(g.detach_delete_node(a.id));
783        assert!(g.node(a.id).is_none());
784        assert!(g.relationship(r1.id).is_none());
785        assert!(g.relationship(r2.id).is_none());
786        assert_eq!(g.all_relationships().len(), 0);
787    }
788
789    #[test]
790    fn duplicate_labels_are_normalized_on_create() {
791        let mut g = InMemoryGraph::new();
792
793        let n = g.create_node(
794            vec!["Person".into(), "Person".into(), "Admin".into()],
795            Properties::new(),
796        );
797
798        assert_eq!(n.labels, vec!["Person".to_string(), "Admin".to_string()]);
799        assert_eq!(g.nodes_by_label("Person").len(), 1);
800        assert_eq!(g.nodes_by_label("Admin").len(), 1);
801    }
802
803    #[test]
804    fn empty_labels_are_ignored() {
805        let mut g = InMemoryGraph::new();
806
807        let n = g.create_node(
808            vec!["Person".into(), "".into(), "   ".into()],
809            Properties::new(),
810        );
811
812        assert_eq!(n.labels, vec!["Person".to_string()]);
813    }
814
815    #[test]
816    fn empty_relationship_type_is_rejected() {
817        let mut g = InMemoryGraph::new();
818
819        let a = g.create_node(vec!["A".into()], Properties::new());
820        let b = g.create_node(vec!["B".into()], Properties::new());
821
822        assert!(g
823            .create_relationship(a.id, b.id, "", Properties::new())
824            .is_none());
825    }
826
827    #[test]
828    fn storage_schema_helpers_work() {
829        let mut g = InMemoryGraph::new();
830
831        let a = g.create_node(
832            vec!["Person".into()],
833            props(&[("name", PropertyValue::String("Alice".into()))]),
834        );
835        let b = g.create_node(
836            vec!["Company".into()],
837            props(&[("title", PropertyValue::String("Acme".into()))]),
838        );
839
840        g.create_relationship(
841            a.id,
842            b.id,
843            "WORKS_AT",
844            props(&[("since", PropertyValue::Int(2020))]),
845        )
846        .unwrap();
847
848        assert!(g.has_label_name("Person"));
849        assert!(g.has_relationship_type_name("WORKS_AT"));
850        assert!(g.has_property_key("name"));
851        assert!(g.has_property_key("since"));
852        assert!(g.label_has_property_key("Person", "name"));
853        assert!(g.rel_type_has_property_key("WORKS_AT", "since"));
854    }
855}