Browse Source

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.
pull/188/head
Jose Olarte III 10 hours ago
parent
commit
f31a76b816
  1. 96
      src/db-sql/migration.ts
  2. 21
      src/services/migrationService.ts

96
src/db-sql/migration.ts

@ -183,6 +183,27 @@ const MIGRATIONS = [
DELETE FROM settings WHERE accountDid IS NULL; DELETE FROM settings WHERE accountDid IS NULL;
UPDATE settings SET activeDid = 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<T>(
? ((accountsResult as DatabaseResult).values?.[0]?.[0] as number) ? ((accountsResult as DatabaseResult).values?.[0]?.[0] as number)
: 0; : 0;
// 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( const activeResult = await sqlQuery(
"SELECT activeDid FROM active_identity WHERE id = 1", "SELECT activeDid FROM active_identity WHERE id = 1",
); );
const activeDid = activeDid =
activeResult && (activeResult as DatabaseResult).values activeResult && (activeResult as DatabaseResult).values
? ((activeResult as DatabaseResult).values?.[0]?.[0] as string) ? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
: null; : null;
} catch (recoveryError) {
logger.error(
"[Migration] Recovery failed - active_identity table still not accessible",
recoveryError,
);
}
}
}
if (accountsCount > 0 && (!activeDid || activeDid === "")) { if (accountsCount > 0 && (!activeDid || activeDid === "")) {
logger.debug("[Migration] Auto-selecting first account as active"); logger.debug("[Migration] Auto-selecting first account as active");

21
src/services/migrationService.ts

@ -73,6 +73,8 @@ interface Migration {
name: string; name: string;
/** SQL statement(s) to execute for this migration */ /** SQL statement(s) to execute for this migration */
sql: string; sql: string;
/** Optional array of individual SQL statements for better error handling */
statements?: string[];
} }
/** /**
@ -676,11 +678,30 @@ export async function runMigrations<T>(
try { try {
// Execute the migration SQL // Execute the migration SQL
migrationLog(`🔧 [Migration] Executing SQL for: ${migration.name}`); migrationLog(`🔧 [Migration] Executing SQL for: ${migration.name}`);
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}`); migrationLog(`🔧 [Migration] SQL content: ${migration.sql}`);
const execResult = await sqlExec(migration.sql); const execResult = await sqlExec(migration.sql);
migrationLog( migrationLog(
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`, `🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
); );
}
// Validate the migration was applied correctly // Validate the migration was applied correctly
const validation = await validateMigrationApplication( const validation = await validateMigrationApplication(

Loading…
Cancel
Save