@@ -701,39 +701,52 @@ impl CallGraphStore {
701701 callgraph_dir : PathBuf ,
702702 project_root : PathBuf ,
703703 ) -> Result < Option < Self > > {
704- Self :: open_ready_with_rebuild_policy ( callgraph_dir, project_root, true )
704+ Self :: open_ready_with_rebuild_policy ( callgraph_dir, project_root, true , true , true )
705+ }
706+
707+ /// Open a ready store for bounded maintenance work without repairing root
708+ /// metadata or starting a cold rebuild. A store that needs either action is
709+ /// reported as unavailable so a background build can own that work.
710+ pub fn open_ready ( callgraph_dir : PathBuf , project_root : PathBuf ) -> Result < Option < Self > > {
711+ Self :: open_ready_with_rebuild_policy ( callgraph_dir, project_root, false , false , false )
705712 }
706713
707714 pub fn open_ready_no_rebuild (
708715 callgraph_dir : PathBuf ,
709716 project_root : PathBuf ,
710717 ) -> Result < Option < Self > > {
711- Self :: open_ready_with_rebuild_policy ( callgraph_dir, project_root, false )
718+ Self :: open_ready_with_rebuild_policy ( callgraph_dir, project_root, false , true , true )
712719 }
713720
714721 fn open_ready_with_rebuild_policy (
715722 callgraph_dir : PathBuf ,
716723 project_root : PathBuf ,
717724 allow_cold_build : bool ,
725+ allow_root_repair : bool ,
726+ allow_borrow_only : bool ,
718727 ) -> Result < Option < Self > > {
719728 let project_key = crate :: search_index:: artifact_cache_key ( & project_root) ;
720729 let Some ( writer_lease) = acquire_writer_lease ( & callgraph_dir, & project_key, & project_root) ?
721730 else {
731+ if !allow_borrow_only {
732+ return Ok ( None ) ;
733+ }
722734 return Self :: open_readonly ( callgraph_dir, project_root)
723735 . map ( |store| store. map ( ReadonlyCallGraphStore :: into_inner) ) ;
724736 } ;
725737 let Some ( ( sqlite_path, generation) ) = resolve_ready_target ( & callgraph_dir, & project_key)
726738 else {
727739 return Ok ( None ) ;
728740 } ;
729- let OpenedStore { store, root_repair } = Self :: open_at_path (
741+ let OpenedStore { store, root_repair } = Self :: open_at_path_with_root_repair (
730742 project_root. clone ( ) ,
731743 project_key,
732744 sqlite_path,
733745 generation,
734746 true ,
735747 Some ( Arc :: clone ( & writer_lease) ) ,
736748 None ,
749+ allow_root_repair,
737750 ) ?;
738751 match root_repair {
739752 OpenRootRepair :: NeedsRebuild { .. } if allow_cold_build => {
@@ -1052,6 +1065,28 @@ impl CallGraphStore {
10521065 use_wal : bool ,
10531066 writer_lease : Option < Arc < crate :: root_cache:: WriterLease > > ,
10541067 read_marker : Option < crate :: root_cache:: ReadMarker > ,
1068+ ) -> Result < OpenedStore > {
1069+ Self :: open_at_path_with_root_repair (
1070+ project_root,
1071+ project_key,
1072+ sqlite_path,
1073+ generation,
1074+ use_wal,
1075+ writer_lease,
1076+ read_marker,
1077+ true ,
1078+ )
1079+ }
1080+
1081+ fn open_at_path_with_root_repair (
1082+ project_root : PathBuf ,
1083+ project_key : String ,
1084+ sqlite_path : PathBuf ,
1085+ generation : Option < String > ,
1086+ use_wal : bool ,
1087+ writer_lease : Option < Arc < crate :: root_cache:: WriterLease > > ,
1088+ read_marker : Option < crate :: root_cache:: ReadMarker > ,
1089+ allow_root_repair : bool ,
10551090 ) -> Result < OpenedStore > {
10561091 if let Some ( lease) = writer_lease. as_ref ( ) {
10571092 verify_writer_lease ( lease) ?;
@@ -1072,7 +1107,7 @@ impl CallGraphStore {
10721107 if let Some ( lease) = writer_lease. as_ref ( ) {
10731108 verify_writer_lease ( lease) ?;
10741109 }
1075- let root_repair = reconcile_workspace_roots ( & mut conn, & project_root) ?;
1110+ let root_repair = reconcile_workspace_roots ( & mut conn, & project_root, allow_root_repair ) ?;
10761111 let read_marker = match ( read_marker, generation. as_deref ( ) , sqlite_path. parent ( ) ) {
10771112 ( Some ( marker) , _, _) => Some ( marker) ,
10781113 ( None , Some ( label) , Some ( cache_dir) ) => {
@@ -4091,7 +4126,11 @@ const STORE_DATA_PATH_COLUMNS: &[(&str, &str)] = &[
40914126/// opener. That can make each clone rebuild on open when they alternate — bounded
40924127/// by open frequency — but each rebuild is correct for its opener, unlike silent
40934128/// cross-clone corruption.
4094- fn reconcile_workspace_roots ( conn : & mut Connection , project_root : & Path ) -> Result < OpenRootRepair > {
4129+ fn reconcile_workspace_roots (
4130+ conn : & mut Connection ,
4131+ project_root : & Path ,
4132+ allow_repair : bool ,
4133+ ) -> Result < OpenRootRepair > {
40954134 let roots = stored_workspace_roots ( conn) ?;
40964135 let current_root = project_root. display ( ) . to_string ( ) ;
40974136 if roots. is_empty ( ) || ( roots. len ( ) == 1 && roots[ 0 ] == current_root) {
@@ -4122,6 +4161,14 @@ fn reconcile_workspace_roots(conn: &mut Connection, project_root: &Path) -> Resu
41224161 }
41234162 }
41244163
4164+ if !allow_repair {
4165+ return Ok ( OpenRootRepair :: NeedsRebuild {
4166+ previous_roots : roots,
4167+ current_root,
4168+ reason : "workspace root metadata requires deferred repair" . to_string ( ) ,
4169+ } ) ;
4170+ }
4171+
41254172 let tx = conn. transaction ( ) ?;
41264173 tx. execute (
41274174 "UPDATE OR IGNORE backend_file_state
@@ -8945,6 +8992,33 @@ mod cold_build_insert_tests {
89458992 use std:: path:: { Path , PathBuf } ;
89468993 use tempfile:: tempdir;
89478994
8995+ #[ test]
8996+ fn nonrepairing_open_policy_leaves_moved_root_metadata_for_maintenance ( ) {
8997+ let dir = tempdir ( ) . unwrap ( ) ;
8998+ let previous_root = dir. path ( ) . join ( "previous-root" ) ;
8999+ let current_root = dir. path ( ) . join ( "current-root" ) ;
9000+ fs:: create_dir_all ( & previous_root) . unwrap ( ) ;
9001+ fs:: create_dir_all ( & current_root) . unwrap ( ) ;
9002+ fs:: remove_dir ( & previous_root) . unwrap ( ) ;
9003+ let mut conn = Connection :: open_in_memory ( ) . unwrap ( ) ;
9004+ initialize_schema ( & conn) . unwrap ( ) ;
9005+ conn. execute (
9006+ "INSERT INTO backend_file_state(
9007+ backend, workspace_root, file_path, content_hash, status, updated_at
9008+ ) VALUES ('rust', ?1, 'src/main.rs', 'hash', 'ready', 1)" ,
9009+ params ! [ previous_root. display( ) . to_string( ) ] ,
9010+ )
9011+ . unwrap ( ) ;
9012+
9013+ let repair = reconcile_workspace_roots ( & mut conn, & current_root, false ) . unwrap ( ) ;
9014+
9015+ assert ! ( matches!( repair, OpenRootRepair :: NeedsRebuild { .. } ) ) ;
9016+ assert_eq ! (
9017+ stored_workspace_roots( & conn) . unwrap( ) ,
9018+ vec![ previous_root. display( ) . to_string( ) ]
9019+ ) ;
9020+ }
9021+
89489022 #[ test]
89499023 fn sqlite_readonly_uri_percent_encodes_windows_paths ( ) {
89509024 assert_eq ! (
0 commit comments