lean_ctx/core/property_graph/
queries.rs1use std::collections::{HashMap, HashSet, VecDeque};
9
10use rusqlite::{params, Connection};
11
12#[derive(Debug, Clone)]
13pub struct GraphQuery;
14
15#[derive(Debug, Clone)]
16pub struct ImpactResult {
17 pub root_file: String,
18 pub affected_files: Vec<String>,
19 pub max_depth_reached: usize,
20 pub edges_traversed: usize,
21}
22
23#[derive(Debug, Clone)]
24pub struct DependencyChain {
25 pub path: Vec<String>,
26 pub depth: usize,
27}
28
29const STRUCTURAL_EDGE_KINDS: &str = "'imports','calls','exports','type_ref','tested_by'";
31
32pub fn edge_weight(kind: &str) -> f64 {
34 match kind {
35 "imports" => 1.0,
36 "calls" => 0.8,
37 "exports" => 0.7,
38 "type_ref" => 0.5,
39 "tested_by" => 0.4,
40 "defines" => 0.3,
41 "changed_in" => 0.2,
42 _ => 0.1,
43 }
44}
45
46pub fn dependents(conn: &Connection, file_path: &str) -> anyhow::Result<Vec<String>> {
48 let sql = format!(
49 "SELECT DISTINCT n_src.file_path
50 FROM edges e
51 JOIN nodes n_src ON e.source_id = n_src.id
52 JOIN nodes n_tgt ON e.target_id = n_tgt.id
53 WHERE n_tgt.file_path = ?1
54 AND n_src.file_path != ?1
55 AND e.kind IN ({STRUCTURAL_EDGE_KINDS})"
56 );
57 let mut stmt = conn.prepare(&sql)?;
58
59 let mut results: Vec<String> = stmt
60 .query_map(params![file_path], |row| row.get(0))?
61 .filter_map(std::result::Result::ok)
62 .collect();
63
64 results.sort();
65 results.dedup();
66 Ok(results)
67}
68
69pub fn dependencies(conn: &Connection, file_path: &str) -> anyhow::Result<Vec<String>> {
71 let sql = format!(
72 "SELECT DISTINCT n_tgt.file_path
73 FROM edges e
74 JOIN nodes n_src ON e.source_id = n_src.id
75 JOIN nodes n_tgt ON e.target_id = n_tgt.id
76 WHERE n_src.file_path = ?1
77 AND n_tgt.file_path != ?1
78 AND e.kind IN ({STRUCTURAL_EDGE_KINDS})"
79 );
80 let mut stmt = conn.prepare(&sql)?;
81
82 let mut results: Vec<String> = stmt
83 .query_map(params![file_path], |row| row.get(0))?
84 .filter_map(std::result::Result::ok)
85 .collect();
86
87 results.sort();
88 results.dedup();
89 Ok(results)
90}
91
92pub fn impact_analysis(
96 conn: &Connection,
97 file_path: &str,
98 max_depth: usize,
99) -> anyhow::Result<ImpactResult> {
100 let reverse_graph = build_weighted_reverse_graph(conn)?;
101 const PROPAGATION_THRESHOLD: f64 = 0.1;
102
103 let mut visited: HashSet<String> = HashSet::new();
104 let mut queue: VecDeque<(String, usize, f64)> = VecDeque::new();
105 let mut max_depth_reached = 0;
106 let mut edges_traversed = 0;
107
108 visited.insert(file_path.to_string());
109 queue.push_back((file_path.to_string(), 0, 1.0));
110
111 while let Some((current, depth, weight)) = queue.pop_front() {
112 if depth >= max_depth {
113 continue;
114 }
115
116 if let Some(dependents) = reverse_graph.get(¤t) {
117 for (dep, ew) in dependents {
118 edges_traversed += 1;
119 let propagated = weight * ew;
120 if propagated < PROPAGATION_THRESHOLD {
121 continue;
122 }
123 if visited.insert(dep.clone()) {
124 let new_depth = depth + 1;
125 if new_depth > max_depth_reached {
126 max_depth_reached = new_depth;
127 }
128 queue.push_back((dep.clone(), new_depth, propagated));
129 }
130 }
131 }
132 }
133
134 visited.remove(file_path);
135
136 Ok(ImpactResult {
137 root_file: file_path.to_string(),
138 affected_files: visited.into_iter().collect(),
139 max_depth_reached,
140 edges_traversed,
141 })
142}
143
144pub fn dependency_chain(
146 conn: &Connection,
147 from: &str,
148 to: &str,
149) -> anyhow::Result<Option<DependencyChain>> {
150 let forward_graph = build_forward_graph(conn)?;
151
152 let mut visited: HashSet<String> = HashSet::new();
153 let mut parent: HashMap<String, String> = HashMap::new();
154 let mut queue: VecDeque<String> = VecDeque::new();
155
156 visited.insert(from.to_string());
157 queue.push_back(from.to_string());
158
159 while let Some(current) = queue.pop_front() {
160 if current == to {
161 let mut path = vec![to.to_string()];
162 let mut cursor = to.to_string();
163 while let Some(prev) = parent.get(&cursor) {
164 path.push(prev.clone());
165 cursor = prev.clone();
166 }
167 path.reverse();
168 let depth = path.len() - 1;
169 return Ok(Some(DependencyChain { path, depth }));
170 }
171
172 if let Some(deps) = forward_graph.get(¤t) {
173 for dep in deps {
174 if visited.insert(dep.clone()) {
175 parent.insert(dep.clone(), current.clone());
176 queue.push_back(dep.clone());
177 }
178 }
179 }
180 }
181
182 Ok(None)
183}
184
185pub fn related_files(
188 conn: &Connection,
189 file_path: &str,
190 limit: usize,
191) -> anyhow::Result<Vec<(String, f64)>> {
192 let sql = format!(
193 "SELECT n_other.file_path, e.kind
194 FROM edges e
195 JOIN nodes n_self ON (e.source_id = n_self.id OR e.target_id = n_self.id)
196 JOIN nodes n_other ON (
197 (e.source_id = n_other.id AND e.target_id = n_self.id)
198 OR (e.target_id = n_other.id AND e.source_id = n_self.id)
199 )
200 WHERE n_self.file_path = ?1
201 AND n_other.file_path != ?1
202 AND e.kind IN ({STRUCTURAL_EDGE_KINDS})"
203 );
204 let mut stmt = conn.prepare(&sql)?;
205
206 let mut scores: HashMap<String, f64> = HashMap::new();
207 let rows = stmt.query_map(params![file_path], |row| {
208 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
209 })?;
210
211 for row in rows {
212 let (path, kind) = row?;
213 *scores.entry(path).or_default() += edge_weight(&kind);
214 }
215
216 let mut results: Vec<(String, f64)> = scores.into_iter().collect();
217 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
218 results.truncate(limit);
219 Ok(results)
220}
221
222pub fn file_connectivity(
224 conn: &Connection,
225 file_path: &str,
226) -> anyhow::Result<HashMap<String, (usize, usize)>> {
227 let mut result: HashMap<String, (usize, usize)> = HashMap::new();
228
229 let mut stmt_out = conn.prepare(
230 "SELECT e.kind, COUNT(*)
231 FROM edges e JOIN nodes n ON e.source_id = n.id
232 WHERE n.file_path = ?1
233 GROUP BY e.kind",
234 )?;
235 let rows = stmt_out.query_map(params![file_path], |row| {
236 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
237 })?;
238 for row in rows {
239 let (kind, count) = row?;
240 result.entry(kind).or_insert((0, 0)).0 = count as usize;
241 }
242
243 let mut stmt_in = conn.prepare(
244 "SELECT e.kind, COUNT(*)
245 FROM edges e JOIN nodes n ON e.target_id = n.id
246 WHERE n.file_path = ?1
247 GROUP BY e.kind",
248 )?;
249 let rows = stmt_in.query_map(params![file_path], |row| {
250 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
251 })?;
252 for row in rows {
253 let (kind, count) = row?;
254 result.entry(kind).or_insert((0, 0)).1 = count as usize;
255 }
256
257 Ok(result)
258}
259
260fn build_weighted_reverse_graph(
261 conn: &Connection,
262) -> anyhow::Result<HashMap<String, Vec<(String, f64)>>> {
263 let sql = format!(
264 "SELECT n_tgt.file_path, n_src.file_path, e.kind
265 FROM edges e
266 JOIN nodes n_src ON e.source_id = n_src.id
267 JOIN nodes n_tgt ON e.target_id = n_tgt.id
268 WHERE e.kind IN ({STRUCTURAL_EDGE_KINDS})
269 AND n_src.file_path != n_tgt.file_path"
270 );
271 let mut stmt = conn.prepare(&sql)?;
272
273 let mut graph: HashMap<String, HashMap<String, f64>> = HashMap::new();
274 let rows = stmt.query_map([], |row| {
275 Ok((
276 row.get::<_, String>(0)?,
277 row.get::<_, String>(1)?,
278 row.get::<_, String>(2)?,
279 ))
280 })?;
281
282 for row in rows {
283 let (target, source, kind) = row?;
284 let w = edge_weight(&kind);
285 let entry = graph
286 .entry(target)
287 .or_default()
288 .entry(source)
289 .or_insert(0.0);
290 if w > *entry {
291 *entry = w;
292 }
293 }
294
295 Ok(graph
296 .into_iter()
297 .map(|(k, v)| (k, v.into_iter().collect()))
298 .collect())
299}
300
301fn build_forward_graph(conn: &Connection) -> anyhow::Result<HashMap<String, Vec<String>>> {
302 let sql = format!(
303 "SELECT DISTINCT n_src.file_path, n_tgt.file_path
304 FROM edges e
305 JOIN nodes n_src ON e.source_id = n_src.id
306 JOIN nodes n_tgt ON e.target_id = n_tgt.id
307 WHERE e.kind IN ({STRUCTURAL_EDGE_KINDS})
308 AND n_src.file_path != n_tgt.file_path"
309 );
310 let mut stmt = conn.prepare(&sql)?;
311
312 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
313 let rows = stmt.query_map([], |row| {
314 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
315 })?;
316
317 for row in rows {
318 let (source, target) = row?;
319 graph.entry(source).or_default().push(target);
320 }
321
322 for deps in graph.values_mut() {
323 deps.sort();
324 deps.dedup();
325 }
326 Ok(graph)
327}