1use super::*;
2
3impl Graph {
4 pub fn dfs(&self, start: NodeId, hops: u8) -> Result<Vec<NodeId>, Error> {
10 self.ensure_csr_fresh()?;
11 let guard = self.matrices.read();
12 let m = guard
13 .as_ref()
14 .ok_or(Error::Corrupt("matrices not initialized"))?;
15 let snap = self.csr_cache.snapshot.load();
16 self.dfs_graphblas(m, &snap, start, hops)
17 }
18
19 pub fn count_triangle_cycles(&self, spec: &TriangleCountSpec) -> Result<u64, Error> {
30 self.ensure_csr_fresh()?;
31 let snap = self.csr_cache.snapshot.load();
32 let n = snap.dense_to_id.len();
33 if n == 0 {
34 return Ok(0);
35 }
36
37 let mut type_ids: [Option<TypeId>; 3] = [None; 3];
39 {
40 let rtxn = self.storage.env.read_txn()?;
41 for (i, name) in spec.rel_types.iter().enumerate() {
42 if let Some(name) = name {
43 match get_type(&self.storage, &rtxn, name)? {
44 Some(tid) => type_ids[i] = Some(tid),
45 None => return Ok(0),
46 }
47 }
48 }
49 }
50
51 let mut masks: [Option<Vec<bool>>; 3] = [None, None, None];
55 for (i, label) in spec.labels.iter().enumerate() {
56 if let Some(name) = label {
57 let mut mask = vec![false; n];
58 for id in self.nodes_by_label(name)? {
59 if let Some(&d) = snap.id_to_dense.get(&id) {
60 mask[d as usize] = true;
61 }
62 }
63 masks[i] = Some(mask);
64 }
65 }
66 let label_ok = |mask: &Option<Vec<bool>>, d: usize| mask.as_ref().is_none_or(|m| m[d]);
67
68 let out1 = typed_out_sorted(&snap, type_ids[0]);
72 let out2_built = if type_ids[1] == type_ids[0] {
73 None
74 } else {
75 Some(typed_out_sorted(&snap, type_ids[1]))
76 };
77 let out2 = out2_built.as_ref().unwrap_or(&out1);
78 let in3 = typed_in_sorted(&snap, type_ids[2]);
79
80 let mut total: u64 = 0;
81 for a in 0..n {
82 if !label_ok(&masks[0], a) {
83 continue;
84 }
85 let in3_row = in3.row(a);
86 if in3_row.is_empty() {
87 continue;
88 }
89 let out1_row = out1.row(a);
90
91 let mut i = 0;
92 while i < out1_row.len() {
93 let b = out1_row[i].0 as usize;
94 let run1_start = i;
95 while i < out1_row.len() && out1_row[i].0 as usize == b {
96 i += 1;
97 }
98 if !label_ok(&masks[1], b) {
99 continue;
100 }
101 let m1 = (i - run1_start) as u64;
102 let out2_row = out2.row(b);
103
104 let (mut j, mut k) = (0, 0);
108 let mut pair_count: u64 = 0;
109 while j < out2_row.len() && k < in3_row.len() {
110 let c2 = out2_row[j].0;
111 let c3 = in3_row[k].0;
112 match c2.cmp(&c3) {
113 std::cmp::Ordering::Less => j += 1,
114 std::cmp::Ordering::Greater => k += 1,
115 std::cmp::Ordering::Equal => {
116 let c = c2 as usize;
117 let j0 = j;
118 while j < out2_row.len() && out2_row[j].0 as usize == c {
119 j += 1;
120 }
121 let k0 = k;
122 while k < in3_row.len() && in3_row[k].0 as usize == c {
123 k += 1;
124 }
125 if !label_ok(&masks[2], c) {
126 continue;
127 }
128 if a == b && c == a {
129 for &(_, e1) in &out1_row[run1_start..run1_start + m1 as usize] {
136 for &(_, e2) in &out2_row[j0..j] {
137 if e2 == e1 {
138 continue;
139 }
140 for &(_, e3) in &in3_row[k0..k] {
141 if e3 != e1 && e3 != e2 {
142 total += 1;
143 }
144 }
145 }
146 }
147 } else {
148 pair_count += ((j - j0) * (k - k0)) as u64;
149 }
150 }
151 }
152 }
153 total += m1 * pair_count;
154 }
155 }
156 Ok(total)
157 }
158
159 pub fn count_linear_paths(&self, spec: &PathCountSpec) -> Result<u64, Error> {
173 let hops = spec.rel_types.len();
174 debug_assert!(hops == 1 || hops == 2, "count_linear_paths: 1 or 2 hops");
175 debug_assert_eq!(spec.labels.len(), hops + 1, "labels must be hops + 1");
176
177 self.ensure_csr_fresh()?;
178 let snap = self.csr_cache.snapshot.load();
179 let n = snap.dense_to_id.len();
180 if n == 0 {
181 return Ok(0);
182 }
183
184 let mut type_ids: Vec<Option<TypeId>> = vec![None; hops];
186 {
187 let rtxn = self.storage.env.read_txn()?;
188 for (i, name) in spec.rel_types.iter().enumerate() {
189 if let Some(name) = name {
190 match get_type(&self.storage, &rtxn, name)? {
191 Some(tid) => type_ids[i] = Some(tid),
192 None => return Ok(0),
193 }
194 }
195 }
196 }
197
198 let mut masks: Vec<Option<Vec<bool>>> = vec![None; hops + 1];
202 for (i, label) in spec.labels.iter().enumerate() {
203 if let Some(name) = label {
204 let mut mask = vec![false; n];
205 for id in self.nodes_by_label(name)? {
206 if let Some(&d) = snap.id_to_dense.get(&id) {
207 mask[d as usize] = true;
208 }
209 }
210 masks[i] = Some(mask);
211 }
212 }
213 for (i, allow) in spec.vertex_allow.iter().enumerate() {
220 let Some(ids) = allow else { continue };
221 let mut amask = vec![false; n];
222 for &id in ids {
223 if let Some(&d) = snap.id_to_dense.get(&id) {
224 amask[d as usize] = true;
225 }
226 }
227 match &mut masks[i] {
228 Some(m) => {
229 for (slot, &keep) in m.iter_mut().zip(amask.iter()) {
230 *slot = *slot && keep;
231 }
232 }
233 None => masks[i] = Some(amask),
234 }
235 }
236 let label_ok = |mask: &Option<Vec<bool>>, d: usize| mask.as_ref().is_none_or(|m| m[d]);
237
238 if hops == 1 {
239 let out1 = typed_out_sorted(&snap, type_ids[0]);
241 let mut total: u64 = 0;
242 for v0 in 0..n {
243 if !label_ok(&masks[0], v0) {
244 continue;
245 }
246 for &(dst, _e) in out1.row(v0) {
247 if label_ok(&masks[1], dst as usize) {
248 total += 1;
249 }
250 }
251 }
252 return Ok(total);
253 }
254
255 let in1 = typed_in_sorted(&snap, type_ids[0]); let out2 = typed_out_sorted(&snap, type_ids[1]); let mut total: u64 = 0;
264 for b in 0..n {
265 if !label_ok(&masks[1], b) {
266 continue;
267 }
268 let in_row = in1.row(b);
269 let indeg = in_row
270 .iter()
271 .filter(|&&(src, _)| label_ok(&masks[0], src as usize))
272 .count() as u64;
273 if indeg == 0 {
274 continue;
275 }
276 let out_row = out2.row(b);
277 let outdeg = out_row
278 .iter()
279 .filter(|&&(dst, _)| label_ok(&masks[2], dst as usize))
280 .count() as u64;
281 total += indeg * outdeg;
282
283 if label_ok(&masks[0], b) && label_ok(&masks[2], b) {
290 let in_self: Vec<EdgeId> = in_row
291 .iter()
292 .filter(|&&(src, _)| src as usize == b)
293 .map(|&(_, e)| e)
294 .collect();
295 if !in_self.is_empty() {
296 let shared = out_row
297 .iter()
298 .filter(|&&(dst, e)| dst as usize == b && in_self.binary_search(&e).is_ok())
299 .count() as u64;
300 total = total.saturating_sub(shared);
301 }
302 }
303 }
304 Ok(total)
305 }
306
307 pub fn grouped_edge_counts(
318 &self,
319 spec: &GroupedDegreeSpec,
320 ) -> Result<Vec<(NodeId, u64)>, Error> {
321 self.ensure_csr_fresh()?;
322 let snap = self.csr_cache.snapshot.load();
323 let n = snap.dense_to_id.len();
324 if n == 0 {
325 return Ok(Vec::new());
326 }
327
328 let type_id = match spec.rel_type {
330 Some(name) => {
331 let rtxn = self.storage.env.read_txn()?;
332 match get_type(&self.storage, &rtxn, name)? {
333 Some(tid) => Some(tid),
334 None => return Ok(Vec::new()),
335 }
336 }
337 None => None,
338 };
339
340 let label_mask = |label: Option<&str>| -> Result<Option<Vec<bool>>, Error> {
343 match label {
344 Some(name) => {
345 let mut mask = vec![false; n];
346 for id in self.nodes_by_label(name)? {
347 if let Some(&d) = snap.id_to_dense.get(&id) {
348 mask[d as usize] = true;
349 }
350 }
351 Ok(Some(mask))
352 }
353 None => Ok(None),
354 }
355 };
356 let group_mask = label_mask(spec.group_label)?;
357 let counted_mask = if spec.counted_label == spec.group_label {
360 group_mask.clone()
361 } else {
362 label_mask(spec.counted_label)?
363 };
364
365 let nonnull_mask: Option<Vec<bool>> = match spec.counted_nonnull_prop {
372 Some(prop) => Some(self.prop_columns.with_fresh(&self.storage, |cols| {
373 let mut mask = vec![false; n];
374 if let Some(col) = cols.cols.get(prop) {
375 for (d, id) in snap.dense_to_id.iter().enumerate() {
376 if let Some(&cd) = cols.id_to_dense.get(id) {
377 mask[d] = col.is_present(cd as usize);
378 }
379 }
380 }
381 mask
382 })?),
383 None => None,
384 };
385
386 let ok = |mask: &Option<Vec<bool>>, d: usize| mask.as_ref().is_none_or(|m| m[d]);
387
388 let mut counts = vec![0u64; n];
396 let mut present = vec![false; n];
397 for v0 in 0..n {
398 for k in snap.row_ptr[v0]..snap.row_ptr[v0 + 1] {
399 if let Some(tid) = type_id {
400 if snap.edge_type[k] != tid {
401 continue;
402 }
403 }
404 let v1 = snap.col_idx[k] as usize;
405 let (group_d, counted_d) = if spec.group_is_dst {
408 (v1, v0)
409 } else {
410 (v0, v1)
411 };
412 if !ok(&group_mask, group_d) || !ok(&counted_mask, counted_d) {
415 continue;
416 }
417 present[group_d] = true;
418 if ok(&nonnull_mask, counted_d) {
419 counts[group_d] += 1;
420 }
421 }
422 }
423
424 let mut out = Vec::new();
425 for (d, &p) in present.iter().enumerate() {
426 if p {
427 out.push((snap.dense_to_id[d], counts[d]));
428 }
429 }
430 Ok(out)
431 }
432
433 pub fn detect_cycle(&self) -> Result<bool, Error> {
435 self.ensure_csr_fresh()?;
436 let guard = self.matrices.read();
437 let m = guard
438 .as_ref()
439 .ok_or(Error::Corrupt("matrices not initialized"))?;
440 let snap = self.csr_cache.snapshot.load();
441 self.detect_cycle_graphblas(m, &snap)
442 }
443
444 pub fn all_neighbors(&self, node: NodeId) -> Result<Vec<DirectedNeighborEntry>, Error> {
446 let rtxn = self.storage.env.read_txn()?;
447 let mut neighbors = Vec::new();
448 for ne in self.out_neighbors_impl(&rtxn, node)? {
449 neighbors.push(DirectedNeighborEntry {
450 node: ne.node,
451 edge: ne.edge,
452 edge_type: ne.edge_type,
453 outgoing: true,
454 });
455 }
456 for ne in self.in_neighbors_impl(&rtxn, node)? {
457 neighbors.push(DirectedNeighborEntry {
458 node: ne.node,
459 edge: ne.edge,
460 edge_type: ne.edge_type,
461 outgoing: false,
462 });
463 }
464 Ok(neighbors)
465 }
466
467 pub fn all_paths(&self, src: NodeId, dst: NodeId) -> Result<Vec<Vec<NodeId>>, Error> {
469 self.ensure_csr_fresh()?;
470 let guard = self.matrices.read();
471 let m = guard
472 .as_ref()
473 .ok_or(Error::Corrupt("matrices not initialized"))?;
474 let snap = self.csr_cache.snapshot.load();
475 self.all_paths_graphblas(m, &snap, src, dst)
476 }
477
478 pub fn all_shortest_paths(&self, src: NodeId, dst: NodeId) -> Result<Vec<Vec<NodeId>>, Error> {
480 self.ensure_csr_fresh()?;
481 let guard = self.matrices.read();
482 let m = guard
483 .as_ref()
484 .ok_or(Error::Corrupt("matrices not initialized"))?;
485 let snap = self.csr_cache.snapshot.load();
486 self.all_shortest_paths_graphblas(m, &snap, src, dst)
487 }
488
489 pub fn longest_path(&self, src: NodeId, dst: NodeId) -> Result<Option<Vec<NodeId>>, Error> {
491 self.ensure_csr_fresh()?;
492 let guard = self.matrices.read();
493 let m = guard
494 .as_ref()
495 .ok_or(Error::Corrupt("matrices not initialized"))?;
496 let snap = self.csr_cache.snapshot.load();
497 self.longest_path_graphblas(m, &snap, src, dst)
498 }
499
500 pub fn shortest_path_dijkstra(
508 &self,
509 src: NodeId,
510 dst: NodeId,
511 ) -> Result<Option<WeightedPath>, Error> {
512 self.ensure_csr_fresh()?;
513 let guard = self.matrices.read();
514 let m = guard
515 .as_ref()
516 .ok_or(Error::Corrupt("matrices not initialized"))?;
517 let snap = self.csr_cache.snapshot.load();
518 self.shortest_path_dijkstra_graphblas(m, &snap, src, dst)
519 }
520
521 pub fn spanning_forest(
523 &self,
524 weight_property: &str,
525 maximum: bool,
526 ) -> Result<Vec<EdgeId>, Error> {
527 self.ensure_csr_fresh()?;
528 let guard = self.matrices.read();
529 let m = guard
530 .as_ref()
531 .ok_or(Error::Corrupt("matrices not initialized"))?;
532 let snap = self.csr_cache.snapshot.load();
533 self.spanning_forest_graphblas(m, &snap, weight_property, maximum)
534 }
535
536 pub fn label_propagation(&self, max_iterations: usize) -> Result<HashMap<NodeId, u64>, Error> {
538 self.ensure_csr_fresh()?;
539 let guard = self.matrices.read();
540 let m = guard
541 .as_ref()
542 .ok_or(Error::Corrupt("matrices not initialized"))?;
543 let snap = self.csr_cache.snapshot.load();
544 self.label_propagation_graphblas(m, &snap, max_iterations)
545 }
546
547 pub fn harmonic_centrality(&self) -> Result<HashMap<NodeId, f64>, Error> {
549 self.ensure_csr_fresh()?;
550 let guard = self.matrices.read();
551 let m = guard
552 .as_ref()
553 .ok_or(Error::Corrupt("matrices not initialized"))?;
554 let snap = self.csr_cache.snapshot.load();
555 self.harmonic_centrality_graphblas(m, &snap)
556 }
557
558 pub fn betweenness_centrality(&self) -> Result<HashMap<NodeId, f64>, Error> {
560 self.ensure_csr_fresh()?;
561 let guard = self.matrices.read();
562 let m = guard
563 .as_ref()
564 .ok_or(Error::Corrupt("matrices not initialized"))?;
565 let snap = self.csr_cache.snapshot.load();
566 self.betweenness_centrality_graphblas(m, &snap)
567 }
568
569 pub fn strongly_connected_components(&self) -> Result<HashMap<NodeId, u64>, Error> {
571 self.ensure_csr_fresh()?;
572 let guard = self.matrices.read();
573 let m = guard
574 .as_ref()
575 .ok_or(Error::Corrupt("matrices not initialized"))?;
576 let snap = self.csr_cache.snapshot.load();
577 self.strongly_connected_components_graphblas(m, &snap)
578 }
579
580 pub fn degree_centrality(
582 &self,
583 direction: DegreeDirection,
584 ) -> Result<HashMap<NodeId, u64>, Error> {
585 self.ensure_matrix_view()?;
586 let guard = self.matrices.read();
587 let m = guard
588 .as_ref()
589 .ok_or(Error::Corrupt("matrices not initialized"))?;
590 self.degree_centrality_graphblas(m, direction)
591 }
592
593 pub fn maximum_flow(
595 &self,
596 source: NodeId,
597 sink: NodeId,
598 capacity_property: &str,
599 ) -> Result<f64, Error> {
600 self.ensure_csr_fresh()?;
601 let guard = self.matrices.read();
602 let m = guard
603 .as_ref()
604 .ok_or(Error::Corrupt("matrices not initialized"))?;
605 let snap = self.csr_cache.snapshot.load();
606 self.maximum_flow_graphblas(m, &snap, source, sink, capacity_property)
607 }
608
609 pub fn shortest_path_top_k(
611 &self,
612 src: NodeId,
613 dst: NodeId,
614 k: usize,
615 weight_property: &str,
616 ) -> Result<Vec<WeightedPath>, Error> {
617 self.ensure_csr_fresh()?;
618 let guard = self.matrices.read();
619 let m = guard
620 .as_ref()
621 .ok_or(Error::Corrupt("matrices not initialized"))?;
622 let snap = self.csr_cache.snapshot.load();
623 let paths = self.shortest_path_top_k_graphblas(m, &snap, src, dst, k, weight_property)?;
624 Ok(paths
625 .into_iter()
626 .map(|(nodes, total_weight)| WeightedPath {
627 nodes,
628 total_weight,
629 })
630 .collect())
631 }
632
633 pub fn bfs(&self, start: NodeId, hops: u8) -> Result<Vec<NodeId>, Error> {
635 self.ensure_matrix_view()?;
636 self.bfs_graphblas(start, hops)
637 }
638
639 pub fn shortest_path(&self, src: NodeId, dst: NodeId) -> Result<Option<Vec<NodeId>>, Error> {
641 self.ensure_csr_fresh()?;
642 self.shortest_path_graphblas(src, dst)
643 }
644
645 pub fn page_rank(&self, iterations: u32, damping: f32) -> Result<HashMap<NodeId, f32>, Error> {
647 self.ensure_csr_fresh()?;
648 self.page_rank_graphblas(iterations, damping)
649 }
650
651 pub(crate) fn ensure_csr_fresh(&self) -> Result<(), Error> {
660 if self.matrices.read().is_none() || self.csr_cache.snapshot_is_stale() {
661 self.rebuild_csr()?;
662 } else {
663 self.ensure_matrix_view()?;
667 }
668 Ok(())
669 }
670
671 pub(crate) fn ensure_snapshot_fresh(&self) -> Result<(), Error> {
676 if self.csr_cache.snapshot_is_stale() {
677 let built_gen = self.csr_cache.current_gen();
678 let snap = CsrSnapshot::build(&self.storage)?;
679 self.csr_cache.install_snapshot(snap, built_gen);
680 }
681 Ok(())
682 }
683
684 pub(crate) fn ensure_matrix_view(&self) -> Result<(), Error> {
694 if self.matrices.read().is_none() || self.csr_cache.pending_force_full() {
697 return self.rebuild_csr();
698 }
699 if !self.csr_cache.has_pending() {
701 return Ok(());
702 }
703
704 let mut guard = self.matrices.write();
705 let delta = self.csr_cache.take_delta();
706 if delta.force_full {
707 drop(guard);
711 return self.rebuild_csr();
712 }
713 if delta.is_empty() {
714 return Ok(());
715 }
716
717 let mut clear_edges = Vec::new();
720 {
721 let rtxn = self.storage.env.read_txn()?;
722 for &(src, dst) in &delta.removed_edges {
723 let still_connected = self
724 .out_neighbors_impl(&rtxn, src)?
725 .into_iter()
726 .any(|ne| ne.node == dst);
727 if !still_connected {
728 clear_edges.push((src, dst));
729 }
730 }
731 }
732
733 if let Some(m) = guard.as_mut() {
734 m.apply_delta(&delta.added_nodes, &delta.added_edges, &clear_edges)?;
735 }
736 Ok(())
737 }
738
739 pub fn all_nodes(&self) -> Result<Vec<NodeId>, Error> {
741 let rtxn = self.storage.env.read_txn()?;
742 self.all_nodes_impl(&rtxn)
743 }
744
745 pub(super) fn all_nodes_impl(&self, rtxn: &heed::RoTxn) -> Result<Vec<NodeId>, Error> {
746 let mut ids = self
747 .storage
748 .nodes
749 .iter(rtxn)?
750 .map(|r| r.map(|(k, _)| k))
751 .collect::<Result<Vec<_>, _>>()?;
752 ids.sort_unstable();
753 Ok(ids)
754 }
755
756 pub fn connected_components(&self) -> Result<HashMap<NodeId, u64>, Error> {
762 self.ensure_matrix_view()?;
763 {
764 let guard = self.matrices.read();
765 if let Some(m) = guard.as_ref() {
766 if m.n_nodes > 0 {
767 return self.connected_components_graphblas(m);
768 }
769 }
770 }
771 let nodes: Vec<NodeId> = {
772 let rtxn = self.storage.env.read_txn()?;
773 self.storage
774 .nodes
775 .iter(&rtxn)?
776 .map(|r| r.map(|(k, _)| k))
777 .collect::<Result<Vec<_>, _>>()?
778 };
779
780 let mut component: HashMap<NodeId, u64> = HashMap::with_capacity(nodes.len());
781 let mut next_id: u64 = 0;
782
783 for &start in &nodes {
784 if component.contains_key(&start) {
785 continue;
786 }
787 let comp_id = next_id;
788 next_id += 1;
789 component.insert(start, comp_id);
790 let mut queue = vec![start];
791 while let Some(node) = queue.pop() {
792 for ne in self.out_neighbors(node)? {
793 if component.insert(ne.node, comp_id).is_none() {
794 queue.push(ne.node);
795 }
796 }
797 for ne in self.in_neighbors(node)? {
798 if component.insert(ne.node, comp_id).is_none() {
799 queue.push(ne.node);
800 }
801 }
802 }
803 }
804
805 Ok(component)
806 }
807
808 pub(super) fn maybe_spawn_rebuild(&self) {
816 self.maybe_spawn_rebuild_n(1);
817 }
818
819 pub(super) fn maybe_spawn_rebuild_n(&self, count: usize) {
820 if self.csr_cache.mark_dirty_n(count as u64) {
821 let cache = Arc::clone(&self.csr_cache);
822 let storage = Arc::clone(&self.storage);
823 let matrices = Arc::clone(&self.matrices);
824 let thread_count = Arc::clone(&self.n_threads);
825 std::thread::spawn(move || {
826 loop {
831 let built_gen = cache.current_gen();
835 cache.clear_delta();
838 match CsrSnapshot::build(&storage) {
839 Ok(snap) => {
840 if let Ok(m) = MatrixSet::materialize(
841 &snap,
842 thread_count.load(std::sync::atomic::Ordering::Acquire),
843 ) {
844 *matrices.write() = Some(m);
845 }
846 if !cache.install(snap, built_gen) {
847 break;
848 }
849 }
850 Err(_) => {
851 cache.cancel_rebuild();
852 break;
853 }
854 }
855 }
856 });
857 }
858 }
859
860 pub(super) fn append_adj(
862 &self,
863 wtxn: &mut heed::RwTxn,
864 node: NodeId,
865 other: NodeId,
866 edge_type: u32,
867 edge_id: EdgeId,
868 outgoing: bool,
869 ) -> Result<(), Error> {
870 let entry = AdjEntry {
871 edge_type,
872 other,
873 edge_id,
874 };
875 let db = if outgoing {
876 &self.storage.out_adj
877 } else {
878 &self.storage.in_adj
879 };
880 db.put(wtxn, &node, entry.as_bytes())?;
881 Ok(())
882 }
883
884 pub(super) fn adj_entries(
886 &self,
887 node: NodeId,
888 outgoing: bool,
889 ) -> Result<Vec<NeighborEntry>, Error> {
890 let rtxn = self.storage.env.read_txn()?;
891 self.adj_entries_impl(&rtxn, node, outgoing)
892 }
893
894 pub(super) fn adj_entries_impl(
895 &self,
896 rtxn: &heed::RoTxn,
897 node: NodeId,
898 outgoing: bool,
899 ) -> Result<Vec<NeighborEntry>, Error> {
900 let db = if outgoing {
901 &self.storage.out_adj
902 } else {
903 &self.storage.in_adj
904 };
905
906 let iter = match db.get_duplicates(rtxn, &node)? {
907 Some(iter) => iter,
908 None => return Ok(vec![]),
909 };
910
911 let mut out = Vec::new();
912 for result in iter {
913 let (_, bytes) = result?;
914 let entry = AdjEntry::read_from_bytes(bytes)
915 .ok()
916 .ok_or(Error::Corrupt("AdjEntry value is not exactly 20 bytes"))?;
917 out.push(NeighborEntry {
918 node: entry.other,
919 edge: entry.edge_id,
920 edge_type: entry.edge_type,
921 });
922 }
923 Ok(out)
924 }
925}
926
927struct TypedSortedAdj {
931 ptr: Vec<usize>,
932 adj: Vec<(u32, EdgeId)>,
933}
934
935impl TypedSortedAdj {
936 fn row(&self, d: usize) -> &[(u32, EdgeId)] {
937 &self.adj[self.ptr[d]..self.ptr[d + 1]]
938 }
939}
940
941fn typed_out_sorted(snap: &CsrSnapshot, type_id: Option<TypeId>) -> TypedSortedAdj {
944 let n = snap.dense_to_id.len();
945 let keep = |idx: usize| type_id.is_none_or(|t| snap.edge_type[idx] == t);
946
947 let mut ptr = vec![0usize; n + 1];
948 for row in 0..n {
949 let mut count = 0;
950 for idx in snap.row_ptr[row]..snap.row_ptr[row + 1] {
951 if keep(idx) {
952 count += 1;
953 }
954 }
955 ptr[row + 1] = ptr[row] + count;
956 }
957
958 let mut adj = vec![(0u32, 0u64); ptr[n]];
959 for row in 0..n {
960 let mut at = ptr[row];
961 for idx in snap.row_ptr[row]..snap.row_ptr[row + 1] {
962 if keep(idx) {
963 adj[at] = (snap.col_idx[idx], snap.edge_id[idx]);
964 at += 1;
965 }
966 }
967 adj[ptr[row]..at].sort_unstable();
968 }
969 TypedSortedAdj { ptr, adj }
970}
971
972fn typed_in_sorted(snap: &CsrSnapshot, type_id: Option<TypeId>) -> TypedSortedAdj {
975 let n = snap.dense_to_id.len();
976 let keep = |idx: usize| type_id.is_none_or(|t| snap.edge_type[idx] == t);
977
978 let mut ptr = vec![0usize; n + 1];
979 for idx in 0..snap.col_idx.len() {
980 if keep(idx) {
981 ptr[snap.col_idx[idx] as usize + 1] += 1;
982 }
983 }
984 for d in 0..n {
985 ptr[d + 1] += ptr[d];
986 }
987
988 let mut at = ptr.clone();
989 let mut adj = vec![(0u32, 0u64); ptr[n]];
990 for row in 0..n {
991 for idx in snap.row_ptr[row]..snap.row_ptr[row + 1] {
992 if keep(idx) {
993 let dst = snap.col_idx[idx] as usize;
994 adj[at[dst]] = (row as u32, snap.edge_id[idx]);
995 at[dst] += 1;
996 }
997 }
998 }
999 for d in 0..n {
1000 adj[ptr[d]..ptr[d + 1]].sort_unstable();
1001 }
1002 TypedSortedAdj { ptr, adj }
1003}
1004
1005#[cfg(test)]
1006mod incremental_matrix_tests {
1007 use issundb_graphblas::Matrix;
1008 use serde_json::json;
1009 use tempfile::TempDir;
1010
1011 use std::collections::{BTreeMap, HashMap};
1012
1013 use crate::Graph;
1014 use crate::graph::DegreeDirection;
1015 use crate::schema::NodeId;
1016
1017 type MatrixView = (Vec<(usize, usize)>, Vec<(usize, usize)>, Vec<NodeId>);
1020
1021 fn canonical_partition(cc: &HashMap<NodeId, u64>) -> BTreeMap<NodeId, NodeId> {
1025 let mut groups: HashMap<u64, Vec<NodeId>> = HashMap::new();
1026 for (&node, &comp) in cc {
1027 groups.entry(comp).or_default().push(node);
1028 }
1029 let mut out = BTreeMap::new();
1030 for members in groups.into_values() {
1031 let rep = *members.iter().min().unwrap();
1032 for n in members {
1033 out.insert(n, rep);
1034 }
1035 }
1036 out
1037 }
1038
1039 fn matrix_coords(m: &Matrix<i32>) -> Vec<(usize, usize)> {
1042 let mut out: Vec<(usize, usize)> = m
1043 .triples()
1044 .expect("triples")
1045 .into_iter()
1046 .map(|(r, c, _)| (r, c))
1047 .collect();
1048 out.sort_unstable();
1049 out.dedup();
1050 out
1051 }
1052
1053 fn extract(graph: &Graph) -> MatrixView {
1056 let guard = graph.matrices.read();
1057 let m = guard.as_ref().expect("matrices materialized");
1058 (
1059 matrix_coords(&m.adjacency),
1060 matrix_coords(&m.adjacency_t),
1061 m.dense_to_id.clone(),
1062 )
1063 }
1064
1065 #[test]
1071 fn incremental_matrices_match_full_rebuild() {
1072 let dir = TempDir::new().unwrap();
1073 let g = Graph::open(dir.path(), 1).unwrap();
1074
1075 let ids: Vec<NodeId> = (0..20)
1077 .map(|i| g.add_node("N", &json!({ "v": i })).unwrap())
1078 .collect();
1079 let mut base_edges = Vec::new();
1080 for i in 0..20 {
1081 base_edges.push(
1082 g.add_edge(ids[i], ids[(i + 1) % 20], "R", &json!({}))
1083 .unwrap(),
1084 );
1085 }
1086 g.rebuild_csr().unwrap();
1088
1089 g.add_edge(ids[0], ids[5], "R", &json!({})).unwrap();
1092 g.add_edge(ids[3], ids[10], "R", &json!({})).unwrap();
1093 let par_a = g.add_edge(ids[2], ids[4], "R", &json!({})).unwrap();
1095 let _par_b = g.add_edge(ids[2], ids[4], "R", &json!({})).unwrap();
1096 let n20 = g.add_node("N", &json!({ "v": 20 })).unwrap();
1098 let n21 = g.add_node("N", &json!({ "v": 21 })).unwrap();
1099 g.add_edge(n20, n21, "R", &json!({})).unwrap();
1100 g.add_edge(ids[1], n20, "R", &json!({})).unwrap();
1101 g.delete_edge(base_edges[7]).unwrap();
1103 g.delete_edge(par_a).unwrap();
1105
1106 g.ensure_matrix_view().unwrap();
1108 let incremental = extract(&g);
1109
1110 g.rebuild_csr().unwrap();
1112 let full = extract(&g);
1113
1114 assert_eq!(incremental.0, full.0, "adjacency element sets differ");
1115 assert_eq!(incremental.1, full.1, "adjacency_t element sets differ");
1116 assert_eq!(incremental.2, full.2, "dense-index mapping differs");
1117 }
1118
1119 #[test]
1122 fn node_deletion_forces_full_rebuild_and_matches() {
1123 let dir = TempDir::new().unwrap();
1124 let g = Graph::open(dir.path(), 1).unwrap();
1125 let ids: Vec<NodeId> = (0..10)
1126 .map(|i| g.add_node("N", &json!({ "v": i })).unwrap())
1127 .collect();
1128 for i in 0..10 {
1129 g.add_edge(ids[i], ids[(i + 1) % 10], "R", &json!({}))
1130 .unwrap();
1131 }
1132 g.rebuild_csr().unwrap();
1133
1134 g.delete_node(ids[3]).unwrap();
1136 g.add_edge(ids[5], ids[7], "R", &json!({})).unwrap();
1137
1138 g.ensure_matrix_view().unwrap();
1139 let incremental = extract(&g);
1140 g.rebuild_csr().unwrap();
1141 let full = extract(&g);
1142
1143 assert_eq!(incremental.0, full.0, "adjacency element sets differ");
1144 assert_eq!(incremental.1, full.1, "adjacency_t element sets differ");
1145 assert_eq!(incremental.2, full.2, "dense-index mapping differs");
1146 }
1147
1148 #[test]
1152 #[ignore = "measurement: prints incremental-apply vs full-rebuild timings"]
1153 fn incremental_apply_cost() {
1154 use std::time::Instant;
1155
1156 fn measure(n_nodes: usize, out_degree: usize, k_added: usize) {
1157 let dir = TempDir::new().unwrap();
1158 let g = Graph::open(dir.path(), 4).unwrap();
1159 let ids: Vec<NodeId> = g
1162 .update(|txn| {
1163 let ids: Vec<NodeId> = (0..n_nodes)
1164 .map(|i| txn.add_node("N", &json!({ "v": i })).unwrap())
1165 .collect();
1166 for i in 0..n_nodes {
1167 for k in 0..out_degree {
1168 let off = 1 + k * 7;
1169 txn.add_edge(ids[i], ids[(i + off) % n_nodes], "R", &json!({}))
1170 .unwrap();
1171 }
1172 }
1173 Ok(ids)
1174 })
1175 .unwrap();
1176 g.rebuild_csr().unwrap();
1177
1178 for j in 0..k_added {
1181 let a = (j * 31) % n_nodes;
1182 let b = (j * 97 + 5) % n_nodes;
1183 g.add_edge(ids[a], ids[b], "R", &json!({})).unwrap();
1184 }
1185 let t = Instant::now();
1186 g.ensure_matrix_view().unwrap();
1187 let incr = t.elapsed();
1188
1189 let mut best_full = std::time::Duration::from_secs(3600);
1192 for _ in 0..3 {
1193 let t = Instant::now();
1194 g.rebuild_csr().unwrap();
1195 let e = t.elapsed();
1196 if e < best_full {
1197 best_full = e;
1198 }
1199 }
1200 let n_edges = n_nodes * out_degree + k_added;
1201 println!(
1202 "{:>7} nodes, {:>9} edges: incremental apply of {} edges = {:>8.3} ms; full rebuild = {:>8.2} ms",
1203 n_nodes,
1204 n_edges,
1205 k_added,
1206 incr.as_secs_f64() * 1e3,
1207 best_full.as_secs_f64() * 1e3,
1208 );
1209 }
1210
1211 measure(10_000, 5, 1_000);
1212 measure(50_000, 5, 1_000);
1213 measure(100_000, 5, 1_000);
1214 }
1215
1216 #[test]
1221 fn incremental_consumers_match_full_rebuild() {
1222 let dir = TempDir::new().unwrap();
1223 let g = Graph::open(dir.path(), 1).unwrap();
1224 let ids: Vec<NodeId> = (0..15)
1225 .map(|i| g.add_node("N", &json!({ "v": i })).unwrap())
1226 .collect();
1227 for i in 0..15 {
1228 g.add_edge(ids[i], ids[(i + 1) % 15], "R", &json!({}))
1229 .unwrap();
1230 }
1231 g.rebuild_csr().unwrap();
1232
1233 g.add_edge(ids[0], ids[7], "R", &json!({})).unwrap();
1235 let n15 = g.add_node("N", &json!({ "v": 15 })).unwrap();
1236 g.add_edge(ids[2], n15, "R", &json!({})).unwrap();
1237 g.add_edge(n15, ids[5], "R", &json!({})).unwrap();
1238
1239 let bfs_incr = {
1241 let mut v = g.bfs(ids[0], 3).unwrap();
1242 v.sort_unstable();
1243 v
1244 };
1245 let deg_incr = g.degree_centrality(DegreeDirection::Both).unwrap();
1246 let cc_incr = canonical_partition(&g.connected_components().unwrap());
1247
1248 g.rebuild_csr().unwrap();
1250 let bfs_full = {
1251 let mut v = g.bfs(ids[0], 3).unwrap();
1252 v.sort_unstable();
1253 v
1254 };
1255 let deg_full = g.degree_centrality(DegreeDirection::Both).unwrap();
1256 let cc_full = canonical_partition(&g.connected_components().unwrap());
1257
1258 assert_eq!(bfs_incr, bfs_full, "bfs: incremental vs full rebuild");
1259 assert_eq!(deg_incr, deg_full, "degree: incremental vs full rebuild");
1260 assert_eq!(cc_incr, cc_full, "components: incremental vs full rebuild");
1261 }
1262
1263 #[test]
1267 fn matrix_view_consumers_reflect_writes_without_rebuild() {
1268 let dir = TempDir::new().unwrap();
1269 let g = Graph::open(dir.path(), 1).unwrap();
1270 let a = g.add_node("N", &json!({})).unwrap();
1271 let b = g.add_node("N", &json!({})).unwrap();
1272 g.rebuild_csr().unwrap();
1273 assert!(
1274 !g.bfs(a, 5).unwrap().contains(&b),
1275 "b is unreachable before the edge exists"
1276 );
1277
1278 g.add_edge(a, b, "R", &json!({})).unwrap();
1280 assert!(
1281 g.bfs(a, 1).unwrap().contains(&b),
1282 "b reachable from a after the edge, without a rebuild"
1283 );
1284
1285 let c = g.add_node("N", &json!({})).unwrap();
1288 g.add_edge(b, c, "R", &json!({})).unwrap();
1289 assert!(
1290 g.bfs(a, 2).unwrap().contains(&c),
1291 "new node c reachable two hops from a, without a rebuild"
1292 );
1293 }
1294
1295 #[test]
1299 fn csr_consumers_reflect_writes_without_rebuild() {
1300 let dir = TempDir::new().unwrap();
1301 let g = Graph::open(dir.path(), 1).unwrap();
1302 let a = g.add_node("N", &json!({})).unwrap();
1303 let b = g.add_node("N", &json!({})).unwrap();
1304 let c = g.add_node("N", &json!({})).unwrap();
1305 g.add_edge(a, b, "R", &json!({})).unwrap();
1306 g.rebuild_csr().unwrap();
1307 assert!(
1308 g.all_paths(a, c).unwrap().is_empty(),
1309 "no path a..c before the edge exists"
1310 );
1311
1312 g.add_edge(b, c, "R", &json!({})).unwrap();
1314 assert!(
1315 !g.all_paths(a, c).unwrap().is_empty(),
1316 "path a->b->c reflected without an explicit rebuild"
1317 );
1318 }
1319
1320 #[test]
1330 fn concurrent_bfs_after_incremental_write_is_consistent() {
1331 use std::sync::Barrier;
1332
1333 let dir = TempDir::new().unwrap();
1334 let g = Graph::open(dir.path(), 1).unwrap();
1335
1336 const N: usize = 30;
1338 let start = g.add_node("N", &json!({ "v": 0 })).unwrap();
1339 let mut prev = start;
1340 for i in 1..N {
1341 let node = g.add_node("N", &json!({ "v": i })).unwrap();
1342 g.add_edge(prev, node, "R", &json!({})).unwrap();
1343 prev = node;
1344 }
1345 g.rebuild_csr().unwrap();
1346
1347 const THREADS: usize = 6;
1348 const ROUNDS: usize = 200;
1349 let mut expected = N;
1350 for r in 0..ROUNDS {
1351 let leaf = g.add_node("N", &json!({ "leaf": r })).unwrap();
1357 g.add_edge(start, leaf, "R", &json!({})).unwrap();
1358 expected += 1;
1359
1360 let barrier = Barrier::new(THREADS);
1361 std::thread::scope(|s| {
1362 for _ in 0..THREADS {
1363 let g = &g;
1364 let barrier = &barrier;
1365 s.spawn(move || {
1366 barrier.wait();
1369 let reached = g.bfs(start, u8::MAX).unwrap();
1370 assert_eq!(
1371 reached.len(),
1372 expected,
1373 "concurrent bfs saw a partially materialized matrix"
1374 );
1375 });
1376 }
1377 });
1378 }
1379 }
1380}
1381
1382#[cfg(test)]
1383mod linear_path_count_tests {
1384 use serde_json::json;
1385 use tempfile::TempDir;
1386
1387 use crate::{Graph, PathCountSpec};
1388
1389 fn open_tmp() -> (TempDir, Graph) {
1390 let dir = TempDir::new().unwrap();
1391 let g = Graph::open(dir.path(), 1).unwrap();
1392 (dir, g)
1393 }
1394
1395 fn spec(
1396 rels: &[Option<&'static str>],
1397 labels: &[Option<&'static str>],
1398 ) -> PathCountSpec<'static> {
1399 PathCountSpec {
1400 rel_types: rels.to_vec(),
1401 labels: labels.to_vec(),
1402 vertex_allow: Vec::new(),
1403 }
1404 }
1405
1406 #[test]
1410 fn two_hop_allow_set_restricts_middle_and_dest() {
1411 let (_dir, g) = open_tmp();
1412 let p: Vec<_> = (0..5)
1414 .map(|i| {
1415 g.add_node("Person", &json!({ "age": 20 + i * 10 }))
1416 .unwrap()
1417 })
1418 .collect();
1419 let edges = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)];
1421 for &(s, d) in &edges {
1422 g.add_edge(p[s], p[d], "FOLLOWS", &json!({})).unwrap();
1423 }
1424
1425 let mid = [p[1], p[2]];
1429 let dst = [p[3], p[4]];
1430 let mut expected = 0u64;
1431 for &(_s1, d1) in &edges {
1432 if !mid.contains(&p[d1]) {
1433 continue;
1434 }
1435 for &(s2, d2) in &edges {
1436 if p[s2] == p[d1] && dst.contains(&p[d2]) {
1437 expected += 1;
1438 }
1439 }
1440 }
1441 assert!(expected > 0, "test graph must have qualifying paths");
1442
1443 let filtered = PathCountSpec {
1444 rel_types: vec![Some("FOLLOWS"), Some("FOLLOWS")],
1445 labels: vec![Some("Person"), Some("Person"), Some("Person")],
1446 vertex_allow: vec![None, Some(mid.to_vec()), Some(dst.to_vec())],
1447 };
1448 assert_eq!(g.count_linear_paths(&filtered).unwrap(), expected);
1449
1450 let unfiltered = g
1453 .count_linear_paths(&spec(
1454 &[Some("FOLLOWS"), Some("FOLLOWS")],
1455 &[Some("Person"); 3],
1456 ))
1457 .unwrap();
1458 assert!(unfiltered > expected);
1459 }
1460
1461 #[test]
1463 fn one_hop_counts_typed_edges() {
1464 let (_dir, g) = open_tmp();
1465 let a = g.add_node("Person", &json!({})).unwrap();
1466 let b = g.add_node("Person", &json!({})).unwrap();
1467 let c = g.add_node("Person", &json!({})).unwrap();
1468 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1469 g.add_edge(a, c, "KNOWS", &json!({})).unwrap();
1470
1471 let n = g
1472 .count_linear_paths(&spec(&[Some("KNOWS")], &[Some("Person"), Some("Person")]))
1473 .unwrap();
1474 assert_eq!(n, 2);
1475 }
1476
1477 #[test]
1480 fn one_hop_label_filter_excludes_endpoint() {
1481 let (_dir, g) = open_tmp();
1482 let a = g.add_node("Person", &json!({})).unwrap();
1483 let b = g.add_node("Person", &json!({})).unwrap();
1484 let c = g.add_node("City", &json!({})).unwrap();
1485 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1486 g.add_edge(a, c, "KNOWS", &json!({})).unwrap();
1487
1488 let n = g
1489 .count_linear_paths(&spec(&[Some("KNOWS")], &[Some("Person"), Some("Person")]))
1490 .unwrap();
1491 assert_eq!(n, 1);
1492 }
1493
1494 #[test]
1496 fn two_hop_distinct_nodes_count_once() {
1497 let (_dir, g) = open_tmp();
1498 let a = g.add_node("Person", &json!({})).unwrap();
1499 let b = g.add_node("Person", &json!({})).unwrap();
1500 let c = g.add_node("Person", &json!({})).unwrap();
1501 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1502 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1503
1504 let n = g
1505 .count_linear_paths(&spec(
1506 &[Some("KNOWS"), Some("KNOWS")],
1507 &[Some("Person"), Some("Person"), Some("Person")],
1508 ))
1509 .unwrap();
1510 assert_eq!(n, 1);
1511 }
1512
1513 #[test]
1515 fn two_hop_parallel_edges_multiply() {
1516 let (_dir, g) = open_tmp();
1517 let a = g.add_node("Person", &json!({})).unwrap();
1518 let b = g.add_node("Person", &json!({})).unwrap();
1519 let c = g.add_node("Person", &json!({})).unwrap();
1520 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1521 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1522 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1523
1524 let n = g
1525 .count_linear_paths(&spec(
1526 &[Some("KNOWS"), Some("KNOWS")],
1527 &[Some("Person"), Some("Person"), Some("Person")],
1528 ))
1529 .unwrap();
1530 assert_eq!(n, 2);
1531 }
1532
1533 #[test]
1536 fn two_hop_self_loop_respects_relationship_uniqueness() {
1537 let (_dir, g) = open_tmp();
1538 let x = g.add_node("Person", &json!({})).unwrap();
1539 let y = g.add_node("Person", &json!({})).unwrap();
1540 g.add_edge(x, x, "KNOWS", &json!({})).unwrap(); g.add_edge(x, y, "KNOWS", &json!({})).unwrap();
1542
1543 let n = g
1547 .count_linear_paths(&spec(
1548 &[Some("KNOWS"), Some("KNOWS")],
1549 &[Some("Person"), Some("Person"), Some("Person")],
1550 ))
1551 .unwrap();
1552 assert_eq!(n, 1);
1553 }
1554
1555 #[test]
1557 fn unknown_relationship_type_counts_zero() {
1558 let (_dir, g) = open_tmp();
1559 let a = g.add_node("Person", &json!({})).unwrap();
1560 let b = g.add_node("Person", &json!({})).unwrap();
1561 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1562
1563 let n = g
1564 .count_linear_paths(&spec(&[Some("LIKES")], &[Some("Person"), Some("Person")]))
1565 .unwrap();
1566 assert_eq!(n, 0);
1567 }
1568}
1569
1570#[cfg(test)]
1571mod triangle_cycle_count_tests {
1572 use serde_json::json;
1573 use tempfile::TempDir;
1574
1575 use crate::{Graph, TriangleCountSpec};
1576
1577 fn open_tmp() -> (TempDir, Graph) {
1578 let dir = TempDir::new().unwrap();
1579 let g = Graph::open(dir.path(), 1).unwrap();
1580 (dir, g)
1581 }
1582
1583 fn spec_all<'a>(rel: &'a str, label: &'a str) -> TriangleCountSpec<'a> {
1584 TriangleCountSpec {
1585 rel_types: [Some(rel); 3],
1586 labels: [Some(label); 3],
1587 }
1588 }
1589
1590 #[test]
1593 fn single_cycle_counts_one_per_rotation() {
1594 let (_dir, g) = open_tmp();
1595 let a = g.add_node("Person", &json!({})).unwrap();
1596 let b = g.add_node("Person", &json!({})).unwrap();
1597 let c = g.add_node("Person", &json!({})).unwrap();
1598 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1599 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1600 g.add_edge(c, a, "KNOWS", &json!({})).unwrap();
1601
1602 let n = g
1603 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1604 .unwrap();
1605 assert_eq!(n, 3);
1606 }
1607
1608 #[test]
1611 fn non_cyclic_orientation_does_not_count() {
1612 let (_dir, g) = open_tmp();
1613 let a = g.add_node("Person", &json!({})).unwrap();
1614 let b = g.add_node("Person", &json!({})).unwrap();
1615 let c = g.add_node("Person", &json!({})).unwrap();
1616 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1617 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1618 g.add_edge(a, c, "KNOWS", &json!({})).unwrap();
1619
1620 let n = g
1621 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1622 .unwrap();
1623 assert_eq!(n, 0);
1624 }
1625
1626 #[test]
1629 fn parallel_edges_multiply() {
1630 let (_dir, g) = open_tmp();
1631 let a = g.add_node("Person", &json!({})).unwrap();
1632 let b = g.add_node("Person", &json!({})).unwrap();
1633 let c = g.add_node("Person", &json!({})).unwrap();
1634 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1635 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1636 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1637 g.add_edge(c, a, "KNOWS", &json!({})).unwrap();
1638
1639 let n = g
1640 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1641 .unwrap();
1642 assert_eq!(n, 6);
1643 }
1644
1645 #[test]
1648 fn per_hop_types_are_positional() {
1649 let (_dir, g) = open_tmp();
1650 let a = g.add_node("Person", &json!({})).unwrap();
1651 let b = g.add_node("Person", &json!({})).unwrap();
1652 let c = g.add_node("Person", &json!({})).unwrap();
1653 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1654 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1655 g.add_edge(c, a, "LIKES", &json!({})).unwrap();
1656
1657 let homogeneous = g
1658 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1659 .unwrap();
1660 assert_eq!(homogeneous, 0);
1661
1662 let mixed = g
1663 .count_triangle_cycles(&TriangleCountSpec {
1664 rel_types: [Some("KNOWS"), Some("KNOWS"), Some("LIKES")],
1665 labels: [Some("Person"); 3],
1666 })
1667 .unwrap();
1668 assert_eq!(mixed, 1);
1669 }
1670
1671 #[test]
1673 fn untyped_hops_match_any_type() {
1674 let (_dir, g) = open_tmp();
1675 let a = g.add_node("Person", &json!({})).unwrap();
1676 let b = g.add_node("Person", &json!({})).unwrap();
1677 let c = g.add_node("Person", &json!({})).unwrap();
1678 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1679 g.add_edge(b, c, "LIKES", &json!({})).unwrap();
1680 g.add_edge(c, a, "FOLLOWS", &json!({})).unwrap();
1681
1682 let n = g
1683 .count_triangle_cycles(&TriangleCountSpec {
1684 rel_types: [None; 3],
1685 labels: [Some("Person"); 3],
1686 })
1687 .unwrap();
1688 assert_eq!(n, 3);
1689 }
1690
1691 #[test]
1694 fn label_filter_applies_per_variable() {
1695 let (_dir, g) = open_tmp();
1696 let a = g.add_node("Person", &json!({})).unwrap();
1697 let b = g.add_node("Person", &json!({})).unwrap();
1698 let c = g.add_node("Robot", &json!({})).unwrap();
1699 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1700 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1701 g.add_edge(c, a, "KNOWS", &json!({})).unwrap();
1702
1703 let strict = g
1704 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1705 .unwrap();
1706 assert_eq!(strict, 0);
1707
1708 g.add_label(c, "Person").unwrap();
1710 let after = g
1711 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1712 .unwrap();
1713 assert_eq!(after, 3);
1714
1715 let unlabeled = g
1716 .count_triangle_cycles(&TriangleCountSpec {
1717 rel_types: [Some("KNOWS"); 3],
1718 labels: [None; 3],
1719 })
1720 .unwrap();
1721 assert_eq!(unlabeled, 3);
1722 }
1723
1724 #[test]
1728 fn self_loop_assignments_respect_relationship_uniqueness() {
1729 let (_dir, g) = open_tmp();
1730 let a = g.add_node("Person", &json!({})).unwrap();
1731 g.add_edge(a, a, "KNOWS", &json!({})).unwrap();
1732 g.add_edge(a, a, "KNOWS", &json!({})).unwrap();
1733
1734 let two = g
1735 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1736 .unwrap();
1737 assert_eq!(two, 0);
1738
1739 g.add_edge(a, a, "KNOWS", &json!({})).unwrap();
1740 let three = g
1741 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1742 .unwrap();
1743 assert_eq!(three, 6);
1744 }
1745
1746 #[test]
1749 fn self_loop_with_two_cycle_counts_each_position() {
1750 let (_dir, g) = open_tmp();
1751 let x = g.add_node("Person", &json!({})).unwrap();
1752 let y = g.add_node("Person", &json!({})).unwrap();
1753 g.add_edge(x, x, "KNOWS", &json!({})).unwrap();
1754 g.add_edge(x, y, "KNOWS", &json!({})).unwrap();
1755 g.add_edge(y, x, "KNOWS", &json!({})).unwrap();
1756
1757 let n = g
1758 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1759 .unwrap();
1760 assert_eq!(n, 3);
1761 }
1762
1763 #[test]
1766 fn unknown_type_or_label_counts_zero() {
1767 let (_dir, g) = open_tmp();
1768 let a = g.add_node("Person", &json!({})).unwrap();
1769 let b = g.add_node("Person", &json!({})).unwrap();
1770 let c = g.add_node("Person", &json!({})).unwrap();
1771 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1772 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1773 g.add_edge(c, a, "KNOWS", &json!({})).unwrap();
1774
1775 assert_eq!(
1776 g.count_triangle_cycles(&spec_all("NOPE", "Person"))
1777 .unwrap(),
1778 0
1779 );
1780 assert_eq!(
1781 g.count_triangle_cycles(&spec_all("KNOWS", "Ghost"))
1782 .unwrap(),
1783 0
1784 );
1785 }
1786
1787 #[test]
1790 fn count_is_fresh_after_writes() {
1791 let (_dir, g) = open_tmp();
1792 let a = g.add_node("Person", &json!({})).unwrap();
1793 let b = g.add_node("Person", &json!({})).unwrap();
1794 let c = g.add_node("Person", &json!({})).unwrap();
1795 g.add_edge(a, b, "KNOWS", &json!({})).unwrap();
1796 g.add_edge(b, c, "KNOWS", &json!({})).unwrap();
1797
1798 let before = g
1799 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1800 .unwrap();
1801 assert_eq!(before, 0);
1802
1803 g.add_edge(c, a, "KNOWS", &json!({})).unwrap();
1804 let after = g
1805 .count_triangle_cycles(&spec_all("KNOWS", "Person"))
1806 .unwrap();
1807 assert_eq!(after, 3);
1808 }
1809
1810 #[test]
1812 fn empty_graph_counts_zero() {
1813 let (_dir, g) = open_tmp();
1814 assert_eq!(
1815 g.count_triangle_cycles(&spec_all("KNOWS", "Person"))
1816 .unwrap(),
1817 0
1818 );
1819 }
1820}