1use std::path::Path;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use domain::error::Result;
5use domain::model::*;
6use domain::ports::GraphStore;
7
8use crate::mapping::*;
9use crate::SqliteStore;
10
11impl SqliteStore {
12 fn query_symbols(
13 &self,
14 stmt: &mut rusqlite::CachedStatement<'_>,
15 params: impl rusqlite::Params,
16 ) -> Result<Vec<SymbolNode>> {
17 let rows = stmt
18 .query_map(params, |row| {
19 Ok((
20 row.get::<_, String>(0)?,
21 row.get::<_, String>(1)?,
22 row.get::<_, String>(2)?,
23 row.get::<_, String>(3)?,
24 row.get::<_, i64>(4)?,
25 row.get::<_, i64>(5)?,
26 row.get::<_, i64>(6)?,
27 row.get::<_, i64>(7)?,
28 row.get::<_, String>(8)?,
29 row.get::<_, i32>(9)?,
30 row.get::<_, i32>(10)?,
31 row.get::<_, i32>(11)?,
32 row.get::<_, Option<String>>(12)?,
33 row.get::<_, Option<String>>(13)?,
34 ))
35 })
36 .map_err(map_rusqlite_error)?;
37 let mut symbols = Vec::new();
38 for row in rows {
39 let (qn, name, kind, file, ls, le, cs, ce, vis, exp, asy, tst, dec, sig) =
40 row.map_err(map_rusqlite_error)?;
41 let decorators: Vec<String> = match dec {
42 Some(ref s) => serde_json::from_str(s)
43 .map_err(|e| domain::error::CodeGraphError::Storage(e.to_string()))?,
44 None => vec![],
45 };
46 symbols.push(SymbolNode {
47 qualified_name: qn,
48 name,
49 kind: symbol_kind_from_str(&kind)?,
50 location: Location {
51 file: file.into(),
52 line_start: ls as usize,
53 line_end: le as usize,
54 col_start: cs as usize,
55 col_end: ce as usize,
56 },
57 visibility: visibility_from_str(&vis)?,
58 is_exported: exp != 0,
59 is_async: asy != 0,
60 is_test: tst != 0,
61 decorators,
62 signature: sig,
63 });
64 }
65 Ok(symbols)
66 }
67}
68
69fn now_epoch() -> i64 {
70 SystemTime::now()
71 .duration_since(UNIX_EPOCH)
72 .unwrap_or_default()
73 .as_secs() as i64
74}
75
76impl GraphStore for SqliteStore {
77 fn upsert_file(&self, file: &FileNode) -> Result<()> {
78 let conn = self.conn()?;
79 conn.prepare_cached(
80 "INSERT OR REPLACE INTO files (path, language, hash, updated_at) VALUES (?1, ?2, ?3, ?4)",
81 )
82 .map_err(map_rusqlite_error)?
83 .execute(rusqlite::params![
84 file.path.to_str().unwrap_or_default(),
85 language_to_str(&file.language),
86 &file.hash,
87 now_epoch(),
88 ])
89 .map_err(map_rusqlite_error)?;
90 Ok(())
91 }
92
93 fn upsert_symbol(&self, symbol: &SymbolNode) -> Result<()> {
94 let conn = self.conn()?;
95 let decorators_json = serde_json::to_string(&symbol.decorators)
96 .map_err(|e| domain::error::CodeGraphError::Storage(e.to_string()))?;
97 conn.prepare_cached(
98 "INSERT OR REPLACE INTO symbols (
99 qualified_name, name, kind, file_path,
100 line_start, line_end, col_start, col_end,
101 visibility, is_exported, is_async, is_test,
102 decorators, signature, updated_at
103 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
104 )
105 .map_err(map_rusqlite_error)?
106 .execute(rusqlite::params![
107 &symbol.qualified_name,
108 &symbol.name,
109 symbol_kind_to_str(&symbol.kind),
110 symbol.location.file.to_str().unwrap_or_default(),
111 symbol.location.line_start as i64,
112 symbol.location.line_end as i64,
113 symbol.location.col_start as i64,
114 symbol.location.col_end as i64,
115 visibility_to_str(&symbol.visibility),
116 symbol.is_exported as i32,
117 symbol.is_async as i32,
118 symbol.is_test as i32,
119 &decorators_json,
120 &symbol.signature,
121 now_epoch(),
122 ])
123 .map_err(map_rusqlite_error)?;
124 Ok(())
125 }
126
127 fn upsert_edge(&self, edge: &Edge) -> Result<()> {
128 let conn = self.conn()?;
129 conn.prepare_cached(
130 "INSERT OR REPLACE INTO edges (kind, source_qualified, target_qualified, metadata)
131 VALUES (?1, ?2, ?3, ?4)",
132 )
133 .map_err(map_rusqlite_error)?
134 .execute(rusqlite::params![
135 edge_kind_to_str(&edge.kind),
136 &edge.source,
137 &edge.target,
138 &edge.metadata,
139 ])
140 .map_err(map_rusqlite_error)?;
141 Ok(())
142 }
143
144 fn get_file(&self, path: &Path) -> Result<Option<FileNode>> {
145 let conn = self.conn()?;
146 let mut stmt = conn
147 .prepare_cached("SELECT path, language, hash FROM files WHERE path = ?1")
148 .map_err(map_rusqlite_error)?;
149 let result = stmt.query_row(
150 rusqlite::params![path.to_str().unwrap_or_default()],
151 |row| {
152 Ok((
153 row.get::<_, String>(0)?,
154 row.get::<_, String>(1)?,
155 row.get::<_, String>(2)?,
156 ))
157 },
158 );
159 match result {
160 Ok((p, lang, hash)) => Ok(Some(FileNode {
161 path: p.into(),
162 language: language_from_str(&lang)?,
163 hash,
164 })),
165 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
166 Err(e) => Err(map_rusqlite_error(e)),
167 }
168 }
169
170 fn get_symbol(&self, qualified_name: &str) -> Result<Option<SymbolNode>> {
171 let conn = self.conn()?;
172 let mut stmt = conn
173 .prepare_cached(
174 "SELECT qualified_name, name, kind, file_path,
175 line_start, line_end, col_start, col_end,
176 visibility, is_exported, is_async, is_test,
177 decorators, signature
178 FROM symbols WHERE qualified_name = ?1",
179 )
180 .map_err(map_rusqlite_error)?;
181 let result = stmt.query_row(rusqlite::params![qualified_name], |row| {
182 Ok((
183 row.get::<_, String>(0)?,
184 row.get::<_, String>(1)?,
185 row.get::<_, String>(2)?,
186 row.get::<_, String>(3)?,
187 row.get::<_, i64>(4)?,
188 row.get::<_, i64>(5)?,
189 row.get::<_, i64>(6)?,
190 row.get::<_, i64>(7)?,
191 row.get::<_, String>(8)?,
192 row.get::<_, i32>(9)?,
193 row.get::<_, i32>(10)?,
194 row.get::<_, i32>(11)?,
195 row.get::<_, Option<String>>(12)?,
196 row.get::<_, Option<String>>(13)?,
197 ))
198 });
199 match result {
200 Ok((qn, name, kind, file, ls, le, cs, ce, vis, exp, asy, tst, dec, sig)) => {
201 let decorators: Vec<String> = match dec {
202 Some(ref s) => serde_json::from_str(s)
203 .map_err(|e| domain::error::CodeGraphError::Storage(e.to_string()))?,
204 None => vec![],
205 };
206 Ok(Some(SymbolNode {
207 qualified_name: qn,
208 name,
209 kind: symbol_kind_from_str(&kind)?,
210 location: Location {
211 file: file.into(),
212 line_start: ls as usize,
213 line_end: le as usize,
214 col_start: cs as usize,
215 col_end: ce as usize,
216 },
217 visibility: visibility_from_str(&vis)?,
218 is_exported: exp != 0,
219 is_async: asy != 0,
220 is_test: tst != 0,
221 decorators,
222 signature: sig,
223 }))
224 }
225 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
226 Err(e) => Err(map_rusqlite_error(e)),
227 }
228 }
229
230 fn get_edges_from(&self, source: &str) -> Result<Vec<Edge>> {
231 let conn = self.conn()?;
232 let mut stmt = conn
233 .prepare_cached(
234 "SELECT kind, source_qualified, target_qualified, metadata
235 FROM edges WHERE source_qualified = ?1",
236 )
237 .map_err(map_rusqlite_error)?;
238 let rows = stmt
239 .query_map(rusqlite::params![source], |row| {
240 Ok((
241 row.get::<_, String>(0)?,
242 row.get::<_, String>(1)?,
243 row.get::<_, String>(2)?,
244 row.get::<_, Option<String>>(3)?,
245 ))
246 })
247 .map_err(map_rusqlite_error)?;
248 let mut edges = Vec::new();
249 for row in rows {
250 let (kind, src, tgt, meta) = row.map_err(map_rusqlite_error)?;
251 edges.push(Edge {
252 kind: edge_kind_from_str(&kind)?,
253 source: src,
254 target: tgt,
255 metadata: meta,
256 });
257 }
258 Ok(edges)
259 }
260
261 fn get_edges_to(&self, target: &str) -> Result<Vec<Edge>> {
262 let conn = self.conn()?;
263 let mut stmt = conn
264 .prepare_cached(
265 "SELECT kind, source_qualified, target_qualified, metadata
266 FROM edges WHERE target_qualified = ?1",
267 )
268 .map_err(map_rusqlite_error)?;
269 let rows = stmt
270 .query_map(rusqlite::params![target], |row| {
271 Ok((
272 row.get::<_, String>(0)?,
273 row.get::<_, String>(1)?,
274 row.get::<_, String>(2)?,
275 row.get::<_, Option<String>>(3)?,
276 ))
277 })
278 .map_err(map_rusqlite_error)?;
279 let mut edges = Vec::new();
280 for row in rows {
281 let (kind, src, tgt, meta) = row.map_err(map_rusqlite_error)?;
282 edges.push(Edge {
283 kind: edge_kind_from_str(&kind)?,
284 source: src,
285 target: tgt,
286 metadata: meta,
287 });
288 }
289 Ok(edges)
290 }
291
292 fn all_files(&self) -> Result<Vec<FileNode>> {
293 let conn = self.conn()?;
294 let mut stmt = conn
295 .prepare_cached("SELECT path, language, hash FROM files")
296 .map_err(map_rusqlite_error)?;
297 let rows = stmt
298 .query_map([], |row| {
299 Ok((
300 row.get::<_, String>(0)?,
301 row.get::<_, String>(1)?,
302 row.get::<_, String>(2)?,
303 ))
304 })
305 .map_err(map_rusqlite_error)?;
306 let mut files = Vec::new();
307 for row in rows {
308 let (path, lang, hash) = row.map_err(map_rusqlite_error)?;
309 files.push(FileNode {
310 path: path.into(),
311 language: language_from_str(&lang)?,
312 hash,
313 });
314 }
315 Ok(files)
316 }
317
318 fn all_symbols(&self) -> Result<Vec<SymbolNode>> {
319 let conn = self.conn()?;
320 let mut stmt = conn
321 .prepare_cached(
322 "SELECT qualified_name, name, kind, file_path,
323 line_start, line_end, col_start, col_end,
324 visibility, is_exported, is_async, is_test,
325 decorators, signature
326 FROM symbols",
327 )
328 .map_err(map_rusqlite_error)?;
329 let rows = stmt
330 .query_map([], |row| {
331 Ok((
332 row.get::<_, String>(0)?,
333 row.get::<_, String>(1)?,
334 row.get::<_, String>(2)?,
335 row.get::<_, String>(3)?,
336 row.get::<_, i64>(4)?,
337 row.get::<_, i64>(5)?,
338 row.get::<_, i64>(6)?,
339 row.get::<_, i64>(7)?,
340 row.get::<_, String>(8)?,
341 row.get::<_, i32>(9)?,
342 row.get::<_, i32>(10)?,
343 row.get::<_, i32>(11)?,
344 row.get::<_, Option<String>>(12)?,
345 row.get::<_, Option<String>>(13)?,
346 ))
347 })
348 .map_err(map_rusqlite_error)?;
349 let mut symbols = Vec::new();
350 for row in rows {
351 let (qn, name, kind, file, ls, le, cs, ce, vis, exp, asy, tst, dec, sig) =
352 row.map_err(map_rusqlite_error)?;
353 let decorators: Vec<String> = match dec {
354 Some(ref s) => serde_json::from_str(s)
355 .map_err(|e| domain::error::CodeGraphError::Storage(e.to_string()))?,
356 None => vec![],
357 };
358 symbols.push(SymbolNode {
359 qualified_name: qn,
360 name,
361 kind: symbol_kind_from_str(&kind)?,
362 location: Location {
363 file: file.into(),
364 line_start: ls as usize,
365 line_end: le as usize,
366 col_start: cs as usize,
367 col_end: ce as usize,
368 },
369 visibility: visibility_from_str(&vis)?,
370 is_exported: exp != 0,
371 is_async: asy != 0,
372 is_test: tst != 0,
373 decorators,
374 signature: sig,
375 });
376 }
377 Ok(symbols)
378 }
379
380 fn all_edges(&self) -> Result<Vec<Edge>> {
381 let conn = self.conn()?;
382 let mut stmt = conn
383 .prepare_cached("SELECT kind, source_qualified, target_qualified, metadata FROM edges")
384 .map_err(map_rusqlite_error)?;
385 let rows = stmt
386 .query_map([], |row| {
387 Ok((
388 row.get::<_, String>(0)?,
389 row.get::<_, String>(1)?,
390 row.get::<_, String>(2)?,
391 row.get::<_, Option<String>>(3)?,
392 ))
393 })
394 .map_err(map_rusqlite_error)?;
395 let mut edges = Vec::new();
396 for row in rows {
397 let (kind, src, tgt, meta) = row.map_err(map_rusqlite_error)?;
398 edges.push(Edge {
399 kind: edge_kind_from_str(&kind)?,
400 source: src,
401 target: tgt,
402 metadata: meta,
403 });
404 }
405 Ok(edges)
406 }
407
408 fn remove_file(&self, path: &Path) -> Result<()> {
409 let conn = self.conn()?;
410 conn.prepare_cached("DELETE FROM files WHERE path = ?1")
411 .map_err(map_rusqlite_error)?
412 .execute(rusqlite::params![path.to_str().unwrap_or_default()])
413 .map_err(map_rusqlite_error)?;
414 Ok(())
415 }
416
417 fn remove_symbols_in_file(&self, path: &Path) -> Result<()> {
418 let conn = self.conn()?;
419 conn.prepare_cached("DELETE FROM symbols WHERE file_path = ?1")
420 .map_err(map_rusqlite_error)?
421 .execute(rusqlite::params![path.to_str().unwrap_or_default()])
422 .map_err(map_rusqlite_error)?;
423 Ok(())
424 }
425
426 fn find_by_name(&self, pattern: &str) -> Result<Vec<SymbolNode>> {
427 let conn = self.conn()?;
428 let mut stmt = conn
430 .prepare_cached(
431 "SELECT qualified_name, name, kind, file_path,
432 line_start, line_end, col_start, col_end,
433 visibility, is_exported, is_async, is_test,
434 decorators, signature
435 FROM symbols WHERE name = ?1",
436 )
437 .map_err(map_rusqlite_error)?;
438 let exact = self.query_symbols(&mut stmt, rusqlite::params![pattern])?;
439 if !exact.is_empty() {
440 return Ok(exact);
441 }
442 let escaped = pattern
444 .replace('\\', "\\\\")
445 .replace('%', "\\%")
446 .replace('_', "\\_");
447 let prefix_pattern = format!("{escaped}%");
448 let mut stmt = conn
449 .prepare_cached(
450 "SELECT qualified_name, name, kind, file_path,
451 line_start, line_end, col_start, col_end,
452 visibility, is_exported, is_async, is_test,
453 decorators, signature
454 FROM symbols WHERE name LIKE ?1 ESCAPE '\\'",
455 )
456 .map_err(map_rusqlite_error)?;
457 self.query_symbols(&mut stmt, rusqlite::params![&prefix_pattern])
458 }
459
460 fn stats(&self) -> Result<GraphStats> {
461 let conn = self.conn()?;
462 let files: usize = conn
463 .query_row("SELECT COUNT(*) FROM files", [], |r| r.get(0))
464 .map_err(map_rusqlite_error)?;
465 let symbols: usize = conn
466 .query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
467 .map_err(map_rusqlite_error)?;
468 let edges: usize = conn
469 .query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))
470 .map_err(map_rusqlite_error)?;
471 Ok(GraphStats {
472 files,
473 symbols,
474 edges,
475 entry_point_count: None,
476 avg_criticality: None,
477 clone_clusters: None,
478 duplication_pct: None,
479 most_duplicated: None,
480 avg_risk: None,
481 p90_risk: None,
482 community_count: None,
483 modularity: None,
484 })
485 }
486
487 fn store_file_data(
488 &self,
489 file: &FileNode,
490 symbols: &[SymbolNode],
491 edges: &[Edge],
492 ) -> Result<()> {
493 let mut conn = self.conn()?;
494 let tx = conn
495 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
496 .map_err(map_rusqlite_error)?;
497
498 let path_str = file.path.to_str().unwrap_or_default();
499
500 tx.prepare_cached(
502 "DELETE FROM edges
503 WHERE source_qualified IN (SELECT qualified_name FROM symbols WHERE file_path = ?1)
504 OR target_qualified IN (SELECT qualified_name FROM symbols WHERE file_path = ?1)",
505 )
506 .map_err(map_rusqlite_error)?
507 .execute(rusqlite::params![path_str])
508 .map_err(map_rusqlite_error)?;
509
510 tx.prepare_cached("DELETE FROM symbols WHERE file_path = ?1")
512 .map_err(map_rusqlite_error)?
513 .execute(rusqlite::params![path_str])
514 .map_err(map_rusqlite_error)?;
515
516 tx.prepare_cached(
518 "INSERT OR REPLACE INTO files (path, language, hash, updated_at) VALUES (?1, ?2, ?3, ?4)",
519 )
520 .map_err(map_rusqlite_error)?
521 .execute(rusqlite::params![
522 path_str,
523 language_to_str(&file.language),
524 &file.hash,
525 now_epoch(),
526 ])
527 .map_err(map_rusqlite_error)?;
528
529 for symbol in symbols {
531 let decorators_json = serde_json::to_string(&symbol.decorators)
532 .map_err(|e| domain::error::CodeGraphError::Storage(e.to_string()))?;
533 tx.prepare_cached(
534 "INSERT OR REPLACE INTO symbols (
535 qualified_name, name, kind, file_path,
536 line_start, line_end, col_start, col_end,
537 visibility, is_exported, is_async, is_test,
538 decorators, signature, updated_at
539 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
540 )
541 .map_err(map_rusqlite_error)?
542 .execute(rusqlite::params![
543 &symbol.qualified_name,
544 &symbol.name,
545 symbol_kind_to_str(&symbol.kind),
546 symbol.location.file.to_str().unwrap_or_default(),
547 symbol.location.line_start as i64,
548 symbol.location.line_end as i64,
549 symbol.location.col_start as i64,
550 symbol.location.col_end as i64,
551 visibility_to_str(&symbol.visibility),
552 symbol.is_exported as i32,
553 symbol.is_async as i32,
554 symbol.is_test as i32,
555 &decorators_json,
556 &symbol.signature,
557 now_epoch(),
558 ])
559 .map_err(map_rusqlite_error)?;
560 }
561
562 for edge in edges {
564 tx.prepare_cached(
565 "INSERT OR REPLACE INTO edges (kind, source_qualified, target_qualified, metadata)
566 VALUES (?1, ?2, ?3, ?4)",
567 )
568 .map_err(map_rusqlite_error)?
569 .execute(rusqlite::params![
570 edge_kind_to_str(&edge.kind),
571 &edge.source,
572 &edge.target,
573 &edge.metadata,
574 ])
575 .map_err(map_rusqlite_error)?;
576 }
577
578 tx.commit().map_err(map_rusqlite_error)?;
579 Ok(())
580 }
581
582 fn symbols_for_files(&self, paths: &[&Path]) -> Result<Vec<SymbolNode>> {
583 if paths.is_empty() {
584 return Ok(vec![]);
585 }
586 let conn = self.conn()?;
587 const CHUNK_SIZE: usize = 500;
589 let mut symbols = Vec::new();
590 for chunk in paths.chunks(CHUNK_SIZE) {
591 let placeholders: String = (1..=chunk.len())
594 .map(|i| format!("?{i}"))
595 .collect::<Vec<_>>()
596 .join(", ");
597 let sql = format!(
598 "SELECT qualified_name, name, kind, file_path,
599 line_start, line_end, col_start, col_end,
600 visibility, is_exported, is_async, is_test,
601 decorators, signature
602 FROM symbols WHERE file_path IN ({placeholders})"
603 );
604 let mut stmt = conn.prepare(&sql).map_err(map_rusqlite_error)?;
605 let params: Vec<&str> = chunk
606 .iter()
607 .map(|p| p.to_str().unwrap_or_default())
608 .collect();
609 let rows = stmt
610 .query_map(rusqlite::params_from_iter(params), |row| {
611 Ok((
612 row.get::<_, String>(0)?,
613 row.get::<_, String>(1)?,
614 row.get::<_, String>(2)?,
615 row.get::<_, String>(3)?,
616 row.get::<_, i64>(4)?,
617 row.get::<_, i64>(5)?,
618 row.get::<_, i64>(6)?,
619 row.get::<_, i64>(7)?,
620 row.get::<_, String>(8)?,
621 row.get::<_, i32>(9)?,
622 row.get::<_, i32>(10)?,
623 row.get::<_, i32>(11)?,
624 row.get::<_, Option<String>>(12)?,
625 row.get::<_, Option<String>>(13)?,
626 ))
627 })
628 .map_err(map_rusqlite_error)?;
629 for row in rows {
630 let (qn, name, kind, file, ls, le, cs, ce, vis, exp, asy, tst, dec, sig) =
631 row.map_err(map_rusqlite_error)?;
632 let decorators: Vec<String> = match dec {
633 Some(ref s) => serde_json::from_str(s)
634 .map_err(|e| domain::error::CodeGraphError::Storage(e.to_string()))?,
635 None => vec![],
636 };
637 symbols.push(SymbolNode {
638 qualified_name: qn,
639 name,
640 kind: symbol_kind_from_str(&kind)?,
641 location: Location {
642 file: file.into(),
643 line_start: ls as usize,
644 line_end: le as usize,
645 col_start: cs as usize,
646 col_end: ce as usize,
647 },
648 visibility: visibility_from_str(&vis)?,
649 is_exported: exp != 0,
650 is_async: asy != 0,
651 is_test: tst != 0,
652 decorators,
653 signature: sig,
654 });
655 }
656 }
657 Ok(symbols)
658 }
659
660 fn edges_streaming(&self, callback: &mut dyn FnMut(Edge) -> Result<()>) -> Result<()> {
661 let conn = self.conn()?;
662 let mut stmt = conn
663 .prepare_cached("SELECT kind, source_qualified, target_qualified, metadata FROM edges")
664 .map_err(map_rusqlite_error)?;
665 let rows = stmt
666 .query_map([], |row| {
667 Ok((
668 row.get::<_, String>(0)?,
669 row.get::<_, String>(1)?,
670 row.get::<_, String>(2)?,
671 row.get::<_, Option<String>>(3)?,
672 ))
673 })
674 .map_err(map_rusqlite_error)?;
675 for row in rows {
676 let (kind, src, tgt, meta) = row.map_err(map_rusqlite_error)?;
677 callback(Edge {
678 kind: edge_kind_from_str(&kind)?,
679 source: src,
680 target: tgt,
681 metadata: meta,
682 })?;
683 }
684 Ok(())
685 }
686
687 fn remove_file_data(&self, path: &Path) -> Result<()> {
688 let mut conn = self.conn()?;
689 let tx = conn
690 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
691 .map_err(map_rusqlite_error)?;
692
693 let path_str = path.to_str().unwrap_or_default();
694
695 tx.prepare_cached(
697 "DELETE FROM edges
698 WHERE source_qualified IN (SELECT qualified_name FROM symbols WHERE file_path = ?1)
699 OR target_qualified IN (SELECT qualified_name FROM symbols WHERE file_path = ?1)",
700 )
701 .map_err(map_rusqlite_error)?
702 .execute(rusqlite::params![path_str])
703 .map_err(map_rusqlite_error)?;
704
705 tx.prepare_cached("DELETE FROM files WHERE path = ?1")
707 .map_err(map_rusqlite_error)?
708 .execute(rusqlite::params![path_str])
709 .map_err(map_rusqlite_error)?;
710
711 tx.commit().map_err(map_rusqlite_error)?;
712 Ok(())
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use super::*;
719 use domain::ports::GraphStore;
720
721 fn test_store() -> SqliteStore {
722 SqliteStore::open_in_memory().unwrap()
723 }
724
725 fn sample_file() -> FileNode {
726 FileNode {
727 path: "src/main.rs".into(),
728 language: Language::Rust,
729 hash: "abc123".into(),
730 }
731 }
732
733 fn sample_symbol() -> SymbolNode {
734 SymbolNode {
735 name: "foo".into(),
736 qualified_name: "src/main.rs::foo".into(),
737 kind: SymbolKind::Function,
738 location: Location {
739 file: "src/main.rs".into(),
740 line_start: 1,
741 line_end: 10,
742 col_start: 0,
743 col_end: 1,
744 },
745 visibility: Visibility::Public,
746 is_exported: true,
747 is_async: false,
748 is_test: false,
749 decorators: vec!["inline".into()],
750 signature: Some("fn foo() -> bool".into()),
751 }
752 }
753
754 fn sample_edge() -> Edge {
755 Edge {
756 kind: EdgeKind::Calls,
757 source: "src/main.rs::foo".into(),
758 target: "src/lib.rs::bar".into(),
759 metadata: None,
760 }
761 }
762
763 #[test]
764 fn upsert_file_insert_then_update() {
765 let store = test_store();
766 let mut file = sample_file();
767 store.upsert_file(&file).unwrap();
768 let got = store.get_file(&file.path).unwrap().unwrap();
769 assert_eq!(got.hash, "abc123");
770
771 file.hash = "def456".into();
772 store.upsert_file(&file).unwrap();
773 let got = store.get_file(&file.path).unwrap().unwrap();
774 assert_eq!(got.hash, "def456");
775 }
776
777 #[test]
778 fn get_file_missing_returns_none() {
779 let store = test_store();
780 assert!(store.get_file("nonexistent".as_ref()).unwrap().is_none());
781 }
782
783 #[test]
784 fn upsert_symbol_insert_then_update() {
785 let store = test_store();
786 store.upsert_file(&sample_file()).unwrap();
787 let mut sym = sample_symbol();
788 store.upsert_symbol(&sym).unwrap();
789 let got = store.get_symbol(&sym.qualified_name).unwrap().unwrap();
790 assert_eq!(got.name, "foo");
791
792 sym.name = "foo_renamed".into();
793 store.upsert_symbol(&sym).unwrap();
794 let got = store.get_symbol(&sym.qualified_name).unwrap().unwrap();
795 assert_eq!(got.name, "foo_renamed");
796 }
797
798 #[test]
799 fn get_symbol_missing_returns_none() {
800 let store = test_store();
801 assert!(store.get_symbol("nonexistent").unwrap().is_none());
802 }
803
804 #[test]
805 fn upsert_edge_idempotent() {
806 let store = test_store();
807 let edge = sample_edge();
808 store.upsert_edge(&edge).unwrap();
809 store.upsert_edge(&edge).unwrap();
810 let edges = store.get_edges_from(&edge.source).unwrap();
811 assert_eq!(edges.len(), 1);
812 }
813
814 #[test]
815 fn get_edges_from_and_to() {
816 let store = test_store();
817 store.upsert_edge(&sample_edge()).unwrap();
818 let from = store.get_edges_from("src/main.rs::foo").unwrap();
819 assert_eq!(from.len(), 1);
820 let to = store.get_edges_to("src/lib.rs::bar").unwrap();
821 assert_eq!(to.len(), 1);
822 assert!(store.get_edges_from("none").unwrap().is_empty());
823 assert!(store.get_edges_to("none").unwrap().is_empty());
824 }
825
826 #[test]
827 fn all_files_symbols_edges() {
828 let store = test_store();
829 store.upsert_file(&sample_file()).unwrap();
830 store.upsert_symbol(&sample_symbol()).unwrap();
831 store.upsert_edge(&sample_edge()).unwrap();
832 assert_eq!(store.all_files().unwrap().len(), 1);
833 assert_eq!(store.all_symbols().unwrap().len(), 1);
834 assert_eq!(store.all_edges().unwrap().len(), 1);
835 }
836
837 #[test]
838 fn remove_file_cascades_to_symbols() {
839 let store = test_store();
840 store.upsert_file(&sample_file()).unwrap();
841 store.upsert_symbol(&sample_symbol()).unwrap();
842 store.remove_file("src/main.rs".as_ref()).unwrap();
843 assert!(store.get_file("src/main.rs".as_ref()).unwrap().is_none());
844 assert!(store.get_symbol("src/main.rs::foo").unwrap().is_none());
845 }
846
847 #[test]
848 fn remove_symbols_in_file_keeps_file() {
849 let store = test_store();
850 store.upsert_file(&sample_file()).unwrap();
851 store.upsert_symbol(&sample_symbol()).unwrap();
852 store
853 .remove_symbols_in_file("src/main.rs".as_ref())
854 .unwrap();
855 assert!(store.get_file("src/main.rs".as_ref()).unwrap().is_some());
856 assert!(store.get_symbol("src/main.rs::foo").unwrap().is_none());
857 }
858
859 #[test]
860 fn stats_returns_correct_counts() {
861 let store = test_store();
862 store.upsert_file(&sample_file()).unwrap();
863 store.upsert_symbol(&sample_symbol()).unwrap();
864 store.upsert_edge(&sample_edge()).unwrap();
865 let s = store.stats().unwrap();
866 assert_eq!(s.files, 1);
867 assert_eq!(s.symbols, 1);
868 assert_eq!(s.edges, 1);
869 }
870
871 #[test]
874 fn store_file_data_stores_all() {
875 let store = test_store();
876 let file = sample_file();
877 let symbols = vec![sample_symbol()];
878 let edges = vec![sample_edge()];
879 store.store_file_data(&file, &symbols, &edges).unwrap();
880 assert!(store.get_file(&file.path).unwrap().is_some());
881 assert!(store.get_symbol("src/main.rs::foo").unwrap().is_some());
882 assert_eq!(store.all_edges().unwrap().len(), 1);
883 }
884
885 #[test]
886 fn store_file_data_replaces_existing() {
887 let store = test_store();
888 let file = sample_file();
889 let sym1 = sample_symbol();
890 store.store_file_data(&file, &[sym1], &[]).unwrap();
891
892 let sym2 = SymbolNode {
893 name: "bar".into(),
894 qualified_name: "src/main.rs::bar".into(),
895 kind: SymbolKind::Function,
896 location: Location {
897 file: "src/main.rs".into(),
898 line_start: 20,
899 line_end: 30,
900 col_start: 0,
901 col_end: 1,
902 },
903 visibility: Visibility::Private,
904 is_exported: false,
905 is_async: false,
906 is_test: false,
907 decorators: vec![],
908 signature: None,
909 };
910 store.store_file_data(&file, &[sym2], &[]).unwrap();
911 assert!(store.get_symbol("src/main.rs::foo").unwrap().is_none());
913 assert!(store.get_symbol("src/main.rs::bar").unwrap().is_some());
914 }
915
916 #[test]
917 fn remove_file_data_cleans_edges() {
918 let store = test_store();
919 let file = sample_file();
920 let lib_file = FileNode {
921 path: "src/lib.rs".into(),
922 language: Language::Rust,
923 hash: "xyz".into(),
924 };
925 store.upsert_file(&lib_file).unwrap();
926 let sym = sample_symbol();
927 let edge = sample_edge();
928 store.store_file_data(&file, &[sym], &[edge]).unwrap();
929
930 store.remove_file_data("src/main.rs".as_ref()).unwrap();
931
932 assert!(store.get_file("src/main.rs".as_ref()).unwrap().is_none());
933 assert!(store.get_symbol("src/main.rs::foo").unwrap().is_none());
934 assert!(store.all_edges().unwrap().is_empty());
935 }
936
937 #[test]
940 fn symbol_roundtrip_preserves_all_fields() {
941 let store = test_store();
942 store.upsert_file(&sample_file()).unwrap();
943 let sym = sample_symbol();
944 store.upsert_symbol(&sym).unwrap();
945 let got = store.get_symbol(&sym.qualified_name).unwrap().unwrap();
946 assert_eq!(got.name, sym.name);
947 assert_eq!(got.kind, sym.kind);
948 assert_eq!(got.visibility, sym.visibility);
949 assert_eq!(got.is_exported, sym.is_exported);
950 assert_eq!(got.is_async, sym.is_async);
951 assert_eq!(got.is_test, sym.is_test);
952 assert_eq!(got.decorators, sym.decorators);
953 assert_eq!(got.signature, sym.signature);
954 assert_eq!(got.location.line_start, sym.location.line_start);
955 assert_eq!(got.location.line_end, sym.location.line_end);
956 }
957
958 fn make_named_symbol(name: &str, qn: &str) -> SymbolNode {
961 SymbolNode {
962 name: name.into(),
963 qualified_name: qn.into(),
964 kind: SymbolKind::Function,
965 location: Location {
966 file: "src/main.rs".into(),
967 line_start: 1,
968 line_end: 10,
969 col_start: 0,
970 col_end: 1,
971 },
972 visibility: Visibility::Public,
973 is_exported: true,
974 is_async: false,
975 is_test: false,
976 decorators: vec![],
977 signature: None,
978 }
979 }
980
981 #[test]
982 fn find_by_name_exact_match() {
983 let store = test_store();
984 store.upsert_file(&sample_file()).unwrap();
985 store
986 .upsert_symbol(&make_named_symbol("foo", "src/main.rs::foo"))
987 .unwrap();
988 store
989 .upsert_symbol(&make_named_symbol("foobar", "src/main.rs::foobar"))
990 .unwrap();
991 store
992 .upsert_symbol(&make_named_symbol("bar", "src/main.rs::bar"))
993 .unwrap();
994
995 let results = store.find_by_name("foo").unwrap();
996 assert_eq!(results.len(), 1);
997 assert_eq!(results[0].name, "foo");
998 }
999
1000 #[test]
1001 fn find_by_name_prefix_fallback() {
1002 let store = test_store();
1003 store.upsert_file(&sample_file()).unwrap();
1004 store
1005 .upsert_symbol(&make_named_symbol("foobar", "src/main.rs::foobar"))
1006 .unwrap();
1007 store
1008 .upsert_symbol(&make_named_symbol("foobaz", "src/main.rs::foobaz"))
1009 .unwrap();
1010
1011 let results = store.find_by_name("foo").unwrap();
1012 assert_eq!(results.len(), 2);
1013 let mut names: Vec<&str> = results.iter().map(|s| s.name.as_str()).collect();
1014 names.sort();
1015 assert_eq!(names, vec!["foobar", "foobaz"]);
1016 }
1017
1018 #[test]
1019 fn find_by_name_no_match() {
1020 let store = test_store();
1021 let results = store.find_by_name("nonexistent").unwrap();
1022 assert!(results.is_empty());
1023 }
1024
1025 #[test]
1026 fn find_by_name_case_sensitive() {
1027 let store = test_store();
1031 store.upsert_file(&sample_file()).unwrap();
1032 store
1033 .upsert_symbol(&make_named_symbol("foo", "src/main.rs::foo"))
1034 .unwrap();
1035
1036 let results = store.find_by_name("Foo").unwrap();
1038 assert_eq!(results.len(), 1);
1039 assert_eq!(results[0].name, "foo");
1040 }
1041
1042 #[test]
1043 fn find_by_name_escapes_like_metacharacters() {
1044 let store = test_store();
1045 store.upsert_file(&sample_file()).unwrap();
1046 store
1047 .upsert_symbol(&make_named_symbol("__init__", "src/main.rs::__init__"))
1048 .unwrap();
1049 store
1050 .upsert_symbol(&make_named_symbol(
1051 "__init_extra",
1052 "src/main.rs::__init_extra",
1053 ))
1054 .unwrap();
1055 store
1056 .upsert_symbol(&make_named_symbol("axbycz", "src/main.rs::axbycz"))
1057 .unwrap();
1058
1059 let results = store.find_by_name("__init__").unwrap();
1061 assert_eq!(results.len(), 1);
1062 assert_eq!(results[0].name, "__init__");
1063
1064 let results = store.find_by_name("__init").unwrap();
1067 assert_eq!(results.len(), 2);
1068 let mut names: Vec<&str> = results.iter().map(|s| s.name.as_str()).collect();
1069 names.sort();
1070 assert_eq!(names, vec!["__init__", "__init_extra"]);
1071 }
1072
1073 #[test]
1076 fn symbols_for_files_returns_filtered_subset() {
1077 let store = test_store();
1078 store
1080 .upsert_file(&FileNode {
1081 path: "src/a.rs".into(),
1082 language: Language::Rust,
1083 hash: "h1".into(),
1084 })
1085 .unwrap();
1086 store
1088 .upsert_file(&FileNode {
1089 path: "src/b.rs".into(),
1090 language: Language::Rust,
1091 hash: "h2".into(),
1092 })
1093 .unwrap();
1094 store
1096 .upsert_symbol(&SymbolNode {
1097 name: "foo".into(),
1098 qualified_name: "src/a.rs::foo".into(),
1099 kind: SymbolKind::Function,
1100 location: Location {
1101 file: "src/a.rs".into(),
1102 line_start: 1,
1103 line_end: 10,
1104 col_start: 0,
1105 col_end: 1,
1106 },
1107 visibility: Visibility::Public,
1108 is_exported: true,
1109 is_async: false,
1110 is_test: false,
1111 decorators: vec![],
1112 signature: None,
1113 })
1114 .unwrap();
1115 store
1117 .upsert_symbol(&SymbolNode {
1118 name: "bar".into(),
1119 qualified_name: "src/b.rs::bar".into(),
1120 kind: SymbolKind::Function,
1121 location: Location {
1122 file: "src/b.rs".into(),
1123 line_start: 1,
1124 line_end: 10,
1125 col_start: 0,
1126 col_end: 1,
1127 },
1128 visibility: Visibility::Public,
1129 is_exported: true,
1130 is_async: false,
1131 is_test: false,
1132 decorators: vec![],
1133 signature: None,
1134 })
1135 .unwrap();
1136 let results = store.symbols_for_files(&[Path::new("src/a.rs")]).unwrap();
1138 assert_eq!(results.len(), 1);
1139 assert_eq!(results[0].name, "foo");
1140 }
1141
1142 #[test]
1143 fn symbols_for_files_empty_paths_returns_empty() {
1144 let store = test_store();
1145 store.upsert_file(&sample_file()).unwrap();
1146 store.upsert_symbol(&sample_symbol()).unwrap();
1147 let results = store.symbols_for_files(&[]).unwrap();
1148 assert!(results.is_empty());
1149 }
1150
1151 #[test]
1152 fn edges_streaming_invokes_callback_per_row() {
1153 let store = test_store();
1154 store
1155 .upsert_edge(&Edge {
1156 kind: EdgeKind::Calls,
1157 source: "a::foo".into(),
1158 target: "b::bar".into(),
1159 metadata: None,
1160 })
1161 .unwrap();
1162 store
1163 .upsert_edge(&Edge {
1164 kind: EdgeKind::ImportsFrom,
1165 source: "c::baz".into(),
1166 target: "d::qux".into(),
1167 metadata: None,
1168 })
1169 .unwrap();
1170 store
1171 .upsert_edge(&Edge {
1172 kind: EdgeKind::Contains,
1173 source: "e::quux".into(),
1174 target: "f::corge".into(),
1175 metadata: None,
1176 })
1177 .unwrap();
1178 let mut count = 0usize;
1179 store
1180 .edges_streaming(&mut |_edge| {
1181 count += 1;
1182 Ok(())
1183 })
1184 .unwrap();
1185 assert_eq!(count, 3);
1186 assert_eq!(store.all_edges().unwrap().len(), 3);
1187 }
1188}