@ -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 ) ;