Skip to main content

lean_ctx/core/property_graph/
queries.rs

1//! Graph traversal queries: dependents, dependencies, impact analysis,
2//! dependency chains (BFS-based shortest path).
3
4use 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
25/// Files that import (depend on) `file_path`.
26pub 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
45/// Files that `file_path` imports (depends on).
46pub 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
65/// BFS from `file_path` following reverse import edges up to `max_depth`.
66/// Returns all transitively affected files.
67pub 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(&current) {
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
111/// BFS shortest path from `from` to `to` following import edges.
112pub 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(&current) {
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}