Skip to main content

issundb_core/graph/
txn.rs

1use super::*;
2
3impl ReadTxn<'_> {
4    pub fn get_node(&self, id: NodeId) -> Result<Option<NodeRecord>, Error> {
5        self.graph.get_node_impl(&self.rtxn, id)
6    }
7
8    pub fn get_edge(&self, id: EdgeId) -> Result<Option<EdgeRecord>, Error> {
9        self.graph.get_edge_impl(&self.rtxn, id)
10    }
11
12    pub fn out_neighbors(&self, node: NodeId) -> Result<Vec<NeighborEntry>, Error> {
13        self.graph.out_neighbors_impl(&self.rtxn, node)
14    }
15
16    pub fn in_neighbors(&self, node: NodeId) -> Result<Vec<NeighborEntry>, Error> {
17        self.graph.in_neighbors_impl(&self.rtxn, node)
18    }
19
20    pub fn nodes_by_label(&self, label: &str) -> Result<Vec<NodeId>, Error> {
21        self.graph.nodes_by_label_impl(&self.rtxn, label)
22    }
23
24    pub fn edges_by_type(&self, etype: &str) -> Result<Vec<EdgeId>, Error> {
25        self.graph.edges_by_type_impl(&self.rtxn, etype)
26    }
27
28    pub fn label_name(&self, id: LabelId) -> Result<Option<String>, Error> {
29        self.graph.label_name_impl(&self.rtxn, id)
30    }
31
32    pub fn type_name(&self, id: TypeId) -> Result<Option<String>, Error> {
33        self.graph.type_name_impl(&self.rtxn, id)
34    }
35
36    pub fn node_count_by_label(&self, label: &str) -> Result<u64, Error> {
37        self.graph.node_count_by_label_impl(&self.rtxn, label)
38    }
39
40    pub fn edge_count_by_type(&self, etype: &str) -> Result<u64, Error> {
41        self.graph.edge_count_by_type_impl(&self.rtxn, etype)
42    }
43
44    pub fn all_nodes(&self) -> Result<Vec<NodeId>, Error> {
45        self.graph.all_nodes_impl(&self.rtxn)
46    }
47
48    #[doc(hidden)]
49    pub fn vector_bytes(&self) -> Result<Vec<(NodeId, Vec<u8>)>, Error> {
50        self.graph.vector_bytes_impl(&self.rtxn)
51    }
52
53    #[doc(hidden)]
54    pub fn get_vector_bytes(&self, n: NodeId) -> Result<Option<Vec<u8>>, Error> {
55        self.graph.get_vector_bytes_impl(&self.rtxn, n)
56    }
57
58    pub fn has_node_property_index(&self, label: &str, property: &str) -> Result<bool, Error> {
59        self.graph
60            .has_node_property_index_impl(&self.rtxn, label, property)
61    }
62
63    pub fn nodes_by_property(
64        &self,
65        label: &str,
66        property: &str,
67        val: PropValue,
68    ) -> Result<Vec<NodeId>, Error> {
69        self.graph
70            .nodes_by_property_impl(&self.rtxn, label, property, val)
71    }
72
73    pub fn nodes_by_property_range(
74        &self,
75        label: &str,
76        property: &str,
77        min_val: Option<PropValue>,
78        min_inclusive: bool,
79        max_val: Option<PropValue>,
80        max_inclusive: bool,
81    ) -> Result<Vec<NodeId>, Error> {
82        self.graph.nodes_by_property_range_impl(
83            &self.rtxn,
84            label,
85            property,
86            min_val,
87            min_inclusive,
88            max_val,
89            max_inclusive,
90        )
91    }
92
93    pub fn edges_by_property(
94        &self,
95        etype: &str,
96        property: &str,
97        val: PropValue,
98    ) -> Result<Vec<EdgeId>, Error> {
99        self.graph
100            .edges_by_property_impl(&self.rtxn, etype, property, val)
101    }
102
103    pub fn edges_by_property_range(
104        &self,
105        etype: &str,
106        property: &str,
107        min_val: Option<PropValue>,
108        max_val: Option<PropValue>,
109    ) -> Result<Vec<EdgeId>, Error> {
110        self.graph
111            .edges_by_property_range_impl(&self.rtxn, etype, property, min_val, max_val)
112    }
113
114    #[doc(hidden)]
115    pub fn has_node_text_index(&self, label: &str, property: &str) -> Result<bool, Error> {
116        self.graph
117            .has_node_text_index_impl(&self.rtxn, label, property)
118    }
119
120    #[doc(hidden)]
121    pub fn fts_stats(&self, label: &str, property: &str) -> Result<Option<(u64, u64)>, Error> {
122        self.graph.fts_stats_impl(&self.rtxn, label, property)
123    }
124
125    #[doc(hidden)]
126    pub fn fts_doc_len(
127        &self,
128        label: &str,
129        property: &str,
130        node_id: NodeId,
131    ) -> Result<Option<u32>, Error> {
132        self.graph
133            .fts_doc_len_impl(&self.rtxn, label, property, node_id)
134    }
135
136    #[doc(hidden)]
137    pub fn fts_postings(
138        &self,
139        label: &str,
140        property: &str,
141        term: &str,
142    ) -> Result<Vec<(NodeId, u32)>, Error> {
143        self.graph
144            .fts_postings_impl(&self.rtxn, label, property, term)
145    }
146
147    #[doc(hidden)]
148    pub fn active_text_indexes(&self) -> Result<Vec<(String, String, Language)>, Error> {
149        self.graph.active_text_indexes_impl(&self.rtxn)
150    }
151}
152
153impl WriteTxn<'_> {
154    pub fn get_node(&self, id: NodeId) -> Result<Option<NodeRecord>, Error> {
155        self.graph.get_node_impl(&self.wtxn, id)
156    }
157
158    pub fn get_edge(&self, id: EdgeId) -> Result<Option<EdgeRecord>, Error> {
159        self.graph.get_edge_impl(&self.wtxn, id)
160    }
161
162    pub fn out_neighbors(&self, node: NodeId) -> Result<Vec<NeighborEntry>, Error> {
163        self.graph.out_neighbors_impl(&self.wtxn, node)
164    }
165
166    pub fn in_neighbors(&self, node: NodeId) -> Result<Vec<NeighborEntry>, Error> {
167        self.graph.in_neighbors_impl(&self.wtxn, node)
168    }
169
170    pub fn nodes_by_label(&self, label: &str) -> Result<Vec<NodeId>, Error> {
171        self.graph.nodes_by_label_impl(&self.wtxn, label)
172    }
173
174    pub fn edges_by_type(&self, etype: &str) -> Result<Vec<EdgeId>, Error> {
175        self.graph.edges_by_type_impl(&self.wtxn, etype)
176    }
177
178    pub fn label_name(&self, id: LabelId) -> Result<Option<String>, Error> {
179        self.graph.label_name_impl(&self.wtxn, id)
180    }
181
182    pub fn type_name(&self, id: TypeId) -> Result<Option<String>, Error> {
183        self.graph.type_name_impl(&self.wtxn, id)
184    }
185
186    pub fn node_count_by_label(&self, label: &str) -> Result<u64, Error> {
187        self.graph.node_count_by_label_impl(&self.wtxn, label)
188    }
189
190    pub fn edge_count_by_type(&self, etype: &str) -> Result<u64, Error> {
191        self.graph.edge_count_by_type_impl(&self.wtxn, etype)
192    }
193
194    pub fn all_nodes(&self) -> Result<Vec<NodeId>, Error> {
195        self.graph.all_nodes_impl(&self.wtxn)
196    }
197
198    #[doc(hidden)]
199    pub fn vector_bytes(&self) -> Result<Vec<(NodeId, Vec<u8>)>, Error> {
200        self.graph.vector_bytes_impl(&self.wtxn)
201    }
202
203    #[doc(hidden)]
204    pub fn get_vector_bytes(&self, n: NodeId) -> Result<Option<Vec<u8>>, Error> {
205        self.graph.get_vector_bytes_impl(&self.wtxn, n)
206    }
207
208    pub fn has_node_property_index(&self, label: &str, property: &str) -> Result<bool, Error> {
209        self.graph
210            .has_node_property_index_impl(&self.wtxn, label, property)
211    }
212
213    pub fn nodes_by_property(
214        &self,
215        label: &str,
216        property: &str,
217        val: PropValue,
218    ) -> Result<Vec<NodeId>, Error> {
219        self.graph
220            .nodes_by_property_impl(&self.wtxn, label, property, val)
221    }
222
223    pub fn nodes_by_property_range(
224        &self,
225        label: &str,
226        property: &str,
227        min_val: Option<PropValue>,
228        min_inclusive: bool,
229        max_val: Option<PropValue>,
230        max_inclusive: bool,
231    ) -> Result<Vec<NodeId>, Error> {
232        self.graph.nodes_by_property_range_impl(
233            &self.wtxn,
234            label,
235            property,
236            min_val,
237            min_inclusive,
238            max_val,
239            max_inclusive,
240        )
241    }
242
243    pub fn edges_by_property(
244        &self,
245        etype: &str,
246        property: &str,
247        val: PropValue,
248    ) -> Result<Vec<EdgeId>, Error> {
249        self.graph
250            .edges_by_property_impl(&self.wtxn, etype, property, val)
251    }
252
253    pub fn edges_by_property_range(
254        &self,
255        etype: &str,
256        property: &str,
257        min_val: Option<PropValue>,
258        max_val: Option<PropValue>,
259    ) -> Result<Vec<EdgeId>, Error> {
260        self.graph
261            .edges_by_property_range_impl(&self.wtxn, etype, property, min_val, max_val)
262    }
263
264    pub fn add_node(&mut self, label: &str, props: &impl Serialize) -> Result<NodeId, Error> {
265        let node_id = self.graph.add_node_impl(&mut self.wtxn, &[label], props)?;
266        self.mutations_count += 1;
267        self.delta.added_nodes.push(node_id);
268        Ok(node_id)
269    }
270
271    /// Insert a node with zero or more labels inside this write transaction.
272    pub fn add_node_multi(
273        &mut self,
274        labels: &[&str],
275        props: &impl Serialize,
276    ) -> Result<NodeId, Error> {
277        let node_id = self.graph.add_node_impl(&mut self.wtxn, labels, props)?;
278        self.mutations_count += 1;
279        self.delta.added_nodes.push(node_id);
280        Ok(node_id)
281    }
282
283    pub fn update_node(&mut self, id: NodeId, props: &impl Serialize) -> Result<(), Error> {
284        self.graph.update_node_impl(&mut self.wtxn, id, props)?;
285        self.mutations_count += 1;
286        self.delta.updated_nodes.push(id);
287        Ok(())
288    }
289
290    /// Add a label to an existing node inside this write transaction.
291    pub fn add_label(&mut self, id: NodeId, label: &str) -> Result<(), Error> {
292        self.graph.add_label_impl(&mut self.wtxn, id, label)?;
293        self.mutations_count += 1;
294        Ok(())
295    }
296
297    /// Remove a label from an existing node inside this write transaction.
298    pub fn remove_label(&mut self, id: NodeId, label: &str) -> Result<(), Error> {
299        self.graph.remove_label_impl(&mut self.wtxn, id, label)?;
300        self.mutations_count += 1;
301        Ok(())
302    }
303
304    pub fn delete_node(&mut self, id: NodeId) -> Result<(), Error> {
305        self.graph.delete_node_impl(&mut self.wtxn, id)?;
306        self.mutations_count += 1;
307        // A node deletion reshuffles the sorted dense-index mapping, so the next
308        // refresh must rebuild fully rather than patch incrementally.
309        self.delta.force_full = true;
310        Ok(())
311    }
312
313    pub fn delete_edge(&mut self, id: EdgeId) -> Result<(), Error> {
314        if let Some((src, dst)) = self.graph.delete_edge_impl(&mut self.wtxn, id)? {
315            self.delta.removed_edges.push((src, dst));
316        }
317        self.mutations_count += 1;
318        Ok(())
319    }
320
321    pub fn add_edge(
322        &mut self,
323        src: NodeId,
324        dst: NodeId,
325        etype: &str,
326        props: &impl Serialize,
327    ) -> Result<EdgeId, Error> {
328        let edge_id = self
329            .graph
330            .add_edge_impl(&mut self.wtxn, src, dst, etype, props)?;
331        self.mutations_count += 1;
332        self.delta.added_edges.push((src, dst));
333        self.delta.added_edge_ids.push(edge_id);
334        Ok(edge_id)
335    }
336
337    #[doc(hidden)]
338    pub fn put_vector_bytes(&mut self, n: NodeId, bytes: &[u8]) -> Result<(), Error> {
339        self.graph.put_vector_bytes_impl(&mut self.wtxn, n, bytes)?;
340        self.mutations_count += 1;
341        Ok(())
342    }
343
344    /// Delete the raw vector bytes for `n` from LMDB. No-op if absent.
345    #[doc(hidden)]
346    pub fn delete_vector_bytes(&mut self, n: NodeId) -> Result<(), Error> {
347        self.graph.delete_vector_bytes_impl(&mut self.wtxn, n)?;
348        self.mutations_count += 1;
349        Ok(())
350    }
351
352    #[doc(hidden)]
353    pub fn create_node_text_index(&mut self, label: &str, property: &str) -> Result<(), Error> {
354        self.graph.create_node_text_index_impl(
355            &mut self.wtxn,
356            label,
357            property,
358            Language::English,
359        )?;
360        self.mutations_count += 1;
361        Ok(())
362    }
363
364    #[doc(hidden)]
365    pub fn drop_node_text_index(&mut self, label: &str, property: &str) -> Result<(), Error> {
366        self.graph
367            .drop_node_text_index_impl(&mut self.wtxn, label, property)?;
368        self.mutations_count += 1;
369        Ok(())
370    }
371
372    #[doc(hidden)]
373    pub fn has_node_text_index(&self, label: &str, property: &str) -> Result<bool, Error> {
374        let rtxn: &heed::RoTxn = &self.wtxn;
375        self.graph.has_node_text_index_impl(rtxn, label, property)
376    }
377
378    #[doc(hidden)]
379    pub fn fts_stats(&self, label: &str, property: &str) -> Result<Option<(u64, u64)>, Error> {
380        let rtxn: &heed::RoTxn = &self.wtxn;
381        self.graph.fts_stats_impl(rtxn, label, property)
382    }
383
384    #[doc(hidden)]
385    pub fn fts_doc_len(
386        &self,
387        label: &str,
388        property: &str,
389        node_id: NodeId,
390    ) -> Result<Option<u32>, Error> {
391        let rtxn: &heed::RoTxn = &self.wtxn;
392        self.graph.fts_doc_len_impl(rtxn, label, property, node_id)
393    }
394
395    #[doc(hidden)]
396    pub fn fts_postings(
397        &self,
398        label: &str,
399        property: &str,
400        term: &str,
401    ) -> Result<Vec<(NodeId, u32)>, Error> {
402        let rtxn: &heed::RoTxn = &self.wtxn;
403        self.graph.fts_postings_impl(rtxn, label, property, term)
404    }
405
406    #[doc(hidden)]
407    pub fn create_node_text_index_with_language(
408        &mut self,
409        label: &str,
410        property: &str,
411        lang: Language,
412    ) -> Result<(), Error> {
413        self.graph
414            .create_node_text_index_impl(&mut self.wtxn, label, property, lang)?;
415        self.mutations_count += 1;
416        Ok(())
417    }
418
419    #[doc(hidden)]
420    pub fn active_text_indexes(&self) -> Result<Vec<(String, String, Language)>, Error> {
421        let rtxn: &heed::RoTxn = &self.wtxn;
422        self.graph.active_text_indexes_impl(rtxn)
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use serde_json::json;
429    use tempfile::TempDir;
430
431    use super::*;
432
433    fn open_tmp() -> (TempDir, Graph) {
434        let dir = TempDir::new().unwrap();
435        let g = Graph::open(dir.path(), 1).unwrap();
436        (dir, g)
437    }
438
439    #[test]
440    fn test_transaction_read_only() {
441        let (_dir, g) = open_tmp();
442        let a = g.add_node("Person", &json!({"name": "Alice"})).unwrap();
443        let b = g.add_node("Person", &json!({"name": "Bob"})).unwrap();
444
445        g.view(|txn| {
446            let node_a = txn.get_node(a).unwrap().unwrap();
447            let props_a: serde_json::Value = rmp_serde::from_slice(&node_a.props).unwrap();
448            assert_eq!(props_a["name"], "Alice");
449
450            let node_b = txn.get_node(b).unwrap().unwrap();
451            let props_b: serde_json::Value = rmp_serde::from_slice(&node_b.props).unwrap();
452            assert_eq!(props_b["name"], "Bob");
453
454            let nodes = txn.all_nodes().unwrap();
455            assert_eq!(nodes.len(), 2);
456
457            Ok(())
458        })
459        .unwrap();
460    }
461
462    #[test]
463    fn test_transaction_write_commit() {
464        let (_dir, g) = open_tmp();
465
466        let (a, b) = g
467            .update(|txn| {
468                let a = txn.add_node("Person", &json!({"name": "Alice"})).unwrap();
469                let b = txn.add_node("Person", &json!({"name": "Bob"})).unwrap();
470                txn.add_edge(a, b, "KNOWS", &json!({"since": 2020}))
471                    .unwrap();
472                Ok((a, b))
473            })
474            .unwrap();
475
476        // After commit, data should be in the DB
477        let node_a = g.get_node(a).unwrap().unwrap();
478        let props_a: serde_json::Value = rmp_serde::from_slice(&node_a.props).unwrap();
479        assert_eq!(props_a["name"], "Alice");
480
481        let neighbors = g.out_neighbors(a).unwrap();
482        assert_eq!(neighbors.len(), 1);
483        assert_eq!(neighbors[0].node, b);
484    }
485
486    #[test]
487    fn test_transaction_write_rollback() {
488        let (_dir, g) = open_tmp();
489
490        let res: Result<(), Error> = g.update(|txn| {
491            txn.add_node("Person", &json!({"name": "Alice"})).unwrap();
492            // Intentionally fail the transaction
493            Err(Error::Corrupt("simulated failure"))
494        });
495
496        assert!(res.is_err());
497
498        // The node should NOT be present in the database since the transaction rolled back
499        let nodes = g.all_nodes().unwrap();
500        assert_eq!(nodes.len(), 0);
501    }
502
503    // --- bfs_multi_source_graphblas ---
504    //
505    // Each test calls `rebuild_csr()` after mutating the graph so the GraphBLAS
506    // adjacency matrix reflects the inserted edges before BFS is invoked.
507
508    #[test]
509    fn graphblas_multi_source_empty_seeds_returns_empty() {
510        let (_dir, g) = open_tmp();
511        g.add_node("N", &json!({})).unwrap();
512        g.rebuild_csr().unwrap();
513        let result = g.bfs_multi_source_graphblas(&[], 2, None).unwrap();
514        assert!(result.is_empty());
515    }
516
517    #[test]
518    fn graphblas_multi_source_hops_zero_returns_only_seeds() {
519        let (_dir, g) = open_tmp();
520        let a = g.add_node("N", &json!({})).unwrap();
521        let b = g.add_node("N", &json!({})).unwrap();
522        let c = g.add_node("N", &json!({})).unwrap();
523        g.add_edge(a, c, "E", &json!({})).unwrap();
524        g.rebuild_csr().unwrap();
525
526        let mut result = g.bfs_multi_source_graphblas(&[a, b], 0, None).unwrap();
527        result.sort_unstable();
528        assert_eq!(result, vec![a, b]);
529        assert!(!result.contains(&c));
530    }
531
532    #[test]
533    fn graphblas_multi_source_expands_to_correct_depth() {
534        let (_dir, g) = open_tmp();
535        // Chain: a → b → c → d
536        let a = g.add_node("N", &json!({})).unwrap();
537        let b = g.add_node("N", &json!({})).unwrap();
538        let c = g.add_node("N", &json!({})).unwrap();
539        let d = g.add_node("N", &json!({})).unwrap();
540        g.add_edge(a, b, "E", &json!({})).unwrap();
541        g.add_edge(b, c, "E", &json!({})).unwrap();
542        g.add_edge(c, d, "E", &json!({})).unwrap();
543        g.rebuild_csr().unwrap();
544
545        let r1 = g.bfs_multi_source_graphblas(&[a], 1, None).unwrap();
546        assert!(r1.contains(&a));
547        assert!(r1.contains(&b));
548        assert!(!r1.contains(&c));
549        assert!(!r1.contains(&d));
550
551        let r2 = g.bfs_multi_source_graphblas(&[a], 2, None).unwrap();
552        assert!(r2.contains(&a));
553        assert!(r2.contains(&b));
554        assert!(r2.contains(&c));
555        assert!(!r2.contains(&d));
556    }
557
558    #[test]
559    fn graphblas_multi_source_max_nodes_cap_respected() {
560        let (_dir, g) = open_tmp();
561        // Star + tail: a → b, c, d; b → e
562        let a = g.add_node("N", &json!({})).unwrap();
563        let b = g.add_node("N", &json!({})).unwrap();
564        let c = g.add_node("N", &json!({})).unwrap();
565        let d = g.add_node("N", &json!({})).unwrap();
566        let e = g.add_node("N", &json!({})).unwrap();
567        g.add_edge(a, b, "E", &json!({})).unwrap();
568        g.add_edge(a, c, "E", &json!({})).unwrap();
569        g.add_edge(a, d, "E", &json!({})).unwrap();
570        g.add_edge(b, e, "E", &json!({})).unwrap();
571        g.rebuild_csr().unwrap();
572
573        let result = g.bfs_multi_source_graphblas(&[a], 2, Some(3)).unwrap();
574        assert!(
575            result.len() <= 3,
576            "expected at most 3 nodes, got {}",
577            result.len()
578        );
579    }
580
581    #[test]
582    fn graphblas_multi_source_two_seeds_union_disconnected_components() {
583        let (_dir, g) = open_tmp();
584        // Two disconnected chains: a → b; c → d
585        let a = g.add_node("N", &json!({})).unwrap();
586        let b = g.add_node("N", &json!({})).unwrap();
587        let c = g.add_node("N", &json!({})).unwrap();
588        let d = g.add_node("N", &json!({})).unwrap();
589        g.add_edge(a, b, "E", &json!({})).unwrap();
590        g.add_edge(c, d, "E", &json!({})).unwrap();
591        g.rebuild_csr().unwrap();
592
593        let result = g.bfs_multi_source_graphblas(&[a, c], 1, None).unwrap();
594        assert!(result.contains(&a));
595        assert!(result.contains(&b));
596        assert!(result.contains(&c));
597        assert!(result.contains(&d));
598    }
599
600    #[test]
601    fn graphblas_multi_source_deduplicates_shared_neighbors() {
602        let (_dir, g) = open_tmp();
603        // a → c; b → c; c must appear once.
604        let a = g.add_node("N", &json!({})).unwrap();
605        let b = g.add_node("N", &json!({})).unwrap();
606        let c = g.add_node("N", &json!({})).unwrap();
607        g.add_edge(a, c, "E", &json!({})).unwrap();
608        g.add_edge(b, c, "E", &json!({})).unwrap();
609        g.rebuild_csr().unwrap();
610
611        let result = g.bfs_multi_source_graphblas(&[a, b], 1, None).unwrap();
612        let count_c = result.iter().filter(|&&n| n == c).count();
613        assert_eq!(count_c, 1);
614        assert_eq!(result.len(), 3); // a, b, c
615    }
616
617    #[test]
618    fn graphblas_multi_source_handles_newly_added_seeds_via_dynamic_materialization() {
619        let (_dir, g) = open_tmp();
620        // Seed a is in the CSR; b is added after rebuild_csr (making snapshot/matrices stale).
621        // The function must detect the new nodes, dynamically rebuild the CSR/matrices, and run successfully.
622        let a = g.add_node("N", &json!({})).unwrap();
623        let c = g.add_node("N", &json!({})).unwrap();
624        g.add_edge(a, c, "E", &json!({})).unwrap();
625        g.rebuild_csr().unwrap();
626
627        // b is inserted AFTER rebuild, so it makes the existing MatrixSet stale.
628        let b = g.add_node("N", &json!({})).unwrap();
629        let d = g.add_node("N", &json!({})).unwrap();
630        g.add_edge(b, d, "E", &json!({})).unwrap();
631
632        // Both seeds must appear in the result; d must be reachable from b via the dynamically rematerialized matrices.
633        let result = g.bfs_multi_source_graphblas(&[a, b], 1, None).unwrap();
634        assert!(result.contains(&a), "seed a must be present");
635        assert!(result.contains(&b), "seed b must be present");
636        assert!(result.contains(&c), "c reachable from a");
637        assert!(result.contains(&d), "d reachable from b");
638    }
639
640    // --- node_count_by_label / edge_count_by_type stats ---
641
642    #[test]
643    fn label_count_increments_on_add_node() {
644        let (_dir, g) = open_tmp();
645        assert_eq!(g.node_count_by_label("Person").unwrap(), 0);
646        g.add_node("Person", &json!({})).unwrap();
647        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
648        g.add_node("Person", &json!({})).unwrap();
649        assert_eq!(g.node_count_by_label("Person").unwrap(), 2);
650        // Other labels are not affected.
651        assert_eq!(g.node_count_by_label("Company").unwrap(), 0);
652    }
653
654    #[test]
655    fn label_count_decrements_on_delete_node() {
656        let (_dir, g) = open_tmp();
657        let a = g.add_node("Person", &json!({})).unwrap();
658        let b = g.add_node("Person", &json!({})).unwrap();
659        assert_eq!(g.node_count_by_label("Person").unwrap(), 2);
660
661        g.delete_node(a).unwrap();
662        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
663
664        g.delete_node(b).unwrap();
665        assert_eq!(g.node_count_by_label("Person").unwrap(), 0);
666
667        // Deleting a non-existent node is a no-op; count stays at 0.
668        g.delete_node(b).unwrap();
669        assert_eq!(g.node_count_by_label("Person").unwrap(), 0);
670    }
671
672    #[test]
673    fn label_count_unchanged_on_update_node() {
674        let (_dir, g) = open_tmp();
675        let id = g.add_node("Person", &json!({})).unwrap();
676        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
677
678        // update_node does not change the label; the count must stay at 1.
679        g.update_node(id, &json!({"name": "Alice"})).unwrap();
680        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
681    }
682
683    #[test]
684    fn update_node_returns_not_found_for_missing_node() {
685        let (_dir, g) = open_tmp();
686        let res = g.update_node(9999, &json!({}));
687        assert!(matches!(res, Err(Error::NodeNotFound(9999))));
688    }
689
690    #[test]
691    fn type_count_increments_on_add_edge() {
692        let (_dir, g) = open_tmp();
693        let a = g.add_node("N", &json!({})).unwrap();
694        let b = g.add_node("N", &json!({})).unwrap();
695        let c = g.add_node("N", &json!({})).unwrap();
696        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
697
698        g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
699        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 1);
700
701        g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
702        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 2);
703
704        // Different type is not affected.
705        assert_eq!(g.edge_count_by_type("WORKS_AT").unwrap(), 0);
706    }
707
708    #[test]
709    fn type_count_decrements_on_delete_node_cascade() {
710        let (_dir, g) = open_tmp();
711        let a = g.add_node("N", &json!({})).unwrap();
712        let b = g.add_node("N", &json!({})).unwrap();
713        g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
714        g.add_edge(b, a, "KNOWS", &json!({})).unwrap();
715        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 2);
716
717        // Deleting node a cascades and removes both edges touching a.
718        g.delete_node(a).unwrap();
719        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
720    }
721
722    #[test]
723    fn delete_edge_correctness() {
724        let (_dir, g) = open_tmp();
725        let a = g.add_node("Person", &json!({})).unwrap();
726        let b = g.add_node("Person", &json!({})).unwrap();
727        let eid = g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
728
729        // 1. Verify exists
730        assert!(g.get_edge(eid).unwrap().is_some());
731        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 1);
732
733        // 2. Verify adjacency lists
734        let out_neighs = g.out_neighbors(a).unwrap();
735        assert_eq!(out_neighs.len(), 1);
736        assert_eq!(out_neighs[0].node, b);
737        assert_eq!(out_neighs[0].edge, eid);
738
739        let in_neighs = g.in_neighbors(b).unwrap();
740        assert_eq!(in_neighs.len(), 1);
741        assert_eq!(in_neighs[0].node, a);
742        assert_eq!(in_neighs[0].edge, eid);
743
744        // 3. Delete the edge
745        g.delete_edge(eid).unwrap();
746
747        // 4. Verify gone
748        assert!(g.get_edge(eid).unwrap().is_none());
749        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
750
751        // 5. Verify adjacency lists updated
752        assert_eq!(g.out_neighbors(a).unwrap().len(), 0);
753        assert_eq!(g.in_neighbors(b).unwrap().len(), 0);
754
755        // 6. Idempotence: delete non-existent edge
756        g.delete_edge(eid).unwrap();
757        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
758    }
759
760    #[test]
761    fn test_node_property_secondary_index_and_scans() {
762        let (_dir, g) = open_tmp();
763
764        // Add nodes
765        let n1 = g
766            .add_node("Person", &json!({"name": "Alice", "age": 30}))
767            .unwrap();
768        let n2 = g
769            .add_node("Person", &json!({"name": "Bob", "age": 25}))
770            .unwrap();
771        let n3 = g
772            .add_node("Person", &json!({"name": "Charlie", "age": 30}))
773            .unwrap();
774        let _n4 = g
775            .add_node("Employee", &json!({"name": "Alice", "age": 40}))
776            .unwrap();
777
778        // Create index on Person(age)
779        g.create_node_property_index("Person", "age").unwrap();
780
781        // Check index exists
782        assert!(g.has_node_property_index("Person", "age").unwrap());
783
784        // Point queries
785        let p30 = g
786            .nodes_by_property("Person", "age", PropValue::Int(30))
787            .unwrap();
788        assert_eq!(p30.len(), 2);
789        assert!(p30.contains(&n1));
790        assert!(p30.contains(&n3));
791
792        let p25 = g
793            .nodes_by_property("Person", "age", PropValue::Int(25))
794            .unwrap();
795        assert_eq!(p25.len(), 1);
796        assert!(p25.contains(&n2));
797
798        // Range queries (e.g. age between 20 and 28)
799        let pr = g
800            .nodes_by_property_range(
801                "Person",
802                "age",
803                Some(PropValue::Int(20)),
804                true,
805                Some(PropValue::Int(28)),
806                true,
807            )
808            .unwrap();
809        assert_eq!(pr.len(), 1);
810        assert!(pr.contains(&n2));
811
812        // Let's create an index on Person(name) to test string sorting/prefix
813        g.create_node_property_index("Person", "name").unwrap();
814        let p_alice = g
815            .nodes_by_property("Person", "name", PropValue::Str("Alice".to_string()))
816            .unwrap();
817        assert_eq!(p_alice.len(), 1);
818        assert!(p_alice.contains(&n1));
819    }
820
821    #[test]
822    fn test_unique_property_constraint() {
823        let (_dir, g) = open_tmp();
824
825        // Create unique constraint on User(email)
826        g.create_node_unique_constraint("User", "email").unwrap();
827
828        // Add first user
829        let _u1 = g
830            .add_node(
831                "User",
832                &json!({"email": "user1@example.com", "name": "User 1"}),
833            )
834            .unwrap();
835
836        // Add second user with duplicate email - should fail
837        let res2 = g.add_node(
838            "User",
839            &json!({"email": "user1@example.com", "name": "User 2"}),
840        );
841        assert!(res2.is_err());
842        assert!(matches!(
843            res2.unwrap_err(),
844            Error::UniqueConstraintViolation { .. }
845        ));
846
847        // Add second user with unique email - should succeed
848        let u2 = g
849            .add_node(
850                "User",
851                &json!({"email": "user2@example.com", "name": "User 2"}),
852            )
853            .unwrap();
854
855        // Update u2 to have u1's email - should fail
856        let update_res =
857            g.update_node(u2, &json!({"email": "user1@example.com", "name": "User 2"}));
858        assert!(update_res.is_err());
859        assert!(matches!(
860            update_res.unwrap_err(),
861            Error::UniqueConstraintViolation { .. }
862        ));
863    }
864
865    #[test]
866    fn test_required_property_constraint() {
867        let (_dir, g) = open_tmp();
868
869        // Create required constraint on Task(title)
870        g.create_node_required_constraint("Task", "title").unwrap();
871
872        // Add task with title - should succeed
873        let t1 = g
874            .add_node("Task", &json!({"title": "Do homework", "done": false}))
875            .unwrap();
876
877        // Add task without title - should fail
878        let res2 = g.add_node("Task", &json!({"done": false}));
879        assert!(res2.is_err());
880        assert!(matches!(
881            res2.unwrap_err(),
882            Error::RequiredConstraintViolation { .. }
883        ));
884
885        // Update t1 to remove title - should fail
886        let update_res = g.update_node(t1, &json!({"done": true}));
887        assert!(update_res.is_err());
888        assert!(matches!(
889            update_res.unwrap_err(),
890            Error::RequiredConstraintViolation { .. }
891        ));
892    }
893
894    #[test]
895    fn test_index_cleanup_on_delete() {
896        let (_dir, g) = open_tmp();
897
898        // Create unique index on Account(number)
899        g.create_node_unique_constraint("Account", "number")
900            .unwrap();
901
902        let a1 = g.add_node("Account", &json!({"number": "12345"})).unwrap();
903
904        // Delete a1
905        g.delete_node(a1).unwrap();
906
907        // Now we should be able to reuse the account number because index was cleaned up!
908        let a2 = g.add_node("Account", &json!({"number": "12345"}));
909        assert!(a2.is_ok());
910    }
911
912    #[test]
913    fn backup_and_restore_roundtrip() {
914        let dir = TempDir::new().unwrap();
915        let backup_file = dir.path().join("snapshot.mdb");
916        let restore_dir = dir.path().join("restored");
917
918        // Write data.
919        let n;
920        {
921            let g = Graph::open(&dir.path().join("primary"), 1).unwrap();
922            n = g
923                .add_node("BackupTest", &serde_json::json!({"x": 42}))
924                .unwrap();
925            g.backup(&backup_file).unwrap();
926        }
927
928        // Restore and verify.
929        Graph::restore(&backup_file, &restore_dir).unwrap();
930        let g2 = Graph::open(&restore_dir, 1).unwrap();
931        let rec = g2
932            .get_node(n)
933            .unwrap()
934            .expect("node must exist in restored graph");
935        let props: serde_json::Value = rmp_serde::from_slice(&rec.props).unwrap();
936        assert_eq!(props["x"], serde_json::json!(42));
937    }
938
939    #[test]
940    fn backup_compact_and_restore_roundtrip() {
941        let dir = TempDir::new().unwrap();
942        let backup_file = dir.path().join("compact.mdb");
943        let restore_dir = dir.path().join("restored");
944
945        // Write data, delete some of it, then take a compacted snapshot.
946        let kept;
947        {
948            let g = Graph::open(&dir.path().join("primary"), 1).unwrap();
949            let doomed = g
950                .add_node("BackupTest", &serde_json::json!({"x": 1}))
951                .unwrap();
952            kept = g
953                .add_node("BackupTest", &serde_json::json!({"x": 42}))
954                .unwrap();
955            g.delete_node(doomed).unwrap();
956            g.backup_compact(&backup_file).unwrap();
957        }
958
959        // Restore and verify the surviving data round-trips.
960        Graph::restore(&backup_file, &restore_dir).unwrap();
961        let g2 = Graph::open(&restore_dir, 1).unwrap();
962        let rec = g2
963            .get_node(kept)
964            .unwrap()
965            .expect("node must exist in restored graph");
966        let props: serde_json::Value = rmp_serde::from_slice(&rec.props).unwrap();
967        assert_eq!(props["x"], serde_json::json!(42));
968        assert_eq!(g2.nodes_by_label("BackupTest").unwrap(), vec![kept]);
969    }
970}