lean_ctx/core/property_graph/
queries.rs1use std::collections::{HashMap, HashSet, VecDeque};
5
6use rusqlite::{params, Connection};
7
8#[derive(Debug, Clone)]
9pub struct GraphQuery;
10
11#[derive(Debug, Clone)]
12pub struct ImpactResult {
13 pub root_file: String,
14 pub affected_files: Vec<String>,
15 pub max_depth_reached: usize,
16 pub edges_traversed: usize,
17}
18
19#[derive(Debug, Clone)]
20pub struct DependencyChain {
21 pub path: Vec<String>,
22 pub depth: usize,
23}
24
25pub fn dependents(conn: &Connection, file_path: &str) -> anyhow::Result<Vec<String>> {
27 let mut stmt = conn.prepare(
28 "SELECT DISTINCT n_src.file_path
29 FROM edges e
30 JOIN nodes n_src ON e.source_id = n_src.id
31 JOIN nodes n_tgt ON e.target_id = n_tgt.id
32 WHERE n_tgt.file_path = ?1
33 AND n_src.file_path != ?1
34 AND e.kind = 'imports'",
35 )?;
36
37 let results: Vec<String> = stmt
38 .query_map(params![file_path], |row| row.get(0))?
39 .filter_map(|r| r.ok())
40 .collect();
41
42 Ok(results)
43}
44
45pub fn dependencies(conn: &Connection, file_path: &str) -> anyhow::Result<Vec<String>> {
47 let mut stmt = conn.prepare(
48 "SELECT DISTINCT n_tgt.file_path
49 FROM edges e
50 JOIN nodes n_src ON e.source_id = n_src.id
51 JOIN nodes n_tgt ON e.target_id = n_tgt.id
52 WHERE n_src.file_path = ?1
53 AND n_tgt.file_path != ?1
54 AND e.kind = 'imports'",
55 )?;
56
57 let results: Vec<String> = stmt
58 .query_map(params![file_path], |row| row.get(0))?
59 .filter_map(|r| r.ok())
60 .collect();
61
62 Ok(results)
63}
64
65pub fn impact_analysis(
68 conn: &Connection,
69 file_path: &str,
70 max_depth: usize,
71) -> anyhow::Result<ImpactResult> {
72 let reverse_graph = build_reverse_import_graph(conn)?;
73
74 let mut visited: HashSet<String> = HashSet::new();
75 let mut queue: VecDeque<(String, usize)> = VecDeque::new();
76 let mut max_depth_reached = 0;
77 let mut edges_traversed = 0;
78
79 visited.insert(file_path.to_string());
80 queue.push_back((file_path.to_string(), 0));
81
82 while let Some((current, depth)) = queue.pop_front() {
83 if depth >= max_depth {
84 continue;
85 }
86
87 if let Some(dependents) = reverse_graph.get(¤t) {
88 for dep in dependents {
89 edges_traversed += 1;
90 if visited.insert(dep.clone()) {
91 let new_depth = depth + 1;
92 if new_depth > max_depth_reached {
93 max_depth_reached = new_depth;
94 }
95 queue.push_back((dep.clone(), new_depth));
96 }
97 }
98 }
99 }
100
101 visited.remove(file_path);
102
103 Ok(ImpactResult {
104 root_file: file_path.to_string(),
105 affected_files: visited.into_iter().collect(),
106 max_depth_reached,
107 edges_traversed,
108 })
109}
110
111pub fn dependency_chain(
113 conn: &Connection,
114 from: &str,
115 to: &str,
116) -> anyhow::Result<Option<DependencyChain>> {
117 let forward_graph = build_forward_import_graph(conn)?;
118
119 let mut visited: HashSet<String> = HashSet::new();
120 let mut parent: HashMap<String, String> = HashMap::new();
121 let mut queue: VecDeque<String> = VecDeque::new();
122
123 visited.insert(from.to_string());
124 queue.push_back(from.to_string());
125
126 while let Some(current) = queue.pop_front() {
127 if current == to {
128 let mut path = vec![to.to_string()];
129 let mut cursor = to.to_string();
130 while let Some(prev) = parent.get(&cursor) {
131 path.push(prev.clone());
132 cursor = prev.clone();
133 }
134 path.reverse();
135 let depth = path.len() - 1;
136 return Ok(Some(DependencyChain { path, depth }));
137 }
138
139 if let Some(deps) = forward_graph.get(¤t) {
140 for dep in deps {
141 if visited.insert(dep.clone()) {
142 parent.insert(dep.clone(), current.clone());
143 queue.push_back(dep.clone());
144 }
145 }
146 }
147 }
148
149 Ok(None)
150}
151
152fn build_reverse_import_graph(conn: &Connection) -> anyhow::Result<HashMap<String, Vec<String>>> {
153 let mut stmt = conn.prepare(
154 "SELECT DISTINCT n_tgt.file_path, n_src.file_path
155 FROM edges e
156 JOIN nodes n_src ON e.source_id = n_src.id
157 JOIN nodes n_tgt ON e.target_id = n_tgt.id
158 WHERE e.kind = 'imports'
159 AND n_src.file_path != n_tgt.file_path",
160 )?;
161
162 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
163 let rows = stmt.query_map([], |row| {
164 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
165 })?;
166
167 for row in rows {
168 let (target, source) = row?;
169 graph.entry(target).or_default().push(source);
170 }
171
172 Ok(graph)
173}
174
175fn build_forward_import_graph(conn: &Connection) -> anyhow::Result<HashMap<String, Vec<String>>> {
176 let mut stmt = conn.prepare(
177 "SELECT DISTINCT n_src.file_path, n_tgt.file_path
178 FROM edges e
179 JOIN nodes n_src ON e.source_id = n_src.id
180 JOIN nodes n_tgt ON e.target_id = n_tgt.id
181 WHERE e.kind = 'imports'
182 AND n_src.file_path != n_tgt.file_path",
183 )?;
184
185 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
186 let rows = stmt.query_map([], |row| {
187 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
188 })?;
189
190 for row in rows {
191 let (source, target) = row?;
192 graph.entry(source).or_default().push(target);
193 }
194
195 Ok(graph)
196}