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        Ok(edge_id)
334    }
335
336    #[doc(hidden)]
337    pub fn put_vector_bytes(&mut self, n: NodeId, bytes: &[u8]) -> Result<(), Error> {
338        self.graph.put_vector_bytes_impl(&mut self.wtxn, n, bytes)?;
339        self.mutations_count += 1;
340        Ok(())
341    }
342
343    /// Delete the raw vector bytes for `n` from LMDB. No-op if absent.
344    #[doc(hidden)]
345    pub fn delete_vector_bytes(&mut self, n: NodeId) -> Result<(), Error> {
346        self.graph.delete_vector_bytes_impl(&mut self.wtxn, n)?;
347        self.mutations_count += 1;
348        Ok(())
349    }
350
351    #[doc(hidden)]
352    pub fn create_node_text_index(&mut self, label: &str, property: &str) -> Result<(), Error> {
353        self.graph.create_node_text_index_impl(
354            &mut self.wtxn,
355            label,
356            property,
357            Language::English,
358        )?;
359        self.mutations_count += 1;
360        Ok(())
361    }
362
363    #[doc(hidden)]
364    pub fn drop_node_text_index(&mut self, label: &str, property: &str) -> Result<(), Error> {
365        self.graph
366            .drop_node_text_index_impl(&mut self.wtxn, label, property)?;
367        self.mutations_count += 1;
368        Ok(())
369    }
370
371    #[doc(hidden)]
372    pub fn has_node_text_index(&self, label: &str, property: &str) -> Result<bool, Error> {
373        let rtxn: &heed::RoTxn = &self.wtxn;
374        self.graph.has_node_text_index_impl(rtxn, label, property)
375    }
376
377    #[doc(hidden)]
378    pub fn fts_stats(&self, label: &str, property: &str) -> Result<Option<(u64, u64)>, Error> {
379        let rtxn: &heed::RoTxn = &self.wtxn;
380        self.graph.fts_stats_impl(rtxn, label, property)
381    }
382
383    #[doc(hidden)]
384    pub fn fts_doc_len(
385        &self,
386        label: &str,
387        property: &str,
388        node_id: NodeId,
389    ) -> Result<Option<u32>, Error> {
390        let rtxn: &heed::RoTxn = &self.wtxn;
391        self.graph.fts_doc_len_impl(rtxn, label, property, node_id)
392    }
393
394    #[doc(hidden)]
395    pub fn fts_postings(
396        &self,
397        label: &str,
398        property: &str,
399        term: &str,
400    ) -> Result<Vec<(NodeId, u32)>, Error> {
401        let rtxn: &heed::RoTxn = &self.wtxn;
402        self.graph.fts_postings_impl(rtxn, label, property, term)
403    }
404
405    #[doc(hidden)]
406    pub fn create_node_text_index_with_language(
407        &mut self,
408        label: &str,
409        property: &str,
410        lang: Language,
411    ) -> Result<(), Error> {
412        self.graph
413            .create_node_text_index_impl(&mut self.wtxn, label, property, lang)?;
414        self.mutations_count += 1;
415        Ok(())
416    }
417
418    #[doc(hidden)]
419    pub fn active_text_indexes(&self) -> Result<Vec<(String, String, Language)>, Error> {
420        let rtxn: &heed::RoTxn = &self.wtxn;
421        self.graph.active_text_indexes_impl(rtxn)
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use serde_json::json;
428    use tempfile::TempDir;
429
430    use super::*;
431
432    fn open_tmp() -> (TempDir, Graph) {
433        let dir = TempDir::new().unwrap();
434        let g = Graph::open(dir.path(), 1).unwrap();
435        (dir, g)
436    }
437
438    #[test]
439    fn test_transaction_read_only() {
440        let (_dir, g) = open_tmp();
441        let a = g.add_node("Person", &json!({"name": "Alice"})).unwrap();
442        let b = g.add_node("Person", &json!({"name": "Bob"})).unwrap();
443
444        g.view(|txn| {
445            let node_a = txn.get_node(a).unwrap().unwrap();
446            let props_a: serde_json::Value = rmp_serde::from_slice(&node_a.props).unwrap();
447            assert_eq!(props_a["name"], "Alice");
448
449            let node_b = txn.get_node(b).unwrap().unwrap();
450            let props_b: serde_json::Value = rmp_serde::from_slice(&node_b.props).unwrap();
451            assert_eq!(props_b["name"], "Bob");
452
453            let nodes = txn.all_nodes().unwrap();
454            assert_eq!(nodes.len(), 2);
455
456            Ok(())
457        })
458        .unwrap();
459    }
460
461    #[test]
462    fn test_transaction_write_commit() {
463        let (_dir, g) = open_tmp();
464
465        let (a, b) = g
466            .update(|txn| {
467                let a = txn.add_node("Person", &json!({"name": "Alice"})).unwrap();
468                let b = txn.add_node("Person", &json!({"name": "Bob"})).unwrap();
469                txn.add_edge(a, b, "KNOWS", &json!({"since": 2020}))
470                    .unwrap();
471                Ok((a, b))
472            })
473            .unwrap();
474
475        // After commit, data should be in the DB
476        let node_a = g.get_node(a).unwrap().unwrap();
477        let props_a: serde_json::Value = rmp_serde::from_slice(&node_a.props).unwrap();
478        assert_eq!(props_a["name"], "Alice");
479
480        let neighbors = g.out_neighbors(a).unwrap();
481        assert_eq!(neighbors.len(), 1);
482        assert_eq!(neighbors[0].node, b);
483    }
484
485    #[test]
486    fn test_transaction_write_rollback() {
487        let (_dir, g) = open_tmp();
488
489        let res: Result<(), Error> = g.update(|txn| {
490            txn.add_node("Person", &json!({"name": "Alice"})).unwrap();
491            // Intentionally fail the transaction
492            Err(Error::Corrupt("simulated failure"))
493        });
494
495        assert!(res.is_err());
496
497        // The node should NOT be present in the database since the transaction rolled back
498        let nodes = g.all_nodes().unwrap();
499        assert_eq!(nodes.len(), 0);
500    }
501
502    // --- bfs_multi_source_graphblas ---
503    //
504    // Each test calls `rebuild_csr()` after mutating the graph so the GraphBLAS
505    // adjacency matrix reflects the inserted edges before BFS is invoked.
506
507    #[test]
508    fn graphblas_multi_source_empty_seeds_returns_empty() {
509        let (_dir, g) = open_tmp();
510        g.add_node("N", &json!({})).unwrap();
511        g.rebuild_csr().unwrap();
512        let result = g.bfs_multi_source_graphblas(&[], 2, None).unwrap();
513        assert!(result.is_empty());
514    }
515
516    #[test]
517    fn graphblas_multi_source_hops_zero_returns_only_seeds() {
518        let (_dir, g) = open_tmp();
519        let a = g.add_node("N", &json!({})).unwrap();
520        let b = g.add_node("N", &json!({})).unwrap();
521        let c = g.add_node("N", &json!({})).unwrap();
522        g.add_edge(a, c, "E", &json!({})).unwrap();
523        g.rebuild_csr().unwrap();
524
525        let mut result = g.bfs_multi_source_graphblas(&[a, b], 0, None).unwrap();
526        result.sort_unstable();
527        assert_eq!(result, vec![a, b]);
528        assert!(!result.contains(&c));
529    }
530
531    #[test]
532    fn graphblas_multi_source_expands_to_correct_depth() {
533        let (_dir, g) = open_tmp();
534        // Chain: a → b → c → d
535        let a = g.add_node("N", &json!({})).unwrap();
536        let b = g.add_node("N", &json!({})).unwrap();
537        let c = g.add_node("N", &json!({})).unwrap();
538        let d = g.add_node("N", &json!({})).unwrap();
539        g.add_edge(a, b, "E", &json!({})).unwrap();
540        g.add_edge(b, c, "E", &json!({})).unwrap();
541        g.add_edge(c, d, "E", &json!({})).unwrap();
542        g.rebuild_csr().unwrap();
543
544        let r1 = g.bfs_multi_source_graphblas(&[a], 1, None).unwrap();
545        assert!(r1.contains(&a));
546        assert!(r1.contains(&b));
547        assert!(!r1.contains(&c));
548        assert!(!r1.contains(&d));
549
550        let r2 = g.bfs_multi_source_graphblas(&[a], 2, None).unwrap();
551        assert!(r2.contains(&a));
552        assert!(r2.contains(&b));
553        assert!(r2.contains(&c));
554        assert!(!r2.contains(&d));
555    }
556
557    #[test]
558    fn graphblas_multi_source_max_nodes_cap_respected() {
559        let (_dir, g) = open_tmp();
560        // Star + tail: a → b, c, d; b → e
561        let a = g.add_node("N", &json!({})).unwrap();
562        let b = g.add_node("N", &json!({})).unwrap();
563        let c = g.add_node("N", &json!({})).unwrap();
564        let d = g.add_node("N", &json!({})).unwrap();
565        let e = g.add_node("N", &json!({})).unwrap();
566        g.add_edge(a, b, "E", &json!({})).unwrap();
567        g.add_edge(a, c, "E", &json!({})).unwrap();
568        g.add_edge(a, d, "E", &json!({})).unwrap();
569        g.add_edge(b, e, "E", &json!({})).unwrap();
570        g.rebuild_csr().unwrap();
571
572        let result = g.bfs_multi_source_graphblas(&[a], 2, Some(3)).unwrap();
573        assert!(
574            result.len() <= 3,
575            "expected at most 3 nodes, got {}",
576            result.len()
577        );
578    }
579
580    #[test]
581    fn graphblas_multi_source_two_seeds_union_disconnected_components() {
582        let (_dir, g) = open_tmp();
583        // Two disconnected chains: a → b; c → d
584        let a = g.add_node("N", &json!({})).unwrap();
585        let b = g.add_node("N", &json!({})).unwrap();
586        let c = g.add_node("N", &json!({})).unwrap();
587        let d = g.add_node("N", &json!({})).unwrap();
588        g.add_edge(a, b, "E", &json!({})).unwrap();
589        g.add_edge(c, d, "E", &json!({})).unwrap();
590        g.rebuild_csr().unwrap();
591
592        let result = g.bfs_multi_source_graphblas(&[a, c], 1, None).unwrap();
593        assert!(result.contains(&a));
594        assert!(result.contains(&b));
595        assert!(result.contains(&c));
596        assert!(result.contains(&d));
597    }
598
599    #[test]
600    fn graphblas_multi_source_deduplicates_shared_neighbors() {
601        let (_dir, g) = open_tmp();
602        // a → c; b → c; c must appear once.
603        let a = g.add_node("N", &json!({})).unwrap();
604        let b = g.add_node("N", &json!({})).unwrap();
605        let c = g.add_node("N", &json!({})).unwrap();
606        g.add_edge(a, c, "E", &json!({})).unwrap();
607        g.add_edge(b, c, "E", &json!({})).unwrap();
608        g.rebuild_csr().unwrap();
609
610        let result = g.bfs_multi_source_graphblas(&[a, b], 1, None).unwrap();
611        let count_c = result.iter().filter(|&&n| n == c).count();
612        assert_eq!(count_c, 1);
613        assert_eq!(result.len(), 3); // a, b, c
614    }
615
616    #[test]
617    fn graphblas_multi_source_handles_newly_added_seeds_via_dynamic_materialization() {
618        let (_dir, g) = open_tmp();
619        // Seed a is in the CSR; b is added after rebuild_csr (making snapshot/matrices stale).
620        // The function must detect the new nodes, dynamically rebuild the CSR/matrices, and run successfully.
621        let a = g.add_node("N", &json!({})).unwrap();
622        let c = g.add_node("N", &json!({})).unwrap();
623        g.add_edge(a, c, "E", &json!({})).unwrap();
624        g.rebuild_csr().unwrap();
625
626        // b is inserted AFTER rebuild, so it makes the existing MatrixSet stale.
627        let b = g.add_node("N", &json!({})).unwrap();
628        let d = g.add_node("N", &json!({})).unwrap();
629        g.add_edge(b, d, "E", &json!({})).unwrap();
630
631        // Both seeds must appear in the result; d must be reachable from b via the dynamically rematerialized matrices.
632        let result = g.bfs_multi_source_graphblas(&[a, b], 1, None).unwrap();
633        assert!(result.contains(&a), "seed a must be present");
634        assert!(result.contains(&b), "seed b must be present");
635        assert!(result.contains(&c), "c reachable from a");
636        assert!(result.contains(&d), "d reachable from b");
637    }
638
639    // --- node_count_by_label / edge_count_by_type stats ---
640
641    #[test]
642    fn label_count_increments_on_add_node() {
643        let (_dir, g) = open_tmp();
644        assert_eq!(g.node_count_by_label("Person").unwrap(), 0);
645        g.add_node("Person", &json!({})).unwrap();
646        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
647        g.add_node("Person", &json!({})).unwrap();
648        assert_eq!(g.node_count_by_label("Person").unwrap(), 2);
649        // Other labels are not affected.
650        assert_eq!(g.node_count_by_label("Company").unwrap(), 0);
651    }
652
653    #[test]
654    fn label_count_decrements_on_delete_node() {
655        let (_dir, g) = open_tmp();
656        let a = g.add_node("Person", &json!({})).unwrap();
657        let b = g.add_node("Person", &json!({})).unwrap();
658        assert_eq!(g.node_count_by_label("Person").unwrap(), 2);
659
660        g.delete_node(a).unwrap();
661        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
662
663        g.delete_node(b).unwrap();
664        assert_eq!(g.node_count_by_label("Person").unwrap(), 0);
665
666        // Deleting a non-existent node is a no-op; count stays at 0.
667        g.delete_node(b).unwrap();
668        assert_eq!(g.node_count_by_label("Person").unwrap(), 0);
669    }
670
671    #[test]
672    fn label_count_unchanged_on_update_node() {
673        let (_dir, g) = open_tmp();
674        let id = g.add_node("Person", &json!({})).unwrap();
675        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
676
677        // update_node does not change the label; the count must stay at 1.
678        g.update_node(id, &json!({"name": "Alice"})).unwrap();
679        assert_eq!(g.node_count_by_label("Person").unwrap(), 1);
680    }
681
682    #[test]
683    fn update_node_returns_not_found_for_missing_node() {
684        let (_dir, g) = open_tmp();
685        let res = g.update_node(9999, &json!({}));
686        assert!(matches!(res, Err(Error::NodeNotFound(9999))));
687    }
688
689    #[test]
690    fn type_count_increments_on_add_edge() {
691        let (_dir, g) = open_tmp();
692        let a = g.add_node("N", &json!({})).unwrap();
693        let b = g.add_node("N", &json!({})).unwrap();
694        let c = g.add_node("N", &json!({})).unwrap();
695        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
696
697        g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
698        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 1);
699
700        g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
701        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 2);
702
703        // Different type is not affected.
704        assert_eq!(g.edge_count_by_type("WORKS_AT").unwrap(), 0);
705    }
706
707    #[test]
708    fn type_count_decrements_on_delete_node_cascade() {
709        let (_dir, g) = open_tmp();
710        let a = g.add_node("N", &json!({})).unwrap();
711        let b = g.add_node("N", &json!({})).unwrap();
712        g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
713        g.add_edge(b, a, "KNOWS", &json!({})).unwrap();
714        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 2);
715
716        // Deleting node a cascades and removes both edges touching a.
717        g.delete_node(a).unwrap();
718        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
719    }
720
721    #[test]
722    fn delete_edge_correctness() {
723        let (_dir, g) = open_tmp();
724        let a = g.add_node("Person", &json!({})).unwrap();
725        let b = g.add_node("Person", &json!({})).unwrap();
726        let eid = g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
727
728        // 1. Verify exists
729        assert!(g.get_edge(eid).unwrap().is_some());
730        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 1);
731
732        // 2. Verify adjacency lists
733        let out_neighs = g.out_neighbors(a).unwrap();
734        assert_eq!(out_neighs.len(), 1);
735        assert_eq!(out_neighs[0].node, b);
736        assert_eq!(out_neighs[0].edge, eid);
737
738        let in_neighs = g.in_neighbors(b).unwrap();
739        assert_eq!(in_neighs.len(), 1);
740        assert_eq!(in_neighs[0].node, a);
741        assert_eq!(in_neighs[0].edge, eid);
742
743        // 3. Delete the edge
744        g.delete_edge(eid).unwrap();
745
746        // 4. Verify gone
747        assert!(g.get_edge(eid).unwrap().is_none());
748        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
749
750        // 5. Verify adjacency lists updated
751        assert_eq!(g.out_neighbors(a).unwrap().len(), 0);
752        assert_eq!(g.in_neighbors(b).unwrap().len(), 0);
753
754        // 6. Idempotence: delete non-existent edge
755        g.delete_edge(eid).unwrap();
756        assert_eq!(g.edge_count_by_type("KNOWS").unwrap(), 0);
757    }
758
759    #[test]
760    fn test_node_property_secondary_index_and_scans() {
761        let (_dir, g) = open_tmp();
762
763        // Add nodes
764        let n1 = g
765            .add_node("Person", &json!({"name": "Alice", "age": 30}))
766            .unwrap();
767        let n2 = g
768            .add_node("Person", &json!({"name": "Bob", "age": 25}))
769            .unwrap();
770        let n3 = g
771            .add_node("Person", &json!({"name": "Charlie", "age": 30}))
772            .unwrap();
773        let _n4 = g
774            .add_node("Employee", &json!({"name": "Alice", "age": 40}))
775            .unwrap();
776
777        // Create index on Person(age)
778        g.create_node_property_index("Person", "age").unwrap();
779
780        // Check index exists
781        assert!(g.has_node_property_index("Person", "age").unwrap());
782
783        // Point queries
784        let p30 = g
785            .nodes_by_property("Person", "age", PropValue::Int(30))
786            .unwrap();
787        assert_eq!(p30.len(), 2);
788        assert!(p30.contains(&n1));
789        assert!(p30.contains(&n3));
790
791        let p25 = g
792            .nodes_by_property("Person", "age", PropValue::Int(25))
793            .unwrap();
794        assert_eq!(p25.len(), 1);
795        assert!(p25.contains(&n2));
796
797        // Range queries (e.g. age between 20 and 28)
798        let pr = g
799            .nodes_by_property_range(
800                "Person",
801                "age",
802                Some(PropValue::Int(20)),
803                true,
804                Some(PropValue::Int(28)),
805                true,
806            )
807            .unwrap();
808        assert_eq!(pr.len(), 1);
809        assert!(pr.contains(&n2));
810
811        // Let's create an index on Person(name) to test string sorting/prefix
812        g.create_node_property_index("Person", "name").unwrap();
813        let p_alice = g
814            .nodes_by_property("Person", "name", PropValue::Str("Alice".to_string()))
815            .unwrap();
816        assert_eq!(p_alice.len(), 1);
817        assert!(p_alice.contains(&n1));
818    }
819
820    #[test]
821    fn test_unique_property_constraint() {
822        let (_dir, g) = open_tmp();
823
824        // Create unique constraint on User(email)
825        g.create_node_unique_constraint("User", "email").unwrap();
826
827        // Add first user
828        let _u1 = g
829            .add_node(
830                "User",
831                &json!({"email": "user1@example.com", "name": "User 1"}),
832            )
833            .unwrap();
834
835        // Add second user with duplicate email - should fail
836        let res2 = g.add_node(
837            "User",
838            &json!({"email": "user1@example.com", "name": "User 2"}),
839        );
840        assert!(res2.is_err());
841        assert!(matches!(
842            res2.unwrap_err(),
843            Error::UniqueConstraintViolation { .. }
844        ));
845
846        // Add second user with unique email - should succeed
847        let u2 = g
848            .add_node(
849                "User",
850                &json!({"email": "user2@example.com", "name": "User 2"}),
851            )
852            .unwrap();
853
854        // Update u2 to have u1's email - should fail
855        let update_res =
856            g.update_node(u2, &json!({"email": "user1@example.com", "name": "User 2"}));
857        assert!(update_res.is_err());
858        assert!(matches!(
859            update_res.unwrap_err(),
860            Error::UniqueConstraintViolation { .. }
861        ));
862    }
863
864    #[test]
865    fn test_required_property_constraint() {
866        let (_dir, g) = open_tmp();
867
868        // Create required constraint on Task(title)
869        g.create_node_required_constraint("Task", "title").unwrap();
870
871        // Add task with title - should succeed
872        let t1 = g
873            .add_node("Task", &json!({"title": "Do homework", "done": false}))
874            .unwrap();
875
876        // Add task without title - should fail
877        let res2 = g.add_node("Task", &json!({"done": false}));
878        assert!(res2.is_err());
879        assert!(matches!(
880            res2.unwrap_err(),
881            Error::RequiredConstraintViolation { .. }
882        ));
883
884        // Update t1 to remove title - should fail
885        let update_res = g.update_node(t1, &json!({"done": true}));
886        assert!(update_res.is_err());
887        assert!(matches!(
888            update_res.unwrap_err(),
889            Error::RequiredConstraintViolation { .. }
890        ));
891    }
892
893    #[test]
894    fn test_index_cleanup_on_delete() {
895        let (_dir, g) = open_tmp();
896
897        // Create unique index on Account(number)
898        g.create_node_unique_constraint("Account", "number")
899            .unwrap();
900
901        let a1 = g.add_node("Account", &json!({"number": "12345"})).unwrap();
902
903        // Delete a1
904        g.delete_node(a1).unwrap();
905
906        // Now we should be able to reuse the account number because index was cleaned up!
907        let a2 = g.add_node("Account", &json!({"number": "12345"}));
908        assert!(a2.is_ok());
909    }
910
911    #[test]
912    fn backup_and_restore_roundtrip() {
913        let dir = TempDir::new().unwrap();
914        let backup_file = dir.path().join("snapshot.mdb");
915        let restore_dir = dir.path().join("restored");
916
917        // Write data.
918        let n;
919        {
920            let g = Graph::open(&dir.path().join("primary"), 1).unwrap();
921            n = g
922                .add_node("BackupTest", &serde_json::json!({"x": 42}))
923                .unwrap();
924            g.backup(&backup_file).unwrap();
925        }
926
927        // Restore and verify.
928        Graph::restore(&backup_file, &restore_dir).unwrap();
929        let g2 = Graph::open(&restore_dir, 1).unwrap();
930        let rec = g2
931            .get_node(n)
932            .unwrap()
933            .expect("node must exist in restored graph");
934        let props: serde_json::Value = rmp_serde::from_slice(&rec.props).unwrap();
935        assert_eq!(props["x"], serde_json::json!(42));
936    }
937
938    #[test]
939    fn backup_compact_and_restore_roundtrip() {
940        let dir = TempDir::new().unwrap();
941        let backup_file = dir.path().join("compact.mdb");
942        let restore_dir = dir.path().join("restored");
943
944        // Write data, delete some of it, then take a compacted snapshot.
945        let kept;
946        {
947            let g = Graph::open(&dir.path().join("primary"), 1).unwrap();
948            let doomed = g
949                .add_node("BackupTest", &serde_json::json!({"x": 1}))
950                .unwrap();
951            kept = g
952                .add_node("BackupTest", &serde_json::json!({"x": 42}))
953                .unwrap();
954            g.delete_node(doomed).unwrap();
955            g.backup_compact(&backup_file).unwrap();
956        }
957
958        // Restore and verify the surviving data round-trips.
959        Graph::restore(&backup_file, &restore_dir).unwrap();
960        let g2 = Graph::open(&restore_dir, 1).unwrap();
961        let rec = g2
962            .get_node(kept)
963            .unwrap()
964            .expect("node must exist in restored graph");
965        let props: serde_json::Value = rmp_serde::from_slice(&rec.props).unwrap();
966        assert_eq!(props["x"], serde_json::json!(42));
967        assert_eq!(g2.nodes_by_label("BackupTest").unwrap(), vec![kept]);
968    }
969}