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
127impl CsrGraph {
128 pub fn new() -> Self {
130 Self {
131 id_to_idx: HashMap::default(),
132 idx_to_id: Vec::new(),
133 csr: CompressedStorage::default(),
134 csc: CompressedStorage::default(),
135 delta_edges: Vec::new(),
136 removed_edges: Vec::new(),
137 }
138 }
139
140 pub fn add_node(&mut self, id: MemoryId) -> u32 {
142 if let Some(&idx) = self.id_to_idx.get(&id) {
143 return idx;
144 }
145 let idx = self.idx_to_id.len() as u32;
146 self.id_to_idx.insert(id, idx);
147 self.idx_to_id.push(id);
148 idx
149 }
150
151 pub fn remove_node(&mut self, id: MemoryId) {
153 let Some(&idx) = self.id_to_idx.get(&id) else {
154 return;
155 };
156 for &neighbor in self.csr.neighbors(idx) {
158 self.removed_edges.push((idx, neighbor));
159 }
160 for &neighbor in self.csc.neighbors(idx) {
161 self.removed_edges.push((neighbor, idx));
162 }
163 self.delta_edges
165 .retain(|e| e.source_idx != idx && e.target_idx != idx);
166 self.id_to_idx.remove(&id);
167 }
168
169 pub fn add_edge(&mut self, edge: &MemoryEdge) {
176 let source_idx = self.add_node(edge.source);
177 let target_idx = self.add_node(edge.target);
178
179 let duplicate = self.outgoing_by_idx(source_idx).into_iter().any(|(t, e)| {
180 t == edge.target && e.edge_type == edge.edge_type && e.valid_until.is_none()
181 });
182 if duplicate {
183 return;
184 }
185
186 self.delta_edges.push(DeltaEdge {
187 source_idx,
188 target_idx,
189 data: StoredEdge::from_memory_edge(edge),
190 });
191 }
192
193 pub fn strengthen_edge(&mut self, source: MemoryId, target: MemoryId, delta: f32) {
200 let (Some(&source_idx), Some(&target_idx)) =
201 (self.id_to_idx.get(&source), self.id_to_idx.get(&target))
202 else {
203 return;
204 };
205
206 if let Some(existing) = self
208 .delta_edges
209 .iter_mut()
210 .find(|e| e.source_idx == source_idx && e.target_idx == target_idx)
211 {
212 existing.data.weight = (existing.data.weight + delta).min(1.0);
213 return;
214 }
215
216 if let Some((_, stored)) = self
220 .outgoing_by_idx(source_idx)
221 .into_iter()
222 .find(|(id, _)| *id == target)
223 {
224 let new_weight = (stored.weight + delta).min(1.0);
225 self.removed_edges.push((source_idx, target_idx));
226 self.delta_edges.push(DeltaEdge {
227 source_idx,
228 target_idx,
229 data: StoredEdge {
230 weight: new_weight,
231 ..stored
232 },
233 });
234 }
235 }
236
237 pub fn remove_edge(&mut self, source: MemoryId, target: MemoryId) {
239 let (Some(&src_idx), Some(&tgt_idx)) =
240 (self.id_to_idx.get(&source), self.id_to_idx.get(&target))
241 else {
242 return;
243 };
244 self.removed_edges.push((src_idx, tgt_idx));
245 self.delta_edges
246 .retain(|e| !(e.source_idx == src_idx && e.target_idx == tgt_idx));
247 }
248
249 pub fn outgoing(&self, id: MemoryId) -> Vec<(MemoryId, StoredEdge)> {
251 let Some(&idx) = self.id_to_idx.get(&id) else {
252 return Vec::new();
253 };
254 self.outgoing_by_idx(idx)
255 }
256
257 pub fn outgoing_valid_at(&self, id: MemoryId, at: Timestamp) -> Vec<(MemoryId, StoredEdge)> {
259 self.outgoing(id)
260 .into_iter()
261 .filter(|(_, e)| e.is_valid_at(at))
262 .collect()
263 }
264
265 pub(crate) fn outgoing_by_idx(&self, idx: u32) -> Vec<(MemoryId, StoredEdge)> {
266 let mut results = Vec::new();
267
268 let neighbors = self.csr.neighbors(idx);
270 let edges = self.csr.edge_data_for(idx);
271 for (i, &neighbor) in neighbors.iter().enumerate() {
272 if !self.is_removed(idx, neighbor)
273 && let Some(&id) = self.idx_to_id.get(neighbor as usize)
274 {
275 results.push((id, edges[i].clone()));
276 }
277 }
278
279 for delta in &self.delta_edges {
281 if delta.source_idx == idx
282 && let Some(&id) = self.idx_to_id.get(delta.target_idx as usize)
283 {
284 results.push((id, delta.data.clone()));
285 }
286 }
287
288 results
289 }
290
291 pub fn incoming(&self, id: MemoryId) -> Vec<(MemoryId, StoredEdge)> {
293 let Some(&idx) = self.id_to_idx.get(&id) else {
294 return Vec::new();
295 };
296 self.incoming_by_idx(idx)
297 }
298
299 pub fn incoming_valid_at(&self, id: MemoryId, at: Timestamp) -> Vec<(MemoryId, StoredEdge)> {
301 self.incoming(id)
302 .into_iter()
303 .filter(|(_, e)| e.is_valid_at(at))
304 .collect()
305 }
306
307 pub(crate) fn incoming_by_idx(&self, idx: u32) -> Vec<(MemoryId, StoredEdge)> {
308 let mut results = Vec::new();
309
310 let neighbors = self.csc.neighbors(idx);
312 let edges = self.csc.edge_data_for(idx);
313 for (i, &neighbor) in neighbors.iter().enumerate() {
314 if !self.is_removed(neighbor, idx)
315 && let Some(&id) = self.idx_to_id.get(neighbor as usize)
316 {
317 results.push((id, edges[i].clone()));
318 }
319 }
320
321 for delta in &self.delta_edges {
323 if delta.target_idx == idx
324 && let Some(&id) = self.idx_to_id.get(delta.source_idx as usize)
325 {
326 results.push((id, delta.data.clone()));
327 }
328 }
329
330 results
331 }
332
333 pub fn contains_node(&self, id: MemoryId) -> bool {
335 self.id_to_idx.contains_key(&id)
336 }
337
338 pub fn node_count(&self) -> usize {
340 self.idx_to_id.len()
341 }
342
343 pub(crate) fn get_idx(&self, id: MemoryId) -> Option<u32> {
345 self.id_to_idx.get(&id).copied()
346 }
347
348 #[allow(dead_code)]
350 pub(crate) fn get_id(&self, idx: u32) -> Option<MemoryId> {
351 self.idx_to_id.get(idx as usize).copied()
352 }
353
354 pub fn node_ids(&self) -> &[MemoryId] {
356 &self.idx_to_id
357 }
358
359 fn is_removed(&self, source: u32, target: u32) -> bool {
360 self.removed_edges
361 .iter()
362 .any(|&(s, t)| s == source && t == target)
363 }
364
365 pub fn compact(&mut self) {
367 let num_nodes = self.idx_to_id.len();
368
369 let mut all_edges: Vec<(u32, u32, StoredEdge)> = Vec::new();
371
372 for row in 0..num_nodes {
374 let row = row as u32;
375 let neighbors = self.csr.neighbors(row);
376 let edges = self.csr.edge_data_for(row);
377 for (i, &col) in neighbors.iter().enumerate() {
378 if !self.is_removed(row, col) {
379 all_edges.push((row, col, edges[i].clone()));
380 }
381 }
382 }
383
384 for delta in &self.delta_edges {
386 all_edges.push((delta.source_idx, delta.target_idx, delta.data.clone()));
387 }
388
389 self.csr = Self::build_compressed(&all_edges, num_nodes, false);
391
392 self.csc = Self::build_compressed(&all_edges, num_nodes, true);
394
395 self.delta_edges.clear();
396 self.removed_edges.clear();
397 }
398
399 fn build_compressed(
400 edges: &[(u32, u32, StoredEdge)],
401 num_nodes: usize,
402 transpose: bool,
403 ) -> CompressedStorage {
404 let mut counts = vec![0u32; num_nodes];
406 for &(src, tgt, ref _data) in edges {
407 let row = if transpose { tgt } else { src };
408 if (row as usize) < num_nodes {
409 counts[row as usize] += 1;
410 }
411 }
412
413 let mut row_offsets = vec![0u32; num_nodes + 1];
415 for i in 0..num_nodes {
416 row_offsets[i + 1] = row_offsets[i] + counts[i];
417 }
418
419 let total = row_offsets[num_nodes] as usize;
420 let mut col_indices = vec![0u32; total];
421 let mut edge_data = vec![
422 StoredEdge {
423 edge_type: EdgeType::Related,
424 weight: 0.0,
425 created_at: 0,
426 valid_from: None,
427 valid_until: None,
428 label: None,
429 };
430 total
431 ];
432
433 let mut cursors = row_offsets[..num_nodes].to_vec();
435 for &(src, tgt, ref data) in edges {
436 let (row, col) = if transpose { (tgt, src) } else { (src, tgt) };
437 if (row as usize) < num_nodes {
438 let pos = cursors[row as usize] as usize;
439 col_indices[pos] = col;
440 edge_data[pos] = data.clone();
441 cursors[row as usize] += 1;
442 }
443 }
444
445 CompressedStorage {
446 row_offsets,
447 col_indices,
448 edge_data,
449 }
450 }
451 pub fn save(&self, path: &std::path::Path) -> MenteResult<()> {
453 let data =
454 serde_json::to_vec(self).map_err(|e| MenteError::Serialization(e.to_string()))?;
455 let tmp_path = path.with_extension("json.tmp");
458 {
459 use std::io::Write;
460 let mut file = std::fs::File::create(&tmp_path)?;
461 file.write_all(&data)?;
462 file.sync_data()?;
463 }
464 std::fs::rename(&tmp_path, path)?;
465 Ok(())
466 }
467
468 pub fn load(path: &std::path::Path) -> MenteResult<Self> {
470 let data = std::fs::read(path)?;
471 let graph: Self =
472 serde_json::from_slice(&data).map_err(|e| MenteError::Serialization(e.to_string()))?;
473 Ok(graph)
474 }
475}
476
477impl Default for CsrGraph {
478 fn default() -> Self {
479 Self::new()
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 fn make_edge(src: MemoryId, tgt: MemoryId, etype: EdgeType) -> MemoryEdge {
488 MemoryEdge {
489 source: src,
490 target: tgt,
491 edge_type: etype,
492 weight: 0.8,
493 created_at: 1000,
494 valid_from: None,
495 valid_until: None,
496 label: None,
497 }
498 }
499
500 #[test]
501 fn test_add_node_idempotent() {
502 let mut g = CsrGraph::new();
503 let id = MemoryId::new();
504 let idx1 = g.add_node(id);
505 let idx2 = g.add_node(id);
506 assert_eq!(idx1, idx2);
507 assert_eq!(g.node_count(), 1);
508 }
509
510 #[test]
511 fn test_add_and_query_edges() {
512 let mut g = CsrGraph::new();
513 let a = MemoryId::new();
514 let b = MemoryId::new();
515 let c = MemoryId::new();
516
517 g.add_edge(&make_edge(a, b, EdgeType::Caused));
518 g.add_edge(&make_edge(a, c, EdgeType::Related));
519
520 let out = g.outgoing(a);
521 assert_eq!(out.len(), 2);
522
523 let inc_b = g.incoming(b);
524 assert_eq!(inc_b.len(), 1);
525 assert_eq!(inc_b[0].0, a);
526 }
527
528 #[test]
529 fn test_remove_edge() {
530 let mut g = CsrGraph::new();
531 let a = MemoryId::new();
532 let b = MemoryId::new();
533
534 g.add_edge(&make_edge(a, b, EdgeType::Caused));
535 assert_eq!(g.outgoing(a).len(), 1);
536
537 g.remove_edge(a, b);
538 assert_eq!(g.outgoing(a).len(), 0);
539 }
540
541 #[test]
542 fn test_compact() {
543 let mut g = CsrGraph::new();
544 let a = MemoryId::new();
545 let b = MemoryId::new();
546 let c = MemoryId::new();
547
548 g.add_edge(&make_edge(a, b, EdgeType::Caused));
549 g.add_edge(&make_edge(b, c, EdgeType::Before));
550 g.compact();
551
552 let out_a = g.outgoing(a);
553 assert_eq!(out_a.len(), 1);
554 assert_eq!(out_a[0].0, b);
555
556 let inc_c = g.incoming(c);
557 assert_eq!(inc_c.len(), 1);
558 assert_eq!(inc_c[0].0, b);
559 }
560
561 #[test]
562 fn test_compact_with_removals() {
563 let mut g = CsrGraph::new();
564 let a = MemoryId::new();
565 let b = MemoryId::new();
566 let c = MemoryId::new();
567
568 g.add_edge(&make_edge(a, b, EdgeType::Caused));
569 g.add_edge(&make_edge(a, c, EdgeType::Related));
570 g.compact();
571
572 g.remove_edge(a, b);
573 g.compact();
574
575 let out = g.outgoing(a);
576 assert_eq!(out.len(), 1);
577 assert_eq!(out[0].0, c);
578 }
579
580 #[test]
581 fn test_remove_node_cleans_id_to_idx() {
582 let mut g = CsrGraph::new();
583 let a = MemoryId::new();
584 let b = MemoryId::new();
585
586 g.add_edge(&make_edge(a, b, EdgeType::Caused));
587 assert!(g.contains_node(a));
588 assert!(g.contains_node(b));
589
590 g.remove_node(a);
591 assert!(
592 !g.contains_node(a),
593 "removed node should not be in id_to_idx"
594 );
595 assert!(g.contains_node(b), "unrelated node should still exist");
596
597 assert!(g.outgoing(a).is_empty());
599 assert!(g.incoming(b).is_empty());
600 }
601
602 #[test]
603 fn test_remove_node_then_readd() {
604 let mut g = CsrGraph::new();
605 let a = MemoryId::new();
606 let b = MemoryId::new();
607 let c = MemoryId::new();
608
609 g.add_edge(&make_edge(a, b, EdgeType::Caused));
610 g.remove_node(a);
611
612 g.add_edge(&make_edge(a, c, EdgeType::Related));
614 assert!(g.contains_node(a));
615 let out = g.outgoing(a);
616 assert_eq!(out.len(), 1);
617 assert_eq!(out[0].0, c);
618 }
619}