@ -126,11 +126,11 @@ class MigrationRegistry {
* ` ` `
* ` ` `
* /
* /
registerMigration ( migration : Migration ) : void {
registerMigration ( migration : Migration ) : void {
if ( ! migration . name || migration . name . trim ( ) === '' ) {
if ( ! migration . name || migration . name . trim ( ) === "" ) {
throw new Error ( 'Migration name cannot be empty' ) ;
throw new Error ( "Migration name cannot be empty" ) ;
}
}
if ( this . migrations . some ( m = > m . name === migration . name ) ) {
if ( this . migrations . some ( ( m ) = > m . name === migration . name ) ) {
throw new Error ( ` Migration with name ' ${ migration . name } ' already exists ` ) ;
throw new Error ( ` Migration with name ' ${ migration . name } ' already exists ` ) ;
}
}
@ -233,36 +233,54 @@ async function validateMigrationApplication<T>(
isValid : true ,
isValid : true ,
tableExists : false ,
tableExists : false ,
hasExpectedColumns : false ,
hasExpectedColumns : false ,
errors : [ ]
errors : [ ] ,
} ;
} ;
try {
try {
if ( migration . name === "001_initial" ) {
if ( migration . name === "001_initial" ) {
// Validate core tables exist for initial migration
// Validate core tables exist for initial migration
const tables = [ 'accounts' , 'secret' , 'settings' , 'contacts' , 'logs' , 'temp' ] ;
const tables = [
"accounts" ,
"secret" ,
"settings" ,
"contacts" ,
"logs" ,
"temp" ,
] ;
for ( const tableName of tables ) {
for ( const tableName of tables ) {
try {
try {
await sqlQuery ( ` SELECT name FROM sqlite_master WHERE type='table' AND name=' ${ tableName } ' ` ) ;
await sqlQuery (
console . log ( ` ✅ [Migration-Validation] Table ${ tableName } exists ` ) ;
` SELECT name FROM sqlite_master WHERE type='table' AND name=' ${ tableName } ' ` ,
) ;
logger . log ( ` ✅ [Migration-Validation] Table ${ tableName } exists ` ) ;
} catch ( error ) {
} catch ( error ) {
validation . isValid = false ;
validation . isValid = false ;
validation . errors . push ( ` Table ${ tableName } missing ` ) ;
validation . errors . push ( ` Table ${ tableName } missing ` ) ;
console . error ( ` ❌ [Migration-Validation] Table ${ tableName } missing: ` , error ) ;
logger . error (
` ❌ [Migration-Validation] Table ${ tableName } missing: ` ,
error ,
) ;
}
}
}
}
validation . tableExists = validation . errors . length === 0 ;
validation . tableExists = validation . errors . length === 0 ;
} else if ( migration . name === "002_add_iViewContent_to_contacts" ) {
} else if ( migration . name === "002_add_iViewContent_to_contacts" ) {
// Validate iViewContent column exists in contacts table
// Validate iViewContent column exists in contacts table
try {
try {
await sqlQuery ( ` SELECT iViewContent FROM contacts LIMIT 1 ` ) ;
await sqlQuery ( ` SELECT iViewContent FROM contacts LIMIT 1 ` ) ;
validation . hasExpectedColumns = true ;
validation . hasExpectedColumns = true ;
console . log ( ` ✅ [Migration-Validation] Column iViewContent exists in contacts table ` ) ;
logger . log (
` ✅ [Migration-Validation] Column iViewContent exists in contacts table ` ,
) ;
} catch ( error ) {
} catch ( error ) {
validation . isValid = false ;
validation . isValid = false ;
validation . errors . push ( ` Column iViewContent missing from contacts table ` ) ;
validation . errors . push (
console . error ( ` ❌ [Migration-Validation] Column iViewContent missing: ` , error ) ;
` Column iViewContent missing from contacts table ` ,
) ;
logger . error (
` ❌ [Migration-Validation] Column iViewContent missing: ` ,
error ,
) ;
}
}
}
}
@ -270,11 +288,13 @@ async function validateMigrationApplication<T>(
// } else if (migration.name === "003_future_migration") {
// } else if (migration.name === "003_future_migration") {
// // Validate future migration schema changes
// // Validate future migration schema changes
// }
// }
} catch ( error ) {
} catch ( error ) {
validation . isValid = false ;
validation . isValid = false ;
validation . errors . push ( ` Validation error: ${ error } ` ) ;
validation . errors . push ( ` Validation error: ${ error } ` ) ;
console . error ( ` ❌ [Migration-Validation] Validation failed for ${ migration . name } : ` , error ) ;
logger . error (
` ❌ [Migration-Validation] Validation failed for ${ migration . name } : ` ,
error ,
) ;
}
}
return validation ;
return validation ;
@ -307,19 +327,24 @@ async function isSchemaAlreadyPresent<T>(
try {
try {
if ( migration . name === "001_initial" ) {
if ( migration . name === "001_initial" ) {
// Check if accounts table exists (primary indicator of initial migration)
// Check if accounts table exists (primary indicator of initial migration)
const result = await sqlQuery ( ` SELECT name FROM sqlite_master WHERE type='table' AND name='accounts' ` ) as any ;
const result = ( await sqlQuery (
const hasTable = result ? . values ? . length > 0 || ( Array . isArray ( result ) && result . length > 0 ) ;
` SELECT name FROM sqlite_master WHERE type='table' AND name='accounts' ` ,
console . log ( ` 🔍 [Migration-Schema] Initial migration schema check - accounts table exists: ${ hasTable } ` ) ;
) ) as unknown as { values : unknown [ ] [ ] } ;
const hasTable =
result ? . values ? . length > 0 ||
( Array . isArray ( result ) && result . length > 0 ) ;
logger . log (
` 🔍 [Migration-Schema] Initial migration schema check - accounts table exists: ${ hasTable } ` ,
) ;
return hasTable ;
return hasTable ;
} else if ( migration . name === "002_add_iViewContent_to_contacts" ) {
} else if ( migration . name === "002_add_iViewContent_to_contacts" ) {
// Check if iViewContent column exists in contacts table
// Check if iViewContent column exists in contacts table
try {
try {
await sqlQuery ( ` SELECT iViewContent FROM contacts LIMIT 1 ` ) ;
await sqlQuery ( ` SELECT iViewContent FROM contacts LIMIT 1 ` ) ;
console . log ( ` 🔍 [Migration-Schema] iViewContent column already exists ` ) ;
logger . log ( ` 🔍 [Migration-Schema] iViewContent column already exists ` ) ;
return true ;
return true ;
} catch ( error ) {
} catch ( error ) {
console . log ( ` 🔍 [Migration-Schema] iViewContent column does not exist ` ) ;
logger . log ( ` 🔍 [Migration-Schema] iViewContent column does not exist ` ) ;
return false ;
return false ;
}
}
}
}
@ -328,9 +353,11 @@ async function isSchemaAlreadyPresent<T>(
// } else if (migration.name === "003_future_migration") {
// } else if (migration.name === "003_future_migration") {
// // Check if future migration schema already exists
// // Check if future migration schema already exists
// }
// }
} catch ( error ) {
} catch ( error ) {
console . log ( ` 🔍 [Migration-Schema] Schema check failed for ${ migration . name } , assuming not present: ` , error ) ;
logger . log (
` 🔍 [Migration-Schema] Schema check failed for ${ migration . name } , assuming not present: ` ,
error ,
) ;
return false ;
return false ;
}
}
@ -382,47 +409,56 @@ export async function runMigrations<T>(
extractMigrationNames : ( result : T ) = > Set < string > ,
extractMigrationNames : ( result : T ) = > Set < string > ,
) : Promise < void > {
) : Promise < void > {
try {
try {
console . log ( "📋 [Migration] Starting migration process..." ) ;
logger . log ( "📋 [Migration] Starting migration process..." ) ;
// Step 1: Create migrations table if it doesn't exist
// Step 1: Create migrations table if it doesn't exist
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
console . log ( "🔧 [Migration] Creating migrations table if it doesn't exist..." ) ;
logger . log (
"🔧 [Migration] Creating migrations table if it doesn't exist..." ,
) ;
await sqlExec ( `
await sqlExec ( `
CREATE TABLE IF NOT EXISTS migrations (
CREATE TABLE IF NOT EXISTS migrations (
name TEXT PRIMARY KEY ,
name TEXT PRIMARY KEY ,
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
) ;
) ;
` );
` );
console . log ( "✅ [Migration] Migrations table ready" ) ;
logger . log ( "✅ [Migration] Migrations table ready" ) ;
// Step 2: Get list of already applied migrations
// Step 2: Get list of already applied migrations
console . log ( "🔍 [Migration] Querying existing migrations..." ) ;
logger . log ( "🔍 [Migration] Querying existing migrations..." ) ;
const appliedMigrationsResult = await sqlQuery (
const appliedMigrationsResult = await sqlQuery (
"SELECT name FROM migrations" ,
"SELECT name FROM migrations" ,
) ;
) ;
console . log ( "📊 [Migration] Raw query result:" , appliedMigrationsResult ) ;
logger . log ( "📊 [Migration] Raw query result:" , appliedMigrationsResult ) ;
const appliedMigrations = extractMigrationNames ( appliedMigrationsResult ) ;
const appliedMigrations = extractMigrationNames ( appliedMigrationsResult ) ;
console . log ( "📋 [Migration] Extracted applied migrations:" , Array . from ( appliedMigrations ) ) ;
logger . log (
"📋 [Migration] Extracted applied migrations:" ,
Array . from ( appliedMigrations ) ,
) ;
// Step 3: Get all registered migrations
// Step 3: Get all registered migrations
const migrations = migrationRegistry . getMigrations ( ) ;
const migrations = migrationRegistry . getMigrations ( ) ;
if ( migrations . length === 0 ) {
if ( migrations . length === 0 ) {
console . log ( "⚠️ [Migration] No migrations registered" ) ;
logger . warn ( "⚠️ [Migration] No migrations registered" ) ;
logger . warn ( "[MigrationService] No migrations registered" ) ;
logger . warn ( "[MigrationService] No migrations registered" ) ;
return ;
return ;
}
}
console . log ( ` 📊 [Migration] Found ${ migrations . length } total migrations, ${ appliedMigrations . size } already applied ` ) ;
logger . log (
console . log ( ` 📝 [Migration] Registered migrations: ${ migrations . map ( m = > m . name ) . join ( ', ' ) } ` ) ;
` 📊 [Migration] Found ${ migrations . length } total migrations, ${ appliedMigrations . size } already applied ` ,
) ;
logger . log (
` 📝 [Migration] Registered migrations: ${ migrations . map ( ( m ) = > m . name ) . join ( ", " ) } ` ,
) ;
let appliedCount = 0 ;
let appliedCount = 0 ;
let skippedCount = 0 ;
let skippedCount = 0 ;
// Step 4: Process each migration
// Step 4: Process each migration
for ( const migration of migrations ) {
for ( const migration of migrations ) {
console . log ( ` \ n🔍 [Migration] Processing migration: ${ migration . name } ` ) ;
logger . log ( ` \ n🔍 [Migration] Processing migration: ${ migration . name } ` ) ;
// Check 1: Is it recorded as applied in migrations table?
// Check 1: Is it recorded as applied in migrations table?
const isRecordedAsApplied = appliedMigrations . has ( migration . name ) ;
const isRecordedAsApplied = appliedMigrations . has ( migration . name ) ;
@ -430,64 +466,88 @@ export async function runMigrations<T>(
// Check 2: Does the schema already exist in the database?
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent ( migration , sqlQuery ) ;
const isSchemaPresent = await isSchemaAlreadyPresent ( migration , sqlQuery ) ;
console . log ( ` 🔍 [Migration] ${ migration . name } - Recorded: ${ isRecordedAsApplied } , Schema: ${ isSchemaPresent } ` ) ;
logger . log (
` 🔍 [Migration] ${ migration . name } - Recorded: ${ isRecordedAsApplied } , Schema: ${ isSchemaPresent } ` ,
) ;
// Skip if already recorded as applied
// Skip if already recorded as applied
if ( isRecordedAsApplied ) {
if ( isRecordedAsApplied ) {
console . log ( ` ⏭️ [Migration] Skipping already applied: ${ migration . name } ` ) ;
logger . log (
` ⏭️ [Migration] Skipping already applied: ${ migration . name } ` ,
) ;
skippedCount ++ ;
skippedCount ++ ;
continue ;
continue ;
}
}
// Handle case where schema exists but isn't recorded
// Handle case where schema exists but isn't recorded
if ( isSchemaPresent ) {
if ( isSchemaPresent ) {
console . log ( ` 🔄 [Migration] Schema exists but not recorded. Marking ${ migration . name } as applied... ` ) ;
logger . log (
` 🔄 [Migration] Schema exists but not recorded. Marking ${ migration . name } as applied... ` ,
) ;
try {
try {
const insertResult = await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
const insertResult = await sqlExec (
migration . name ,
"INSERT INTO migrations (name) VALUES (?)" ,
] ) ;
[ migration . name ] ,
console . log ( ` ✅ [Migration] Migration record inserted: ` , insertResult ) ;
) ;
console . log ( ` ✅ [Migration] Marked existing schema as applied: ${ migration . name } ` ) ;
logger . log ( ` ✅ [Migration] Migration record inserted: ` , insertResult ) ;
logger . log (
` ✅ [Migration] Marked existing schema as applied: ${ migration . name } ` ,
) ;
skippedCount ++ ;
skippedCount ++ ;
continue ;
continue ;
} catch ( insertError ) {
} catch ( insertError ) {
console . warn ( ` ⚠️ [Migration] Could not record existing schema ${ migration . name } : ` , insertError ) ;
logger . warn (
` ⚠️ [Migration] Could not record existing schema ${ migration . name } : ` ,
insertError ,
) ;
// Continue with normal migration process as fallback
// Continue with normal migration process as fallback
}
}
}
}
// Apply the migration
// Apply the migration
console . log ( ` 🔄 [Migration] Applying migration: ${ migration . name } ` ) ;
logger . log ( ` 🔄 [Migration] Applying migration: ${ migration . name } ` ) ;
try {
try {
// Execute the migration SQL
// Execute the migration SQL
console . log ( ` 🔧 [Migration] Executing SQL for ${ migration . name } ... ` ) ;
logger . log ( ` 🔧 [Migration] Executing SQL for ${ migration . name } ... ` ) ;
await sqlExec ( migration . sql ) ;
await sqlExec ( migration . sql ) ;
console . log ( ` ✅ [Migration] SQL executed successfully for ${ migration . name } ` ) ;
logger . log (
` ✅ [Migration] SQL executed successfully for ${ migration . name } ` ,
) ;
// Validate the migration was applied correctly
// Validate the migration was applied correctly
const validation = await validateMigrationApplication ( migration , sqlQuery ) ;
const validation = await validateMigrationApplication (
migration ,
sqlQuery ,
) ;
if ( ! validation . isValid ) {
if ( ! validation . isValid ) {
console . warn ( ` ⚠️ [Migration] Validation failed for ${ migration . name } : ` , validation . errors ) ;
logger . warn (
` ⚠️ [Migration] Validation failed for ${ migration . name } : ` ,
validation . errors ,
) ;
} else {
} else {
console . log ( ` ✅ [Migration] Schema validation passed for ${ migration . name } ` ) ;
logger . log (
` ✅ [Migration] Schema validation passed for ${ migration . name } ` ,
) ;
}
}
// Record that the migration was applied
// Record that the migration was applied
console . log ( ` 📝 [Migration] Recording migration ${ migration . name } as applied... ` ) ;
logger . log (
const insertResult = await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
` 📝 [Migration] Recording migration ${ migration . name } as applied... ` ,
migration . name ,
) ;
] ) ;
const insertResult = await sqlExec (
console . log ( ` ✅ [Migration] Migration record inserted: ` , insertResult ) ;
"INSERT INTO migrations (name) VALUES (?)" ,
[ migration . name ] ,
) ;
logger . log ( ` ✅ [Migration] Migration record inserted: ` , insertResult ) ;
console . log ( ` 🎉 [Migration] Successfully applied: ${ migration . name } ` ) ;
logger . log ( ` 🎉 [Migration] Successfully applied: ${ migration . name } ` ) ;
logger . info (
logger . info (
` [MigrationService] Successfully applied migration: ${ migration . name } ` ,
` [MigrationService] Successfully applied migration: ${ migration . name } ` ,
) ;
) ;
appliedCount ++ ;
appliedCount ++ ;
} catch ( error ) {
} catch ( error ) {
console . error ( ` ❌ [Migration] Error applying ${ migration . name } : ` , error ) ;
logger . error ( ` ❌ [Migration] Error applying ${ migration . name } : ` , error ) ;
// Handle specific cases where the migration might be partially applied
// Handle specific cases where the migration might be partially applied
const errorMessage = String ( error ) . toLowerCase ( ) ;
const errorMessage = String ( error ) . toLowerCase ( ) ;
@ -497,33 +557,53 @@ export async function runMigrations<T>(
errorMessage . includes ( "duplicate column" ) ||
errorMessage . includes ( "duplicate column" ) ||
errorMessage . includes ( "column already exists" ) ||
errorMessage . includes ( "column already exists" ) ||
errorMessage . includes ( "already exists" ) ||
errorMessage . includes ( "already exists" ) ||
errorMessage . includes ( "table" ) && errorMessage . includes ( "already exists" )
( errorMessage . includes ( "table" ) &&
errorMessage . includes ( "already exists" ) )
) {
) {
console . log ( ` ⚠️ [Migration] ${ migration . name } appears already applied ( ${ errorMessage } ). Validating and marking as complete. ` ) ;
logger . log (
` ⚠️ [Migration] ${ migration . name } appears already applied ( ${ errorMessage } ). Validating and marking as complete. ` ,
) ;
// Validate the existing schema
// Validate the existing schema
const validation = await validateMigrationApplication ( migration , sqlQuery ) ;
const validation = await validateMigrationApplication (
migration ,
sqlQuery ,
) ;
if ( validation . isValid ) {
if ( validation . isValid ) {
console . log ( ` ✅ [Migration] Schema validation passed for ${ migration . name } ` ) ;
logger . log (
` ✅ [Migration] Schema validation passed for ${ migration . name } ` ,
) ;
} else {
} else {
console . warn ( ` ⚠️ [Migration] Schema validation failed for ${ migration . name } : ` , validation . errors ) ;
logger . warn (
` ⚠️ [Migration] Schema validation failed for ${ migration . name } : ` ,
validation . errors ,
) ;
}
}
// Mark the migration as applied since the schema change already exists
// Mark the migration as applied since the schema change already exists
try {
try {
console . log ( ` 📝 [Migration] Attempting to record ${ migration . name } as applied despite error... ` ) ;
logger . log (
const insertResult = await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
` 📝 [Migration] Attempting to record ${ migration . name } as applied despite error... ` ,
migration . name ,
) ;
] ) ;
const insertResult = await sqlExec (
console . log ( ` ✅ [Migration] Migration record inserted after error: ` , insertResult ) ;
"INSERT INTO migrations (name) VALUES (?)" ,
console . log ( ` ✅ [Migration] Marked as applied: ${ migration . name } ` ) ;
[ migration . name ] ,
) ;
logger . log (
` ✅ [Migration] Migration record inserted after error: ` ,
insertResult ,
) ;
logger . log ( ` ✅ [Migration] Marked as applied: ${ migration . name } ` ) ;
logger . info (
logger . info (
` [MigrationService] Successfully marked migration as applied: ${ migration . name } ` ,
` [MigrationService] Successfully marked migration as applied: ${ migration . name } ` ,
) ;
) ;
appliedCount ++ ;
appliedCount ++ ;
} catch ( insertError ) {
} catch ( insertError ) {
// If we can't insert the migration record, log it but don't fail
// If we can't insert the migration record, log it but don't fail
console . warn ( ` ⚠️ [Migration] Could not record ${ migration . name } as applied: ` , insertError ) ;
logger . warn (
` ⚠️ [Migration] Could not record ${ migration . name } as applied: ` ,
insertError ,
) ;
logger . warn (
logger . warn (
` [MigrationService] Could not record migration ${ migration . name } as applied: ` ,
` [MigrationService] Could not record migration ${ migration . name } as applied: ` ,
insertError ,
insertError ,
@ -531,40 +611,55 @@ export async function runMigrations<T>(
}
}
} else {
} else {
// For other types of errors, still fail the migration
// For other types of errors, still fail the migration
console . error ( ` ❌ [Migration] Failed to apply ${ migration . name } : ` , error ) ;
logger . error (
logger . error (
` ❌ [Migration] Failed to apply ${ migration . name } : ` ,
` [MigrationService] Failed to apply migration ${ migration . name } : ` ,
error ,
error ,
) ;
) ;
logger . error (
throw new Error ( ` Migration ${ migration . name } failed: ${ error } ` ) ;
` [MigrationService] Failed to apply migration ${ migration . name } : ` ,
error ,
) ;
throw new Error ( ` Migration ${ migration . name } failed: ${ error } ` ) ;
}
}
}
}
}
}
// Step 5: Final validation - verify all migrations are properly recorded
// Step 5: Final validation - verify all migrations are properly recorded
console . log ( "\n🔍 [Migration] Final validation - checking migrations table..." ) ;
logger . log (
"\n🔍 [Migration] Final validation - checking migrations table..." ,
) ;
const finalMigrationsResult = await sqlQuery ( "SELECT name FROM migrations" ) ;
const finalMigrationsResult = await sqlQuery ( "SELECT name FROM migrations" ) ;
const finalAppliedMigrations = extractMigrationNames ( finalMigrationsResult ) ;
const finalAppliedMigrations = extractMigrationNames ( finalMigrationsResult ) ;
console . log ( "📋 [Migration] Final applied migrations:" , Array . from ( finalAppliedMigrations ) ) ;
logger . log (
"📋 [Migration] Final applied migrations:" ,
Array . from ( finalAppliedMigrations ) ,
) ;
// Check that all expected migrations are recorded
// Check that all expected migrations are recorded
const expectedMigrations = new Set ( migrations . map ( m = > m . name ) ) ;
const expectedMigrations = new Set ( migrations . map ( ( m ) = > m . name ) ) ;
const missingMigrations = [ . . . expectedMigrations ] . filter ( name = > ! finalAppliedMigrations . has ( name ) ) ;
const missingMigrations = [ . . . expectedMigrations ] . filter (
( name ) = > ! finalAppliedMigrations . has ( name ) ,
) ;
if ( missingMigrations . length > 0 ) {
if ( missingMigrations . length > 0 ) {
console . warn ( ` ⚠️ [Migration] Missing migration records: ${ missingMigrations . join ( ', ' ) } ` ) ;
logger . warn (
logger . warn ( ` [MigrationService] Missing migration records: ${ missingMigrations . join ( ', ' ) } ` ) ;
` ⚠️ [Migration] Missing migration records: ${ missingMigrations . join ( ", " ) } ` ,
) ;
logger . warn (
` [MigrationService] Missing migration records: ${ missingMigrations . join ( ", " ) } ` ,
) ;
}
}
console . log ( ` \ n🎉 [Migration] Migration process complete! ` ) ;
logger . log ( ` \ n🎉 [Migration] Migration process complete! ` ) ;
console . log ( ` 📊 [Migration] Summary: Applied: ${ appliedCount } , Skipped: ${ skippedCount } , Total: ${ migrations . length } ` ) ;
logger . log (
logger . info ( ` [MigrationService] Migration process complete. Applied: ${ appliedCount } , Skipped: ${ skippedCount } ` ) ;
` 📊 [Migration] Summary: Applied: ${ appliedCount } , Skipped: ${ skippedCount } , Total: ${ migrations . length } ` ,
) ;
logger . info (
` [MigrationService] Migration process complete. Applied: ${ appliedCount } , Skipped: ${ skippedCount } ` ,
) ;
} catch ( error ) {
} catch ( error ) {
console . error ( "\n💥 [Migration] Migration process failed:" , error ) ;
logger . error ( "\n💥 [Migration] Migration process failed:" , error ) ;
logger . error ( "[MigrationService] Migration process failed:" , error ) ;
logger . error ( "[MigrationService] Migration process failed:" , error ) ;
throw error ;
throw error ;
}
}
}
}