From 15d626cbbb4fc20419908686f75689f582097dbc Mon Sep 17 00:00:00 2001 From: le king fu Date: Sun, 1 Mar 2026 09:17:35 -0500 Subject: [PATCH] Fix migration checksum mismatch on startup Add repair_migrations Tauri command that deletes stale migration 1 checksum from _sqlx_migrations before Database.load(). Migration 1 is idempotent (CREATE IF NOT EXISTS) so re-applying is safe. Fixes "migration 1 was previously applied but has been modified". Co-Authored-By: Claude Opus 4.6 --- src-tauri/Cargo.toml | 1 + src-tauri/src/commands/profile_commands.rs | 43 ++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/services/db.ts | 10 +++++ 4 files changed, 55 insertions(+) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c067e62..0c4c1b2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -25,6 +25,7 @@ tauri-plugin-dialog = "2" tauri-plugin-updater = "2" tauri-plugin-process = "2" libsqlite3-sys = { version = "0.30", features = ["bundled"] } +rusqlite = { version = "0.32", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" diff --git a/src-tauri/src/commands/profile_commands.rs b/src-tauri/src/commands/profile_commands.rs index b4c2bce..dd667f1 100644 --- a/src-tauri/src/commands/profile_commands.rs +++ b/src-tauri/src/commands/profile_commands.rs @@ -155,3 +155,46 @@ pub fn verify_pin(pin: String, stored_hash: String) -> Result { fn hex_encode(bytes: &[u8]) -> String { bytes.iter().map(|b| format!("{:02x}", b)).collect() } + +/// Repair migration checksums for a profile database. +/// Deletes migration records that have mismatched checksums so they can be re-applied. +/// This fixes the "migration X was previously applied but has been modified" error. +#[tauri::command] +pub fn repair_migrations(app: tauri::AppHandle, db_filename: String) -> Result { + let app_dir = app + .path() + .app_data_dir() + .map_err(|e| format!("Cannot get app data dir: {}", e))?; + let db_path = app_dir.join(&db_filename); + + if !db_path.exists() { + return Ok(false); + } + + let conn = rusqlite::Connection::open(&db_path) + .map_err(|e| format!("Cannot open database: {}", e))?; + + // Check if _sqlx_migrations table exists + let table_exists: bool = conn + .query_row( + "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name='_sqlx_migrations'", + [], + |row| row.get(0), + ) + .unwrap_or(false); + + if !table_exists { + return Ok(false); + } + + // Delete migration 1 record so it can be cleanly re-applied + // Migration 1 is idempotent (CREATE IF NOT EXISTS + INSERT OR IGNORE) + let deleted = conn + .execute( + "DELETE FROM _sqlx_migrations WHERE version = 1", + [], + ) + .map_err(|e| format!("Cannot repair migrations: {}", e))?; + + Ok(deleted > 0) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e8ef87a..401ced3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -113,6 +113,7 @@ pub fn run() { commands::get_new_profile_init_sql, commands::hash_pin, commands::verify_pin, + commands::repair_migrations, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/services/db.ts b/src/services/db.ts index 00ab9d5..129f4a7 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -1,4 +1,5 @@ import Database from "@tauri-apps/plugin-sql"; +import { invoke } from "@tauri-apps/api/core"; let dbInstance: Database | null = null; @@ -14,6 +15,15 @@ export async function connectToProfile(dbFilename: string): Promise { await dbInstance.close(); dbInstance = null; } + // Repair migration checksums before loading (fixes "migration was modified" error) + try { + const repaired = await invoke("repair_migrations", { dbFilename }); + if (repaired) { + console.warn("Migration checksums repaired for", dbFilename); + } + } catch (e) { + console.error("Migration repair failed:", e); + } dbInstance = await Database.load(`sqlite:${dbFilename}`); }