1use std::fs;
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21use anyhow::Result;
22
23pub struct GraphStore {
24 root: PathBuf,
25 mem: Option<Arc<dashmap::DashMap<(String, String), std::collections::HashSet<String>>>>,
27}
28
29impl GraphStore {
30 pub fn new(db_root: &Path) -> Result<Self> {
31 let root = db_root.join("graph");
32 fs::create_dir_all(&root)?;
33 Ok(Self { root, mem: None })
34 }
35
36 pub fn in_memory() -> Self {
38 Self {
39 root: PathBuf::from(":memory:"),
40 mem: Some(Arc::new(dashmap::DashMap::new())),
41 }
42 }
43
44 fn edge_path(&self, from: &str, edge_type: &str, to: &str) -> PathBuf {
45 self.root.join(from).join(edge_type).join(to)
46 }
47
48 pub fn add_edge(&self, from: &str, edge_type: &str, to: &str) -> Result<()> {
50 if let Some(ref mem) = self.mem {
51 mem.entry((from.to_string(), edge_type.to_string()))
52 .or_default()
53 .insert(to.to_string());
54 return Ok(());
55 }
56 let path = self.edge_path(from, edge_type, to);
57 fs::create_dir_all(path.parent().unwrap())?;
58 if !path.exists() {
59 fs::write(&path, b"")?;
60 }
61 Ok(())
62 }
63
64 pub fn outgoing(&self, from: &str, edge_type: &str) -> Vec<String> {
66 if let Some(ref mem) = self.mem {
67 return mem.get(&(from.to_string(), edge_type.to_string()))
68 .map(|s| s.iter().cloned().collect())
69 .unwrap_or_default();
70 }
71 let dir = self.root.join(from).join(edge_type);
72 fs::read_dir(&dir)
73 .into_iter()
74 .flatten()
75 .filter_map(|e| e.ok())
76 .map(|e| e.file_name().to_string_lossy().to_string())
77 .collect()
78 }
79
80 pub fn incoming(&self, to: &str, reverse_edge_type: &str) -> Vec<String> {
83 self.outgoing(to, reverse_edge_type)
84 }
85
86 pub fn trace(
89 &self,
90 start: &str,
91 edge_type: &str,
92 reverse: bool,
93 limit: usize,
94 ) -> Vec<String> {
95 let mut result = Vec::new();
96 let mut queue = vec![start.to_string()];
97 let mut seen = std::collections::HashSet::new();
98 seen.insert(start.to_string());
99
100 while !queue.is_empty() && result.len() < limit {
101 let current = queue.remove(0);
102 result.push(current.clone());
103
104 let next_hashes = if reverse {
105 let rev_type = format!("{}_rev", edge_type);
107 self.outgoing(¤t, &rev_type)
108 } else {
109 self.outgoing(¤t, edge_type)
110 };
111
112 for next in next_hashes {
113 if !seen.contains(&next) {
114 seen.insert(next.clone());
115 queue.push(next);
116 }
117 }
118 }
119 result
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use tempfile::tempdir;
127
128 #[test]
129 fn add_and_traverse_edge() {
130 let dir = tempdir().unwrap();
131 let g = GraphStore::new(dir.path()).unwrap();
132
133 g.add_edge("hash_c", "caused_by", "hash_b").unwrap();
134 g.add_edge("hash_b", "caused_by", "hash_a").unwrap();
135
136 let trace = g.trace("hash_c", "caused_by", false, 10);
137 assert_eq!(trace, vec!["hash_c", "hash_b", "hash_a"]);
138 }
139
140 #[test]
141 fn idempotent_edge() {
142 let dir = tempdir().unwrap();
143 let g = GraphStore::new(dir.path()).unwrap();
144 g.add_edge("a", "caused_by", "b").unwrap();
145 g.add_edge("a", "caused_by", "b").unwrap(); assert_eq!(g.outgoing("a", "caused_by").len(), 1);
147 }
148}