1use std::collections::{HashMap, HashSet, VecDeque};
2
3use crate::graph::MaterializedGraph;
4
5pub fn bfs(
7 graph: &MaterializedGraph,
8 start: &str,
9 max_depth: Option<usize>,
10 edge_type_filter: Option<&str>,
11) -> Vec<String> {
12 let mut visited = HashSet::new();
13 let mut result = Vec::new();
14 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
15
16 if graph.get_node(start).is_none() {
17 return result;
18 }
19
20 visited.insert(start.to_string());
21 queue.push_back((start.to_string(), 0));
22
23 while let Some((node_id, depth)) = queue.pop_front() {
24 result.push(node_id.clone());
25
26 if let Some(max) = max_depth {
27 if depth >= max {
28 continue;
29 }
30 }
31
32 let edges = graph.outgoing_edges(&node_id);
33 for edge in edges {
34 if let Some(filter) = edge_type_filter {
35 if edge.edge_type != filter {
36 continue;
37 }
38 }
39 if !visited.contains(&edge.target_id) {
40 visited.insert(edge.target_id.clone());
41 queue.push_back((edge.target_id.clone(), depth + 1));
42 }
43 }
44 }
45
46 result
47}
48
49pub fn shortest_path(graph: &MaterializedGraph, start: &str, end: &str) -> Option<Vec<String>> {
53 if graph.get_node(start).is_none() || graph.get_node(end).is_none() {
54 return None;
55 }
56 if start == end {
57 return Some(vec![start.to_string()]);
58 }
59
60 let mut visited = HashSet::new();
61 let mut parent: HashMap<String, String> = HashMap::new();
62 let mut queue: VecDeque<String> = VecDeque::new();
63
64 visited.insert(start.to_string());
65 queue.push_back(start.to_string());
66
67 while let Some(current) = queue.pop_front() {
68 for edge in graph.outgoing_edges(¤t) {
69 if !visited.contains(&edge.target_id) {
70 visited.insert(edge.target_id.clone());
71 parent.insert(edge.target_id.clone(), current.clone());
72 if edge.target_id == end {
73 let mut path = vec![end.to_string()];
75 let mut cur = end.to_string();
76 while let Some(p) = parent.get(&cur) {
77 path.push(p.clone());
78 cur = p.clone();
79 }
80 path.reverse();
81 return Some(path);
82 }
83 queue.push_back(edge.target_id.clone());
84 }
85 }
86 }
87
88 None
89}
90
91pub fn impact_analysis(
94 graph: &MaterializedGraph,
95 node_id: &str,
96 max_depth: Option<usize>,
97) -> Vec<String> {
98 let mut visited = HashSet::new();
99 let mut result = Vec::new();
100 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
101
102 if graph.get_node(node_id).is_none() {
103 return result;
104 }
105
106 visited.insert(node_id.to_string());
107 queue.push_back((node_id.to_string(), 0));
108
109 while let Some((current, depth)) = queue.pop_front() {
110 result.push(current.clone());
111
112 if let Some(max) = max_depth {
113 if depth >= max {
114 continue;
115 }
116 }
117
118 for edge in graph.incoming_edges(¤t) {
119 if !visited.contains(&edge.source_id) {
120 visited.insert(edge.source_id.clone());
121 queue.push_back((edge.source_id.clone(), depth + 1));
122 }
123 }
124 }
125
126 result
127}
128
129pub fn subgraph(graph: &MaterializedGraph, start: &str, hops: usize) -> (Vec<String>, Vec<String>) {
132 let mut visited_nodes = HashSet::new();
133 let mut visited_edges = HashSet::new();
134 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
135
136 if graph.get_node(start).is_none() {
137 return (vec![], vec![]);
138 }
139
140 visited_nodes.insert(start.to_string());
141 queue.push_back((start.to_string(), 0));
142
143 while let Some((node_id, depth)) = queue.pop_front() {
144 if depth >= hops {
145 continue;
146 }
147
148 for edge in graph.outgoing_edges(&node_id) {
150 visited_edges.insert(edge.edge_id.clone());
151 if !visited_nodes.contains(&edge.target_id) {
152 visited_nodes.insert(edge.target_id.clone());
153 queue.push_back((edge.target_id.clone(), depth + 1));
154 }
155 }
156 for edge in graph.incoming_edges(&node_id) {
158 visited_edges.insert(edge.edge_id.clone());
159 if !visited_nodes.contains(&edge.source_id) {
160 visited_nodes.insert(edge.source_id.clone());
161 queue.push_back((edge.source_id.clone(), depth + 1));
162 }
163 }
164 }
165
166 (
167 visited_nodes.into_iter().collect(),
168 visited_edges.into_iter().collect(),
169 )
170}
171
172pub fn pattern_match(
181 graph: &MaterializedGraph,
182 type_sequence: &[&str],
183 max_results: usize,
184) -> Vec<Vec<String>> {
185 if type_sequence.is_empty() {
186 return vec![];
187 }
188
189 let mut results = Vec::new();
190
191 let start_nodes = graph.nodes_by_type(type_sequence[0]);
193 for start in start_nodes {
194 let mut chains = vec![vec![start.node_id.clone()]];
195
196 for &next_type in &type_sequence[1..] {
197 let mut extended = Vec::new();
198 for chain in &chains {
199 let last = chain.last().unwrap();
200 for edge in graph.outgoing_edges(last) {
201 if let Some(target_node) = graph.get_node(&edge.target_id) {
202 if target_node.node_type == next_type && !chain.contains(&edge.target_id) {
203 let mut new_chain = chain.clone();
204 new_chain.push(edge.target_id.clone());
205 extended.push(new_chain);
206 if results.len() + extended.len() >= max_results {
207 results.extend(extended);
208 results.truncate(max_results);
209 return results;
210 }
211 }
212 }
213 }
214 }
215 chains = extended;
216 }
217
218 results.extend(chains);
219 if results.len() >= max_results {
220 results.truncate(max_results);
221 return results;
222 }
223 }
224
225 results
226}
227
228pub fn topological_sort(graph: &MaterializedGraph) -> Option<Vec<String>> {
231 let nodes = graph.all_nodes();
232 let node_ids: HashSet<String> = nodes.iter().map(|n| n.node_id.clone()).collect();
233
234 let mut in_degree: HashMap<String, usize> = node_ids.iter().map(|id| (id.clone(), 0)).collect();
236 for edge in graph.all_edges() {
237 if node_ids.contains(&edge.target_id) && node_ids.contains(&edge.source_id) {
238 *in_degree.entry(edge.target_id.clone()).or_default() += 1;
239 }
240 }
241
242 let mut queue: VecDeque<String> = in_degree
243 .iter()
244 .filter(|(_, °)| deg == 0)
245 .map(|(id, _)| id.clone())
246 .collect();
247
248 let mut sorted: Vec<String> = queue.drain(..).collect();
250 sorted.sort();
251 queue.extend(sorted);
252
253 let mut result = Vec::new();
254 while let Some(node_id) = queue.pop_front() {
255 result.push(node_id.clone());
256 for edge in graph.outgoing_edges(&node_id) {
257 if let Some(deg) = in_degree.get_mut(&edge.target_id) {
258 *deg -= 1;
259 if *deg == 0 {
260 queue.push_back(edge.target_id.clone());
261 }
262 }
263 }
264 }
265
266 if result.len() == node_ids.len() {
267 Some(result)
268 } else {
269 None }
271}
272
273pub fn has_cycle(graph: &MaterializedGraph) -> bool {
275 topological_sort(graph).is_none()
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::clock::LamportClock;
282 use crate::entry::{Entry, GraphOp};
283 use crate::ontology::{EdgeTypeDef, NodeTypeDef, Ontology};
284 use std::collections::BTreeMap;
285
286 fn test_ontology() -> Ontology {
287 Ontology {
288 node_types: BTreeMap::from([
289 (
290 "entity".into(),
291 NodeTypeDef {
292 description: None,
293 properties: BTreeMap::new(),
294 subtypes: None,
295 },
296 ),
297 (
298 "signal".into(),
299 NodeTypeDef {
300 description: None,
301 properties: BTreeMap::new(),
302 subtypes: None,
303 },
304 ),
305 (
306 "rule".into(),
307 NodeTypeDef {
308 description: None,
309 properties: BTreeMap::new(),
310 subtypes: None,
311 },
312 ),
313 (
314 "plan".into(),
315 NodeTypeDef {
316 description: None,
317 properties: BTreeMap::new(),
318 subtypes: None,
319 },
320 ),
321 (
322 "action".into(),
323 NodeTypeDef {
324 description: None,
325 properties: BTreeMap::new(),
326 subtypes: None,
327 },
328 ),
329 ]),
330 edge_types: BTreeMap::from([
331 (
332 "DEPENDS_ON".into(),
333 EdgeTypeDef {
334 description: None,
335 source_types: vec!["entity".into()],
336 target_types: vec!["entity".into()],
337 properties: BTreeMap::new(),
338 },
339 ),
340 (
341 "TRIGGERS".into(),
342 EdgeTypeDef {
343 description: None,
344 source_types: vec!["signal".into()],
345 target_types: vec!["rule".into()],
346 properties: BTreeMap::new(),
347 },
348 ),
349 (
350 "PRODUCES".into(),
351 EdgeTypeDef {
352 description: None,
353 source_types: vec!["rule".into(), "plan".into(), "action".into()],
354 target_types: vec!["plan".into(), "action".into(), "signal".into()],
355 properties: BTreeMap::new(),
356 },
357 ),
358 ]),
359 }
360 }
361
362 fn make_entry(op: GraphOp, clock_time: u64) -> Entry {
363 Entry::new(
364 op,
365 vec![],
366 vec![],
367 LamportClock::with_values("test", clock_time, 0),
368 "test",
369 )
370 }
371
372 fn add_node(id: &str, ntype: &str, clock: u64) -> Entry {
373 make_entry(
374 GraphOp::AddNode {
375 node_id: id.into(),
376 node_type: ntype.into(),
377 label: id.into(),
378 properties: BTreeMap::new(),
379 subtype: None,
380 },
381 clock,
382 )
383 }
384
385 fn add_edge(id: &str, etype: &str, src: &str, tgt: &str, clock: u64) -> Entry {
386 make_entry(
387 GraphOp::AddEdge {
388 edge_id: id.into(),
389 edge_type: etype.into(),
390 source_id: src.into(),
391 target_id: tgt.into(),
392 properties: BTreeMap::new(),
393 },
394 clock,
395 )
396 }
397
398 fn linear_graph() -> MaterializedGraph {
400 let mut g = MaterializedGraph::new(test_ontology());
401 g.apply(&add_node("a", "entity", 1));
402 g.apply(&add_node("b", "entity", 2));
403 g.apply(&add_node("c", "entity", 3));
404 g.apply(&add_node("d", "entity", 4));
405 g.apply(&add_edge("ab", "DEPENDS_ON", "a", "b", 5));
406 g.apply(&add_edge("bc", "DEPENDS_ON", "b", "c", 6));
407 g.apply(&add_edge("cd", "DEPENDS_ON", "c", "d", 7));
408 g
409 }
410
411 #[test]
412 fn bfs_traversal_from_node() {
413 let g = linear_graph();
414 let visited = bfs(&g, "a", None, None);
415 assert_eq!(visited, vec!["a", "b", "c", "d"]);
416 }
417
418 #[test]
419 fn bfs_respects_depth_limit() {
420 let g = linear_graph();
421 let visited = bfs(&g, "a", Some(2), None);
422 assert_eq!(visited, vec!["a", "b", "c"]);
424 }
425
426 #[test]
427 fn bfs_filters_edge_types() {
428 let mut g = MaterializedGraph::new(test_ontology());
429 g.apply(&add_node("a", "entity", 1));
430 g.apply(&add_node("b", "entity", 2));
431 g.apply(&add_node("c", "entity", 3));
432 g.apply(&add_edge("ab", "DEPENDS_ON", "a", "b", 4));
433 g.apply(&add_edge("ac", "DEPENDS_ON", "a", "c", 5));
434 let visited = bfs(&g, "a", None, Some("DEPENDS_ON"));
438 assert!(visited.contains(&"a".to_string()));
439 assert!(visited.contains(&"b".to_string()));
440 assert!(visited.contains(&"c".to_string()));
441
442 let visited2 = bfs(&g, "a", None, Some("NONEXISTENT"));
444 assert_eq!(visited2, vec!["a"]);
445 }
446
447 #[test]
448 fn shortest_path_finds_path() {
449 let g = linear_graph();
450 let path = shortest_path(&g, "a", "d").unwrap();
451 assert_eq!(path, vec!["a", "b", "c", "d"]);
452 }
453
454 #[test]
455 fn shortest_path_no_path() {
456 let mut g = MaterializedGraph::new(test_ontology());
457 g.apply(&add_node("a", "entity", 1));
458 g.apply(&add_node("b", "entity", 2));
459 assert!(shortest_path(&g, "a", "b").is_none());
461 }
462
463 #[test]
464 fn impact_analysis_reverse_traversal() {
465 let g = linear_graph(); let impact = impact_analysis(&g, "d", None);
468 assert!(impact.contains(&"d".to_string()));
469 assert!(impact.contains(&"c".to_string()));
470 assert!(impact.contains(&"b".to_string()));
471 assert!(impact.contains(&"a".to_string()));
472 }
473
474 #[test]
475 fn subgraph_extraction() {
476 let g = linear_graph(); let (nodes, edges) = subgraph(&g, "b", 1);
478 assert!(nodes.contains(&"b".to_string()));
480 assert!(nodes.contains(&"a".to_string()));
481 assert!(nodes.contains(&"c".to_string()));
482 assert!(!nodes.contains(&"d".to_string())); assert_eq!(edges.len(), 2); }
485
486 #[test]
487 fn pattern_match_mape_k_loop() {
488 let mut g = MaterializedGraph::new(test_ontology());
489 g.apply(&add_node("sig1", "signal", 1));
490 g.apply(&add_node("rule1", "rule", 2));
491 g.apply(&add_node("plan1", "plan", 3));
492 g.apply(&add_node("act1", "action", 4));
493 g.apply(&add_edge("e1", "TRIGGERS", "sig1", "rule1", 5));
494 g.apply(&add_edge("e2", "PRODUCES", "rule1", "plan1", 6));
495 g.apply(&add_edge("e3", "PRODUCES", "plan1", "act1", 7));
496
497 let chains = pattern_match(&g, &["signal", "rule", "plan", "action"], 1000);
498 assert_eq!(chains.len(), 1);
499 assert_eq!(chains[0], vec!["sig1", "rule1", "plan1", "act1"]);
500 }
501
502 #[test]
503 fn topological_sort_dependency_order() {
504 let g = linear_graph(); let sorted = topological_sort(&g).unwrap();
506 let pos = |id: &str| sorted.iter().position(|x| x == id).unwrap();
508 assert!(pos("a") < pos("b"));
509 assert!(pos("b") < pos("c"));
510 assert!(pos("c") < pos("d"));
511 }
512
513 #[test]
514 fn cycle_detection() {
515 let mut g = MaterializedGraph::new(test_ontology());
516 g.apply(&add_node("a", "entity", 1));
517 g.apply(&add_node("b", "entity", 2));
518 g.apply(&add_node("c", "entity", 3));
519 g.apply(&add_edge("ab", "DEPENDS_ON", "a", "b", 4));
520 g.apply(&add_edge("bc", "DEPENDS_ON", "b", "c", 5));
521 g.apply(&add_edge("ca", "DEPENDS_ON", "c", "a", 6)); assert!(has_cycle(&g));
524 assert!(topological_sort(&g).is_none());
525 }
526}