1use postgres::GenericClient;
2use std::collections::HashSet;
3
4use crate::config::{self, Context};
5use crate::db;
6use crate::graph::code_graph;
7use crate::index::indexer;
8use crate::utils::short_id;
9use crate::vector::code_symbols;
10
11use super::projects::stale_projects;
12use super::shared::{collect_projects, display_name};
13
14const ORPHAN_PROJECT_WARNING_LIMIT: usize = 5;
15const ORPHAN_PROJECT_SUMMARY_LIMIT: usize = 8;
16
17#[derive(Debug, PartialEq, Eq)]
18enum ProjectionCleanupScope {
19 AllIndexedProjects,
20 ResolvedProjectOverride,
21}
22
23#[derive(Default)]
24struct ProjectionPruneTotals {
25 graph_projects_cleaned: usize,
26 graph_projects_skipped: usize,
27 graph_stale_files_deleted: usize,
28 graph_nodes_deleted: usize,
29 vector_projects_cleaned: usize,
30 vector_projects_skipped: usize,
31 vector_orphan_files_deleted: usize,
32 vectors_deleted: usize,
33}
34
35impl ProjectionPruneTotals {
36 fn record_graph_cleanup(&mut self, cleanup: crate::graph::code_graph::GraphOrphanCleanup) {
37 self.graph_projects_cleaned += 1;
38 self.graph_stale_files_deleted += cleanup.stale_files_deleted;
39 self.graph_nodes_deleted += cleanup.graph_nodes_deleted;
40 }
41
42 fn record_vector_cleanup(&mut self, cleanup: code_symbols::VectorOrphanCleanup) {
43 self.vector_projects_cleaned += 1;
44 self.vector_orphan_files_deleted += cleanup.orphan_files_deleted;
45 self.vectors_deleted += cleanup.vectors_deleted;
46 }
47
48 fn add(&mut self, other: ProjectionPruneTotals) {
49 self.graph_projects_cleaned += other.graph_projects_cleaned;
50 self.graph_projects_skipped += other.graph_projects_skipped;
51 self.graph_stale_files_deleted += other.graph_stale_files_deleted;
52 self.graph_nodes_deleted += other.graph_nodes_deleted;
53 self.vector_projects_cleaned += other.vector_projects_cleaned;
54 self.vector_projects_skipped += other.vector_projects_skipped;
55 self.vector_orphan_files_deleted += other.vector_orphan_files_deleted;
56 self.vectors_deleted += other.vectors_deleted;
57 }
58}
59
60#[derive(Default, Debug, PartialEq, Eq)]
61pub(super) struct OrphanSqlDeletionCounts {
62 symbols_deleted: u64,
63 files_deleted: u64,
64 content_chunks_deleted: u64,
65 imports_deleted: u64,
66 calls_deleted: u64,
67}
68
69impl OrphanSqlDeletionCounts {
70 fn total(&self) -> u64 {
71 self.symbols_deleted
72 + self.files_deleted
73 + self.content_chunks_deleted
74 + self.imports_deleted
75 + self.calls_deleted
76 }
77}
78
79#[derive(Default)]
80struct OrphanProjectReconcileTotals {
81 project_ids: Vec<String>,
82 sql: OrphanSqlDeletionCounts,
83 graph_projects_cleared: usize,
84 graph_projects_skipped: usize,
85 vector_collections_deleted: usize,
86 vector_projects_skipped: usize,
87}
88
89impl OrphanProjectReconcileTotals {
90 fn record_sql(&mut self, project_id: String, counts: OrphanSqlDeletionCounts) {
91 self.project_ids.push(project_id);
92 self.sql.symbols_deleted += counts.symbols_deleted;
93 self.sql.files_deleted += counts.files_deleted;
94 self.sql.content_chunks_deleted += counts.content_chunks_deleted;
95 self.sql.imports_deleted += counts.imports_deleted;
96 self.sql.calls_deleted += counts.calls_deleted;
97 }
98}
99
100fn projection_cleanup_scope(project_override: Option<&str>) -> ProjectionCleanupScope {
101 if project_override.is_some() {
102 ProjectionCleanupScope::ResolvedProjectOverride
103 } else {
104 ProjectionCleanupScope::AllIndexedProjects
105 }
106}
107
108pub fn prune(force: bool, project_override: Option<&str>, quiet: bool) -> anyhow::Result<()> {
109 if prune_stale_projects(force)?.is_none() {
110 return Ok(());
111 }
112
113 let orphan_totals = reconcile_orphan_projects(quiet)?;
114 print_orphan_project_reconcile_totals(&orphan_totals);
115
116 match projection_cleanup_scope(project_override) {
117 ProjectionCleanupScope::AllIndexedProjects => prune_all_project_projections(quiet),
118 ProjectionCleanupScope::ResolvedProjectOverride => {
119 prune_resolved_project_projections(project_override, quiet)
120 }
121 }
122}
123
124fn prune_resolved_project_projections(
125 project_override: Option<&str>,
126 quiet: bool,
127) -> anyhow::Result<()> {
128 match Context::resolve_with_services(
129 project_override,
130 quiet,
131 config::ServiceConfigSelection::projection_cleanup(),
132 ) {
133 Ok(ctx) => prune_current_project_projections(&ctx),
134 Err(error) if project_override.is_none() && is_missing_project_context(&error) => Ok(()),
135 Err(error) => Err(error),
136 }
137}
138
139fn prune_stale_projects(force: bool) -> anyhow::Result<Option<usize>> {
140 let all_projects = collect_projects()?;
141 let stale = stale_projects(&all_projects);
142
143 if stale.is_empty() {
144 eprintln!("No stale projects found.");
145 return Ok(Some(0));
146 }
147
148 eprintln!("Found {} stale project(s):", stale.len());
149 for stale_project in &stale {
150 eprintln!(
151 " {} — {}",
152 display_name(stale_project.project),
153 stale_project.reason
154 );
155 }
156
157 if !force {
158 eprint!("\nRemove these entries and their indexed data? [y/N] ");
159 let _ = std::io::Write::flush(&mut std::io::stderr());
160
161 let mut input = String::new();
162 std::io::stdin().read_line(&mut input)?;
163 if !input.trim().eq_ignore_ascii_case("y") {
164 eprintln!("Aborted.");
165 return Ok(None);
166 }
167 }
168
169 let daemon_url = gobby_core::daemon_url::daemon_url();
170 let database_url = db::resolve_database_url()?;
171 let mut conn = db::connect_readwrite(&database_url)?;
172
173 for stale_project in &stale {
174 indexer::invalidate(&mut conn, &stale_project.project.id, Some(&daemon_url))?;
175 }
176
177 eprintln!("Pruned {} stale project(s).", stale.len());
178 Ok(Some(stale.len()))
179}
180
181fn reconcile_orphan_projects(quiet: bool) -> anyhow::Result<OrphanProjectReconcileTotals> {
182 let database_url = db::resolve_database_url()?;
183 let mut conn = db::connect_readwrite(&database_url)?;
184 let project_ids = collect_orphan_project_ids(&mut conn)?;
185 let mut totals = OrphanProjectReconcileTotals::default();
186 let mut warnings_emitted = 0usize;
187
188 for project_id in project_ids {
189 if cleanup_orphan_project_projections(
190 &project_id,
191 quiet,
192 &mut totals,
193 &mut warnings_emitted,
194 ) {
195 let counts = delete_orphan_project_sql_rows(&mut conn, &project_id)?;
196 totals.record_sql(project_id, counts);
197 }
198 }
199
200 Ok(totals)
201}
202
203pub(super) fn collect_orphan_project_ids(
204 conn: &mut impl GenericClient,
205) -> anyhow::Result<Vec<String>> {
206 let rows = conn.query(
207 "SELECT child.project_id
208 FROM (
209 SELECT project_id FROM code_indexed_files
210 UNION
211 SELECT project_id FROM code_symbols
212 UNION
213 SELECT project_id FROM code_content_chunks
214 UNION
215 SELECT project_id FROM code_imports
216 UNION
217 SELECT project_id FROM code_calls
218 ) child
219 LEFT JOIN code_indexed_projects parent ON parent.id = child.project_id
220 WHERE parent.id IS NULL
221 ORDER BY child.project_id",
222 &[],
223 )?;
224
225 rows.into_iter()
226 .map(|row| row.try_get(0).map_err(anyhow::Error::from))
227 .collect()
228}
229
230pub(super) fn delete_orphan_project_sql_rows(
231 conn: &mut impl GenericClient,
232 project_id: &str,
233) -> anyhow::Result<OrphanSqlDeletionCounts> {
234 let calls_deleted = conn.execute(
235 "DELETE FROM code_calls WHERE project_id = $1",
236 &[&project_id],
237 )?;
238 let imports_deleted = conn.execute(
239 "DELETE FROM code_imports WHERE project_id = $1",
240 &[&project_id],
241 )?;
242 let content_chunks_deleted = conn.execute(
243 "DELETE FROM code_content_chunks WHERE project_id = $1",
244 &[&project_id],
245 )?;
246 let files_deleted = conn.execute(
247 "DELETE FROM code_indexed_files WHERE project_id = $1",
248 &[&project_id],
249 )?;
250 let symbols_deleted = conn.execute(
251 "DELETE FROM code_symbols WHERE project_id = $1",
252 &[&project_id],
253 )?;
254
255 Ok(OrphanSqlDeletionCounts {
256 symbols_deleted,
257 files_deleted,
258 content_chunks_deleted,
259 imports_deleted,
260 calls_deleted,
261 })
262}
263
264fn cleanup_orphan_project_projections(
265 project_id: &str,
266 quiet: bool,
267 totals: &mut OrphanProjectReconcileTotals,
268 warnings_emitted: &mut usize,
269) -> bool {
270 let mut cleaned = true;
271 let ctx = match Context::resolve_for_project_id_with_services(
272 project_id,
273 quiet,
274 config::ServiceConfigSelection::projection_cleanup(),
275 ) {
276 Ok(ctx) => ctx,
277 Err(error) => {
278 warn_orphan_projection_cleanup_failure(
279 "service config",
280 project_id,
281 error,
282 warnings_emitted,
283 );
284 totals.graph_projects_skipped += 1;
285 totals.vector_projects_skipped += 1;
286 return false;
287 }
288 };
289
290 if ctx.falkordb.is_some() {
291 if let Err(error) = code_graph::clear_project(&ctx) {
292 warn_orphan_projection_cleanup_failure("graph", project_id, error, warnings_emitted);
293 cleaned = false;
294 } else {
295 totals.graph_projects_cleared += 1;
296 }
297 } else {
298 totals.graph_projects_skipped += 1;
299 }
300
301 if let Some(qdrant) = &ctx.qdrant {
302 match code_symbols::delete_project_collection(qdrant, project_id) {
303 Ok(deleted) => totals.vector_collections_deleted += deleted,
304 Err(error) => {
305 warn_orphan_projection_cleanup_failure(
306 "vector",
307 project_id,
308 anyhow::Error::from(error),
309 warnings_emitted,
310 );
311 cleaned = false;
312 }
313 }
314 } else {
315 totals.vector_projects_skipped += 1;
316 }
317 cleaned
318}
319
320fn print_orphan_project_reconcile_totals(totals: &OrphanProjectReconcileTotals) {
321 if totals.project_ids.is_empty() {
322 return;
323 }
324
325 eprintln!(
326 "Reconciled {} orphan code-index project(s): deleted {} SQL row(s) ({} file(s), {} symbol(s), {} content chunk(s), {} import(s), {} call(s)).",
327 totals.project_ids.len(),
328 totals.sql.total(),
329 totals.sql.files_deleted,
330 totals.sql.symbols_deleted,
331 totals.sql.content_chunks_deleted,
332 totals.sql.imports_deleted,
333 totals.sql.calls_deleted
334 );
335 eprintln!(
336 " Project IDs: {}",
337 bounded_project_id_summary(&totals.project_ids)
338 );
339 eprintln!(
340 " Cleared projections: {} graph project(s), {} vector collection(s); skipped {} graph, {} vector project(s).",
341 totals.graph_projects_cleared,
342 totals.vector_collections_deleted,
343 totals.graph_projects_skipped,
344 totals.vector_projects_skipped
345 );
346}
347
348fn bounded_project_id_summary(project_ids: &[String]) -> String {
349 let mut ids = project_ids
350 .iter()
351 .take(ORPHAN_PROJECT_SUMMARY_LIMIT)
352 .map(|id| short_id(id))
353 .collect::<Vec<_>>();
354 if project_ids.len() > ORPHAN_PROJECT_SUMMARY_LIMIT {
355 ids.push(format!(
356 "+{} more",
357 project_ids.len() - ORPHAN_PROJECT_SUMMARY_LIMIT
358 ));
359 }
360 ids.join(", ")
361}
362
363fn warn_orphan_projection_cleanup_failure(
364 store: &str,
365 project_id: &str,
366 error: anyhow::Error,
367 warnings_emitted: &mut usize,
368) {
369 if *warnings_emitted < ORPHAN_PROJECT_WARNING_LIMIT {
370 eprintln!(
371 "Warning: {store} cleanup failed for orphan project {}: {error}",
372 short_id(project_id)
373 );
374 } else if *warnings_emitted == ORPHAN_PROJECT_WARNING_LIMIT {
375 eprintln!(
376 "Warning: additional orphan project projection cleanup failures omitted after {ORPHAN_PROJECT_WARNING_LIMIT} warning(s)."
377 );
378 }
379 *warnings_emitted += 1;
380}
381
382fn prune_all_project_projections(quiet: bool) -> anyhow::Result<()> {
383 let projects = collect_projects()?;
384 if projects.is_empty() {
385 eprintln!("No indexed projects remain for projection cleanup.");
386 return Ok(());
387 }
388
389 let mut totals = ProjectionPruneTotals::default();
390 for project in &projects {
391 let label = display_name(project);
392 match Context::resolve_for_project_id_with_services(
393 &project.id,
394 quiet,
395 config::ServiceConfigSelection::projection_cleanup(),
396 ) {
397 Ok(ctx) => totals.add(prune_project_orphan_projections(&ctx, Some(&label))),
398 Err(error) => {
399 eprintln!("Warning: projection orphan cleanup failed for {label}: {error}")
400 }
401 }
402 }
403
404 print_all_project_projection_totals(totals);
405 Ok(())
406}
407
408fn prune_current_project_projections(ctx: &Context) -> anyhow::Result<()> {
409 let totals = prune_project_orphan_projections(ctx, None);
410 print_current_project_projection_totals(totals);
411 Ok(())
412}
413
414fn prune_project_orphan_projections(
415 ctx: &Context,
416 project_label: Option<&str>,
417) -> ProjectionPruneTotals {
418 let mut totals = ProjectionPruneTotals::default();
419
420 match prune_graph_orphans(ctx) {
421 Ok(Some(cleanup)) => totals.record_graph_cleanup(cleanup),
422 Ok(None) => totals.graph_projects_skipped += 1,
423 Err(error) => warn_projection_cleanup_failure("graph", project_label, error),
424 }
425
426 match prune_vector_orphans(ctx) {
427 Ok(Some(cleanup)) => totals.record_vector_cleanup(cleanup),
428 Ok(None) => totals.vector_projects_skipped += 1,
429 Err(error) => warn_projection_cleanup_failure("vector", project_label, error),
430 }
431
432 totals
433}
434
435fn print_current_project_projection_totals(totals: ProjectionPruneTotals) {
436 if totals.graph_projects_cleaned > 0 {
437 eprintln!(
438 "Pruned graph projection: {} stale file(s), {} file-scoped node(s).",
439 totals.graph_stale_files_deleted, totals.graph_nodes_deleted
440 );
441 } else if totals.graph_projects_skipped > 0 {
442 eprintln!("Skipped graph projection orphan cleanup: FalkorDB is not configured.");
443 }
444
445 if totals.vector_projects_cleaned > 0 {
446 eprintln!(
447 "Pruned vector projection: {} stale file(s), {} vector point(s).",
448 totals.vector_orphan_files_deleted, totals.vectors_deleted
449 );
450 } else if totals.vector_projects_skipped > 0 {
451 eprintln!("Skipped vector projection orphan cleanup: Qdrant is not configured.");
452 }
453}
454
455fn print_all_project_projection_totals(totals: ProjectionPruneTotals) {
456 if totals.graph_projects_cleaned > 0 {
457 eprintln!(
458 "Pruned graph projections for {} project(s): {} stale file(s), {} file-scoped node(s).",
459 totals.graph_projects_cleaned,
460 totals.graph_stale_files_deleted,
461 totals.graph_nodes_deleted
462 );
463 } else if totals.graph_projects_skipped > 0 {
464 eprintln!(
465 "Skipped graph projection orphan cleanup for all indexed projects: FalkorDB is not configured."
466 );
467 }
468
469 if totals.vector_projects_cleaned > 0 {
470 eprintln!(
471 "Pruned vector projections for {} project(s): {} stale file(s), {} vector point(s).",
472 totals.vector_projects_cleaned,
473 totals.vector_orphan_files_deleted,
474 totals.vectors_deleted
475 );
476 } else if totals.vector_projects_skipped > 0 {
477 eprintln!(
478 "Skipped vector projection orphan cleanup for all indexed projects: Qdrant is not configured."
479 );
480 }
481}
482
483fn warn_projection_cleanup_failure(store: &str, project_label: Option<&str>, error: anyhow::Error) {
484 if let Some(project_label) = project_label {
485 eprintln!("Warning: {store} projection orphan cleanup failed for {project_label}: {error}");
486 } else {
487 eprintln!("Warning: {store} projection orphan cleanup failed: {error}");
488 }
489}
490
491fn prune_graph_orphans(
492 ctx: &Context,
493) -> anyhow::Result<Option<crate::graph::code_graph::GraphOrphanCleanup>> {
494 if ctx.falkordb.is_none() {
495 return Ok(None);
496 }
497 crate::commands::graph::cleanup_deleted_project_graph(ctx).map(Some)
498}
499
500fn prune_vector_orphans(
501 ctx: &Context,
502) -> anyhow::Result<Option<code_symbols::VectorOrphanCleanup>> {
503 let Some(qdrant) = &ctx.qdrant else {
504 return Ok(None);
505 };
506 let mut conn = db::connect_readonly(&ctx.database_url)?;
507 let indexed_file_paths = db::list_indexed_file_paths(&mut conn, &ctx.project_id)?
508 .into_iter()
509 .collect::<HashSet<_>>();
510 code_symbols::cleanup_orphan_file_vectors(qdrant, &ctx.project_id, &indexed_file_paths)
511 .map(Some)
512 .map_err(anyhow::Error::from)
513}
514
515fn is_missing_project_context(error: &anyhow::Error) -> bool {
516 error
517 .to_string()
518 .contains("No gcode project found. Run `gcode init`")
519}
520
521#[cfg(test)]
522mod tests {
523 use std::time::{SystemTime, UNIX_EPOCH};
524
525 use super::*;
526
527 #[test]
528 fn prune_without_project_uses_all_indexed_projection_scope() {
529 assert_eq!(
530 projection_cleanup_scope(None),
531 ProjectionCleanupScope::AllIndexedProjects
532 );
533 }
534
535 #[test]
536 fn prune_with_project_uses_single_resolved_projection_scope() {
537 assert_eq!(
538 projection_cleanup_scope(Some("/tmp/project")),
539 ProjectionCleanupScope::ResolvedProjectOverride
540 );
541 }
542
543 #[test]
544 fn bounded_project_id_summary_caps_ids() {
545 let ids = (0..10)
546 .map(|idx| format!("project-{idx:02}-abcdef"))
547 .collect::<Vec<_>>();
548
549 let summary = bounded_project_id_summary(&ids);
550
551 assert!(summary.contains("project-"));
552 assert!(summary.contains("+2 more"));
553 }
554
555 #[test]
556 #[cfg_attr(
557 not(gcode_postgres_tests),
558 ignore = "requires a PostgreSQL test database URL"
559 )]
560 #[serial_test::serial(serial_db)]
561 fn orphan_project_discovery_and_sql_deletion_counts() {
562 let (mut conn, database_url) = connect_test_db();
563 let valid_project_id = unique_test_project_id("gcode-orphan-valid");
564 let orphan_project_id = unique_test_project_id("gcode-orphan-missing-parent");
565 cleanup_project(&mut conn, &valid_project_id).expect("pre-clean valid project rows");
566 cleanup_project(&mut conn, &orphan_project_id).expect("pre-clean orphan project rows");
567 let _valid_cleanup = ProjectCleanup {
568 database_url: database_url.clone(),
569 project_id: valid_project_id.clone(),
570 };
571 let _orphan_cleanup = ProjectCleanup {
572 database_url,
573 project_id: orphan_project_id.clone(),
574 };
575
576 seed_project_with_child_rows(&mut conn, &valid_project_id, true);
577 seed_project_with_child_rows(&mut conn, &orphan_project_id, false);
578
579 let orphan_ids = collect_orphan_project_ids(&mut conn).expect("discover orphan projects");
580 assert!(orphan_ids.contains(&orphan_project_id));
581 assert!(!orphan_ids.contains(&valid_project_id));
582
583 let counts = delete_orphan_project_sql_rows(&mut conn, &orphan_project_id)
584 .expect("delete orphan rows");
585
586 assert_eq!(
587 counts,
588 OrphanSqlDeletionCounts {
589 symbols_deleted: 1,
590 files_deleted: 1,
591 content_chunks_deleted: 1,
592 imports_deleted: 1,
593 calls_deleted: 1,
594 }
595 );
596 assert_eq!(project_child_row_count(&mut conn, &orphan_project_id), 0);
597 assert_eq!(project_child_row_count(&mut conn, &valid_project_id), 5);
598 }
599
600 struct ProjectCleanup {
601 database_url: String,
602 project_id: String,
603 }
604
605 impl Drop for ProjectCleanup {
606 fn drop(&mut self) {
607 if let Ok(mut conn) = db::connect_readwrite(&self.database_url) {
608 let _ = cleanup_project(&mut conn, &self.project_id);
609 }
610 }
611 }
612
613 fn connect_test_db() -> (postgres::Client, String) {
614 let database_url = crate::test_env::postgres_test_database_url("prune tests");
615 let conn = db::connect_readwrite(&database_url).expect("connect prune PostgreSQL test DB");
616 (conn, database_url)
617 }
618
619 fn unique_test_project_id(prefix: &str) -> String {
620 let nanos = SystemTime::now()
621 .duration_since(UNIX_EPOCH)
622 .expect("system time after epoch")
623 .as_nanos();
624 format!("{prefix}-{nanos}")
625 }
626
627 fn seed_project_with_child_rows(
628 conn: &mut postgres::Client,
629 project_id: &str,
630 include_project_row: bool,
631 ) {
632 let file_path = "src/lib.rs";
633 let file_id = format!("{project_id}-file");
634 let symbol_id = format!("{project_id}-symbol");
635 if include_project_row {
636 conn.execute(
637 "INSERT INTO code_indexed_projects
638 (id, root_path, total_files, total_symbols, last_indexed_at, index_duration_ms)
639 VALUES ($1, $2, 1, 1, NOW(), 0)",
640 &[&project_id, &format!("/tmp/{project_id}")],
641 )
642 .expect("insert indexed project");
643 }
644 conn.execute(
645 "INSERT INTO code_indexed_files
646 (id, project_id, file_path, language, content_hash, symbol_count, byte_size)
647 VALUES ($1, $2, $3, 'rust', 'hash-1', 1, 19)",
648 &[&file_id, &project_id, &file_path],
649 )
650 .expect("insert indexed file");
651 conn.execute(
652 "INSERT INTO code_symbols
653 (id, project_id, file_path, name, qualified_name, kind, language, byte_start,
654 byte_end, line_start, line_end, signature, docstring, parent_symbol_id,
655 content_hash, summary, created_at, updated_at)
656 VALUES ($1, $2, $3, 'indexed', 'crate::indexed', 'function', 'rust', 0, 19,
657 1, 1, 'pub fn indexed()', NULL, NULL, 'hash-1', NULL, NOW(), NOW())",
658 &[&symbol_id, &project_id, &file_path],
659 )
660 .expect("insert symbol");
661 conn.execute(
662 "INSERT INTO code_content_chunks
663 (id, project_id, file_path, chunk_index, line_start, line_end, content, language)
664 VALUES ($1, $2, $3, 0, 1, 1, 'pub fn indexed() {}', 'rust')",
665 &[&format!("{project_id}-chunk"), &project_id, &file_path],
666 )
667 .expect("insert content chunk");
668 conn.execute(
669 "INSERT INTO code_imports (project_id, source_file, target_module)
670 VALUES ($1, $2, 'std::fmt')",
671 &[&project_id, &file_path],
672 )
673 .expect("insert import");
674 conn.execute(
675 "INSERT INTO code_calls
676 (project_id, caller_symbol_id, callee_symbol_id, callee_name,
677 callee_target_kind, callee_external_module, file_path, line)
678 VALUES ($1, $2, '', 'missing', 'unresolved', '', $3, 1)",
679 &[&project_id, &symbol_id, &file_path],
680 )
681 .expect("insert call");
682 }
683
684 fn cleanup_project(conn: &mut postgres::Client, project_id: &str) -> anyhow::Result<()> {
685 conn.execute(
686 "DELETE FROM code_calls WHERE project_id = $1",
687 &[&project_id],
688 )?;
689 conn.execute(
690 "DELETE FROM code_imports WHERE project_id = $1",
691 &[&project_id],
692 )?;
693 conn.execute(
694 "DELETE FROM code_content_chunks WHERE project_id = $1",
695 &[&project_id],
696 )?;
697 conn.execute(
698 "DELETE FROM code_symbols WHERE project_id = $1",
699 &[&project_id],
700 )?;
701 conn.execute(
702 "DELETE FROM code_indexed_files WHERE project_id = $1",
703 &[&project_id],
704 )?;
705 conn.execute(
706 "DELETE FROM code_indexed_projects WHERE id = $1",
707 &[&project_id],
708 )?;
709 Ok(())
710 }
711
712 fn project_child_row_count(conn: &mut postgres::Client, project_id: &str) -> i64 {
713 let files = count_rows(conn, "code_indexed_files", project_id);
714 let symbols = count_rows(conn, "code_symbols", project_id);
715 let chunks = count_rows(conn, "code_content_chunks", project_id);
716 let imports = count_rows(conn, "code_imports", project_id);
717 let calls = count_rows(conn, "code_calls", project_id);
718 files + symbols + chunks + imports + calls
719 }
720
721 fn count_rows(conn: &mut postgres::Client, table: &str, project_id: &str) -> i64 {
722 conn.query_one(
723 &format!("SELECT COUNT(*)::BIGINT FROM {table} WHERE project_id = $1"),
724 &[&project_id],
725 )
726 .expect("count rows")
727 .get(0)
728 }
729}