1use anyhow::{Context, Result};
2use rusqlite::{Connection, OptionalExtension, params};
3
4use super::{DirStats, FileRow, IndexDb, NewCall, NewImport, NewSymbol, SymbolRow, SymbolWithFile};
5
6fn fts5_escape(query: &str) -> String {
10 let tokens: Vec<String> = query
11 .split(|c: char| c.is_whitespace() || c == '_' || c == '-')
12 .filter(|t| !t.is_empty())
13 .map(|token| {
14 let escaped = token.replace('"', "\"\"");
15 format!("{escaped}*")
17 })
18 .collect();
19 if tokens.is_empty() {
20 let escaped = query.replace('"', "\"\"");
21 return format!("{escaped}*");
22 }
23 tokens.join(" OR ")
24}
25
26pub(crate) fn get_fresh_file(
31 conn: &Connection,
32 relative_path: &str,
33 mtime_ms: i64,
34 content_hash: &str,
35) -> Result<Option<FileRow>> {
36 conn.query_row(
37 "SELECT id, relative_path, mtime_ms, content_hash, size_bytes, language
38 FROM files WHERE relative_path = ?1 AND mtime_ms = ?2 AND content_hash = ?3",
39 params![relative_path, mtime_ms, content_hash],
40 |row| {
41 Ok(FileRow {
42 id: row.get(0)?,
43 relative_path: row.get(1)?,
44 mtime_ms: row.get(2)?,
45 content_hash: row.get(3)?,
46 size_bytes: row.get(4)?,
47 language: row.get(5)?,
48 })
49 },
50 )
51 .optional()
52 .context("get_fresh_file query failed")
53}
54
55pub(crate) fn upsert_file(
57 conn: &Connection,
58 relative_path: &str,
59 mtime_ms: i64,
60 content_hash: &str,
61 size_bytes: i64,
62 language: Option<&str>,
63) -> Result<i64> {
64 let now = std::time::SystemTime::now()
65 .duration_since(std::time::UNIX_EPOCH)
66 .unwrap_or_default()
67 .as_millis() as i64;
68
69 let id: i64 = conn.query_row(
70 "INSERT INTO files (relative_path, mtime_ms, content_hash, size_bytes, language, indexed_at)
71 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
72 ON CONFLICT(relative_path) DO UPDATE SET
73 mtime_ms = excluded.mtime_ms,
74 content_hash = excluded.content_hash,
75 size_bytes = excluded.size_bytes,
76 language = excluded.language,
77 indexed_at = excluded.indexed_at
78 RETURNING id",
79 params![relative_path, mtime_ms, content_hash, size_bytes, language, now],
80 |row| row.get(0),
81 )?;
82
83 conn.execute("DELETE FROM symbols WHERE file_id = ?1", params![id])?;
84 conn.execute("DELETE FROM imports WHERE source_file_id = ?1", params![id])?;
85 conn.execute("DELETE FROM calls WHERE caller_file_id = ?1", params![id])?;
86
87 Ok(id)
88}
89
90pub(crate) fn delete_file(conn: &Connection, relative_path: &str) -> Result<()> {
92 conn.execute(
93 "DELETE FROM files WHERE relative_path = ?1",
94 params![relative_path],
95 )?;
96 Ok(())
97}
98
99pub(crate) fn dir_stats(conn: &Connection) -> Result<Vec<DirStats>> {
101 let mut stmt = conn.prepare_cached(
103 "SELECT f.relative_path, COUNT(s.id) AS sym_count
104 FROM files f LEFT JOIN symbols s ON s.file_id = f.id
105 GROUP BY f.id",
106 )?;
107 let rows = stmt.query_map([], |row| {
108 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
109 })?;
110
111 let mut dir_map: std::collections::HashMap<String, (usize, usize)> =
112 std::collections::HashMap::new();
113 for row in rows {
114 let (path, sym_count) = row?;
115 let dir = match path.rfind('/') {
116 Some(pos) => &path[..=pos],
117 None => ".",
118 };
119 let entry = dir_map.entry(dir.to_owned()).or_insert((0, 0));
120 entry.0 += 1; entry.1 += sym_count; }
123
124 let mut result: Vec<DirStats> = dir_map
125 .into_iter()
126 .map(|(dir, (files, symbols))| DirStats {
127 dir,
128 files,
129 symbols,
130 imports_from_others: 0,
131 })
132 .collect();
133 result.sort_by(|a, b| b.symbols.cmp(&a.symbols));
134 Ok(result)
135}
136
137pub(crate) fn all_file_paths(conn: &Connection) -> Result<Vec<String>> {
139 let mut stmt = conn.prepare_cached("SELECT relative_path FROM files")?;
140 let rows = stmt.query_map([], |row| row.get(0))?;
141 let mut paths = Vec::new();
142 for row in rows {
143 paths.push(row?);
144 }
145 Ok(paths)
146}
147
148pub(crate) fn files_with_symbol_kinds(conn: &Connection, kinds: &[&str]) -> Result<Vec<String>> {
150 if kinds.is_empty() {
151 return Ok(Vec::new());
152 }
153 let placeholders: String = kinds.iter().map(|_| "?").collect::<Vec<_>>().join(",");
154 let sql = format!(
155 "SELECT DISTINCT f.relative_path FROM files f \
156 JOIN symbols s ON s.file_id = f.id \
157 WHERE s.kind IN ({placeholders})"
158 );
159 let mut stmt = conn.prepare_cached(&sql)?;
160 let params: Vec<&dyn rusqlite::types::ToSql> = kinds
161 .iter()
162 .map(|k| k as &dyn rusqlite::types::ToSql)
163 .collect();
164 let rows = stmt.query_map(params.as_slice(), |row| row.get(0))?;
165 let mut paths = Vec::new();
166 for row in rows {
167 paths.push(row?);
168 }
169 Ok(paths)
170}
171
172pub(crate) fn insert_symbols(
174 conn: &Connection,
175 file_id: i64,
176 symbols: &[NewSymbol<'_>],
177) -> Result<Vec<i64>> {
178 let mut ids = Vec::with_capacity(symbols.len());
179 let mut stmt = conn.prepare_cached(
180 "INSERT INTO symbols (file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id, end_line)
181 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
182 )?;
183 for sym in symbols {
184 stmt.execute(params![
185 file_id,
186 sym.name,
187 sym.kind,
188 sym.line,
189 sym.column_num,
190 sym.start_byte,
191 sym.end_byte,
192 sym.signature,
193 sym.name_path,
194 sym.parent_id,
195 sym.end_line,
196 ])?;
197 ids.push(conn.last_insert_rowid());
198 }
199 Ok(ids)
200}
201
202pub(crate) fn insert_imports(conn: &Connection, file_id: i64, imports: &[NewImport]) -> Result<()> {
204 let mut stmt = conn.prepare_cached(
205 "INSERT OR REPLACE INTO imports (source_file_id, target_path, raw_import)
206 VALUES (?1, ?2, ?3)",
207 )?;
208 for imp in imports {
209 stmt.execute(params![file_id, imp.target_path, imp.raw_import])?;
210 }
211 Ok(())
212}
213
214pub(crate) fn insert_calls(conn: &Connection, file_id: i64, calls: &[NewCall]) -> Result<()> {
216 conn.execute(
217 "DELETE FROM calls WHERE caller_file_id = ?1",
218 params![file_id],
219 )?;
220 let mut stmt = conn.prepare_cached(
221 "INSERT INTO calls (caller_file_id, caller_name, callee_name, line)
222 VALUES (?1, ?2, ?3, ?4)",
223 )?;
224 for call in calls {
225 stmt.execute(params![
226 file_id,
227 call.caller_name,
228 call.callee_name,
229 call.line
230 ])?;
231 }
232 Ok(())
233}
234
235impl IndexDb {
238 pub fn get_fresh_file_by_mtime(
242 &self,
243 relative_path: &str,
244 mtime_ms: i64,
245 ) -> Result<Option<FileRow>> {
246 self.conn
247 .query_row(
248 "SELECT id, relative_path, mtime_ms, content_hash, size_bytes, language
249 FROM files WHERE relative_path = ?1 AND mtime_ms = ?2",
250 params![relative_path, mtime_ms],
251 |row| {
252 Ok(FileRow {
253 id: row.get(0)?,
254 relative_path: row.get(1)?,
255 mtime_ms: row.get(2)?,
256 content_hash: row.get(3)?,
257 size_bytes: row.get(4)?,
258 language: row.get(5)?,
259 })
260 },
261 )
262 .optional()
263 .context("get_fresh_file_by_mtime query failed")
264 }
265
266 pub fn get_fresh_file(
268 &self,
269 relative_path: &str,
270 mtime_ms: i64,
271 content_hash: &str,
272 ) -> Result<Option<FileRow>> {
273 get_fresh_file(&self.conn, relative_path, mtime_ms, content_hash)
274 }
275
276 pub fn get_file(&self, relative_path: &str) -> Result<Option<FileRow>> {
278 self.conn
279 .query_row(
280 "SELECT id, relative_path, mtime_ms, content_hash, size_bytes, language
281 FROM files WHERE relative_path = ?1",
282 params![relative_path],
283 |row| {
284 Ok(FileRow {
285 id: row.get(0)?,
286 relative_path: row.get(1)?,
287 mtime_ms: row.get(2)?,
288 content_hash: row.get(3)?,
289 size_bytes: row.get(4)?,
290 language: row.get(5)?,
291 })
292 },
293 )
294 .optional()
295 .context("get_file query failed")
296 }
297
298 pub fn upsert_file(
300 &self,
301 relative_path: &str,
302 mtime_ms: i64,
303 content_hash: &str,
304 size_bytes: i64,
305 language: Option<&str>,
306 ) -> Result<i64> {
307 upsert_file(
308 &self.conn,
309 relative_path,
310 mtime_ms,
311 content_hash,
312 size_bytes,
313 language,
314 )
315 }
316
317 pub fn delete_file(&self, relative_path: &str) -> Result<()> {
319 delete_file(&self.conn, relative_path)
320 }
321
322 pub fn file_count(&self) -> Result<usize> {
324 let count: i64 = self
325 .conn
326 .query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0))?;
327 Ok(count as usize)
328 }
329
330 pub fn all_file_paths(&self) -> Result<Vec<String>> {
332 all_file_paths(&self.conn)
333 }
334
335 pub fn files_with_symbol_kinds(&self, kinds: &[&str]) -> Result<Vec<String>> {
337 files_with_symbol_kinds(&self.conn, kinds)
338 }
339
340 pub fn dir_stats(&self) -> Result<Vec<DirStats>> {
341 dir_stats(&self.conn)
342 }
343
344 pub fn insert_symbols(&self, file_id: i64, symbols: &[NewSymbol<'_>]) -> Result<Vec<i64>> {
348 insert_symbols(&self.conn, file_id, symbols)
349 }
350
351 pub fn find_symbols_by_name(
353 &self,
354 name: &str,
355 file_path: Option<&str>,
356 exact: bool,
357 max_results: usize,
358 ) -> Result<Vec<SymbolRow>> {
359 let (sql, use_file_filter) = match (exact, file_path.is_some()) {
360 (true, true) => (
361 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num, s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id, s.end_line
362 FROM symbols s JOIN files f ON s.file_id = f.id
363 WHERE s.name = ?1 AND f.relative_path = ?2
364 LIMIT ?3",
365 true,
366 ),
367 (true, false) => (
368 "SELECT id, file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id, end_line
369 FROM symbols WHERE name = ?1
370 LIMIT ?2",
371 false,
372 ),
373 (false, true) => (
374 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num, s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id, s.end_line
375 FROM symbols s JOIN files f ON s.file_id = f.id
376 WHERE s.name LIKE '%' || ?1 || '%' AND f.relative_path = ?2
377 ORDER BY LENGTH(s.name), s.name
378 LIMIT ?3",
379 true,
380 ),
381 (false, false) => (
382 "SELECT id, file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id, end_line
383 FROM symbols WHERE name LIKE '%' || ?1 || '%'
384 ORDER BY LENGTH(name), name
385 LIMIT ?2",
386 false,
387 ),
388 };
389
390 let mut stmt = self.conn.prepare_cached(sql)?;
391 let mut rows = if use_file_filter {
392 stmt.query(params![name, file_path.unwrap_or(""), max_results as i64])?
393 } else {
394 stmt.query(params![name, max_results as i64])?
395 };
396
397 let mut results = Vec::new();
398 while let Some(row) = rows.next()? {
399 results.push(SymbolRow {
400 id: row.get(0)?,
401 file_id: row.get(1)?,
402 name: row.get(2)?,
403 kind: row.get(3)?,
404 line: row.get(4)?,
405 column_num: row.get(5)?,
406 start_byte: row.get(6)?,
407 end_byte: row.get(7)?,
408 signature: row.get(8)?,
409 name_path: row.get(9)?,
410 parent_id: row.get(10)?,
411 end_line: row.get(11)?,
412 });
413 }
414 Ok(results)
415 }
416
417 pub fn find_symbols_with_path(
420 &self,
421 name: &str,
422 exact: bool,
423 max_results: usize,
424 ) -> Result<Vec<(SymbolRow, String)>> {
425 let sql = if exact {
426 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
427 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
428 s.end_line, f.relative_path
429 FROM symbols s JOIN files f ON s.file_id = f.id
430 WHERE s.name = ?1
431 LIMIT ?2"
432 } else {
433 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
434 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
435 s.end_line, f.relative_path
436 FROM symbols s JOIN files f ON s.file_id = f.id
437 WHERE s.name LIKE '%' || ?1 || '%'
438 LIMIT ?2"
439 };
440
441 let mut stmt = self.conn.prepare_cached(sql)?;
442 let mut rows = stmt.query(params![name, max_results as i64])?;
443 let mut results = Vec::new();
444 while let Some(row) = rows.next()? {
445 results.push((
446 SymbolRow {
447 id: row.get(0)?,
448 file_id: row.get(1)?,
449 name: row.get(2)?,
450 kind: row.get(3)?,
451 line: row.get(4)?,
452 column_num: row.get(5)?,
453 start_byte: row.get(6)?,
454 end_byte: row.get(7)?,
455 signature: row.get(8)?,
456 name_path: row.get(9)?,
457 parent_id: row.get(10)?,
458 end_line: row.get(11)?,
459 },
460 row.get::<_, String>(12)?,
461 ));
462 }
463 Ok(results)
464 }
465
466 pub fn get_file_symbols(&self, file_id: i64) -> Result<Vec<SymbolRow>> {
468 let mut stmt = self.conn.prepare_cached(
469 "SELECT id, file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id, end_line
470 FROM symbols WHERE file_id = ?1 ORDER BY start_byte",
471 )?;
472 let rows = stmt.query_map(params![file_id], |row| {
473 Ok(SymbolRow {
474 id: row.get(0)?,
475 file_id: row.get(1)?,
476 name: row.get(2)?,
477 kind: row.get(3)?,
478 line: row.get(4)?,
479 column_num: row.get(5)?,
480 start_byte: row.get(6)?,
481 end_byte: row.get(7)?,
482 signature: row.get(8)?,
483 name_path: row.get(9)?,
484 parent_id: row.get(10)?,
485 end_line: row.get(11)?,
486 })
487 })?;
488 let mut results = Vec::new();
489 for row in rows {
490 results.push(row?);
491 }
492 Ok(results)
493 }
494
495 pub fn search_symbols_fts(
498 &self,
499 query: &str,
500 max_results: usize,
501 ) -> Result<Vec<(SymbolRow, String, f64)>> {
502 let fts_exists: bool = self
504 .conn
505 .query_row(
506 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='symbols_fts'",
507 [],
508 |row| row.get::<_, i64>(0),
509 )
510 .map(|c| c > 0)
511 .unwrap_or(false);
512
513 if !fts_exists {
514 return self
516 .find_symbols_with_path(query, false, max_results)
517 .map(|rows| rows.into_iter().map(|(r, p)| (r, p, 0.0)).collect());
518 }
519
520 let now_secs = std::time::SystemTime::now()
524 .duration_since(std::time::UNIX_EPOCH)
525 .map(|d| d.as_secs() as i64)
526 .unwrap_or(0);
527 let last_rebuild_ts: i64 = self
528 .conn
529 .query_row(
530 "SELECT value FROM meta WHERE key = 'fts_rebuild_ts'",
531 [],
532 |row| row.get::<_, String>(0),
533 )
534 .optional()?
535 .and_then(|v| v.parse::<i64>().ok())
536 .unwrap_or(0);
537
538 if now_secs - last_rebuild_ts > 30 {
539 let fts_fresh: bool = self
540 .conn
541 .query_row(
542 "SELECT value FROM meta WHERE key = 'fts_symbol_count'",
543 [],
544 |row| row.get::<_, String>(0),
545 )
546 .optional()?
547 .and_then(|v| v.parse::<i64>().ok())
548 .map(|cached_count| {
549 let current: i64 = self
550 .conn
551 .query_row("SELECT COUNT(*) FROM symbols", [], |row| row.get(0))
552 .unwrap_or(0);
553 cached_count == current
554 })
555 .unwrap_or(false);
556
557 if !fts_fresh {
558 let sym_count: i64 = self
559 .conn
560 .query_row("SELECT COUNT(*) FROM symbols", [], |row| row.get(0))
561 .unwrap_or(0);
562 if sym_count > 0 {
563 let _ = self
564 .conn
565 .execute_batch("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild')");
566 let _ = self.conn.execute(
567 "INSERT OR REPLACE INTO meta (key, value) VALUES ('fts_symbol_count', ?1)",
568 params![sym_count.to_string()],
569 );
570 }
571 let _ = self.conn.execute(
572 "INSERT OR REPLACE INTO meta (key, value) VALUES ('fts_rebuild_ts', ?1)",
573 params![now_secs.to_string()],
574 );
575 }
576 }
577
578 let fts_query = fts5_escape(query);
580 let mut stmt = self.conn.prepare_cached(
581 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
582 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
583 s.end_line, f.relative_path, rank
584 FROM symbols_fts
585 JOIN symbols s ON symbols_fts.rowid = s.id
586 JOIN files f ON s.file_id = f.id
587 WHERE symbols_fts MATCH ?1
588 ORDER BY rank
589 LIMIT ?2",
590 )?;
591
592 let mut rows = stmt.query(params![fts_query, max_results as i64])?;
593 let mut results = Vec::new();
594 while let Some(row) = rows.next()? {
595 results.push((
596 SymbolRow {
597 id: row.get(0)?,
598 file_id: row.get(1)?,
599 name: row.get(2)?,
600 kind: row.get(3)?,
601 line: row.get(4)?,
602 column_num: row.get(5)?,
603 start_byte: row.get(6)?,
604 end_byte: row.get(7)?,
605 signature: row.get(8)?,
606 name_path: row.get(9)?,
607 parent_id: row.get(10)?,
608 end_line: row.get(11)?,
609 },
610 row.get::<_, String>(12)?,
611 row.get::<_, f64>(13)?,
612 ));
613 }
614 Ok(results)
615 }
616
617 pub fn get_symbols_for_directory(&self, prefix: &str) -> Result<Vec<(String, Vec<SymbolRow>)>> {
620 let pattern = if prefix.is_empty() || prefix == "." {
621 "%".to_owned()
622 } else {
623 format!("{prefix}%")
624 };
625 let mut stmt = self.conn.prepare_cached(
626 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
627 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
628 s.end_line, f.relative_path
629 FROM symbols s
630 JOIN files f ON s.file_id = f.id
631 WHERE f.relative_path LIKE ?1
632 ORDER BY s.file_id, s.start_byte",
633 )?;
634 let rows = stmt.query_map(params![pattern], |row| {
635 Ok((
636 SymbolRow {
637 id: row.get(0)?,
638 file_id: row.get(1)?,
639 name: row.get(2)?,
640 kind: row.get(3)?,
641 line: row.get(4)?,
642 column_num: row.get(5)?,
643 start_byte: row.get(6)?,
644 end_byte: row.get(7)?,
645 signature: row.get(8)?,
646 name_path: row.get(9)?,
647 parent_id: row.get(10)?,
648 end_line: row.get(11)?,
649 },
650 row.get::<_, String>(12)?,
651 ))
652 })?;
653
654 let mut groups: Vec<(String, Vec<SymbolRow>)> = Vec::new();
655 let mut current_path = String::new();
656 for row in rows {
657 let (sym, path) = row?;
658 if path != current_path {
659 current_path = path.clone();
660 groups.push((path, Vec::new()));
661 }
662 groups.last_mut().unwrap().1.push(sym);
663 }
664 Ok(groups)
665 }
666
667 #[allow(clippy::type_complexity)]
669 pub fn all_symbol_names(&self) -> Result<Vec<(String, String, String, i64, String, String)>> {
670 let mut stmt = self.conn.prepare_cached(
671 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path
672 FROM symbols s JOIN files f ON s.file_id = f.id",
673 )?;
674 let rows = stmt.query_map([], |row| {
675 Ok((
676 row.get::<_, String>(0)?,
677 row.get::<_, String>(1)?,
678 row.get::<_, String>(2)?,
679 row.get::<_, i64>(3)?,
680 row.get::<_, String>(4)?,
681 row.get::<_, String>(5)?,
682 ))
683 })?;
684 let mut results = Vec::new();
685 for row in rows {
686 results.push(row?);
687 }
688 Ok(results)
689 }
690
691 pub fn all_symbols_with_bytes(&self) -> Result<Vec<SymbolWithFile>> {
693 let mut stmt = self.conn.prepare_cached(
694 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
695 s.start_byte, s.end_byte
696 FROM symbols s JOIN files f ON s.file_id = f.id
697 ORDER BY s.file_id, s.start_byte",
698 )?;
699 let rows = stmt.query_map([], |row| {
700 Ok(SymbolWithFile {
701 name: row.get(0)?,
702 kind: row.get(1)?,
703 file_path: row.get(2)?,
704 line: row.get(3)?,
705 signature: row.get(4)?,
706 name_path: row.get(5)?,
707 start_byte: row.get(6)?,
708 end_byte: row.get(7)?,
709 })
710 })?;
711 let mut results = Vec::new();
712 for row in rows {
713 results.push(row?);
714 }
715 Ok(results)
716 }
717
718 pub fn for_each_symbol_with_bytes<F>(&self, mut callback: F) -> Result<usize>
721 where
722 F: FnMut(SymbolWithFile) -> Result<()>,
723 {
724 let mut stmt = self.conn.prepare_cached(
725 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
726 s.start_byte, s.end_byte
727 FROM symbols s JOIN files f ON s.file_id = f.id
728 ORDER BY s.file_id, s.start_byte",
729 )?;
730 let mut rows = stmt.query([])?;
731 let mut count = 0usize;
732 while let Some(row) = rows.next()? {
733 callback(SymbolWithFile {
734 name: row.get(0)?,
735 kind: row.get(1)?,
736 file_path: row.get(2)?,
737 line: row.get(3)?,
738 signature: row.get(4)?,
739 name_path: row.get(5)?,
740 start_byte: row.get(6)?,
741 end_byte: row.get(7)?,
742 })?;
743 count += 1;
744 }
745 Ok(count)
746 }
747
748 pub fn for_each_file_symbols_with_bytes<F>(&self, mut callback: F) -> Result<usize>
751 where
752 F: FnMut(String, Vec<SymbolWithFile>) -> Result<()>,
753 {
754 let mut stmt = self.conn.prepare_cached(
755 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
756 s.start_byte, s.end_byte
757 FROM symbols s JOIN files f ON s.file_id = f.id
758 ORDER BY f.relative_path, s.start_byte",
759 )?;
760 let mut rows = stmt.query([])?;
761 let mut count = 0usize;
762 let mut current_file: Option<String> = None;
763 let mut current_symbols: Vec<SymbolWithFile> = Vec::new();
764
765 while let Some(row) = rows.next()? {
766 let symbol = SymbolWithFile {
767 name: row.get(0)?,
768 kind: row.get(1)?,
769 file_path: row.get(2)?,
770 line: row.get(3)?,
771 signature: row.get(4)?,
772 name_path: row.get(5)?,
773 start_byte: row.get(6)?,
774 end_byte: row.get(7)?,
775 };
776
777 if current_file.as_deref() != Some(symbol.file_path.as_str())
778 && let Some(previous_file) = current_file.replace(symbol.file_path.clone())
779 {
780 callback(previous_file, std::mem::take(&mut current_symbols))?;
781 }
782
783 current_symbols.push(symbol);
784 count += 1;
785 }
786
787 if let Some(file_path) = current_file {
788 callback(file_path, current_symbols)?;
789 }
790
791 Ok(count)
792 }
793
794 pub fn symbols_for_files(&self, file_paths: &[&str]) -> Result<Vec<SymbolWithFile>> {
796 if file_paths.is_empty() {
797 return Ok(Vec::new());
798 }
799 let placeholders: Vec<String> = (1..=file_paths.len()).map(|i| format!("?{i}")).collect();
800 let sql = format!(
801 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
802 s.start_byte, s.end_byte
803 FROM symbols s JOIN files f ON s.file_id = f.id
804 WHERE f.relative_path IN ({})
805 ORDER BY s.file_id, s.start_byte",
806 placeholders.join(", ")
807 );
808 let mut stmt = self.conn.prepare(&sql)?;
809 let params: Vec<&dyn rusqlite::types::ToSql> = file_paths
810 .iter()
811 .map(|p| p as &dyn rusqlite::types::ToSql)
812 .collect();
813 let rows = stmt.query_map(params.as_slice(), |row| {
814 Ok(SymbolWithFile {
815 name: row.get(0)?,
816 kind: row.get(1)?,
817 file_path: row.get(2)?,
818 line: row.get(3)?,
819 signature: row.get(4)?,
820 name_path: row.get(5)?,
821 start_byte: row.get(6)?,
822 end_byte: row.get(7)?,
823 })
824 })?;
825 let mut results = Vec::new();
826 for row in rows {
827 results.push(row?);
828 }
829 Ok(results)
830 }
831
832 pub fn get_file_path(&self, file_id: i64) -> Result<Option<String>> {
834 self.conn
835 .query_row(
836 "SELECT relative_path FROM files WHERE id = ?1",
837 params![file_id],
838 |row| row.get(0),
839 )
840 .optional()
841 .context("get_file_path query failed")
842 }
843
844 pub fn insert_imports(&self, file_id: i64, imports: &[NewImport]) -> Result<()> {
848 insert_imports(&self.conn, file_id, imports)
849 }
850
851 pub fn get_importers(&self, target_path: &str) -> Result<Vec<String>> {
853 let mut stmt = self.conn.prepare_cached(
854 "SELECT f.relative_path FROM imports i
855 JOIN files f ON i.source_file_id = f.id
856 WHERE i.target_path = ?1
857 ORDER BY f.relative_path",
858 )?;
859 let rows = stmt.query_map(params![target_path], |row| row.get(0))?;
860 let mut results = Vec::new();
861 for row in rows {
862 results.push(row?);
863 }
864 Ok(results)
865 }
866
867 pub fn get_imports_of(&self, relative_path: &str) -> Result<Vec<String>> {
869 let mut stmt = self.conn.prepare_cached(
870 "SELECT i.target_path FROM imports i
871 JOIN files f ON i.source_file_id = f.id
872 WHERE f.relative_path = ?1
873 ORDER BY i.target_path",
874 )?;
875 let rows = stmt.query_map(params![relative_path], |row| row.get(0))?;
876 let mut results = Vec::new();
877 for row in rows {
878 results.push(row?);
879 }
880 Ok(results)
881 }
882
883 #[allow(clippy::type_complexity)]
885 pub fn build_import_graph(
886 &self,
887 ) -> Result<std::collections::HashMap<String, (Vec<String>, Vec<String>)>> {
888 let mut graph = std::collections::HashMap::new();
889
890 for path in self.all_file_paths()? {
891 graph.insert(path, (Vec::new(), Vec::new()));
892 }
893
894 let mut stmt = self.conn.prepare_cached(
895 "SELECT f.relative_path, i.target_path FROM imports i
896 JOIN files f ON i.source_file_id = f.id",
897 )?;
898 let rows = stmt.query_map([], |row| {
899 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
900 })?;
901 for row in rows {
902 let (source, target) = row?;
903 if let Some(entry) = graph.get_mut(&source) {
904 entry.0.push(target.clone());
905 }
906 if let Some(entry) = graph.get_mut(&target) {
907 entry.1.push(source.clone());
908 }
909 }
910
911 Ok(graph)
912 }
913
914 pub fn insert_calls(&self, file_id: i64, calls: &[NewCall]) -> Result<()> {
918 insert_calls(&self.conn, file_id, calls)
919 }
920
921 pub fn get_callers_cached(
923 &self,
924 callee_name: &str,
925 max_results: usize,
926 ) -> Result<Vec<(String, String, i64)>> {
927 let mut stmt = self.conn.prepare_cached(
928 "SELECT f.relative_path, c.caller_name, c.line FROM calls c
929 JOIN files f ON c.caller_file_id = f.id
930 WHERE c.callee_name = ?1
931 ORDER BY f.relative_path, c.line
932 LIMIT ?2",
933 )?;
934 let mut rows = stmt.query(params![callee_name, max_results as i64])?;
935 let mut results = Vec::new();
936 while let Some(row) = rows.next()? {
937 results.push((row.get(0)?, row.get(1)?, row.get(2)?));
938 }
939 Ok(results)
940 }
941
942 pub fn get_callees_cached(
944 &self,
945 caller_name: &str,
946 file_path: Option<&str>,
947 max_results: usize,
948 ) -> Result<Vec<(String, i64)>> {
949 let (sql, use_file) = match file_path {
950 Some(_) => (
951 "SELECT c.callee_name, c.line FROM calls c
952 JOIN files f ON c.caller_file_id = f.id
953 WHERE c.caller_name = ?1 AND f.relative_path = ?2
954 ORDER BY c.line LIMIT ?3",
955 true,
956 ),
957 None => (
958 "SELECT c.callee_name, c.line FROM calls c
959 WHERE c.caller_name = ?1
960 ORDER BY c.line LIMIT ?2",
961 false,
962 ),
963 };
964 let mut stmt = self.conn.prepare_cached(sql)?;
965 let mut rows = if use_file {
966 stmt.query(params![
967 caller_name,
968 file_path.unwrap_or(""),
969 max_results as i64
970 ])?
971 } else {
972 stmt.query(params![caller_name, max_results as i64])?
973 };
974 let mut results = Vec::new();
975 while let Some(row) = rows.next()? {
976 results.push((row.get(0)?, row.get(1)?));
977 }
978 Ok(results)
979 }
980
981 pub fn has_call_data(&self) -> Result<bool> {
983 let count: i64 = self
984 .conn
985 .query_row("SELECT COUNT(*) FROM calls", [], |row| row.get(0))?;
986 Ok(count > 0)
987 }
988
989 pub fn record_index_failure(
993 &self,
994 file_path: &str,
995 error_type: &str,
996 error_message: &str,
997 ) -> Result<()> {
998 let now = std::time::SystemTime::now()
999 .duration_since(std::time::UNIX_EPOCH)
1000 .unwrap_or_default()
1001 .as_secs() as i64;
1002 self.conn.execute(
1003 "INSERT INTO index_failures (file_path, error_type, error_message, failed_at, retry_count)
1004 VALUES (?1, ?2, ?3, ?4, 1)
1005 ON CONFLICT(file_path) DO UPDATE SET
1006 error_type = excluded.error_type,
1007 error_message = excluded.error_message,
1008 failed_at = excluded.failed_at,
1009 retry_count = retry_count + 1",
1010 params![file_path, error_type, error_message, now],
1011 )?;
1012 Ok(())
1013 }
1014
1015 pub fn clear_index_failure(&self, file_path: &str) -> Result<()> {
1017 self.conn.execute(
1018 "DELETE FROM index_failures WHERE file_path = ?1",
1019 params![file_path],
1020 )?;
1021 Ok(())
1022 }
1023
1024 pub fn invalidate_fts(&self) -> Result<()> {
1026 self.conn
1027 .execute("DELETE FROM meta WHERE key = 'fts_symbol_count'", [])?;
1028 Ok(())
1029 }
1030
1031 pub fn index_failure_count(&self) -> Result<usize> {
1033 let count: i64 = self
1034 .conn
1035 .query_row("SELECT COUNT(*) FROM index_failures", [], |row| row.get(0))?;
1036 Ok(count as usize)
1037 }
1038
1039 pub fn prune_missing_index_failures(&self, project_root: &std::path::Path) -> Result<usize> {
1041 let mut stmt = self
1042 .conn
1043 .prepare_cached("SELECT file_path FROM index_failures ORDER BY file_path")?;
1044 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1045 let mut missing = Vec::new();
1046 for row in rows {
1047 let relative_path = row?;
1048 if !project_root.join(&relative_path).is_file() {
1049 missing.push(relative_path);
1050 }
1051 }
1052 for relative_path in &missing {
1053 self.clear_index_failure(relative_path)?;
1054 }
1055 Ok(missing.len())
1056 }
1057
1058 pub fn index_failure_summary(
1060 &self,
1061 recent_window_secs: i64,
1062 ) -> Result<crate::db::IndexFailureSummary> {
1063 let now = std::time::SystemTime::now()
1064 .duration_since(std::time::UNIX_EPOCH)
1065 .unwrap_or_default()
1066 .as_secs() as i64;
1067 let recent_cutoff = now.saturating_sub(recent_window_secs.max(0));
1068
1069 let total_failures: i64 =
1070 self.conn
1071 .query_row("SELECT COUNT(*) FROM index_failures", [], |row| row.get(0))?;
1072 let recent_failures: i64 = self.conn.query_row(
1073 "SELECT COUNT(*) FROM index_failures WHERE failed_at >= ?1",
1074 params![recent_cutoff],
1075 |row| row.get(0),
1076 )?;
1077 let persistent_failures: i64 = self.conn.query_row(
1078 "SELECT COUNT(*) FROM index_failures WHERE retry_count >= 3",
1079 [],
1080 |row| row.get(0),
1081 )?;
1082
1083 Ok(crate::db::IndexFailureSummary {
1084 total_failures: total_failures as usize,
1085 recent_failures: recent_failures as usize,
1086 stale_failures: total_failures.saturating_sub(recent_failures) as usize,
1087 persistent_failures: persistent_failures as usize,
1088 })
1089 }
1090
1091 pub fn get_persistent_failures(&self, min_retries: i64) -> Result<Vec<(String, String, i64)>> {
1093 let mut stmt = self.conn.prepare_cached(
1094 "SELECT file_path, error_message, retry_count FROM index_failures WHERE retry_count >= ?1 ORDER BY retry_count DESC",
1095 )?;
1096 let mut rows = stmt.query(params![min_retries])?;
1097 let mut results = Vec::new();
1098 while let Some(row) = rows.next()? {
1099 results.push((row.get(0)?, row.get(1)?, row.get(2)?));
1100 }
1101 Ok(results)
1102 }
1103}