diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index eda0a7e8..457b9f79 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -133,16 +133,15 @@ const MIGRATIONS = [ { name: "003_active_did_separation", sql: ` - -- CONSOLIDATED MIGRATION: Combines original migrations 003, 004, 005, and 006 - -- This migration handles the complete separation of activeDid from settings - -- and establishes proper data integrity constraints + -- SIMPLIFIED MIGRATION: Create active_identity table and migrate data + -- This migration handles the separation of activeDid from settings + -- using a simpler, more reliable approach that works on all SQLite versions -- Create new active_identity table with proper constraints CREATE TABLE IF NOT EXISTS active_identity ( id INTEGER PRIMARY KEY CHECK (id = 1), - activeDid TEXT NOT NULL, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE + activeDid TEXT NOT NULL DEFAULT '', + lastUpdated TEXT NOT NULL DEFAULT (datetime('now')) ); -- Add performance indexes @@ -154,36 +153,14 @@ const MIGRATIONS = [ -- MIGRATE EXISTING DATA: Copy activeDid from settings to active_identity -- This prevents data loss when migration runs on existing databases + -- Use a more robust approach that handles missing columns gracefully 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 != ''); - - -- Remove activeDid column from settings table (moved to active_identity) - -- Note: SQLite doesn't support DROP COLUMN in older versions - -- This migration will be skipped if DROP COLUMN is not supported - -- The activeDid column will remain but won't be used by the application - ALTER TABLE settings DROP COLUMN activeDid; - - -- Eliminate MASTER_SETTINGS_KEY concept - remove confusing id=1 row - -- This creates clean separation: active_identity for current identity, settings for identity config - DELETE FROM settings WHERE id = 1 AND accountDid IS NULL; - - -- Reset auto-increment to start from 1 again - DELETE FROM sqlite_sequence WHERE name = 'settings'; - - -- Add unique constraint to prevent duplicate accountDid values - -- This ensures data integrity: each identity can only have one settings record - DELETE FROM settings - WHERE id NOT IN ( - SELECT MAX(id) - FROM settings - WHERE accountDid IS NOT NULL - GROUP BY accountDid - ) AND accountDid IS NOT NULL; - - CREATE UNIQUE INDEX IF NOT EXISTS idx_settings_accountDid_unique ON settings(accountDid); + SET activeDid = COALESCE( + (SELECT activeDid FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''), + '' + ), + lastUpdated = datetime('now') + WHERE id = 1; `, }, ]; diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 93e769f4..9869512d 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -280,6 +280,40 @@ async function validateMigrationApplication( error, ); } + } else if (migration.name === "003_active_did_separation") { + // Validate active_identity table exists and has correct structure + try { + // Check that active_identity table exists + const activeIdentityResult = await sqlQuery( + `SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`, + ); + const hasActiveIdentityTable = + (activeIdentityResult as unknown as { values: unknown[][] })?.values?.length > 0 || + (Array.isArray(activeIdentityResult) && activeIdentityResult.length > 0); + + if (!hasActiveIdentityTable) { + validation.isValid = false; + validation.errors.push(`Table active_identity missing`); + } + + // Check that active_identity has the expected structure + try { + await sqlQuery(`SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`); + validation.hasExpectedColumns = true; + } catch (error) { + validation.isValid = false; + validation.errors.push(`active_identity table missing expected columns`); + } + + validation.tableExists = hasActiveIdentityTable; + } catch (error) { + validation.isValid = false; + validation.errors.push(`Validation error for active_did_separation: ${error}`); + logger.error( + `❌ [Migration-Validation] Validation failed for ${migration.name}:`, + error, + ); + } } // Add validation for future migrations here @@ -343,6 +377,35 @@ async function isSchemaAlreadyPresent( // Reduced logging - only log on error return false; } + } else if (migration.name === "003_active_did_separation") { + // Check if active_identity table exists and has correct structure + try { + // Check that active_identity table exists + const activeIdentityResult = await sqlQuery( + `SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`, + ); + const hasActiveIdentityTable = + (activeIdentityResult as unknown as { values: unknown[][] })?.values?.length > 0 || + (Array.isArray(activeIdentityResult) && activeIdentityResult.length > 0); + + if (!hasActiveIdentityTable) { + return false; + } + + // Check that active_identity has the expected structure + try { + await sqlQuery(`SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`); + return true; + } catch (error) { + return false; + } + } catch (error) { + logger.error( + `🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`, + error, + ); + return false; + } } // Add schema checks for future migrations here