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