diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 6da03c14..fb61e4de 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -18,6 +18,16 @@ interface Migration { sql: string; } +/** + * Migration validation result + */ +interface MigrationValidation { + isValid: boolean; + tableExists: boolean; + hasExpectedColumns: boolean; + errors: string[]; +} + /** * Migration registry to store and manage database migrations */ @@ -65,6 +75,89 @@ export function registerMigration(migration: Migration): void { migrationRegistry.registerMigration(migration); } +/** + * Validate that a migration was successfully applied by checking schema + */ +async function validateMigrationApplication( + migration: Migration, + sqlQuery: (sql: string, params?: unknown[]) => Promise, +): Promise { + const validation: MigrationValidation = { + isValid: true, + tableExists: false, + hasExpectedColumns: false, + errors: [] + }; + + try { + if (migration.name === "001_initial") { + // Validate core tables exist + const tables = ['accounts', 'secret', 'settings', 'contacts', 'logs', 'temp']; + + for (const tableName of tables) { + try { + await sqlQuery(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`); + console.log(`✅ [Migration-Validation] Table ${tableName} exists`); + } catch (error) { + validation.isValid = false; + validation.errors.push(`Table ${tableName} missing`); + console.error(`❌ [Migration-Validation] Table ${tableName} missing:`, error); + } + } + } else if (migration.name === "002_add_iViewContent_to_contacts") { + // Validate iViewContent column exists + try { + await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`); + validation.hasExpectedColumns = true; + console.log(`✅ [Migration-Validation] Column iViewContent exists in contacts table`); + } catch (error) { + validation.isValid = false; + validation.errors.push(`Column iViewContent missing from contacts table`); + console.error(`❌ [Migration-Validation] Column iViewContent missing:`, error); + } + } + } catch (error) { + validation.isValid = false; + validation.errors.push(`Validation error: ${error}`); + console.error(`❌ [Migration-Validation] Validation failed for ${migration.name}:`, error); + } + + return validation; +} + +/** + * Check if migration is already applied by examining actual schema + */ +async function isSchemaAlreadyPresent( + migration: Migration, + sqlQuery: (sql: string, params?: unknown[]) => Promise, +): Promise { + try { + if (migration.name === "001_initial") { + // Check if accounts table exists (primary indicator) + const result = await sqlQuery(`SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'`) as any; + const hasTable = result?.values?.length > 0 || (Array.isArray(result) && result.length > 0); + console.log(`🔍 [Migration-Schema] Initial migration schema check - accounts table exists: ${hasTable}`); + return hasTable; + } else if (migration.name === "002_add_iViewContent_to_contacts") { + // Check if iViewContent column exists + try { + await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`); + console.log(`🔍 [Migration-Schema] iViewContent column already exists`); + return true; + } catch (error) { + console.log(`🔍 [Migration-Schema] iViewContent column does not exist`); + return false; + } + } + } catch (error) { + console.log(`🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`, error); + return false; + } + + return false; +} + /** * Run all registered migrations against the database * @@ -123,12 +216,36 @@ export async function runMigrations( // Run each migration that hasn't been applied yet for (const migration of migrations) { - if (appliedMigrations.has(migration.name)) { + // First check: Is it recorded as applied in migrations table? + const isRecordedAsApplied = appliedMigrations.has(migration.name); + + // Second check: Does the schema already exist? + const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery); + + console.log(`🔍 [Migration] ${migration.name} - Recorded: ${isRecordedAsApplied}, Schema: ${isSchemaPresent}`); + + if (isRecordedAsApplied) { console.log(`⏭️ [Migration] Skipping already applied: ${migration.name}`); skippedCount++; continue; } + if (isSchemaPresent) { + console.log(`🔄 [Migration] Schema exists but not recorded. Marking ${migration.name} as applied...`); + try { + const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ + migration.name, + ]); + console.log(`✅ [Migration] Migration record inserted:`, insertResult); + console.log(`✅ [Migration] Marked existing schema as applied: ${migration.name}`); + skippedCount++; + continue; + } catch (insertError) { + console.warn(`⚠️ [Migration] Could not record existing schema ${migration.name}:`, insertError); + // Continue with normal migration process + } + } + console.log(`🔄 [Migration] Applying migration: ${migration.name}`); try { @@ -137,6 +254,12 @@ export async function runMigrations( await sqlExec(migration.sql); console.log(`✅ [Migration] SQL executed successfully for ${migration.name}`); + // Validate the migration was applied correctly + const validation = await validateMigrationApplication(migration, sqlQuery); + if (!validation.isValid) { + console.warn(`⚠️ [Migration] Validation failed for ${migration.name}:`, validation.errors); + } + // Record that the migration was applied console.log(`📝 [Migration] Recording migration ${migration.name} as applied...`); const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ @@ -162,10 +285,15 @@ export async function runMigrations( errorMessage.includes("already exists") || errorMessage.includes("table") && errorMessage.includes("already exists") ) { - console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Marking as complete.`); - logger.warn( - `[MigrationService] Migration ${migration.name} appears to be already applied (${errorMessage}). Marking as complete.`, - ); + console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`); + + // Validate the existing schema + const validation = await validateMigrationApplication(migration, sqlQuery); + if (validation.isValid) { + console.log(`✅ [Migration] Schema validation passed for ${migration.name}`); + } else { + console.warn(`⚠️ [Migration] Schema validation failed for ${migration.name}:`, validation.errors); + } // Mark the migration as applied since the schema change already exists try { @@ -199,6 +327,12 @@ export async function runMigrations( } } + // Final validation: Verify all migrations are properly recorded + console.log("🔍 [Migration] Final validation - checking migrations table..."); + const finalMigrationsResult = await sqlQuery("SELECT name FROM migrations"); + const finalAppliedMigrations = extractMigrationNames(finalMigrationsResult); + console.log("📋 [Migration] Final applied migrations:", Array.from(finalAppliedMigrations)); + console.log(`🎉 [Migration] Migration process complete! Applied: ${appliedCount}, Skipped: ${skippedCount}`); } catch (error) { console.error("💥 [Migration] Migration process failed:", error); diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 3c9f4dcf..e9f1b6d2 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -286,7 +286,88 @@ export class CapacitorPlatformService implements PlatformService { return new Set(names); }; - await runMigrations(sqlExec, sqlQuery, extractMigrationNames); + try { + await runMigrations(sqlExec, sqlQuery, extractMigrationNames); + + // After migrations, run integrity check + await this.verifyDatabaseIntegrity(); + } catch (error) { + console.error(`❌ [CapacitorMigration] Migration failed:`, error); + // Still try to verify what we have + await this.verifyDatabaseIntegrity(); + throw error; + } + } + + /** + * Verify database integrity and migration status + */ + private async verifyDatabaseIntegrity(): Promise { + if (!this.db) { + console.error(`❌ [DB-Integrity] Database not initialized`); + return; + } + + console.log(`🔍 [DB-Integrity] Starting database integrity check...`); + + try { + // Check migrations table + const migrationsResult = await this.db.query("SELECT name, applied_at FROM migrations ORDER BY applied_at"); + console.log(`📊 [DB-Integrity] Applied migrations:`, migrationsResult); + + // Check core tables exist + const coreTableNames = ['accounts', 'secret', 'settings', 'contacts', 'logs', 'temp']; + const existingTables: string[] = []; + + for (const tableName of coreTableNames) { + try { + const tableCheck = await this.db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`); + if (tableCheck.values && tableCheck.values.length > 0) { + existingTables.push(tableName); + console.log(`✅ [DB-Integrity] Table ${tableName} exists`); + } else { + console.error(`❌ [DB-Integrity] Table ${tableName} missing`); + } + } catch (error) { + console.error(`❌ [DB-Integrity] Error checking table ${tableName}:`, error); + } + } + + // Check contacts table schema (including iViewContent column) + if (existingTables.includes('contacts')) { + try { + const contactsSchema = await this.db.query("PRAGMA table_info(contacts)"); + console.log(`📊 [DB-Integrity] Contacts table schema:`, contactsSchema); + + const hasIViewContent = contactsSchema.values?.some((col: any) => + (col.name === 'iViewContent') || (Array.isArray(col) && col[1] === 'iViewContent') + ); + + if (hasIViewContent) { + console.log(`✅ [DB-Integrity] iViewContent column exists in contacts table`); + } else { + console.error(`❌ [DB-Integrity] iViewContent column missing from contacts table`); + } + } catch (error) { + console.error(`❌ [DB-Integrity] Error checking contacts schema:`, error); + } + } + + // Check for data integrity + try { + const accountCount = await this.db.query("SELECT COUNT(*) as count FROM accounts"); + const settingsCount = await this.db.query("SELECT COUNT(*) as count FROM settings"); + const contactsCount = await this.db.query("SELECT COUNT(*) as count FROM contacts"); + + console.log(`📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`); + } catch (error) { + console.error(`❌ [DB-Integrity] Error checking data counts:`, error); + } + + console.log(`✅ [DB-Integrity] Database integrity check completed`); + } catch (error) { + console.error(`❌ [DB-Integrity] Database integrity check failed:`, error); + } } /**