From 88f21dfd1d19624b666eddce0ddb64d809c32690 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 30 Jun 2025 07:15:20 +0000 Subject: [PATCH] feat: Implement comprehensive migration validation and integrity checking - Add multi-layered migration validation strategy with schema detection - Implement database integrity checker that validates all core tables and columns - Add schema-based migration skipping to prevent re-running applied migrations - Enhanced error handling for duplicate table/column scenarios with validation - Add comprehensive logging for migration tracking and database state verification - Include final validation step to ensure all migrations are properly recorded The system now properly: 1. Checks if migrations are recorded in the migrations table 2. Validates actual schema exists before attempting to apply migrations 3. Handles edge cases where schema exists but isn't recorded 4. Provides detailed integrity checking of database structure 5. Eliminates SQL errors from duplicate table/column creation attempts Migration tracking is now working correctly with both migrations properly recorded. --- src/services/migrationService.ts | 144 +++++++++++++++++- .../platforms/CapacitorPlatformService.ts | 83 +++++++++- 2 files changed, 221 insertions(+), 6 deletions(-) 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); + } } /**