forked from trent_larson/crowd-funder-for-time-pwa
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.
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
const activeResult = await sqlQuery(
|
// Check if active_identity table exists, and if not, try to recover
|
||||||
"SELECT activeDid FROM active_identity WHERE id = 1",
|
let activeDid: string | null = null;
|
||||||
);
|
try {
|
||||||
const activeDid =
|
const activeResult = await sqlQuery(
|
||||||
activeResult && (activeResult as DatabaseResult).values
|
"SELECT activeDid FROM active_identity WHERE id = 1",
|
||||||
? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
|
);
|
||||||
: null;
|
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 === "")) {
|
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
|
||||||
logger.debug("[Migration] Auto-selecting first account as active");
|
logger.debug("[Migration] Auto-selecting first account as active");
|
||||||
|
|||||||
@@ -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}`);
|
||||||
migrationLog(`🔧 [Migration] SQL content: ${migration.sql}`);
|
|
||||||
const execResult = await sqlExec(migration.sql);
|
if (migration.statements && migration.statements.length > 0) {
|
||||||
migrationLog(
|
// Execute individual statements for better error handling
|
||||||
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
|
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
|
// Validate the migration was applied correctly
|
||||||
const validation = await validateMigrationApplication(
|
const validation = await validateMigrationApplication(
|
||||||
|
|||||||
Reference in New Issue
Block a user