@@ -6,20 +6,61 @@ use rusqlite::{Connection as RusqliteConnection, OptionalExtension};
66use sqlx:: sqlite:: { SqliteConnectOptions , SqlitePoolOptions } ;
77use sqlx:: { Row , SqlitePool } ;
88
9- const MIGRATION_FILES : [ & str ; 10 ] = [
10- "2026-04-06_create_ets_profiles.sql" ,
11- "2026-04-06_create_ets_saves.sql" ,
12- "2026-04-06_create_ets_job_links.sql" ,
13- "2026-04-06_create_ets_job_link_audit.sql" ,
14- "2026-04-06_create_vtc_job_ledger.sql" ,
15- "2026-04-06_create_ets2_datasets.sql" ,
16- "2026-04-06_create_ets_save_snapshot.sql" ,
17- "2026-04-06_add_resolved_tokens_to_ets_job_links.sql" ,
18- "2026-04-06_add_cargo_resolution_to_ets_job_links.sql" ,
19- "2026-04-07_add_vtc_local_persistence.sql" ,
9+ const APP_RUNTIME_DIR_NAME : & str = "SimNexusHub" ;
10+ const RUNTIME_MIGRATIONS : [ ( & str , & str ) ; 10 ] = [
11+ (
12+ "2026-04-06_create_ets_profiles.sql" ,
13+ include_str ! ( "migrations/2026-04-06_create_ets_profiles.sql" ) ,
14+ ) ,
15+ (
16+ "2026-04-06_create_ets_saves.sql" ,
17+ include_str ! ( "migrations/2026-04-06_create_ets_saves.sql" ) ,
18+ ) ,
19+ (
20+ "2026-04-06_create_ets_job_links.sql" ,
21+ include_str ! ( "migrations/2026-04-06_create_ets_job_links.sql" ) ,
22+ ) ,
23+ (
24+ "2026-04-06_create_ets_job_link_audit.sql" ,
25+ include_str ! ( "migrations/2026-04-06_create_ets_job_link_audit.sql" ) ,
26+ ) ,
27+ (
28+ "2026-04-06_create_vtc_job_ledger.sql" ,
29+ include_str ! ( "migrations/2026-04-06_create_vtc_job_ledger.sql" ) ,
30+ ) ,
31+ (
32+ "2026-04-06_create_ets2_datasets.sql" ,
33+ include_str ! ( "migrations/2026-04-06_create_ets2_datasets.sql" ) ,
34+ ) ,
35+ (
36+ "2026-04-06_create_ets_save_snapshot.sql" ,
37+ include_str ! ( "migrations/2026-04-06_create_ets_save_snapshot.sql" ) ,
38+ ) ,
39+ (
40+ "2026-04-06_add_resolved_tokens_to_ets_job_links.sql" ,
41+ include_str ! ( "migrations/2026-04-06_add_resolved_tokens_to_ets_job_links.sql" ) ,
42+ ) ,
43+ (
44+ "2026-04-06_add_cargo_resolution_to_ets_job_links.sql" ,
45+ include_str ! ( "migrations/2026-04-06_add_cargo_resolution_to_ets_job_links.sql" ) ,
46+ ) ,
47+ (
48+ "2026-04-07_add_vtc_local_persistence.sql" ,
49+ include_str ! ( "migrations/2026-04-07_add_vtc_local_persistence.sql" ) ,
50+ ) ,
2051] ;
2152
2253pub fn app_db_path ( ) -> PathBuf {
54+ app_runtime_dir ( ) . join ( "app.sqlite" )
55+ }
56+
57+ fn app_runtime_dir ( ) -> PathBuf {
58+ dirs:: data_local_dir ( )
59+ . unwrap_or_else ( || std:: env:: current_dir ( ) . unwrap_or_else ( |_| PathBuf :: from ( "." ) ) )
60+ . join ( APP_RUNTIME_DIR_NAME )
61+ }
62+
63+ fn legacy_repo_db_path ( ) -> PathBuf {
2364 Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) )
2465 . parent ( )
2566 . unwrap_or_else ( || Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) ) )
@@ -29,10 +70,20 @@ pub fn app_db_path() -> PathBuf {
2970
3071pub async fn init_sqlite ( ) -> Result < SqlitePool , String > {
3172 let db_path = app_db_path ( ) ;
73+ let legacy_db_path = legacy_repo_db_path ( ) ;
74+
75+ crate :: dev_log!( "[db] Resolved runtime DB path: {}" , db_path. display( ) ) ;
76+ crate :: dev_log!(
77+ "[db] Legacy repo DB candidate: {} (exists={})" ,
78+ legacy_db_path. display( ) ,
79+ legacy_db_path. exists( )
80+ ) ;
81+
3282 validate_sqlite_extension ( & db_path) ?;
3383 if let Some ( parent) = db_path. parent ( ) {
3484 std:: fs:: create_dir_all ( parent) . map_err ( |error| error. to_string ( ) ) ?;
3585 }
86+ migrate_legacy_db_if_needed ( & db_path, & legacy_db_path) ?;
3687 run_runtime_migrations ( & db_path) ?;
3788
3889 let options = SqliteConnectOptions :: new ( )
@@ -81,6 +132,63 @@ pub fn validate_sqlite_extension(path: &Path) -> Result<(), String> {
81132 Ok ( ( ) )
82133}
83134
135+ fn migrate_legacy_db_if_needed ( db_path : & Path , legacy_db_path : & Path ) -> Result < ( ) , String > {
136+ if db_path. exists ( ) {
137+ crate :: dev_log!( "[db] Runtime DB already exists: {}" , db_path. display( ) ) ;
138+ return Ok ( ( ) ) ;
139+ }
140+
141+ if !legacy_db_path. exists ( ) {
142+ crate :: dev_log!(
143+ "[db] No legacy DB to migrate from: {}" ,
144+ legacy_db_path. display( )
145+ ) ;
146+ return Ok ( ( ) ) ;
147+ }
148+
149+ if let Some ( parent) = db_path. parent ( ) {
150+ std:: fs:: create_dir_all ( parent) . map_err ( |error| error. to_string ( ) ) ?;
151+ }
152+
153+ std:: fs:: copy ( legacy_db_path, db_path) . map_err ( |error| {
154+ format ! (
155+ "copy legacy db {} -> {} failed: {}" ,
156+ legacy_db_path. display( ) ,
157+ db_path. display( ) ,
158+ error
159+ )
160+ } ) ?;
161+ crate :: dev_log!(
162+ "[db] Migrated legacy DB: {} -> {}" ,
163+ legacy_db_path. display( ) ,
164+ db_path. display( )
165+ ) ;
166+
167+ for suffix in [ "-wal" , "-shm" ] {
168+ let legacy_sidecar = PathBuf :: from ( format ! ( "{}{}" , legacy_db_path. display( ) , suffix) ) ;
169+ if !legacy_sidecar. exists ( ) {
170+ continue ;
171+ }
172+
173+ let target_sidecar = PathBuf :: from ( format ! ( "{}{}" , db_path. display( ) , suffix) ) ;
174+ std:: fs:: copy ( & legacy_sidecar, & target_sidecar) . map_err ( |error| {
175+ format ! (
176+ "copy legacy sqlite sidecar {} -> {} failed: {}" ,
177+ legacy_sidecar. display( ) ,
178+ target_sidecar. display( ) ,
179+ error
180+ )
181+ } ) ?;
182+ crate :: dev_log!(
183+ "[db] Migrated SQLite sidecar: {} -> {}" ,
184+ legacy_sidecar. display( ) ,
185+ target_sidecar. display( )
186+ ) ;
187+ }
188+
189+ Ok ( ( ) )
190+ }
191+
84192#[ derive( Debug , Clone , serde:: Serialize ) ]
85193#[ serde( rename_all = "camelCase" ) ]
86194pub struct SqliteInfoDto {
@@ -191,14 +299,8 @@ async fn count_rows(pool: &SqlitePool, table: &str) -> Result<i64, String> {
191299 . map_err ( |error| error. to_string ( ) )
192300}
193301
194- fn migration_directory ( ) -> PathBuf {
195- Path :: new ( env ! ( "CARGO_MANIFEST_DIR" ) )
196- . join ( "src" )
197- . join ( "db" )
198- . join ( "migrations" )
199- }
200-
201302fn run_runtime_migrations ( db_path : & Path ) -> Result < ( ) , String > {
303+ crate :: dev_log!( "[db] Running runtime migrations for {}" , db_path. display( ) ) ;
202304 let mut connection = RusqliteConnection :: open ( db_path) . map_err ( |error| error. to_string ( ) ) ?;
203305 connection
204306 . busy_timeout ( std:: time:: Duration :: from_secs ( 5 ) )
@@ -218,9 +320,8 @@ fn run_runtime_migrations(db_path: &Path) -> Result<(), String> {
218320 let tx = connection
219321 . transaction ( )
220322 . map_err ( |error| error. to_string ( ) ) ?;
221- let migration_dir = migration_directory ( ) ;
222323
223- for filename in MIGRATION_FILES {
324+ for ( filename, sql ) in RUNTIME_MIGRATIONS {
224325 let already_applied: Option < String > = tx
225326 . query_row (
226327 "SELECT filename FROM ets_feature_migrations WHERE filename = ?1" ,
@@ -233,25 +334,19 @@ fn run_runtime_migrations(db_path: &Path) -> Result<(), String> {
233334 continue ;
234335 }
235336
236- let migration_path = migration_dir. join ( filename) ;
237- let sql = std:: fs:: read_to_string ( & migration_path) . map_err ( |error| {
238- format ! (
239- "read migration {} failed: {}" ,
240- migration_path. display( ) ,
241- error
242- )
243- } ) ?;
244337 tx. execute_batch ( & sql)
245338 . map_err ( |error| format ! ( "apply migration {} failed: {}" , filename, error) ) ?;
246339 tx. execute (
247340 "INSERT INTO ets_feature_migrations (filename, applied_at_utc) VALUES (?1, ?2)" ,
248341 rusqlite:: params![ filename, Utc :: now( ) . to_rfc3339( ) ] ,
249342 )
250343 . map_err ( |error| format ! ( "record migration {} failed: {}" , filename, error) ) ?;
344+ crate :: dev_log!( "[db] Applied runtime migration: {}" , filename) ;
251345 }
252346
253347 tx. commit ( ) . map_err ( |error| error. to_string ( ) ) ?;
254348 ensure_runtime_columns ( & connection) ?;
349+ crate :: dev_log!( "[db] Runtime migrations complete" ) ;
255350 Ok ( ( ) )
256351}
257352
0 commit comments