Browse Source

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.
streamline-attempt
Matthew Raymer 5 days ago
parent
commit
88f21dfd1d
  1. 144
      src/services/migrationService.ts
  2. 83
      src/services/platforms/CapacitorPlatformService.ts

144
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<T>(
migration: Migration,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<MigrationValidation> {
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<T>(
migration: Migration,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
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<T>(
// 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<T>(
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<T>(
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<T>(
}
}
// 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);

83
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<void> {
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);
}
}
/**

Loading…
Cancel
Save