1use std::path::Path;
2
3use anyhow::{Context, Result};
4use rusqlite::{params, Connection};
5use serde::{Deserialize, Serialize};
6
7use reposcry_graph::edge::EdgeKind;
8use reposcry_graph::symbol::{CallSite, Import, Symbol};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct CachedFile {
12 pub id: i64,
13 pub path: String,
14 pub language: String,
15 pub hash: String,
16 pub size_bytes: i64,
17 pub loc: i64,
18 pub last_indexed_at: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CachedImport {
23 pub id: i64,
24 pub file_id: i64,
25 pub source: String,
26 pub target: String,
27 pub is_relative: bool,
28 pub imported_names: Vec<String>,
29 pub line: u32,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct CachedEdge {
34 pub id: i64,
35 pub source_file_id: i64,
36 pub target_file_id: Option<i64>,
37 pub target_path: Option<String>,
38 pub kind: String,
39 pub confidence: f64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CachedCallSite {
44 pub id: i64,
45 pub file_id: i64,
46 pub caller: String,
47 pub callee: String,
48 pub line: u32,
49 pub confidence: f64,
50 pub resolution_strategy: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct CachedSymbolEdge {
55 pub id: i64,
56 pub source_symbol_id: i64,
57 pub target_symbol_id: i64,
58 pub source_file_id: i64,
59 pub target_file_id: i64,
60 pub kind: String,
61 pub line: u32,
62 pub confidence: f64,
63 pub resolution_strategy: Option<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct CachedSearchHit {
68 pub node_id: i64,
69 pub file_path: String,
70 pub kind: String,
71 pub name: String,
72 pub signature: Option<String>,
73 pub score: f64,
74 pub match_reason: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct CachedSearchVector {
79 pub node_id: i64,
80 pub file_path: String,
81 pub kind: String,
82 pub name: String,
83 pub signature: Option<String>,
84 pub backend: String,
85 pub dims: u32,
86 pub vector: Vec<f32>,
87}
88
89pub struct CacheDb {
90 conn: Connection,
91}
92
93impl CacheDb {
94 pub fn open(path: &Path) -> Result<Self> {
95 if let Some(parent) = path.parent() {
96 std::fs::create_dir_all(parent)?;
97 }
98 let conn = Connection::open(path).context("Failed to open cache database")?;
99 let db = Self { conn };
100 db.initialize()?;
101 Ok(db)
102 }
103
104 pub fn open_in_memory() -> Result<Self> {
105 let conn = Connection::open_in_memory()?;
106 let db = Self { conn };
107 db.initialize()?;
108 Ok(db)
109 }
110
111 fn initialize(&self) -> Result<()> {
112 self.conn.execute_batch(
113 "
114 PRAGMA foreign_keys = ON;
115 PRAGMA journal_mode = WAL;
116 PRAGMA synchronous = NORMAL;
117 PRAGMA cache_size = -64000;
118 PRAGMA temp_store = MEMORY;
119 PRAGMA busy_timeout = 5000;
120 CREATE TABLE IF NOT EXISTS files (
121 id INTEGER PRIMARY KEY,
122 path TEXT UNIQUE NOT NULL,
123 language TEXT NOT NULL DEFAULT '',
124 hash TEXT NOT NULL,
125 size_bytes INTEGER NOT NULL DEFAULT 0,
126 loc INTEGER NOT NULL DEFAULT 0,
127 last_indexed_at TEXT NOT NULL DEFAULT (datetime('now'))
128 );
129 CREATE TABLE IF NOT EXISTS symbols (
130 id INTEGER PRIMARY KEY,
131 file_id INTEGER NOT NULL,
132 name TEXT NOT NULL,
133 kind TEXT NOT NULL,
134 start_line INTEGER NOT NULL DEFAULT 0,
135 end_line INTEGER NOT NULL DEFAULT 0,
136 signature TEXT,
137 visibility TEXT,
138 doc_comment TEXT,
139 FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
140 );
141 CREATE TABLE IF NOT EXISTS imports (
142 id INTEGER PRIMARY KEY,
143 file_id INTEGER NOT NULL,
144 source TEXT NOT NULL,
145 target TEXT NOT NULL,
146 is_relative INTEGER NOT NULL DEFAULT 0,
147 imported_names TEXT NOT NULL DEFAULT '[]',
148 line INTEGER NOT NULL DEFAULT 0,
149 FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
150 );
151 CREATE TABLE IF NOT EXISTS edges (
152 id INTEGER PRIMARY KEY,
153 source_file_id INTEGER NOT NULL,
154 target_file_id INTEGER,
155 target_path TEXT,
156 kind TEXT NOT NULL,
157 confidence REAL NOT NULL DEFAULT 1.0,
158 FOREIGN KEY (source_file_id) REFERENCES files(id) ON DELETE CASCADE,
159 FOREIGN KEY (target_file_id) REFERENCES files(id) ON DELETE CASCADE
160 );
161 CREATE TABLE IF NOT EXISTS call_sites (
162 id INTEGER PRIMARY KEY,
163 file_id INTEGER NOT NULL,
164 caller TEXT NOT NULL,
165 callee TEXT NOT NULL,
166 line INTEGER NOT NULL DEFAULT 0,
167 confidence REAL NOT NULL DEFAULT 1.0,
168 resolution_strategy TEXT,
169 FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
170 );
171 CREATE TABLE IF NOT EXISTS symbol_edges (
172 id INTEGER PRIMARY KEY,
173 source_symbol_id INTEGER NOT NULL,
174 target_symbol_id INTEGER NOT NULL,
175 source_file_id INTEGER NOT NULL,
176 target_file_id INTEGER NOT NULL,
177 kind TEXT NOT NULL,
178 line INTEGER NOT NULL DEFAULT 0,
179 confidence REAL NOT NULL DEFAULT 1.0,
180 resolution_strategy TEXT,
181 FOREIGN KEY (source_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE,
182 FOREIGN KEY (target_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE,
183 FOREIGN KEY (source_file_id) REFERENCES files(id) ON DELETE CASCADE,
184 FOREIGN KEY (target_file_id) REFERENCES files(id) ON DELETE CASCADE
185 );
186 CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
187 node_id UNINDEXED,
188 file_path,
189 kind,
190 name,
191 signature,
192 doc_comment,
193 imports,
194 content
195 );
196 CREATE TABLE IF NOT EXISTS search_vectors (
197 node_id INTEGER NOT NULL,
198 file_path TEXT NOT NULL,
199 kind TEXT NOT NULL,
200 name TEXT NOT NULL,
201 signature TEXT,
202 backend TEXT NOT NULL,
203 dims INTEGER NOT NULL,
204 vector BLOB NOT NULL,
205 PRIMARY KEY (node_id, backend)
206 );
207 CREATE TABLE IF NOT EXISTS git_changes (
208 id INTEGER PRIMARY KEY,
209 path TEXT NOT NULL,
210 status TEXT NOT NULL DEFAULT 'modified',
211 lines_added INTEGER NOT NULL DEFAULT 0,
212 lines_deleted INTEGER NOT NULL DEFAULT 0,
213 recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
214 );
215 CREATE TABLE IF NOT EXISTS config (
216 key TEXT PRIMARY KEY,
217 value TEXT NOT NULL DEFAULT ''
218 );
219 CREATE INDEX IF NOT EXISTS idx_symbols_file_id ON symbols(file_id);
220 CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
221 CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
222 CREATE INDEX IF NOT EXISTS idx_imports_file_id ON imports(file_id);
223 CREATE INDEX IF NOT EXISTS idx_imports_target ON imports(target);
224 CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind);
225 CREATE INDEX IF NOT EXISTS idx_edges_source_file_id ON edges(source_file_id);
226 CREATE INDEX IF NOT EXISTS idx_edges_target_file_id ON edges(target_file_id);
227 CREATE INDEX IF NOT EXISTS idx_call_sites_file_id ON call_sites(file_id);
228 CREATE INDEX IF NOT EXISTS idx_call_sites_callee ON call_sites(callee);
229 CREATE INDEX IF NOT EXISTS idx_symbol_edges_kind ON symbol_edges(kind);
230 CREATE INDEX IF NOT EXISTS idx_symbol_edges_source_file_id ON symbol_edges(source_file_id);
231 CREATE INDEX IF NOT EXISTS idx_symbol_edges_target_file_id ON symbol_edges(target_file_id);
232 CREATE INDEX IF NOT EXISTS idx_search_vectors_backend_kind ON search_vectors(backend, kind);
233 CREATE INDEX IF NOT EXISTS idx_git_changes_path ON git_changes(path);
234 ",
235 )?;
236 self.migrate_imports_table()?;
237 Ok(())
238 }
239
240 fn migrate_imports_table(&self) -> Result<()> {
241 let has_imported_names = {
242 let mut stmt = self.conn.prepare("PRAGMA table_info(imports)")?;
243 let columns = stmt.query_map([], |row| row.get::<_, String>(1))?;
244 let mut has_imported_names = false;
245 for col in columns {
246 if col? == "imported_names" {
247 has_imported_names = true;
248 break;
249 }
250 }
251 has_imported_names
252 };
253 if !has_imported_names {
254 self.conn.execute(
255 "ALTER TABLE imports ADD COLUMN imported_names TEXT NOT NULL DEFAULT '[]'",
256 [],
257 )?;
258 }
259 Ok(())
260 }
261
262 pub fn get_file_by_path(&self, path: &str) -> Result<Option<CachedFile>> {
263 let mut stmt = self.conn.prepare(
264 "SELECT id, path, language, hash, size_bytes, loc, last_indexed_at \
265 FROM files WHERE path = ?1",
266 )?;
267 let mut rows = stmt.query_map(params![path], |row| {
268 Ok(CachedFile {
269 id: row.get(0)?,
270 path: row.get(1)?,
271 language: row.get(2)?,
272 hash: row.get(3)?,
273 size_bytes: row.get(4)?,
274 loc: row.get(5)?,
275 last_indexed_at: row.get(6)?,
276 })
277 })?;
278 match rows.next() {
279 Some(Ok(file)) => Ok(Some(file)),
280 Some(Err(e)) => Err(e.into()),
281 None => Ok(None),
282 }
283 }
284
285 pub fn upsert_file(
286 &self,
287 path: &str,
288 language: &str,
289 hash: &str,
290 size_bytes: i64,
291 loc: i64,
292 ) -> Result<i64> {
293 self.conn.execute(
294 "INSERT INTO files (path, language, hash, size_bytes, loc, last_indexed_at) \
295 VALUES (?1, ?2, ?3, ?4, ?5, datetime('now')) \
296 ON CONFLICT(path) DO UPDATE SET \
297 language = excluded.language, \
298 hash = excluded.hash, \
299 size_bytes = excluded.size_bytes, \
300 loc = excluded.loc, \
301 last_indexed_at = datetime('now')",
302 params![path, language, hash, size_bytes, loc],
303 )?;
304 self.get_file_by_path(path)?
305 .map(|file| file.id)
306 .ok_or_else(|| anyhow::anyhow!("file not found after upsert: {}", path))
307 }
308
309 pub fn delete_file(&self, path: &str) -> Result<()> {
310 if let Some(file) = self.get_file_by_path(path)? {
311 self.conn
312 .execute("DELETE FROM files WHERE id = ?1", params![file.id])?;
313 }
314 Ok(())
315 }
316
317 pub fn insert_symbols(&self, file_id: i64, symbols: &[Symbol]) -> Result<()> {
318 let tx = self.conn.unchecked_transaction()?;
319 tx.execute("DELETE FROM symbols WHERE file_id = ?1", params![file_id])?;
320 for sym in symbols {
321 tx.execute(
322 "INSERT INTO symbols (file_id, name, kind, start_line, end_line, signature, visibility, doc_comment) \
323 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
324 params![
325 file_id,
326 sym.name,
327 sym.kind,
328 sym.start_line,
329 sym.end_line,
330 sym.signature,
331 sym.visibility,
332 sym.doc_comment,
333 ],
334 )?;
335 }
336 tx.commit()?;
337 Ok(())
338 }
339
340 pub fn insert_imports(&self, file_id: i64, imports: &[Import]) -> Result<()> {
341 let tx = self.conn.unchecked_transaction()?;
342 tx.execute("DELETE FROM imports WHERE file_id = ?1", params![file_id])?;
343 for import in imports {
344 let imported_names = serde_json::to_string(&import.imported_names)?;
345 tx.execute(
346 "INSERT INTO imports (file_id, source, target, is_relative, imported_names, line) \
347 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
348 params![
349 file_id,
350 import.source,
351 import.target,
352 if import.is_relative { 1 } else { 0 },
353 imported_names,
354 import.line,
355 ],
356 )?;
357 }
358 tx.commit()?;
359 Ok(())
360 }
361
362 pub fn insert_call_sites(&self, file_id: i64, call_sites: &[CallSite]) -> Result<()> {
363 let tx = self.conn.unchecked_transaction()?;
364 tx.execute(
365 "DELETE FROM call_sites WHERE file_id = ?1",
366 params![file_id],
367 )?;
368 for call_site in call_sites {
369 tx.execute(
370 "INSERT INTO call_sites (file_id, caller, callee, line, confidence, resolution_strategy) \
371 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
372 params![
373 file_id,
374 call_site.caller,
375 call_site.callee,
376 call_site.line,
377 call_site.confidence,
378 call_site.resolution_strategy,
379 ],
380 )?;
381 }
382 tx.commit()?;
383 Ok(())
384 }
385
386 pub fn get_symbols_by_file(&self, file_id: i64) -> Result<Vec<Symbol>> {
387 let mut stmt = self.conn.prepare(
388 "SELECT s.id, s.name, s.kind, s.start_line, s.end_line, s.signature, s.visibility, s.doc_comment, f.path \
389 FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.file_id = ?1 \
390 ORDER BY s.start_line ASC, s.name ASC",
391 )?;
392 let rows = stmt.query_map(params![file_id], |row| {
393 Ok(Symbol {
394 id: row.get(0)?,
395 file_path: row.get(8)?,
396 name: row.get(1)?,
397 kind: row.get(2)?,
398 start_line: row.get(3)?,
399 end_line: row.get(4)?,
400 signature: row.get(5)?,
401 visibility: row.get(6)?,
402 doc_comment: row.get(7)?,
403 })
404 })?;
405 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
406 }
407
408 pub fn get_imports_by_file(&self, file_id: i64) -> Result<Vec<CachedImport>> {
409 let mut stmt = self.conn.prepare(
410 "SELECT id, file_id, source, target, is_relative, imported_names, line \
411 FROM imports WHERE file_id = ?1 \
412 ORDER BY line ASC, target ASC",
413 )?;
414 let rows = stmt.query_map(params![file_id], |row| {
415 let imported_names_json: String = row.get(5)?;
416 let imported_names = serde_json::from_str(&imported_names_json).unwrap_or_default();
417 Ok(CachedImport {
418 id: row.get(0)?,
419 file_id: row.get(1)?,
420 source: row.get(2)?,
421 target: row.get(3)?,
422 is_relative: row.get::<_, i64>(4)? != 0,
423 imported_names,
424 line: row.get::<_, i64>(6)? as u32,
425 })
426 })?;
427 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
428 }
429
430 pub fn get_all_imports(&self) -> Result<Vec<CachedImport>> {
431 let mut stmt = self.conn.prepare(
432 "SELECT id, file_id, source, target, is_relative, imported_names, line \
433 FROM imports \
434 ORDER BY file_id ASC, line ASC, target ASC",
435 )?;
436 let rows = stmt.query_map([], |row| {
437 let imported_names_json: String = row.get(5)?;
438 let imported_names = serde_json::from_str(&imported_names_json).unwrap_or_default();
439 Ok(CachedImport {
440 id: row.get(0)?,
441 file_id: row.get(1)?,
442 source: row.get(2)?,
443 target: row.get(3)?,
444 is_relative: row.get::<_, i64>(4)? != 0,
445 imported_names,
446 line: row.get::<_, i64>(6)? as u32,
447 })
448 })?;
449 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
450 }
451
452 pub fn get_call_sites_by_file(&self, file_id: i64) -> Result<Vec<CachedCallSite>> {
453 let mut stmt = self.conn.prepare(
454 "SELECT id, file_id, caller, callee, line, confidence, resolution_strategy \
455 FROM call_sites WHERE file_id = ?1 \
456 ORDER BY line ASC, callee ASC",
457 )?;
458 let rows = stmt.query_map(params![file_id], |row| {
459 Ok(CachedCallSite {
460 id: row.get(0)?,
461 file_id: row.get(1)?,
462 caller: row.get(2)?,
463 callee: row.get(3)?,
464 line: row.get::<_, i64>(4)? as u32,
465 confidence: row.get(5)?,
466 resolution_strategy: row.get(6)?,
467 })
468 })?;
469 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
470 }
471
472 pub fn get_all_call_sites(&self) -> Result<Vec<CachedCallSite>> {
473 let mut stmt = self.conn.prepare(
474 "SELECT id, file_id, caller, callee, line, confidence, resolution_strategy \
475 FROM call_sites \
476 ORDER BY file_id ASC, line ASC, callee ASC",
477 )?;
478 let rows = stmt.query_map([], |row| {
479 Ok(CachedCallSite {
480 id: row.get(0)?,
481 file_id: row.get(1)?,
482 caller: row.get(2)?,
483 callee: row.get(3)?,
484 line: row.get::<_, i64>(4)? as u32,
485 confidence: row.get(5)?,
486 resolution_strategy: row.get(6)?,
487 })
488 })?;
489 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
490 }
491
492 pub fn clear_edges_by_kind(&self, kind: EdgeKind) -> Result<()> {
493 self.conn
494 .execute("DELETE FROM edges WHERE kind = ?1", params![kind.as_str()])?;
495 Ok(())
496 }
497
498 pub fn clear_symbol_edges_by_kind(&self, kind: &str) -> Result<()> {
499 self.conn
500 .execute("DELETE FROM symbol_edges WHERE kind = ?1", params![kind])?;
501 Ok(())
502 }
503
504 pub fn delete_edges_by_source(&self, source_file_id: i64, kind: EdgeKind) -> Result<()> {
505 self.conn.execute(
506 "DELETE FROM edges WHERE source_file_id = ?1 AND kind = ?2",
507 params![source_file_id, kind.as_str()],
508 )?;
509 Ok(())
510 }
511
512 pub fn delete_symbol_edges_by_source(&self, source_file_id: i64, kind: &str) -> Result<()> {
513 self.conn.execute(
514 "DELETE FROM symbol_edges WHERE source_file_id = ?1 AND kind = ?2",
515 params![source_file_id, kind],
516 )?;
517 Ok(())
518 }
519
520 pub fn clear_search_index(&self) -> Result<()> {
521 self.conn.execute("DELETE FROM search_index", [])?;
522 Ok(())
523 }
524
525 pub fn clear_search_vectors(&self, backend: Option<&str>) -> Result<()> {
526 match backend {
527 Some(backend) => {
528 self.conn.execute(
529 "DELETE FROM search_vectors WHERE backend = ?1",
530 params![backend],
531 )?;
532 }
533 None => {
534 self.conn.execute("DELETE FROM search_vectors", [])?;
535 }
536 }
537 Ok(())
538 }
539
540 pub fn has_search_vector(&self, node_id: i64, backend: &str) -> Result<bool> {
541 let mut stmt = self.conn.prepare(
542 "SELECT 1 FROM search_vectors WHERE node_id = ?1 AND backend = ?2 LIMIT 1",
543 )?;
544 let mut rows = stmt.query_map(params![node_id, backend], |row| row.get::<_, i64>(0))?;
545 match rows.next() {
546 Some(Ok(_)) => Ok(true),
547 Some(Err(error)) => Err(error.into()),
548 None => Ok(false),
549 }
550 }
551
552 pub fn prune_search_vectors_to_index(&self, backend: &str) -> Result<()> {
553 self.conn.execute(
554 "DELETE FROM search_vectors \
555 WHERE backend = ?1 \
556 AND node_id NOT IN (SELECT CAST(node_id AS INTEGER) FROM search_index)",
557 params![backend],
558 )?;
559 Ok(())
560 }
561
562 pub fn insert_edge(
563 &self,
564 source_file_id: i64,
565 target_file_id: Option<i64>,
566 target_path: Option<&str>,
567 kind: EdgeKind,
568 confidence: f64,
569 ) -> Result<()> {
570 self.conn.execute(
571 "INSERT INTO edges (source_file_id, target_file_id, target_path, kind, confidence) \
572 VALUES (?1, ?2, ?3, ?4, ?5)",
573 params![
574 source_file_id,
575 target_file_id,
576 target_path,
577 kind.as_str(),
578 confidence,
579 ],
580 )?;
581 Ok(())
582 }
583
584 pub fn get_edges_by_kind(&self, kind: EdgeKind) -> Result<Vec<CachedEdge>> {
585 let mut stmt = self.conn.prepare(
586 "SELECT id, source_file_id, target_file_id, target_path, kind, confidence \
587 FROM edges WHERE kind = ?1 \
588 ORDER BY source_file_id ASC, target_file_id ASC, target_path ASC",
589 )?;
590 let rows = stmt.query_map(params![kind.as_str()], |row| {
591 Ok(CachedEdge {
592 id: row.get(0)?,
593 source_file_id: row.get(1)?,
594 target_file_id: row.get(2)?,
595 target_path: row.get(3)?,
596 kind: row.get(4)?,
597 confidence: row.get(5)?,
598 })
599 })?;
600 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
601 }
602
603 pub fn insert_symbol_edges(&self, edges: &[CachedSymbolEdge]) -> Result<()> {
604 if edges.is_empty() {
605 return Ok(());
606 }
607 let tx = self.conn.unchecked_transaction()?;
608 for edge in edges {
609 tx.execute(
610 "INSERT INTO symbol_edges (source_symbol_id, target_symbol_id, source_file_id, target_file_id, kind, line, confidence, resolution_strategy) \
611 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
612 params![
613 edge.source_symbol_id,
614 edge.target_symbol_id,
615 edge.source_file_id,
616 edge.target_file_id,
617 edge.kind,
618 edge.line,
619 edge.confidence,
620 edge.resolution_strategy,
621 ],
622 )?;
623 }
624 tx.commit()?;
625 Ok(())
626 }
627
628 pub fn get_symbol_edges_by_kind(&self, kind: &str) -> Result<Vec<CachedSymbolEdge>> {
629 let mut stmt = self.conn.prepare(
630 "SELECT id, source_symbol_id, target_symbol_id, source_file_id, target_file_id, kind, line, confidence, resolution_strategy \
631 FROM symbol_edges WHERE kind = ?1 \
632 ORDER BY source_symbol_id ASC, target_symbol_id ASC, line ASC",
633 )?;
634 let rows = stmt.query_map(params![kind], |row| {
635 Ok(CachedSymbolEdge {
636 id: row.get(0)?,
637 source_symbol_id: row.get(1)?,
638 target_symbol_id: row.get(2)?,
639 source_file_id: row.get(3)?,
640 target_file_id: row.get(4)?,
641 kind: row.get(5)?,
642 line: row.get::<_, i64>(6)? as u32,
643 confidence: row.get(7)?,
644 resolution_strategy: row.get(8)?,
645 })
646 })?;
647 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
648 }
649
650 pub fn insert_search_document(
651 &self,
652 node_id: i64,
653 file_path: &str,
654 kind: &str,
655 name: &str,
656 signature: Option<&str>,
657 doc_comment: Option<&str>,
658 imports: &str,
659 content: &str,
660 ) -> Result<()> {
661 self.conn.execute(
662 "INSERT INTO search_index (node_id, file_path, kind, name, signature, doc_comment, imports, content) \
663 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
664 params![
665 node_id,
666 file_path,
667 kind,
668 name,
669 signature,
670 doc_comment,
671 imports,
672 content,
673 ],
674 )?;
675 Ok(())
676 }
677
678 pub fn insert_search_vector(
679 &self,
680 node_id: i64,
681 file_path: &str,
682 kind: &str,
683 name: &str,
684 signature: Option<&str>,
685 backend: &str,
686 vector: &[f32],
687 ) -> Result<()> {
688 let mut bytes = Vec::with_capacity(vector.len() * std::mem::size_of::<f32>());
689 for value in vector {
690 bytes.extend_from_slice(&value.to_le_bytes());
691 }
692 self.conn.execute(
693 "INSERT OR REPLACE INTO search_vectors \
694 (node_id, file_path, kind, name, signature, backend, dims, vector) \
695 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
696 params![
697 node_id,
698 file_path,
699 kind,
700 name,
701 signature,
702 backend,
703 i64::try_from(vector.len()).unwrap_or(0),
704 bytes,
705 ],
706 )?;
707 Ok(())
708 }
709
710 pub fn search_nodes_fts(
711 &self,
712 query: &str,
713 kind: Option<&str>,
714 limit: usize,
715 ) -> Result<Vec<CachedSearchHit>> {
716 let limit = i64::try_from(limit).unwrap_or(50);
717 let hits = if let Some(kind) = kind {
718 let mut stmt = self.conn.prepare(
719 "SELECT node_id, file_path, kind, name, signature, bm25(search_index) \
720 FROM search_index \
721 WHERE search_index MATCH ?1 AND kind = ?2 \
722 ORDER BY bm25(search_index) \
723 LIMIT ?3",
724 )?;
725 let rows = stmt.query_map(params![query, kind, limit], |row| {
726 let score: f64 = row.get(5)?;
727 Ok(CachedSearchHit {
728 node_id: row.get(0)?,
729 file_path: row.get(1)?,
730 kind: row.get(2)?,
731 name: row.get(3)?,
732 signature: row.get(4)?,
733 score: -score,
734 match_reason: "fts5".to_string(),
735 })
736 })?;
737 rows.collect::<std::result::Result<Vec<_>, _>>()?
738 } else {
739 let mut stmt = self.conn.prepare(
740 "SELECT node_id, file_path, kind, name, signature, bm25(search_index) \
741 FROM search_index \
742 WHERE search_index MATCH ?1 \
743 ORDER BY bm25(search_index) \
744 LIMIT ?2",
745 )?;
746 let rows = stmt.query_map(params![query, limit], |row| {
747 let score: f64 = row.get(5)?;
748 Ok(CachedSearchHit {
749 node_id: row.get(0)?,
750 file_path: row.get(1)?,
751 kind: row.get(2)?,
752 name: row.get(3)?,
753 signature: row.get(4)?,
754 score: -score,
755 match_reason: "fts5".to_string(),
756 })
757 })?;
758 rows.collect::<std::result::Result<Vec<_>, _>>()?
759 };
760 Ok(hits)
761 }
762
763 pub fn get_search_vectors(
764 &self,
765 backend: &str,
766 kind: Option<&str>,
767 ) -> Result<Vec<CachedSearchVector>> {
768 let query = match kind {
769 Some(_) => {
770 "SELECT node_id, file_path, kind, name, signature, backend, dims, vector \
771 FROM search_vectors WHERE backend = ?1 AND kind = ?2"
772 }
773 None => {
774 "SELECT node_id, file_path, kind, name, signature, backend, dims, vector \
775 FROM search_vectors WHERE backend = ?1"
776 }
777 };
778 let mut stmt = self.conn.prepare(query)?;
779 let map_row = |row: &rusqlite::Row<'_>| {
780 let blob: Vec<u8> = row.get(7)?;
781 let mut vector = Vec::with_capacity(blob.len() / 4);
782 for chunk in blob.chunks_exact(4) {
783 vector.push(f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
784 }
785 Ok(CachedSearchVector {
786 node_id: row.get(0)?,
787 file_path: row.get(1)?,
788 kind: row.get(2)?,
789 name: row.get(3)?,
790 signature: row.get(4)?,
791 backend: row.get(5)?,
792 dims: row.get::<_, i64>(6)? as u32,
793 vector,
794 })
795 };
796 let rows = match kind {
797 Some(kind) => stmt.query_map(params![backend, kind], map_row)?,
798 None => stmt.query_map(params![backend], map_row)?,
799 };
800 Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
801 }
802
803 pub fn set_config(&self, key: &str, value: &str) -> Result<()> {
804 self.conn.execute(
805 "INSERT INTO config (key, value) VALUES (?1, ?2) \
806 ON CONFLICT(key) DO UPDATE SET value = excluded.value",
807 params![key, value],
808 )?;
809 Ok(())
810 }
811
812 pub fn get_config(&self, key: &str) -> Result<Option<String>> {
813 let mut stmt = self
814 .conn
815 .prepare("SELECT value FROM config WHERE key = ?1")?;
816 let mut rows = stmt.query_map(params![key], |row| row.get(0))?;
817 match rows.next() {
818 Some(Ok(val)) => Ok(Some(val)),
819 Some(Err(e)) => Err(e.into()),
820 None => Ok(None),
821 }
822 }
823
824 pub fn file_count(&self) -> Result<i64> {
825 let count: i64 = self
826 .conn
827 .query_row("SELECT COUNT(*) FROM files", [], |row| row.get(0))?;
828 Ok(count)
829 }
830
831 pub fn symbol_count(&self) -> Result<i64> {
832 let count: i64 = self
833 .conn
834 .query_row("SELECT COUNT(*) FROM symbols", [], |row| row.get(0))?;
835 Ok(count)
836 }
837
838 pub fn import_count(&self) -> Result<i64> {
839 let count: i64 = self
840 .conn
841 .query_row("SELECT COUNT(*) FROM imports", [], |row| row.get(0))?;
842 Ok(count)
843 }
844
845 pub fn call_site_count(&self) -> Result<i64> {
846 let count: i64 = self
847 .conn
848 .query_row("SELECT COUNT(*) FROM call_sites", [], |row| row.get(0))?;
849 Ok(count)
850 }
851
852 pub fn symbol_edge_count(&self) -> Result<i64> {
853 let count: i64 = self
854 .conn
855 .query_row("SELECT COUNT(*) FROM symbol_edges", [], |row| row.get(0))?;
856 Ok(count)
857 }
858
859 pub fn edge_count(&self) -> Result<i64> {
860 let count: i64 = self
861 .conn
862 .query_row("SELECT COUNT(*) FROM edges", [], |row| row.get(0))?;
863 Ok(count)
864 }
865
866 pub fn get_all_files(&self) -> Result<Vec<CachedFile>> {
867 let mut stmt = self.conn.prepare(
868 "SELECT id, path, language, hash, size_bytes, loc, last_indexed_at \
869 FROM files \
870 ORDER BY path ASC",
871 )?;
872 let rows = stmt.query_map([], |row| {
873 Ok(CachedFile {
874 id: row.get(0)?,
875 path: row.get(1)?,
876 language: row.get(2)?,
877 hash: row.get(3)?,
878 size_bytes: row.get(4)?,
879 loc: row.get(5)?,
880 last_indexed_at: row.get(6)?,
881 })
882 })?;
883 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
884 }
885
886 pub fn language_stats(&self) -> Result<Vec<(String, i64)>> {
887 let mut stmt = self.conn.prepare(
888 "SELECT language, COUNT(*) as cnt \
889 FROM files \
890 WHERE language != '' \
891 GROUP BY language \
892 ORDER BY cnt DESC",
893 )?;
894 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
895 rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
896 }
897}