@ -1,5 +1,11 @@
/ * *
/ * *
* Manage database migrations as people upgrade their app over time
* Database Migration Service for TimeSafari
*
* Manages database migrations as people upgrade their app over time .
* Provides safe , tracked migrations with rollback capabilities and
* detailed logging for debugging .
*
* @author Matthew Raymer
* /
* /
import { logger } from "../utils/logger" ;
import { logger } from "../utils/logger" ;
@ -78,6 +84,8 @@ export async function runMigrations<T>(
extractMigrationNames : ( result : T ) = > Set < string > ,
extractMigrationNames : ( result : T ) = > Set < string > ,
) : Promise < void > {
) : Promise < void > {
try {
try {
console . log ( "📋 [Migration] Checking migration status..." ) ;
// Create migrations table if it doesn't exist
// Create migrations table if it doesn't exist
await sqlExec ( `
await sqlExec ( `
CREATE TABLE IF NOT EXISTS migrations (
CREATE TABLE IF NOT EXISTS migrations (
@ -96,28 +104,45 @@ export async function runMigrations<T>(
const migrations = migrationRegistry . getMigrations ( ) ;
const migrations = migrationRegistry . getMigrations ( ) ;
if ( migrations . length === 0 ) {
if ( migrations . length === 0 ) {
console . log ( "⚠️ [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 ` ) ;
let appliedCount = 0 ;
let skippedCount = 0 ;
// Run each migration that hasn't been applied yet
// Run each migration that hasn't been applied yet
for ( const migration of migrations ) {
for ( const migration of migrations ) {
if ( appliedMigrations . has ( migration . name ) ) {
if ( appliedMigrations . has ( migration . name ) ) {
console . log ( ` ⏭️ [Migration] Skipping already applied: ${ migration . name } ` ) ;
skippedCount ++ ;
continue ;
continue ;
}
}
console . log ( ` 🔄 [Migration] Applying migration: ${ migration . name } ` ) ;
try {
try {
// Special handling for column addition migrations
if ( migration . sql . includes ( "ALTER TABLE" ) && migration . sql . includes ( "ADD COLUMN" ) ) {
await handleColumnAddition ( migration , sqlExec , sqlQuery ) ;
} else {
// Execute the migration SQL
// Execute the migration SQL
await sqlExec ( migration . sql ) ;
await sqlExec ( migration . sql ) ;
}
// Record that the migration was applied
// Record that the migration was applied
await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
migration . name ,
migration . name ,
] ) ;
] ) ;
console . log ( ` ✅ [Migration] Successfully applied: ${ migration . name } ` ) ;
logger . info (
logger . info (
` [MigrationService] Successfully applied migration: ${ migration . name } ` ,
` [MigrationService] Successfully applied migration: ${ migration . name } ` ,
) ;
) ;
appliedCount ++ ;
} catch ( error ) {
} catch ( 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 ( ) ;
@ -128,6 +153,7 @@ export async function runMigrations<T>(
errorMessage . includes ( "column already exists" ) ||
errorMessage . includes ( "column already exists" ) ||
errorMessage . includes ( "already exists" )
errorMessage . includes ( "already exists" )
) {
) {
console . log ( ` ⚠️ [Migration] ${ migration . name } appears already applied ( ${ errorMessage } ). Marking as complete. ` ) ;
logger . warn (
logger . warn (
` [MigrationService] Migration ${ migration . name } appears to be already applied ( ${ errorMessage } ). Marking as complete. ` ,
` [MigrationService] Migration ${ migration . name } appears to be already applied ( ${ errorMessage } ). Marking as complete. ` ,
) ;
) ;
@ -137,11 +163,14 @@ export async function runMigrations<T>(
await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
await sqlExec ( "INSERT INTO migrations (name) VALUES (?)" , [
migration . name ,
migration . name ,
] ) ;
] ) ;
console . 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 ++ ;
} 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 (
logger . warn (
` [MigrationService] Could not record migration ${ migration . name } as applied: ` ,
` [MigrationService] Could not record migration ${ migration . name } as applied: ` ,
insertError ,
insertError ,
@ -149,6 +178,7 @@ 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 (
` [MigrationService] Failed to apply migration ${ migration . name } : ` ,
` [MigrationService] Failed to apply migration ${ migration . name } : ` ,
error ,
error ,
@ -157,8 +187,62 @@ export async function runMigrations<T>(
}
}
}
}
}
}
console . log ( ` 🎉 [Migration] Migration process complete! Applied: ${ appliedCount } , Skipped: ${ skippedCount } ` ) ;
} catch ( error ) {
} catch ( error ) {
console . error ( "💥 [Migration] Migration process failed:" , error ) ;
logger . error ( "[MigrationService] Migration process failed:" , error ) ;
logger . error ( "[MigrationService] Migration process failed:" , error ) ;
throw error ;
throw error ;
}
}
}
}
/ * *
* Handle column addition migrations with proper existence checking
*
* @param migration - The migration containing column addition
* @param sqlExec - Function to execute SQL statements
* @param sqlQuery - Function to query SQL data
* /
async function handleColumnAddition < T > (
migration : Migration ,
sqlExec : ( sql : string , params? : unknown [ ] ) = > Promise < unknown > ,
sqlQuery : ( sql : string , params? : unknown [ ] ) = > Promise < T > ,
) : Promise < void > {
// Extract table name and column name from ALTER TABLE statement
const alterMatch = migration . sql . match ( /ALTER TABLE\s+(\w+)\s+ADD COLUMN\s+(\w+)/i ) ;
if ( ! alterMatch ) {
// If we can't parse it, just try to execute normally
await sqlExec ( migration . sql ) ;
return ;
}
const [ , tableName , columnName ] = alterMatch ;
try {
// Check if column already exists
const columnCheckResult = await sqlQuery (
` SELECT COUNT(*) as count FROM pragma_table_info(' ${ tableName } ') WHERE name = ? ` ,
[ columnName ]
) as any ;
const columnExists = columnCheckResult ? . values ? . [ 0 ] ? . count > 0 ||
columnCheckResult ? . count > 0 ||
( Array . isArray ( columnCheckResult ) && columnCheckResult . length > 0 ) ;
if ( columnExists ) {
console . log ( ` ⏭️ [Migration] Column ${ columnName } already exists in table ${ tableName } , skipping ` ) ;
return ;
}
// Column doesn't exist, so add it
console . log ( ` 🔄 [Migration] Adding column ${ columnName } to table ${ tableName } ` ) ;
await sqlExec ( migration . sql ) ;
console . log ( ` ✅ [Migration] Successfully added column ${ columnName } to table ${ tableName } ` ) ;
} catch ( error ) {
// If the column check fails, try the original migration and let error handling catch duplicates
console . log ( ` ⚠️ [Migration] Column check failed, attempting migration anyway: ${ error } ` ) ;
await sqlExec ( migration . sql ) ;
}
}