1use rusqlite::{Connection, OptionalExtension, params};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum NodeKind {
7 File,
8 Symbol,
9 Module,
10 Commit,
11 Test,
12 CIRun,
13 Knowledge,
14 Issue,
15}
16
17impl NodeKind {
18 pub fn as_str(&self) -> &'static str {
19 match self {
20 Self::File => "file",
21 Self::Symbol => "symbol",
22 Self::Module => "module",
23 Self::Commit => "commit",
24 Self::Test => "test",
25 Self::CIRun => "ci_run",
26 Self::Knowledge => "knowledge",
27 Self::Issue => "issue",
28 }
29 }
30
31 pub fn parse(s: &str) -> Self {
32 match s {
33 "symbol" => Self::Symbol,
34 "module" => Self::Module,
35 "commit" => Self::Commit,
36 "test" => Self::Test,
37 "ci_run" => Self::CIRun,
38 "knowledge" => Self::Knowledge,
39 "issue" => Self::Issue,
40 _ => Self::File,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
46pub struct Node {
47 pub id: Option<i64>,
48 pub kind: NodeKind,
49 pub name: String,
50 pub file_path: String,
51 pub line_start: Option<usize>,
52 pub line_end: Option<usize>,
53 pub metadata: Option<String>,
54}
55
56impl Node {
57 pub fn file(path: &str) -> Self {
58 Self {
59 id: None,
60 kind: NodeKind::File,
61 name: path.to_string(),
62 file_path: path.to_string(),
63 line_start: None,
64 line_end: None,
65 metadata: None,
66 }
67 }
68
69 pub fn symbol(name: &str, file_path: &str, kind: NodeKind) -> Self {
70 Self {
71 id: None,
72 kind,
73 name: name.to_string(),
74 file_path: file_path.to_string(),
75 line_start: None,
76 line_end: None,
77 metadata: None,
78 }
79 }
80
81 pub fn with_lines(mut self, start: usize, end: usize) -> Self {
82 self.line_start = Some(start);
83 self.line_end = Some(end);
84 self
85 }
86
87 pub fn with_metadata(mut self, meta: &str) -> Self {
88 self.metadata = Some(meta.to_string());
89 self
90 }
91
92 pub fn commit(hash: &str, message: &str) -> Self {
93 Self {
94 id: None,
95 kind: NodeKind::Commit,
96 name: hash.to_string(),
97 file_path: String::new(),
98 line_start: None,
99 line_end: None,
100 metadata: Some(message.to_string()),
101 }
102 }
103
104 pub fn test(path: &str, test_name: &str) -> Self {
105 Self {
106 id: None,
107 kind: NodeKind::Test,
108 name: test_name.to_string(),
109 file_path: path.to_string(),
110 line_start: None,
111 line_end: None,
112 metadata: None,
113 }
114 }
115
116 pub fn knowledge(id: &str, summary: &str) -> Self {
117 Self {
118 id: None,
119 kind: NodeKind::Knowledge,
120 name: id.to_string(),
121 file_path: String::new(),
122 line_start: None,
123 line_end: None,
124 metadata: Some(summary.to_string()),
125 }
126 }
127
128 pub fn issue(id: &str, title: &str) -> Self {
129 Self {
130 id: None,
131 kind: NodeKind::Issue,
132 name: id.to_string(),
133 file_path: String::new(),
134 line_start: None,
135 line_end: None,
136 metadata: Some(title.to_string()),
137 }
138 }
139}
140
141pub(super) fn upsert(conn: &Connection, node: &Node) -> anyhow::Result<i64> {
142 let file_id = super::path_id::intern(conn, &node.file_path)?;
143 conn.execute(
144 "INSERT INTO nodes (kind, name, file_id, line_start, line_end, metadata)
145 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
146 ON CONFLICT(kind, name, file_id) DO UPDATE SET
147 line_start = excluded.line_start,
148 line_end = excluded.line_end,
149 metadata = excluded.metadata",
150 params![
151 node.kind.as_str(),
152 node.name,
153 file_id.raw(),
154 node.line_start.map(|v| v as i64),
155 node.line_end.map(|v| v as i64),
156 node.metadata,
157 ],
158 )?;
159
160 let id: i64 = conn.query_row(
161 "SELECT id FROM nodes WHERE kind = ?1 AND name = ?2 AND file_id = ?3",
162 params![node.kind.as_str(), node.name, file_id.raw()],
163 |row| row.get(0),
164 )?;
165
166 Ok(id)
167}
168
169pub(super) fn get_by_path(conn: &Connection, file_path: &str) -> anyhow::Result<Option<Node>> {
170 let result = conn
171 .query_row(
172 "SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
173 FROM nodes n JOIN paths p ON p.id = n.file_id
174 WHERE n.kind = 'file' AND p.path = ?1",
175 params![file_path],
176 |row| {
177 Ok(Node {
178 id: Some(row.get(0)?),
179 kind: NodeKind::parse(&row.get::<_, String>(1)?),
180 name: row.get(2)?,
181 file_path: row.get(3)?,
182 line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
183 line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
184 metadata: row.get(6)?,
185 })
186 },
187 )
188 .optional()?;
189 Ok(result)
190}
191
192pub(super) fn get_by_symbol(
193 conn: &Connection,
194 name: &str,
195 file_path: &str,
196) -> anyhow::Result<Option<Node>> {
197 let result = conn
198 .query_row(
199 "SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
200 FROM nodes n JOIN paths p ON p.id = n.file_id
201 WHERE n.name = ?1 AND p.path = ?2 AND n.kind != 'file'",
202 params![name, file_path],
203 |row| {
204 Ok(Node {
205 id: Some(row.get(0)?),
206 kind: NodeKind::parse(&row.get::<_, String>(1)?),
207 name: row.get(2)?,
208 file_path: row.get(3)?,
209 line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
210 line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
211 metadata: row.get(6)?,
212 })
213 },
214 )
215 .optional()?;
216 Ok(result)
217}
218
219pub(super) fn remove_by_file(conn: &Connection, file_path: &str) -> anyhow::Result<()> {
220 conn.execute(
221 "DELETE FROM edges WHERE source_id IN (
222 SELECT n.id FROM nodes n JOIN paths p ON p.id = n.file_id WHERE p.path = ?1
223 ) OR target_id IN (
224 SELECT n.id FROM nodes n JOIN paths p ON p.id = n.file_id WHERE p.path = ?1
225 )",
226 params![file_path],
227 )?;
228 conn.execute(
229 "DELETE FROM nodes WHERE file_id = (SELECT id FROM paths WHERE path = ?1)",
230 params![file_path],
231 )?;
232 Ok(())
233}
234
235pub(super) fn find_symbols(
236 conn: &Connection,
237 name: &str,
238 file_filter: Option<&str>,
239 kind_filter: Option<&str>,
240) -> anyhow::Result<Vec<Node>> {
241 let name_lower = name.to_lowercase();
242 let mut sql = String::from(
243 "SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
244 FROM nodes n JOIN paths p ON p.id = n.file_id WHERE n.kind != 'file'
245 AND LOWER(name) LIKE '%' || ?1 || '%'",
246 );
247 let mut param_idx = 2;
248 if file_filter.is_some() {
249 sql.push_str(&format!(" AND p.path LIKE '%' || ?{param_idx} || '%'"));
250 param_idx += 1;
251 }
252 if kind_filter.is_some() {
253 sql.push_str(&format!(" AND kind = ?{param_idx}"));
254 }
255 sql.push_str(" ORDER BY p.path, n.line_start LIMIT 100");
256
257 let mut stmt = conn.prepare(&sql)?;
258
259 let params_vec: Vec<Box<dyn rusqlite::types::ToSql>> = {
260 let mut v: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(name_lower)];
261 if let Some(f) = file_filter {
262 v.push(Box::new(f.to_string()));
263 }
264 if let Some(k) = kind_filter {
265 v.push(Box::new(k.to_string()));
266 }
267 v
268 };
269 let refs: Vec<&dyn rusqlite::types::ToSql> =
270 params_vec.iter().map(std::convert::AsRef::as_ref).collect();
271
272 let rows = stmt.query_map(refs.as_slice(), |row| {
273 Ok(Node {
274 id: Some(row.get(0)?),
275 kind: NodeKind::parse(&row.get::<_, String>(1)?),
276 name: row.get(2)?,
277 file_path: row.get(3)?,
278 line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
279 line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
280 metadata: row.get(6)?,
281 })
282 })?;
283
284 let mut results = Vec::new();
285 for r in rows {
286 results.push(r?);
287 }
288 Ok(results)
289}
290
291pub(super) fn resolve_symbol_def_files(
301 conn: &Connection,
302 name: &str,
303) -> anyhow::Result<Vec<String>> {
304 let mut stmt = conn.prepare(
305 "SELECT DISTINCT p.path FROM nodes n JOIN paths p ON p.id = n.file_id
306 WHERE n.kind = 'symbol' AND n.name = ?1 AND p.path != ''
307 ORDER BY p.path",
308 )?;
309 let rows = stmt.query_map(params![name], |row| row.get::<_, String>(0))?;
310 let mut out = Vec::new();
311 for r in rows {
312 out.push(r?);
313 }
314 Ok(out)
315}
316
317pub(super) fn all_symbols(conn: &Connection) -> anyhow::Result<Vec<Node>> {
321 let mut stmt = conn.prepare(
322 "SELECT n.id, n.kind, n.name, p.path, n.line_start, n.line_end, n.metadata
323 FROM nodes n JOIN paths p ON p.id = n.file_id WHERE n.kind != 'file'
324 ORDER BY p.path, n.line_start",
325 )?;
326 let rows = stmt.query_map([], |row| {
327 Ok(Node {
328 id: Some(row.get(0)?),
329 kind: NodeKind::parse(&row.get::<_, String>(1)?),
330 name: row.get(2)?,
331 file_path: row.get(3)?,
332 line_start: row.get::<_, Option<i64>>(4)?.map(|v| v as usize),
333 line_end: row.get::<_, Option<i64>>(5)?.map(|v| v as usize),
334 metadata: row.get(6)?,
335 })
336 })?;
337 let mut results = Vec::new();
338 for r in rows {
339 results.push(r?);
340 }
341 Ok(results)
342}
343
344pub(super) fn symbol_count(conn: &Connection) -> anyhow::Result<usize> {
345 let c: i64 = conn.query_row(
346 "SELECT COUNT(*) FROM nodes WHERE kind != 'file'",
347 [],
348 |row| row.get(0),
349 )?;
350 Ok(c as usize)
351}
352
353pub(super) fn file_count(conn: &Connection) -> anyhow::Result<usize> {
357 let c: i64 = conn.query_row(
358 "SELECT COUNT(*) FROM nodes WHERE kind = 'file'",
359 [],
360 |row| row.get(0),
361 )?;
362 Ok(c as usize)
363}
364
365pub(super) fn all_edges_flat(
366 conn: &Connection,
367) -> anyhow::Result<Vec<(String, String, String, f64)>> {
368 let mut stmt = conn.prepare(
373 "SELECT p1.path, p2.path, e.kind
374 FROM edges e
375 JOIN nodes n1 ON e.source_id = n1.id
376 JOIN nodes n2 ON e.target_id = n2.id
377 JOIN paths p1 ON p1.id = n1.file_id
378 JOIN paths p2 ON p2.id = n2.file_id
379 WHERE n1.kind = 'file' AND n2.kind = 'file'",
380 )?;
381 let rows = stmt.query_map([], |row| {
382 Ok((
383 row.get::<_, String>(0)?,
384 row.get::<_, String>(1)?,
385 row.get::<_, String>(2)?,
386 ))
387 })?;
388 let mut result = Vec::new();
389 for r in rows {
390 let (from, to, kind) = r?;
391 let weight = super::queries::edge_weight(&kind);
392 result.push((from, to, kind, weight));
393 }
394 Ok(result)
395}
396
397pub(super) fn count(conn: &Connection) -> anyhow::Result<usize> {
398 let c: i64 = conn.query_row("SELECT COUNT(*) FROM nodes", [], |row| row.get(0))?;
399 Ok(c as usize)
400}