From f31a76b816b2c03ebea7e57893f29f5984942df9 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 16 Sep 2025 20:14:58 +0800 Subject: [PATCH] fix: resolve iOS migration 004 failure with enhanced error handling - Fix multi-statement SQL execution issue in Capacitor SQLite - Add individual statement execution for migration 004_active_identity_management - Implement automatic recovery for missing active_identity table - Enhance migration system with better error handling and logging Problem: Migration 004 was marked as applied but active_identity table wasn't created due to multi-statement SQL execution failing silently in Capacitor SQLite. Solution: - Extended Migration interface with optional statements array - Modified migration execution to handle individual statements - Added bootstrapping hook recovery for missing tables - Enhanced logging for better debugging Files changed: - src/services/migrationService.ts: Enhanced migration execution logic - src/db-sql/migration.ts: Added recovery mechanism and individual statements This fix ensures the app automatically recovers from the current broken state and prevents similar issues in future migrations. --- src/db-sql/migration.ts | 108 +++++++++++++++++++++++++++++-- src/services/migrationService.ts | 31 +++++++-- 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index 314e8b93..4b541cf8 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -183,6 +183,27 @@ const MIGRATIONS = [ DELETE FROM settings WHERE accountDid IS NULL; UPDATE settings SET activeDid = NULL; `, + // Split into individual statements for better error handling + statements: [ + "PRAGMA foreign_keys = ON", + "CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)", + `CREATE TABLE IF NOT EXISTS active_identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT, + lastUpdated TEXT NOT NULL DEFAULT (datetime('now')) + )`, + "CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)", + `INSERT INTO active_identity (id, activeDid, lastUpdated) + SELECT 1, NULL, datetime('now') + WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`, + `UPDATE active_identity + SET activeDid = (SELECT activeDid FROM settings WHERE id = 1), + lastUpdated = datetime('now') + WHERE id = 1 + AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`, + "DELETE FROM settings WHERE accountDid IS NULL", + "UPDATE settings SET activeDid = NULL", + ], }, ]; @@ -216,13 +237,86 @@ export async function runMigrations( ? ((accountsResult as DatabaseResult).values?.[0]?.[0] as number) : 0; - const activeResult = await sqlQuery( - "SELECT activeDid FROM active_identity WHERE id = 1", - ); - const activeDid = - activeResult && (activeResult as DatabaseResult).values - ? ((activeResult as DatabaseResult).values?.[0]?.[0] as string) - : null; + // Check if active_identity table exists, and if not, try to recover + let activeDid: string | null = null; + try { + const activeResult = await sqlQuery( + "SELECT activeDid FROM active_identity WHERE id = 1", + ); + activeDid = + activeResult && (activeResult as DatabaseResult).values + ? ((activeResult as DatabaseResult).values?.[0]?.[0] as string) + : null; + } catch (error) { + // Table doesn't exist - this means migration 004 failed but was marked as applied + logger.warn( + "[Migration] active_identity table missing, attempting recovery", + ); + + // Check if migration 004 is marked as applied + const migrationResult = await sqlQuery( + "SELECT name FROM migrations WHERE name = '004_active_identity_management'", + ); + const isMigrationMarked = + migrationResult && (migrationResult as DatabaseResult).values + ? ((migrationResult as DatabaseResult).values?.length ?? 0) > 0 + : false; + + if (isMigrationMarked) { + logger.warn( + "[Migration] Migration 004 marked as applied but table missing - recreating table", + ); + + // Recreate the active_identity table using the individual statements + const statements = [ + "PRAGMA foreign_keys = ON", + "CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)", + `CREATE TABLE IF NOT EXISTS active_identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT, + lastUpdated TEXT NOT NULL DEFAULT (datetime('now')) + )`, + "CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)", + `INSERT INTO active_identity (id, activeDid, lastUpdated) + SELECT 1, NULL, datetime('now') + WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`, + `UPDATE active_identity + SET activeDid = (SELECT activeDid FROM settings WHERE id = 1), + lastUpdated = datetime('now') + WHERE id = 1 + AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`, + "DELETE FROM settings WHERE accountDid IS NULL", + "UPDATE settings SET activeDid = NULL", + ]; + + for (const statement of statements) { + try { + await sqlExec(statement); + } catch (stmtError) { + logger.warn( + `[Migration] Recovery statement failed: ${statement}`, + stmtError, + ); + } + } + + // Try to get activeDid again after recovery + try { + const activeResult = await sqlQuery( + "SELECT activeDid FROM active_identity WHERE id = 1", + ); + activeDid = + activeResult && (activeResult as DatabaseResult).values + ? ((activeResult as DatabaseResult).values?.[0]?.[0] as string) + : null; + } catch (recoveryError) { + logger.error( + "[Migration] Recovery failed - active_identity table still not accessible", + recoveryError, + ); + } + } + } if (accountsCount > 0 && (!activeDid || activeDid === "")) { logger.debug("[Migration] Auto-selecting first account as active"); diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index e75b777f..390ad5a5 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -73,6 +73,8 @@ interface Migration { name: string; /** SQL statement(s) to execute for this migration */ sql: string; + /** Optional array of individual SQL statements for better error handling */ + statements?: string[]; } /** @@ -676,11 +678,30 @@ export async function runMigrations( try { // Execute the migration SQL migrationLog(`🔧 [Migration] Executing SQL for: ${migration.name}`); - migrationLog(`🔧 [Migration] SQL content: ${migration.sql}`); - const execResult = await sqlExec(migration.sql); - migrationLog( - `🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`, - ); + + if (migration.statements && migration.statements.length > 0) { + // Execute individual statements for better error handling + migrationLog( + `🔧 [Migration] Executing ${migration.statements.length} individual statements`, + ); + for (let i = 0; i < migration.statements.length; i++) { + const statement = migration.statements[i]; + migrationLog( + `🔧 [Migration] Statement ${i + 1}/${migration.statements.length}: ${statement}`, + ); + const execResult = await sqlExec(statement); + migrationLog( + `🔧 [Migration] Statement ${i + 1} result: ${JSON.stringify(execResult)}`, + ); + } + } else { + // Execute as single SQL block (legacy behavior) + migrationLog(`🔧 [Migration] SQL content: ${migration.sql}`); + const execResult = await sqlExec(migration.sql); + migrationLog( + `🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`, + ); + } // Validate the migration was applied correctly const validation = await validateMigrationApplication(