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)
181 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
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 ])?;
196 ids.push(conn.last_insert_rowid());
197 }
198 Ok(ids)
199}
200
201pub(crate) fn insert_imports(conn: &Connection, file_id: i64, imports: &[NewImport]) -> Result<()> {
203 let mut stmt = conn.prepare_cached(
204 "INSERT OR REPLACE INTO imports (source_file_id, target_path, raw_import)
205 VALUES (?1, ?2, ?3)",
206 )?;
207 for imp in imports {
208 stmt.execute(params![file_id, imp.target_path, imp.raw_import])?;
209 }
210 Ok(())
211}
212
213pub(crate) fn insert_calls(conn: &Connection, file_id: i64, calls: &[NewCall]) -> Result<()> {
215 conn.execute(
216 "DELETE FROM calls WHERE caller_file_id = ?1",
217 params![file_id],
218 )?;
219 let mut stmt = conn.prepare_cached(
220 "INSERT INTO calls (caller_file_id, caller_name, callee_name, line)
221 VALUES (?1, ?2, ?3, ?4)",
222 )?;
223 for call in calls {
224 stmt.execute(params![
225 file_id,
226 call.caller_name,
227 call.callee_name,
228 call.line
229 ])?;
230 }
231 Ok(())
232}
233
234impl IndexDb {
237 pub fn get_fresh_file_by_mtime(
241 &self,
242 relative_path: &str,
243 mtime_ms: i64,
244 ) -> Result<Option<FileRow>> {
245 self.conn
246 .query_row(
247 "SELECT id, relative_path, mtime_ms, content_hash, size_bytes, language
248 FROM files WHERE relative_path = ?1 AND mtime_ms = ?2",
249 params![relative_path, mtime_ms],
250 |row| {
251 Ok(FileRow {
252 id: row.get(0)?,
253 relative_path: row.get(1)?,
254 mtime_ms: row.get(2)?,
255 content_hash: row.get(3)?,
256 size_bytes: row.get(4)?,
257 language: row.get(5)?,
258 })
259 },
260 )
261 .optional()
262 .context("get_fresh_file_by_mtime query failed")
263 }
264
265 pub fn get_fresh_file(
267 &self,
268 relative_path: &str,
269 mtime_ms: i64,
270 content_hash: &str,
271 ) -> Result<Option<FileRow>> {
272 get_fresh_file(&self.conn, relative_path, mtime_ms, content_hash)
273 }
274
275 pub fn get_file(&self, relative_path: &str) -> Result<Option<FileRow>> {
277 self.conn
278 .query_row(
279 "SELECT id, relative_path, mtime_ms, content_hash, size_bytes, language
280 FROM files WHERE relative_path = ?1",
281 params![relative_path],
282 |row| {
283 Ok(FileRow {
284 id: row.get(0)?,
285 relative_path: row.get(1)?,
286 mtime_ms: row.get(2)?,
287 content_hash: row.get(3)?,
288 size_bytes: row.get(4)?,
289 language: row.get(5)?,
290 })
291 },
292 )
293 .optional()
294 .context("get_file query failed")
295 }
296
297 pub fn upsert_file(
299 &self,
300 relative_path: &str,
301 mtime_ms: i64,
302 content_hash: &str,
303 size_bytes: i64,
304 language: Option<&str>,
305 ) -> Result<i64> {
306 upsert_file(
307 &self.conn,
308 relative_path,
309 mtime_ms,
310 content_hash,
311 size_bytes,
312 language,
313 )
314 }
315
316 pub fn delete_file(&self, relative_path: &str) -> Result<()> {
318 delete_file(&self.conn, relative_path)
319 }
320
321 pub fn file_count(&self) -> Result<usize> {
323 let count: i64 = self
324 .conn
325 .query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0))?;
326 Ok(count as usize)
327 }
328
329 pub fn all_file_paths(&self) -> Result<Vec<String>> {
331 all_file_paths(&self.conn)
332 }
333
334 pub fn files_with_symbol_kinds(&self, kinds: &[&str]) -> Result<Vec<String>> {
336 files_with_symbol_kinds(&self.conn, kinds)
337 }
338
339 pub fn dir_stats(&self) -> Result<Vec<DirStats>> {
340 dir_stats(&self.conn)
341 }
342
343 pub fn insert_symbols(&self, file_id: i64, symbols: &[NewSymbol<'_>]) -> Result<Vec<i64>> {
347 insert_symbols(&self.conn, file_id, symbols)
348 }
349
350 pub fn find_symbols_by_name(
352 &self,
353 name: &str,
354 file_path: Option<&str>,
355 exact: bool,
356 max_results: usize,
357 ) -> Result<Vec<SymbolRow>> {
358 let (sql, use_file_filter) = match (exact, file_path.is_some()) {
359 (true, true) => (
360 "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
361 FROM symbols s JOIN files f ON s.file_id = f.id
362 WHERE s.name = ?1 AND f.relative_path = ?2
363 LIMIT ?3",
364 true,
365 ),
366 (true, false) => (
367 "SELECT id, file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id
368 FROM symbols WHERE name = ?1
369 LIMIT ?2",
370 false,
371 ),
372 (false, true) => (
373 "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
374 FROM symbols s JOIN files f ON s.file_id = f.id
375 WHERE s.name LIKE '%' || ?1 || '%' AND f.relative_path = ?2
376 ORDER BY LENGTH(s.name), s.name
377 LIMIT ?3",
378 true,
379 ),
380 (false, false) => (
381 "SELECT id, file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id
382 FROM symbols WHERE name LIKE '%' || ?1 || '%'
383 ORDER BY LENGTH(name), name
384 LIMIT ?2",
385 false,
386 ),
387 };
388
389 let mut stmt = self.conn.prepare_cached(sql)?;
390 let mut rows = if use_file_filter {
391 stmt.query(params![name, file_path.unwrap_or(""), max_results as i64])?
392 } else {
393 stmt.query(params![name, max_results as i64])?
394 };
395
396 let mut results = Vec::new();
397 while let Some(row) = rows.next()? {
398 results.push(SymbolRow {
399 id: row.get(0)?,
400 file_id: row.get(1)?,
401 name: row.get(2)?,
402 kind: row.get(3)?,
403 line: row.get(4)?,
404 column_num: row.get(5)?,
405 start_byte: row.get(6)?,
406 end_byte: row.get(7)?,
407 signature: row.get(8)?,
408 name_path: row.get(9)?,
409 parent_id: row.get(10)?,
410 });
411 }
412 Ok(results)
413 }
414
415 pub fn find_symbols_with_path(
418 &self,
419 name: &str,
420 exact: bool,
421 max_results: usize,
422 ) -> Result<Vec<(SymbolRow, String)>> {
423 let sql = if exact {
424 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
425 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
426 f.relative_path
427 FROM symbols s JOIN files f ON s.file_id = f.id
428 WHERE s.name = ?1
429 LIMIT ?2"
430 } else {
431 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
432 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
433 f.relative_path
434 FROM symbols s JOIN files f ON s.file_id = f.id
435 WHERE s.name LIKE '%' || ?1 || '%'
436 LIMIT ?2"
437 };
438
439 let mut stmt = self.conn.prepare_cached(sql)?;
440 let mut rows = stmt.query(params![name, max_results as i64])?;
441 let mut results = Vec::new();
442 while let Some(row) = rows.next()? {
443 results.push((
444 SymbolRow {
445 id: row.get(0)?,
446 file_id: row.get(1)?,
447 name: row.get(2)?,
448 kind: row.get(3)?,
449 line: row.get(4)?,
450 column_num: row.get(5)?,
451 start_byte: row.get(6)?,
452 end_byte: row.get(7)?,
453 signature: row.get(8)?,
454 name_path: row.get(9)?,
455 parent_id: row.get(10)?,
456 },
457 row.get::<_, String>(11)?,
458 ));
459 }
460 Ok(results)
461 }
462
463 pub fn get_file_symbols(&self, file_id: i64) -> Result<Vec<SymbolRow>> {
465 let mut stmt = self.conn.prepare_cached(
466 "SELECT id, file_id, name, kind, line, column_num, start_byte, end_byte, signature, name_path, parent_id
467 FROM symbols WHERE file_id = ?1 ORDER BY start_byte",
468 )?;
469 let rows = stmt.query_map(params![file_id], |row| {
470 Ok(SymbolRow {
471 id: row.get(0)?,
472 file_id: row.get(1)?,
473 name: row.get(2)?,
474 kind: row.get(3)?,
475 line: row.get(4)?,
476 column_num: row.get(5)?,
477 start_byte: row.get(6)?,
478 end_byte: row.get(7)?,
479 signature: row.get(8)?,
480 name_path: row.get(9)?,
481 parent_id: row.get(10)?,
482 })
483 })?;
484 let mut results = Vec::new();
485 for row in rows {
486 results.push(row?);
487 }
488 Ok(results)
489 }
490
491 pub fn search_symbols_fts(
494 &self,
495 query: &str,
496 max_results: usize,
497 ) -> Result<Vec<(SymbolRow, String, f64)>> {
498 let fts_exists: bool = self
500 .conn
501 .query_row(
502 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='symbols_fts'",
503 [],
504 |row| row.get::<_, i64>(0),
505 )
506 .map(|c| c > 0)
507 .unwrap_or(false);
508
509 if !fts_exists {
510 return self
512 .find_symbols_with_path(query, false, max_results)
513 .map(|rows| rows.into_iter().map(|(r, p)| (r, p, 0.0)).collect());
514 }
515
516 let now_secs = std::time::SystemTime::now()
520 .duration_since(std::time::UNIX_EPOCH)
521 .map(|d| d.as_secs() as i64)
522 .unwrap_or(0);
523 let last_rebuild_ts: i64 = self
524 .conn
525 .query_row(
526 "SELECT value FROM meta WHERE key = 'fts_rebuild_ts'",
527 [],
528 |row| row.get::<_, String>(0),
529 )
530 .optional()?
531 .and_then(|v| v.parse::<i64>().ok())
532 .unwrap_or(0);
533
534 if now_secs - last_rebuild_ts > 30 {
535 let fts_fresh: bool = self
536 .conn
537 .query_row(
538 "SELECT value FROM meta WHERE key = 'fts_symbol_count'",
539 [],
540 |row| row.get::<_, String>(0),
541 )
542 .optional()?
543 .and_then(|v| v.parse::<i64>().ok())
544 .map(|cached_count| {
545 let current: i64 = self
546 .conn
547 .query_row("SELECT COUNT(*) FROM symbols", [], |row| row.get(0))
548 .unwrap_or(0);
549 cached_count == current
550 })
551 .unwrap_or(false);
552
553 if !fts_fresh {
554 let sym_count: i64 = self
555 .conn
556 .query_row("SELECT COUNT(*) FROM symbols", [], |row| row.get(0))
557 .unwrap_or(0);
558 if sym_count > 0 {
559 let _ = self
560 .conn
561 .execute_batch("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild')");
562 let _ = self.conn.execute(
563 "INSERT OR REPLACE INTO meta (key, value) VALUES ('fts_symbol_count', ?1)",
564 params![sym_count.to_string()],
565 );
566 }
567 let _ = self.conn.execute(
568 "INSERT OR REPLACE INTO meta (key, value) VALUES ('fts_rebuild_ts', ?1)",
569 params![now_secs.to_string()],
570 );
571 }
572 }
573
574 let fts_query = fts5_escape(query);
576 let mut stmt = self.conn.prepare_cached(
577 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
578 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
579 f.relative_path, rank
580 FROM symbols_fts
581 JOIN symbols s ON symbols_fts.rowid = s.id
582 JOIN files f ON s.file_id = f.id
583 WHERE symbols_fts MATCH ?1
584 ORDER BY rank
585 LIMIT ?2",
586 )?;
587
588 let mut rows = stmt.query(params![fts_query, max_results as i64])?;
589 let mut results = Vec::new();
590 while let Some(row) = rows.next()? {
591 results.push((
592 SymbolRow {
593 id: row.get(0)?,
594 file_id: row.get(1)?,
595 name: row.get(2)?,
596 kind: row.get(3)?,
597 line: row.get(4)?,
598 column_num: row.get(5)?,
599 start_byte: row.get(6)?,
600 end_byte: row.get(7)?,
601 signature: row.get(8)?,
602 name_path: row.get(9)?,
603 parent_id: row.get(10)?,
604 },
605 row.get::<_, String>(11)?,
606 row.get::<_, f64>(12)?,
607 ));
608 }
609 Ok(results)
610 }
611
612 pub fn get_symbols_for_directory(&self, prefix: &str) -> Result<Vec<(String, Vec<SymbolRow>)>> {
615 let pattern = if prefix.is_empty() || prefix == "." {
616 "%".to_owned()
617 } else {
618 format!("{prefix}%")
619 };
620 let mut stmt = self.conn.prepare_cached(
621 "SELECT s.id, s.file_id, s.name, s.kind, s.line, s.column_num,
622 s.start_byte, s.end_byte, s.signature, s.name_path, s.parent_id,
623 f.relative_path
624 FROM symbols s
625 JOIN files f ON s.file_id = f.id
626 WHERE f.relative_path LIKE ?1
627 ORDER BY s.file_id, s.start_byte",
628 )?;
629 let rows = stmt.query_map(params![pattern], |row| {
630 Ok((
631 SymbolRow {
632 id: row.get(0)?,
633 file_id: row.get(1)?,
634 name: row.get(2)?,
635 kind: row.get(3)?,
636 line: row.get(4)?,
637 column_num: row.get(5)?,
638 start_byte: row.get(6)?,
639 end_byte: row.get(7)?,
640 signature: row.get(8)?,
641 name_path: row.get(9)?,
642 parent_id: row.get(10)?,
643 },
644 row.get::<_, String>(11)?,
645 ))
646 })?;
647
648 let mut groups: Vec<(String, Vec<SymbolRow>)> = Vec::new();
649 let mut current_path = String::new();
650 for row in rows {
651 let (sym, path) = row?;
652 if path != current_path {
653 current_path = path.clone();
654 groups.push((path, Vec::new()));
655 }
656 groups.last_mut().unwrap().1.push(sym);
657 }
658 Ok(groups)
659 }
660
661 #[allow(clippy::type_complexity)]
663 pub fn all_symbol_names(&self) -> Result<Vec<(String, String, String, i64, String, String)>> {
664 let mut stmt = self.conn.prepare_cached(
665 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path
666 FROM symbols s JOIN files f ON s.file_id = f.id",
667 )?;
668 let rows = stmt.query_map([], |row| {
669 Ok((
670 row.get::<_, String>(0)?,
671 row.get::<_, String>(1)?,
672 row.get::<_, String>(2)?,
673 row.get::<_, i64>(3)?,
674 row.get::<_, String>(4)?,
675 row.get::<_, String>(5)?,
676 ))
677 })?;
678 let mut results = Vec::new();
679 for row in rows {
680 results.push(row?);
681 }
682 Ok(results)
683 }
684
685 pub fn all_symbols_with_bytes(&self) -> Result<Vec<SymbolWithFile>> {
687 let mut stmt = self.conn.prepare_cached(
688 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
689 s.start_byte, s.end_byte
690 FROM symbols s JOIN files f ON s.file_id = f.id
691 ORDER BY s.file_id, s.start_byte",
692 )?;
693 let rows = stmt.query_map([], |row| {
694 Ok(SymbolWithFile {
695 name: row.get(0)?,
696 kind: row.get(1)?,
697 file_path: row.get(2)?,
698 line: row.get(3)?,
699 signature: row.get(4)?,
700 name_path: row.get(5)?,
701 start_byte: row.get(6)?,
702 end_byte: row.get(7)?,
703 })
704 })?;
705 let mut results = Vec::new();
706 for row in rows {
707 results.push(row?);
708 }
709 Ok(results)
710 }
711
712 pub fn for_each_symbol_with_bytes<F>(&self, mut callback: F) -> Result<usize>
715 where
716 F: FnMut(SymbolWithFile) -> Result<()>,
717 {
718 let mut stmt = self.conn.prepare_cached(
719 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
720 s.start_byte, s.end_byte
721 FROM symbols s JOIN files f ON s.file_id = f.id
722 ORDER BY s.file_id, s.start_byte",
723 )?;
724 let mut rows = stmt.query([])?;
725 let mut count = 0usize;
726 while let Some(row) = rows.next()? {
727 callback(SymbolWithFile {
728 name: row.get(0)?,
729 kind: row.get(1)?,
730 file_path: row.get(2)?,
731 line: row.get(3)?,
732 signature: row.get(4)?,
733 name_path: row.get(5)?,
734 start_byte: row.get(6)?,
735 end_byte: row.get(7)?,
736 })?;
737 count += 1;
738 }
739 Ok(count)
740 }
741
742 pub fn for_each_file_symbols_with_bytes<F>(&self, mut callback: F) -> Result<usize>
745 where
746 F: FnMut(String, Vec<SymbolWithFile>) -> Result<()>,
747 {
748 let mut stmt = self.conn.prepare_cached(
749 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
750 s.start_byte, s.end_byte
751 FROM symbols s JOIN files f ON s.file_id = f.id
752 ORDER BY f.relative_path, s.start_byte",
753 )?;
754 let mut rows = stmt.query([])?;
755 let mut count = 0usize;
756 let mut current_file: Option<String> = None;
757 let mut current_symbols: Vec<SymbolWithFile> = Vec::new();
758
759 while let Some(row) = rows.next()? {
760 let symbol = SymbolWithFile {
761 name: row.get(0)?,
762 kind: row.get(1)?,
763 file_path: row.get(2)?,
764 line: row.get(3)?,
765 signature: row.get(4)?,
766 name_path: row.get(5)?,
767 start_byte: row.get(6)?,
768 end_byte: row.get(7)?,
769 };
770
771 if current_file.as_deref() != Some(symbol.file_path.as_str())
772 && let Some(previous_file) = current_file.replace(symbol.file_path.clone())
773 {
774 callback(previous_file, std::mem::take(&mut current_symbols))?;
775 }
776
777 current_symbols.push(symbol);
778 count += 1;
779 }
780
781 if let Some(file_path) = current_file {
782 callback(file_path, current_symbols)?;
783 }
784
785 Ok(count)
786 }
787
788 pub fn symbols_for_files(&self, file_paths: &[&str]) -> Result<Vec<SymbolWithFile>> {
790 if file_paths.is_empty() {
791 return Ok(Vec::new());
792 }
793 let placeholders: Vec<String> = (1..=file_paths.len()).map(|i| format!("?{i}")).collect();
794 let sql = format!(
795 "SELECT s.name, s.kind, f.relative_path, s.line, s.signature, s.name_path,
796 s.start_byte, s.end_byte
797 FROM symbols s JOIN files f ON s.file_id = f.id
798 WHERE f.relative_path IN ({})
799 ORDER BY s.file_id, s.start_byte",
800 placeholders.join(", ")
801 );
802 let mut stmt = self.conn.prepare(&sql)?;
803 let params: Vec<&dyn rusqlite::types::ToSql> = file_paths
804 .iter()
805 .map(|p| p as &dyn rusqlite::types::ToSql)
806 .collect();
807 let rows = stmt.query_map(params.as_slice(), |row| {
808 Ok(SymbolWithFile {
809 name: row.get(0)?,
810 kind: row.get(1)?,
811 file_path: row.get(2)?,
812 line: row.get(3)?,
813 signature: row.get(4)?,
814 name_path: row.get(5)?,
815 start_byte: row.get(6)?,
816 end_byte: row.get(7)?,
817 })
818 })?;
819 let mut results = Vec::new();
820 for row in rows {
821 results.push(row?);
822 }
823 Ok(results)
824 }
825
826 pub fn get_file_path(&self, file_id: i64) -> Result<Option<String>> {
828 self.conn
829 .query_row(
830 "SELECT relative_path FROM files WHERE id = ?1",
831 params![file_id],
832 |row| row.get(0),
833 )
834 .optional()
835 .context("get_file_path query failed")
836 }
837
838 pub fn insert_imports(&self, file_id: i64, imports: &[NewImport]) -> Result<()> {
842 insert_imports(&self.conn, file_id, imports)
843 }
844
845 pub fn get_importers(&self, target_path: &str) -> Result<Vec<String>> {
847 let mut stmt = self.conn.prepare_cached(
848 "SELECT f.relative_path FROM imports i
849 JOIN files f ON i.source_file_id = f.id
850 WHERE i.target_path = ?1
851 ORDER BY f.relative_path",
852 )?;
853 let rows = stmt.query_map(params![target_path], |row| row.get(0))?;
854 let mut results = Vec::new();
855 for row in rows {
856 results.push(row?);
857 }
858 Ok(results)
859 }
860
861 pub fn get_imports_of(&self, relative_path: &str) -> Result<Vec<String>> {
863 let mut stmt = self.conn.prepare_cached(
864 "SELECT i.target_path FROM imports i
865 JOIN files f ON i.source_file_id = f.id
866 WHERE f.relative_path = ?1
867 ORDER BY i.target_path",
868 )?;
869 let rows = stmt.query_map(params![relative_path], |row| row.get(0))?;
870 let mut results = Vec::new();
871 for row in rows {
872 results.push(row?);
873 }
874 Ok(results)
875 }
876
877 #[allow(clippy::type_complexity)]
879 pub fn build_import_graph(
880 &self,
881 ) -> Result<std::collections::HashMap<String, (Vec<String>, Vec<String>)>> {
882 let mut graph = std::collections::HashMap::new();
883
884 for path in self.all_file_paths()? {
885 graph.insert(path, (Vec::new(), Vec::new()));
886 }
887
888 let mut stmt = self.conn.prepare_cached(
889 "SELECT f.relative_path, i.target_path FROM imports i
890 JOIN files f ON i.source_file_id = f.id",
891 )?;
892 let rows = stmt.query_map([], |row| {
893 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
894 })?;
895 for row in rows {
896 let (source, target) = row?;
897 if let Some(entry) = graph.get_mut(&source) {
898 entry.0.push(target.clone());
899 }
900 if let Some(entry) = graph.get_mut(&target) {
901 entry.1.push(source.clone());
902 }
903 }
904
905 Ok(graph)
906 }
907
908 pub fn insert_calls(&self, file_id: i64, calls: &[NewCall]) -> Result<()> {
912 insert_calls(&self.conn, file_id, calls)
913 }
914
915 pub fn get_callers_cached(
917 &self,
918 callee_name: &str,
919 max_results: usize,
920 ) -> Result<Vec<(String, String, i64)>> {
921 let mut stmt = self.conn.prepare_cached(
922 "SELECT f.relative_path, c.caller_name, c.line FROM calls c
923 JOIN files f ON c.caller_file_id = f.id
924 WHERE c.callee_name = ?1
925 ORDER BY f.relative_path, c.line
926 LIMIT ?2",
927 )?;
928 let mut rows = stmt.query(params![callee_name, max_results as i64])?;
929 let mut results = Vec::new();
930 while let Some(row) = rows.next()? {
931 results.push((row.get(0)?, row.get(1)?, row.get(2)?));
932 }
933 Ok(results)
934 }
935
936 pub fn get_callees_cached(
938 &self,
939 caller_name: &str,
940 file_path: Option<&str>,
941 max_results: usize,
942 ) -> Result<Vec<(String, i64)>> {
943 let (sql, use_file) = match file_path {
944 Some(_) => (
945 "SELECT c.callee_name, c.line FROM calls c
946 JOIN files f ON c.caller_file_id = f.id
947 WHERE c.caller_name = ?1 AND f.relative_path = ?2
948 ORDER BY c.line LIMIT ?3",
949 true,
950 ),
951 None => (
952 "SELECT c.callee_name, c.line FROM calls c
953 WHERE c.caller_name = ?1
954 ORDER BY c.line LIMIT ?2",
955 false,
956 ),
957 };
958 let mut stmt = self.conn.prepare_cached(sql)?;
959 let mut rows = if use_file {
960 stmt.query(params![
961 caller_name,
962 file_path.unwrap_or(""),
963 max_results as i64
964 ])?
965 } else {
966 stmt.query(params![caller_name, max_results as i64])?
967 };
968 let mut results = Vec::new();
969 while let Some(row) = rows.next()? {
970 results.push((row.get(0)?, row.get(1)?));
971 }
972 Ok(results)
973 }
974
975 pub fn has_call_data(&self) -> Result<bool> {
977 let count: i64 = self
978 .conn
979 .query_row("SELECT COUNT(*) FROM calls", [], |row| row.get(0))?;
980 Ok(count > 0)
981 }
982
983 pub fn record_index_failure(
987 &self,
988 file_path: &str,
989 error_type: &str,
990 error_message: &str,
991 ) -> Result<()> {
992 let now = std::time::SystemTime::now()
993 .duration_since(std::time::UNIX_EPOCH)
994 .unwrap_or_default()
995 .as_secs() as i64;
996 self.conn.execute(
997 "INSERT INTO index_failures (file_path, error_type, error_message, failed_at, retry_count)
998 VALUES (?1, ?2, ?3, ?4, 1)
999 ON CONFLICT(file_path) DO UPDATE SET
1000 error_type = excluded.error_type,
1001 error_message = excluded.error_message,
1002 failed_at = excluded.failed_at,
1003 retry_count = retry_count + 1",
1004 params![file_path, error_type, error_message, now],
1005 )?;
1006 Ok(())
1007 }
1008
1009 pub fn clear_index_failure(&self, file_path: &str) -> Result<()> {
1011 self.conn.execute(
1012 "DELETE FROM index_failures WHERE file_path = ?1",
1013 params![file_path],
1014 )?;
1015 Ok(())
1016 }
1017
1018 pub fn invalidate_fts(&self) -> Result<()> {
1020 self.conn
1021 .execute("DELETE FROM meta WHERE key = 'fts_symbol_count'", [])?;
1022 Ok(())
1023 }
1024
1025 pub fn index_failure_count(&self) -> Result<usize> {
1027 let count: i64 = self
1028 .conn
1029 .query_row("SELECT COUNT(*) FROM index_failures", [], |row| row.get(0))?;
1030 Ok(count as usize)
1031 }
1032
1033 pub fn prune_missing_index_failures(&self, project_root: &std::path::Path) -> Result<usize> {
1035 let mut stmt = self
1036 .conn
1037 .prepare_cached("SELECT file_path FROM index_failures ORDER BY file_path")?;
1038 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1039 let mut missing = Vec::new();
1040 for row in rows {
1041 let relative_path = row?;
1042 if !project_root.join(&relative_path).is_file() {
1043 missing.push(relative_path);
1044 }
1045 }
1046 for relative_path in &missing {
1047 self.clear_index_failure(relative_path)?;
1048 }
1049 Ok(missing.len())
1050 }
1051
1052 pub fn index_failure_summary(
1054 &self,
1055 recent_window_secs: i64,
1056 ) -> Result<crate::db::IndexFailureSummary> {
1057 let now = std::time::SystemTime::now()
1058 .duration_since(std::time::UNIX_EPOCH)
1059 .unwrap_or_default()
1060 .as_secs() as i64;
1061 let recent_cutoff = now.saturating_sub(recent_window_secs.max(0));
1062
1063 let total_failures: i64 =
1064 self.conn
1065 .query_row("SELECT COUNT(*) FROM index_failures", [], |row| row.get(0))?;
1066 let recent_failures: i64 = self.conn.query_row(
1067 "SELECT COUNT(*) FROM index_failures WHERE failed_at >= ?1",
1068 params![recent_cutoff],
1069 |row| row.get(0),
1070 )?;
1071 let persistent_failures: i64 = self.conn.query_row(
1072 "SELECT COUNT(*) FROM index_failures WHERE retry_count >= 3",
1073 [],
1074 |row| row.get(0),
1075 )?;
1076
1077 Ok(crate::db::IndexFailureSummary {
1078 total_failures: total_failures as usize,
1079 recent_failures: recent_failures as usize,
1080 stale_failures: total_failures.saturating_sub(recent_failures) as usize,
1081 persistent_failures: persistent_failures as usize,
1082 })
1083 }
1084
1085 pub fn get_persistent_failures(&self, min_retries: i64) -> Result<Vec<(String, String, i64)>> {
1087 let mut stmt = self.conn.prepare_cached(
1088 "SELECT file_path, error_message, retry_count FROM index_failures WHERE retry_count >= ?1 ORDER BY retry_count DESC",
1089 )?;
1090 let mut rows = stmt.query(params![min_retries])?;
1091 let mut results = Vec::new();
1092 while let Some(row) = rows.next()? {
1093 results.push((row.get(0)?, row.get(1)?, row.get(2)?));
1094 }
1095 Ok(results)
1096 }
1097}