1use ahash::HashMap;
4use mentedb_core::edge::{EdgeType, MemoryEdge};
5use mentedb_core::error::{MenteError, MenteResult};
6use mentedb_core::types::{MemoryId, Timestamp};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct StoredEdge {
12 pub edge_type: EdgeType,
14 pub weight: f32,
16 pub created_at: Timestamp,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub valid_from: Option<Timestamp>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub valid_until: Option<Timestamp>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub label: Option<String>,
27}
28
29impl StoredEdge {
30 pub fn from_memory_edge(edge: &MemoryEdge) -> Self {
32 Self {
33 edge_type: edge.edge_type,
34 weight: edge.weight,
35 created_at: edge.created_at,
36 valid_from: edge.valid_from,
37 valid_until: edge.valid_until,
38 label: edge.label.clone(),
39 }
40 }
41
42 pub fn is_valid_at(&self, at: Timestamp) -> bool {
44 let from = self.valid_from.unwrap_or(0);
45 match self.valid_until {
46 Some(until) => at >= from && at < until,
47 None => at >= from,
48 }
49 }
50
51 pub fn is_invalidated(&self) -> bool {
53 self.valid_until.is_some()
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59struct DeltaEdge {
60 source_idx: u32,
61 target_idx: u32,
62 data: StoredEdge,
63}
64
65#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67struct CompressedStorage {
68 row_offsets: Vec<u32>,
70 col_indices: Vec<u32>,
72 edge_data: Vec<StoredEdge>,
74}
75
76impl CompressedStorage {
77 #[allow(dead_code)]
78 fn new(num_nodes: usize) -> Self {
79 Self {
80 row_offsets: vec![0; num_nodes + 1],
81 col_indices: Vec::new(),
82 edge_data: Vec::new(),
83 }
84 }
85
86 fn neighbors(&self, row: u32) -> &[u32] {
88 let row = row as usize;
89 if row + 1 >= self.row_offsets.len() {
90 return &[];
91 }
92 let start = self.row_offsets[row] as usize;
93 let end = self.row_offsets[row + 1] as usize;
94 &self.col_indices[start..end]
95 }
96
97 fn edge_data_for(&self, row: u32) -> &[StoredEdge] {
98 let row = row as usize;
99 if row + 1 >= self.row_offsets.len() {
100 return &[];
101 }
102 let start = self.row_offsets[row] as usize;
103 let end = self.row_offsets[row + 1] as usize;
104 &self.edge_data[start..end]
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct CsrGraph {
111 id_to_idx: HashMap<MemoryId, u32>,
113 idx_to_id: Vec<MemoryId>,
115
116 csr: CompressedStorage,
118 csc: CompressedStorage,
120
121 delta_edges: Vec<DeltaEdge>,
123 removed_edges: Vec<(u32, u32)>,
125
126 #[serde(default)]
132 edge_type_counts: HashMap<EdgeType, usize>,
133}
134
135impl CsrGraph {
136 pub fn new() -> Self {
138 Self {
139 id_to_idx: HashMap::default(),
140 idx_to_id: Vec::new(),
141 csr: CompressedStorage::default(),
142 csc: CompressedStorage::default(),
143 delta_edges: Vec::new(),
144 removed_edges: Vec::new(),
145 edge_type_counts: HashMap::default(),
146 }
147 }
148
149 pub fn add_node(&mut self, id: MemoryId) -> u32 {
151 if let Some(&idx) = self.id_to_idx.get(&id) {
152 return idx;
153 }
154 let idx = self.idx_to_id.len() as u32;
155 self.id_to_idx.insert(id, idx);
156 self.idx_to_id.push(id);
157 idx
158 }
159
160 pub fn remove_node(&mut self, id: MemoryId) {
162 let Some(&idx) = self.id_to_idx.get(&id) else {
163 return;
164 };
165
166 let mut decrements: Vec<EdgeType> = Vec::new();
170 {
171 let out_neighbors = self.csr.neighbors(idx).to_vec();
172 let out_types: Vec<EdgeType> = self
173 .csr
174 .edge_data_for(idx)
175 .iter()
176 .map(|e| e.edge_type)
177 .collect();
178 for (i, &neighbor) in out_neighbors.iter().enumerate() {
179 if !self.is_removed(idx, neighbor) {
180 decrements.push(out_types[i]);
181 }
182 }
183 let in_neighbors = self.csc.neighbors(idx).to_vec();
184 let in_types: Vec<EdgeType> = self
185 .csc
186 .edge_data_for(idx)
187 .iter()
188 .map(|e| e.edge_type)
189 .collect();
190 for (i, &neighbor) in in_neighbors.iter().enumerate() {
191 if !self.is_removed(neighbor, idx) {
192 decrements.push(in_types[i]);
193 }
194 }
195 for e in &self.delta_edges {
196 if e.source_idx == idx || e.target_idx == idx {
197 decrements.push(e.data.edge_type);
198 }
199 }
200 }
201 for et in decrements {
202 if let Some(c) = self.edge_type_counts.get_mut(&et) {
203 *c = c.saturating_sub(1);
204 }
205 }
206
207 for &neighbor in self.csr.neighbors(idx) {
209 self.removed_edges.push((idx, neighbor));
210 }
211 for &neighbor in self.csc.neighbors(idx) {
212 self.removed_edges.push((neighbor, idx));
213 }
214 self.delta_edges
216 .retain(|e| e.source_idx != idx && e.target_idx != idx);
217 self.id_to_idx.remove(&id);
218 }
219
220 pub fn add_edge(&mut self, edge: &MemoryEdge) {
227 let source_idx = self.add_node(edge.source);
228 let target_idx = self.add_node(edge.target);
229
230 let duplicate = self.outgoing_by_idx(source_idx).into_iter().any(|(t, e)| {
231 t == edge.target && e.edge_type == edge.edge_type && e.valid_until.is_none()
232 });
233 if duplicate {
234 return;
235 }
236
237 self.delta_edges.push(DeltaEdge {
238 source_idx,
239 target_idx,
240 data: StoredEdge::from_memory_edge(edge),
241 });
242 *self.edge_type_counts.entry(edge.edge_type).or_default() += 1;
243 }
244
245 pub fn strengthen_edge(&mut self, source: MemoryId, target: MemoryId, delta: f32) {
252 let (Some(&source_idx), Some(&target_idx)) =
253 (self.id_to_idx.get(&source), self.id_to_idx.get(&target))
254 else {
255 return;
256 };
257
258 if let Some(existing) = self
260 .delta_edges
261 .iter_mut()
262 .find(|e| e.source_idx == source_idx && e.target_idx == target_idx)
263 {
264 existing.data.weight = (existing.data.weight + delta).min(1.0);
265 return;
266 }
267
268 if let Some((_, stored)) = self
272 .outgoing_by_idx(source_idx)
273 .into_iter()
274 .find(|(id, _)| *id == target)
275 {
276 let new_weight = (stored.weight + delta).min(1.0);
277 self.removed_edges.push((source_idx, target_idx));
278 self.delta_edges.push(DeltaEdge {
279 source_idx,
280 target_idx,
281 data: StoredEdge {
282 weight: new_weight,
283 ..stored
284 },
285 });
286 }
287 }
288
289 pub fn remove_edge(&mut self, source: MemoryId, target: MemoryId) {
291 let (Some(&src_idx), Some(&tgt_idx)) =
292 (self.id_to_idx.get(&source), self.id_to_idx.get(&target))
293 else {
294 return;
295 };
296 self.removed_edges.push((src_idx, tgt_idx));
297 self.delta_edges
298 .retain(|e| !(e.source_idx == src_idx && e.target_idx == tgt_idx));
299 }
300
301 pub fn remove_edges_of_types(&mut self, types: &[EdgeType]) -> usize {
312 let matches = |et: EdgeType| types.contains(&et);
313 let mut removed = 0usize;
314
315 let before = self.delta_edges.len();
317 self.delta_edges.retain(|e| !matches(e.data.edge_type));
318 removed += before - self.delta_edges.len();
319
320 let mut restore: Vec<DeltaEdge> = Vec::new();
323 let n = self.idx_to_id.len() as u32;
324 for src_idx in 0..n {
325 let row: Vec<(u32, StoredEdge)> = {
326 let neighbors = self.csr.neighbors(src_idx);
327 let edges = self.csr.edge_data_for(src_idx);
328 let mut v = Vec::new();
329 for (i, &t) in neighbors.iter().enumerate() {
330 if !self.is_removed(src_idx, t) {
331 v.push((t, edges[i].clone()));
332 }
333 }
334 v
335 };
336 let matched_pairs: std::collections::HashSet<u32> = row
337 .iter()
338 .filter(|(_, e)| matches(e.edge_type))
339 .map(|(t, _)| *t)
340 .collect();
341 if matched_pairs.is_empty() {
342 continue;
343 }
344 for &tgt_idx in &matched_pairs {
345 self.removed_edges.push((src_idx, tgt_idx));
346 }
347 for (tgt_idx, e) in row {
348 if !matched_pairs.contains(&tgt_idx) {
349 continue;
350 }
351 if matches(e.edge_type) {
352 removed += 1;
353 } else {
354 restore.push(DeltaEdge {
355 source_idx: src_idx,
356 target_idx: tgt_idx,
357 data: e,
358 });
359 }
360 }
361 }
362 self.delta_edges.extend(restore);
363 self.recompute_edge_counts();
366 removed
367 }
368
369 pub fn outgoing(&self, id: MemoryId) -> Vec<(MemoryId, StoredEdge)> {
371 let Some(&idx) = self.id_to_idx.get(&id) else {
372 return Vec::new();
373 };
374 self.outgoing_by_idx(idx)
375 }
376
377 pub fn outgoing_valid_at(&self, id: MemoryId, at: Timestamp) -> Vec<(MemoryId, StoredEdge)> {
379 self.outgoing(id)
380 .into_iter()
381 .filter(|(_, e)| e.is_valid_at(at))
382 .collect()
383 }
384
385 pub(crate) fn outgoing_by_idx(&self, idx: u32) -> Vec<(MemoryId, StoredEdge)> {
386 let mut results = Vec::new();
387
388 let neighbors = self.csr.neighbors(idx);
390 let edges = self.csr.edge_data_for(idx);
391 for (i, &neighbor) in neighbors.iter().enumerate() {
392 if !self.is_removed(idx, neighbor)
393 && let Some(&id) = self.idx_to_id.get(neighbor as usize)
394 {
395 results.push((id, edges[i].clone()));
396 }
397 }
398
399 for delta in &self.delta_edges {
401 if delta.source_idx == idx
402 && let Some(&id) = self.idx_to_id.get(delta.target_idx as usize)
403 {
404 results.push((id, delta.data.clone()));
405 }
406 }
407
408 results
409 }
410
411 pub fn conflict_edge_counts(&self) -> (usize, usize) {
415 (
416 self.edge_type_counts
417 .get(&EdgeType::Contradicts)
418 .copied()
419 .unwrap_or(0),
420 self.edge_type_counts
421 .get(&EdgeType::Supersedes)
422 .copied()
423 .unwrap_or(0),
424 )
425 }
426
427 pub fn recompute_edge_counts(&mut self) {
431 let mut counts: HashMap<EdgeType, usize> = HashMap::default();
432 let node_count = self.idx_to_id.len() as u32;
433 for idx in 0..node_count {
434 let neighbors = self.csr.neighbors(idx);
435 let data = self.csr.edge_data_for(idx);
436 for (i, &nbr) in neighbors.iter().enumerate() {
437 if !self.is_removed(idx, nbr) {
438 *counts.entry(data[i].edge_type).or_default() += 1;
439 }
440 }
441 }
442 for d in &self.delta_edges {
443 if !self.is_removed(d.source_idx, d.target_idx) {
444 *counts.entry(d.data.edge_type).or_default() += 1;
445 }
446 }
447 self.edge_type_counts = counts;
448 }
449
450 pub fn incoming(&self, id: MemoryId) -> Vec<(MemoryId, StoredEdge)> {
452 let Some(&idx) = self.id_to_idx.get(&id) else {
453 return Vec::new();
454 };
455 self.incoming_by_idx(idx)
456 }
457
458 pub fn incoming_valid_at(&self, id: MemoryId, at: Timestamp) -> Vec<(MemoryId, StoredEdge)> {
460 self.incoming(id)
461 .into_iter()
462 .filter(|(_, e)| e.is_valid_at(at))
463 .collect()
464 }
465
466 pub(crate) fn incoming_by_idx(&self, idx: u32) -> Vec<(MemoryId, StoredEdge)> {
467 let mut results = Vec::new();
468
469 let neighbors = self.csc.neighbors(idx);
471 let edges = self.csc.edge_data_for(idx);
472 for (i, &neighbor) in neighbors.iter().enumerate() {
473 if !self.is_removed(neighbor, idx)
474 && let Some(&id) = self.idx_to_id.get(neighbor as usize)
475 {
476 results.push((id, edges[i].clone()));
477 }
478 }
479
480 for delta in &self.delta_edges {
482 if delta.target_idx == idx
483 && let Some(&id) = self.idx_to_id.get(delta.source_idx as usize)
484 {
485 results.push((id, delta.data.clone()));
486 }
487 }
488
489 results
490 }
491
492 pub fn contains_node(&self, id: MemoryId) -> bool {
494 self.id_to_idx.contains_key(&id)
495 }
496
497 pub fn node_count(&self) -> usize {
499 self.idx_to_id.len()
500 }
501
502 pub(crate) fn get_idx(&self, id: MemoryId) -> Option<u32> {
504 self.id_to_idx.get(&id).copied()
505 }
506
507 #[allow(dead_code)]
509 pub(crate) fn get_id(&self, idx: u32) -> Option<MemoryId> {
510 self.idx_to_id.get(idx as usize).copied()
511 }
512
513 pub fn node_ids(&self) -> &[MemoryId] {
515 &self.idx_to_id
516 }
517
518 fn is_removed(&self, source: u32, target: u32) -> bool {
519 self.removed_edges
520 .iter()
521 .any(|&(s, t)| s == source && t == target)
522 }
523
524 pub fn compact(&mut self) {
526 let num_nodes = self.idx_to_id.len();
527
528 let mut all_edges: Vec<(u32, u32, StoredEdge)> = Vec::new();
530
531 for row in 0..num_nodes {
533 let row = row as u32;
534 let neighbors = self.csr.neighbors(row);
535 let edges = self.csr.edge_data_for(row);
536 for (i, &col) in neighbors.iter().enumerate() {
537 if !self.is_removed(row, col) {
538 all_edges.push((row, col, edges[i].clone()));
539 }
540 }
541 }
542
543 for delta in &self.delta_edges {
545 all_edges.push((delta.source_idx, delta.target_idx, delta.data.clone()));
546 }
547
548 self.csr = Self::build_compressed(&all_edges, num_nodes, false);
550
551 self.csc = Self::build_compressed(&all_edges, num_nodes, true);
553
554 self.delta_edges.clear();
555 self.removed_edges.clear();
556
557 self.recompute_edge_counts();
560 }
561
562 fn build_compressed(
563 edges: &[(u32, u32, StoredEdge)],
564 num_nodes: usize,
565 transpose: bool,
566 ) -> CompressedStorage {
567 let mut counts = vec![0u32; num_nodes];
569 for &(src, tgt, ref _data) in edges {
570 let row = if transpose { tgt } else { src };
571 if (row as usize) < num_nodes {
572 counts[row as usize] += 1;
573 }
574 }
575
576 let mut row_offsets = vec![0u32; num_nodes + 1];
578 for i in 0..num_nodes {
579 row_offsets[i + 1] = row_offsets[i] + counts[i];
580 }
581
582 let total = row_offsets[num_nodes] as usize;
583 let mut col_indices = vec![0u32; total];
584 let mut edge_data = vec![
585 StoredEdge {
586 edge_type: EdgeType::Related,
587 weight: 0.0,
588 created_at: 0,
589 valid_from: None,
590 valid_until: None,
591 label: None,
592 };
593 total
594 ];
595
596 let mut cursors = row_offsets[..num_nodes].to_vec();
598 for &(src, tgt, ref data) in edges {
599 let (row, col) = if transpose { (tgt, src) } else { (src, tgt) };
600 if (row as usize) < num_nodes {
601 let pos = cursors[row as usize] as usize;
602 col_indices[pos] = col;
603 edge_data[pos] = data.clone();
604 cursors[row as usize] += 1;
605 }
606 }
607
608 CompressedStorage {
609 row_offsets,
610 col_indices,
611 edge_data,
612 }
613 }
614 pub fn save(&self, path: &std::path::Path) -> MenteResult<()> {
616 let data =
617 serde_json::to_vec(self).map_err(|e| MenteError::Serialization(e.to_string()))?;
618 let tmp_path = path.with_extension("json.tmp");
621 {
622 use std::io::Write;
623 let mut file = std::fs::File::create(&tmp_path)?;
624 file.write_all(&data)?;
625 file.sync_data()?;
626 }
627 std::fs::rename(&tmp_path, path)?;
628 Ok(())
629 }
630
631 pub fn load(path: &std::path::Path) -> MenteResult<Self> {
633 let data = std::fs::read(path)?;
634 let mut graph: Self =
635 serde_json::from_slice(&data).map_err(|e| MenteError::Serialization(e.to_string()))?;
636 graph.recompute_edge_counts();
641 Ok(graph)
642 }
643}
644
645impl Default for CsrGraph {
646 fn default() -> Self {
647 Self::new()
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 fn make_edge(src: MemoryId, tgt: MemoryId, etype: EdgeType) -> MemoryEdge {
656 MemoryEdge {
657 source: src,
658 target: tgt,
659 edge_type: etype,
660 weight: 0.8,
661 created_at: 1000,
662 valid_from: None,
663 valid_until: None,
664 label: None,
665 }
666 }
667
668 #[test]
669 fn test_add_node_idempotent() {
670 let mut g = CsrGraph::new();
671 let id = MemoryId::new();
672 let idx1 = g.add_node(id);
673 let idx2 = g.add_node(id);
674 assert_eq!(idx1, idx2);
675 assert_eq!(g.node_count(), 1);
676 }
677
678 #[test]
679 fn edge_type_counts_track_add_compact_and_remove() {
680 let mut g = CsrGraph::new();
681 let (a, b, c, d) = (
682 MemoryId::new(),
683 MemoryId::new(),
684 MemoryId::new(),
685 MemoryId::new(),
686 );
687
688 g.add_edge(&make_edge(a, b, EdgeType::Contradicts));
691 g.add_edge(&make_edge(c, d, EdgeType::Supersedes));
692 g.add_edge(&make_edge(a, c, EdgeType::Related));
693 assert_eq!(g.conflict_edge_counts(), (1, 1));
694 g.add_edge(&make_edge(a, b, EdgeType::Contradicts));
696 assert_eq!(g.conflict_edge_counts(), (1, 1));
697
698 g.compact();
701 assert_eq!(g.conflict_edge_counts(), (1, 1));
702
703 g.add_edge(&make_edge(a, d, EdgeType::Supersedes));
705 assert_eq!(g.conflict_edge_counts(), (1, 2));
706
707 g.remove_node(a);
710 assert_eq!(g.conflict_edge_counts(), (0, 1));
711 }
712
713 #[test]
714 fn test_add_and_query_edges() {
715 let mut g = CsrGraph::new();
716 let a = MemoryId::new();
717 let b = MemoryId::new();
718 let c = MemoryId::new();
719
720 g.add_edge(&make_edge(a, b, EdgeType::Caused));
721 g.add_edge(&make_edge(a, c, EdgeType::Related));
722
723 let out = g.outgoing(a);
724 assert_eq!(out.len(), 2);
725
726 let inc_b = g.incoming(b);
727 assert_eq!(inc_b.len(), 1);
728 assert_eq!(inc_b[0].0, a);
729 }
730
731 #[test]
732 fn test_remove_edge() {
733 let mut g = CsrGraph::new();
734 let a = MemoryId::new();
735 let b = MemoryId::new();
736
737 g.add_edge(&make_edge(a, b, EdgeType::Caused));
738 assert_eq!(g.outgoing(a).len(), 1);
739
740 g.remove_edge(a, b);
741 assert_eq!(g.outgoing(a).len(), 0);
742 }
743
744 #[test]
745 fn test_remove_edges_of_types_preserves_siblings() {
746 let mut g = CsrGraph::new();
747 let a = MemoryId::new();
748 let b = MemoryId::new();
749 let c = MemoryId::new();
750
751 g.add_edge(&make_edge(a, b, EdgeType::Supersedes));
754 g.add_edge(&make_edge(a, b, EdgeType::Related));
755 g.add_edge(&make_edge(a, c, EdgeType::Contradicts));
757 g.add_edge(&make_edge(b, c, EdgeType::Caused));
759
760 g.compact();
763
764 let removed = g.remove_edges_of_types(&[EdgeType::Contradicts, EdgeType::Supersedes]);
765 g.compact();
766 assert_eq!(removed, 2, "one Supersedes + one Contradicts");
767
768 let ab: Vec<_> = g.outgoing(a).into_iter().filter(|(t, _)| *t == b).collect();
770 assert_eq!(ab.len(), 1);
771 assert_eq!(ab[0].1.edge_type, EdgeType::Related);
772
773 assert!(g.outgoing(a).into_iter().all(|(t, _)| t != c));
775
776 assert_eq!(g.outgoing(b).len(), 1);
778 let inc_c = g.incoming(c);
779 assert_eq!(inc_c.len(), 1);
780 assert_eq!(inc_c[0].0, b);
781 }
782
783 #[test]
784 fn test_compact() {
785 let mut g = CsrGraph::new();
786 let a = MemoryId::new();
787 let b = MemoryId::new();
788 let c = MemoryId::new();
789
790 g.add_edge(&make_edge(a, b, EdgeType::Caused));
791 g.add_edge(&make_edge(b, c, EdgeType::Before));
792 g.compact();
793
794 let out_a = g.outgoing(a);
795 assert_eq!(out_a.len(), 1);
796 assert_eq!(out_a[0].0, b);
797
798 let inc_c = g.incoming(c);
799 assert_eq!(inc_c.len(), 1);
800 assert_eq!(inc_c[0].0, b);
801 }
802
803 #[test]
804 fn test_compact_with_removals() {
805 let mut g = CsrGraph::new();
806 let a = MemoryId::new();
807 let b = MemoryId::new();
808 let c = MemoryId::new();
809
810 g.add_edge(&make_edge(a, b, EdgeType::Caused));
811 g.add_edge(&make_edge(a, c, EdgeType::Related));
812 g.compact();
813
814 g.remove_edge(a, b);
815 g.compact();
816
817 let out = g.outgoing(a);
818 assert_eq!(out.len(), 1);
819 assert_eq!(out[0].0, c);
820 }
821
822 #[test]
823 fn test_remove_node_cleans_id_to_idx() {
824 let mut g = CsrGraph::new();
825 let a = MemoryId::new();
826 let b = MemoryId::new();
827
828 g.add_edge(&make_edge(a, b, EdgeType::Caused));
829 assert!(g.contains_node(a));
830 assert!(g.contains_node(b));
831
832 g.remove_node(a);
833 assert!(
834 !g.contains_node(a),
835 "removed node should not be in id_to_idx"
836 );
837 assert!(g.contains_node(b), "unrelated node should still exist");
838
839 assert!(g.outgoing(a).is_empty());
841 assert!(g.incoming(b).is_empty());
842 }
843
844 #[test]
845 fn test_remove_node_then_readd() {
846 let mut g = CsrGraph::new();
847 let a = MemoryId::new();
848 let b = MemoryId::new();
849 let c = MemoryId::new();
850
851 g.add_edge(&make_edge(a, b, EdgeType::Caused));
852 g.remove_node(a);
853
854 g.add_edge(&make_edge(a, c, EdgeType::Related));
856 assert!(g.contains_node(a));
857 let out = g.outgoing(a);
858 assert_eq!(out.len(), 1);
859 assert_eq!(out[0].0, c);
860 }
861}