@@ -125,10 +125,15 @@ fn load_file_hashes(conn: &Connection) -> Option<HashMap<String, FileHashRow>> {
125125}
126126
127127/// Detect removed files: files in DB but not in current file list.
128+ ///
129+ /// When `scoped_rel_paths` is provided (scoped rebuild), only files within that
130+ /// scope are considered candidates for removal. Without it, all DB files not
131+ /// found on disk are treated as removed.
128132fn detect_removed_files (
129133 existing : & HashMap < String , FileHashRow > ,
130134 all_files : & [ String ] ,
131135 root_dir : & str ,
136+ scoped_rel_paths : Option < & HashSet < String > > ,
132137) -> Vec < String > {
133138 let current: HashSet < String > = all_files
134139 . iter ( )
@@ -137,7 +142,14 @@ fn detect_removed_files(
137142
138143 existing
139144 . keys ( )
140- . filter ( |f| !current. contains ( * f) )
145+ . filter ( |f| {
146+ // When scope is set, only consider files within scope as candidates.
147+ if let Some ( scope) = scoped_rel_paths {
148+ scope. contains ( * f) && !current. contains ( * f)
149+ } else {
150+ !current. contains ( * f)
151+ }
152+ } )
141153 . cloned ( )
142154 . collect ( )
143155}
@@ -371,6 +383,10 @@ pub fn find_reverse_dependencies(
371383}
372384
373385/// Purge graph data for changed/removed files and delete outgoing edges for reverse deps.
386+ ///
387+ /// Deletion order: analysis dependents → edges → nodes (matches `native_db::purge_files_data`).
388+ /// Analysis tables use join-based queries (node_id IN SELECT id FROM nodes) because they
389+ /// reference nodes by ID, not by file path directly.
374390pub fn purge_changed_files (
375391 conn : & Connection ,
376392 files_to_purge : & [ String ] ,
@@ -385,48 +401,36 @@ pub fn purge_changed_files(
385401 Err ( _) => return ,
386402 } ;
387403
388- // Purge nodes and edges for changed/removed files
389- if !files_to_purge. is_empty ( ) {
390- // Delete edges where source or target is in the purged files
391- if let Ok ( mut stmt) =
392- tx. prepare ( "DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)" )
393- {
394- for f in files_to_purge {
395- let _ = stmt. execute ( [ f] ) ;
396- }
397- }
398- if let Ok ( mut stmt) =
399- tx. prepare ( "DELETE FROM edges WHERE target_id IN (SELECT id FROM nodes WHERE file = ?)" )
400- {
401- for f in files_to_purge {
402- let _ = stmt. execute ( [ f] ) ;
403- }
404- }
405- // Delete nodes
406- if let Ok ( mut stmt) = tx. prepare ( "DELETE FROM nodes WHERE file = ?" ) {
407- for f in files_to_purge {
408- let _ = stmt. execute ( [ f] ) ;
409- }
410- }
411- // Delete analysis data
412- for table in & [
413- "function_complexity" ,
414- "cfg_blocks" ,
415- "cfg_edges" ,
416- "dataflow" ,
417- "ast_nodes" ,
418- "node_metrics" ,
419- ] {
420- let sql = format ! ( "DELETE FROM {table} WHERE file = ?" ) ;
421- if let Ok ( mut stmt) = tx. prepare ( & sql) {
422- for f in files_to_purge {
423- let _ = stmt. execute ( [ f] ) ;
404+ // Purge each file across all tables. Optional tables are silently skipped
405+ // if they don't exist. Order: analysis dependents → edges → nodes.
406+ let purge_sql: & [ ( & str , bool ) ] = & [
407+ // Analysis tables (optional — may not exist)
408+ ( "DELETE FROM embeddings WHERE node_id IN (SELECT id FROM nodes WHERE file = ?1)" , false ) ,
409+ ( "DELETE FROM cfg_edges WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?1)" , false ) ,
410+ ( "DELETE FROM cfg_blocks WHERE function_node_id IN (SELECT id FROM nodes WHERE file = ?1)" , false ) ,
411+ ( "DELETE FROM dataflow WHERE source_id IN (SELECT id FROM nodes WHERE file = ?1) OR target_id IN (SELECT id FROM nodes WHERE file = ?1)" , false ) ,
412+ ( "DELETE FROM function_complexity WHERE node_id IN (SELECT id FROM nodes WHERE file = ?1)" , false ) ,
413+ ( "DELETE FROM node_metrics WHERE node_id IN (SELECT id FROM nodes WHERE file = ?1)" , false ) ,
414+ ( "DELETE FROM ast_nodes WHERE file = ?1" , false ) ,
415+ // Core tables (errors logged)
416+ ( "DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?1) OR target_id IN (SELECT id FROM nodes WHERE file = ?1)" , true ) ,
417+ ( "DELETE FROM nodes WHERE file = ?1" , true ) ,
418+ ] ;
419+
420+ for file in files_to_purge {
421+ for & ( sql, required) in purge_sql {
422+ match tx. execute ( sql, rusqlite:: params![ file] ) {
423+ Ok ( _) => { }
424+ Err ( e) if required => {
425+ eprintln ! ( "[codegraph] purge failed for \" {file}\" : {e}" ) ;
424426 }
427+ Err ( _) => { } // optional table missing — skip
425428 }
426429 }
427430 }
428431
429- // Delete outgoing edges for reverse-dep files (they'll be re-built)
432+ // Delete outgoing edges for reverse-dep files (they'll be re-built).
433+ // These files keep their nodes but need outgoing edges rebuilt.
430434 if !reverse_dep_files. is_empty ( ) {
431435 if let Ok ( mut stmt) =
432436 tx. prepare ( "DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = ?)" )
@@ -487,12 +491,16 @@ pub fn heal_metadata(conn: &Connection, updates: &[MetadataUpdate]) {
487491/// Main entry point: detect changes using the tiered strategy.
488492///
489493/// Returns `None` for full builds (no file_hashes table or force flag).
494+ ///
495+ /// When `scoped_rel_paths` is provided, removal detection is limited to files
496+ /// within that scope — non-scoped files in the DB are left untouched.
490497pub fn detect_changes (
491498 conn : & Connection ,
492499 all_files : & [ String ] ,
493500 root_dir : & str ,
494501 incremental : bool ,
495502 force_full_rebuild : bool ,
503+ scoped_rel_paths : Option < & HashSet < String > > ,
496504) -> ChangeResult {
497505 if !incremental || force_full_rebuild {
498506 return ChangeResult {
@@ -539,7 +547,7 @@ pub fn detect_changes(
539547 }
540548 } ;
541549
542- let removed = detect_removed_files ( & existing, all_files, root_dir) ;
550+ let removed = detect_removed_files ( & existing, all_files, root_dir, scoped_rel_paths ) ;
543551
544552 // Try Tier 0 (journal) first
545553 if let Some ( result) = try_journal_tier ( conn, & existing, root_dir, & removed) {
@@ -597,7 +605,7 @@ mod tests {
597605 ) ;
598606
599607 let all_files = vec ! [ "/project/src/a.ts" . to_string( ) ] ;
600- let removed = detect_removed_files ( & existing, & all_files, "/project" ) ;
608+ let removed = detect_removed_files ( & existing, & all_files, "/project" , None ) ;
601609 assert_eq ! ( removed, vec![ "src/b.ts" ] ) ;
602610 }
603611}
0 commit comments