1use std::collections::{HashMap, HashSet, VecDeque};
2use crate::graph::{Graph, Node};
3
4pub struct QueryEngine<'a> {
6 graph: &'a Graph,
7}
8
9impl<'a> QueryEngine<'a> {
10 pub fn new(graph: &'a Graph) -> Self {
11 Self { graph }
12 }
13
14 pub fn impact(&self, node_id: &str) -> Vec<&'a Node> {
18 self.impact_filtered(node_id, None)
19 }
20
21 pub fn impact_filtered(&self, node_id: &str, relations: Option<&[&str]>) -> Vec<&'a Node> {
24 let mut visited = HashSet::new();
25 let mut queue = VecDeque::new();
26 queue.push_back(node_id.to_string());
27 visited.insert(node_id.to_string());
28
29 while let Some(current) = queue.pop_front() {
30 for edge in &self.graph.edges {
32 if edge.to != current {
33 continue;
34 }
35 if let Some(rels) = relations {
37 if !rels.contains(&edge.relation.as_str()) {
38 continue;
39 }
40 }
41 if visited.insert(edge.from.clone()) {
42 queue.push_back(edge.from.clone());
43 }
44 }
45 }
46
47 visited.remove(node_id);
48 self.graph.nodes.iter()
49 .filter(|n| visited.contains(&n.id))
50 .collect()
51 }
52
53 pub fn deps(&self, node_id: &str, transitive: bool) -> Vec<&'a Node> {
56 self.deps_filtered(node_id, transitive, None)
57 }
58
59 pub fn deps_filtered(&self, node_id: &str, transitive: bool, relations: Option<&[&str]>) -> Vec<&'a Node> {
62 if !transitive {
63 let dep_ids: HashSet<&str> = self.graph.edges.iter()
65 .filter(|e| {
66 if e.from != node_id {
67 return false;
68 }
69 if let Some(rels) = relations {
70 rels.contains(&e.relation.as_str())
71 } else {
72 true
73 }
74 })
75 .map(|e| e.to.as_str())
76 .collect();
77 return self.graph.nodes.iter()
78 .filter(|n| dep_ids.contains(n.id.as_str()))
79 .collect();
80 }
81
82 let mut visited = HashSet::new();
83 let mut queue = VecDeque::new();
84 queue.push_back(node_id.to_string());
85 visited.insert(node_id.to_string());
86
87 while let Some(current) = queue.pop_front() {
88 for edge in &self.graph.edges {
89 if edge.from != current {
90 continue;
91 }
92 if let Some(rels) = relations {
93 if !rels.contains(&edge.relation.as_str()) {
94 continue;
95 }
96 }
97 if visited.insert(edge.to.clone()) {
98 queue.push_back(edge.to.clone());
99 }
100 }
101 }
102
103 visited.remove(node_id);
104 self.graph.nodes.iter()
105 .filter(|n| visited.contains(&n.id))
106 .collect()
107 }
108
109 pub fn path(&self, from: &str, to: &str) -> Option<Vec<String>> {
111 let mut visited = HashSet::new();
112 let mut queue = VecDeque::new();
113 let mut parent: HashMap<String, String> = HashMap::new();
114
115 queue.push_back(from.to_string());
116 visited.insert(from.to_string());
117
118 while let Some(current) = queue.pop_front() {
119 if current == to {
120 let mut path = vec![to.to_string()];
122 let mut cur = to.to_string();
123 while let Some(p) = parent.get(&cur) {
124 path.push(p.clone());
125 cur = p.clone();
126 }
127 path.reverse();
128 return Some(path);
129 }
130
131 for edge in &self.graph.edges {
133 let neighbor = if edge.from == current {
134 &edge.to
135 } else if edge.to == current {
136 &edge.from
137 } else {
138 continue;
139 };
140 if visited.insert(neighbor.clone()) {
141 parent.insert(neighbor.clone(), current.clone());
142 queue.push_back(neighbor.clone());
143 }
144 }
145 }
146
147 None
148 }
149
150 pub fn common_cause(&self, node_a: &str, node_b: &str) -> Vec<&'a Node> {
152 let deps_a: HashSet<String> = self.deps(node_a, true)
153 .iter().map(|n| n.id.clone()).collect();
154 let deps_b: HashSet<String> = self.deps(node_b, true)
155 .iter().map(|n| n.id.clone()).collect();
156 let common: HashSet<&String> = deps_a.intersection(&deps_b).collect();
157
158 self.graph.nodes.iter()
159 .filter(|n| common.contains(&n.id))
160 .collect()
161 }
162
163 pub fn topological_sort(&self) -> anyhow::Result<Vec<String>> {
165 let mut in_degree: HashMap<&str, usize> = HashMap::new();
166 for node in &self.graph.nodes {
167 in_degree.entry(&node.id).or_insert(0);
168 }
169 for edge in &self.graph.edges {
170 if edge.relation == "depends_on" {
171 *in_degree.entry(&edge.from).or_insert(0) += 1;
172 }
173 }
174
175 let mut queue: VecDeque<&str> = in_degree.iter()
176 .filter(|(_, °)| deg == 0)
177 .map(|(&id, _)| id)
178 .collect();
179
180 let mut sorted = Vec::new();
181 while let Some(node) = queue.pop_front() {
182 sorted.push(node.to_string());
183 for edge in &self.graph.edges {
184 if edge.to == node && edge.relation == "depends_on" {
185 if let Some(deg) = in_degree.get_mut(edge.from.as_str()) {
186 *deg -= 1;
187 if *deg == 0 {
188 queue.push_back(&edge.from);
189 }
190 }
191 }
192 }
193 }
194
195 if sorted.len() != self.graph.nodes.len() {
196 anyhow::bail!("Cycle detected in graph");
197 }
198
199 Ok(sorted)
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::graph::{Graph, Node, Edge};
207
208 fn make_edge(from: &str, to: &str, relation: &str) -> Edge {
209 Edge {
210 from: from.into(),
211 to: to.into(),
212 relation: relation.into(),
213 weight: None,
214 confidence: None,
215 metadata: None,
216 }
217 }
218
219 fn make_test_graph() -> Graph {
220 let nodes = vec![
224 Node::new("A", "A"),
225 Node::new("B", "B"),
226 Node::new("C", "C"),
227 Node::new("D", "D"),
228 Node::new("E", "E"),
229 ];
230 let edges = vec![
231 make_edge("A", "B", "depends_on"),
232 make_edge("B", "C", "depends_on"),
233 make_edge("A", "D", "implements"),
234 make_edge("E", "D", "belongs_to"),
235 ];
236 Graph { nodes, edges, ..Default::default() }
237 }
238
239 #[test]
240 fn test_impact_multi_relation() {
241 let graph = make_test_graph();
242 let qe = QueryEngine::new(&graph);
243
244 let impacted = qe.impact("C");
247 let ids: Vec<&str> = impacted.iter().map(|n| n.id.as_str()).collect();
248 assert!(ids.contains(&"B"));
249 assert!(ids.contains(&"A"));
250 }
251
252 #[test]
253 fn test_impact_filtered_depends_on_only() {
254 let graph = make_test_graph();
255 let qe = QueryEngine::new(&graph);
256
257 let impacted = qe.impact_filtered("D", Some(&["depends_on"]));
259 let ids: Vec<&str> = impacted.iter().map(|n| n.id.as_str()).collect();
260 assert!(ids.is_empty());
262 }
263
264 #[test]
265 fn test_impact_filtered_all_relations() {
266 let graph = make_test_graph();
267 let qe = QueryEngine::new(&graph);
268
269 let impacted = qe.impact_filtered("D", None);
271 let ids: Vec<&str> = impacted.iter().map(|n| n.id.as_str()).collect();
272 assert!(ids.contains(&"A"), "A implements D");
273 assert!(ids.contains(&"E"), "E belongs_to D");
274 }
275
276 #[test]
277 fn test_deps_multi_relation() {
278 let graph = make_test_graph();
279 let qe = QueryEngine::new(&graph);
280
281 let deps = qe.deps("A", true);
283 let ids: Vec<&str> = deps.iter().map(|n| n.id.as_str()).collect();
284 assert!(ids.contains(&"B"));
285 assert!(ids.contains(&"C"));
286 assert!(ids.contains(&"D"));
287 }
288
289 #[test]
290 fn test_deps_filtered_depends_on_only() {
291 let graph = make_test_graph();
292 let qe = QueryEngine::new(&graph);
293
294 let deps = qe.deps_filtered("A", true, Some(&["depends_on"]));
296 let ids: Vec<&str> = deps.iter().map(|n| n.id.as_str()).collect();
297 assert!(ids.contains(&"B"));
298 assert!(ids.contains(&"C"));
299 assert!(!ids.contains(&"D"), "D should be excluded — implements, not depends_on");
300 }
301
302 #[test]
303 fn test_deps_non_transitive_filtered() {
304 let graph = make_test_graph();
305 let qe = QueryEngine::new(&graph);
306
307 let deps = qe.deps_filtered("A", false, None);
309 let ids: Vec<&str> = deps.iter().map(|n| n.id.as_str()).collect();
310 assert!(ids.contains(&"B"));
311 assert!(ids.contains(&"D"));
312 assert!(!ids.contains(&"C"), "C is transitive, should be excluded");
313 }
314
315 #[test]
316 fn test_backward_compat_impact_uses_all_relations() {
317 let graph = make_test_graph();
318 let qe = QueryEngine::new(&graph);
319
320 let impacted = qe.impact("D");
322 let ids: Vec<&str> = impacted.iter().map(|n| n.id.as_str()).collect();
323 assert!(ids.contains(&"A"));
325 assert!(ids.contains(&"E"));
326 }
327}