@ -1,23 +1,23 @@
/ * *
/ * *
* Database Migration Service for TimeSafari
* Database Migration Service for TimeSafari
*
*
* This module provides a comprehensive database migration system that manages
* This module provides a comprehensive database migration system that manages
* schema changes as users upgrade their TimeSafari application over time .
* schema changes as users upgrade their TimeSafari application over time .
* The system ensures that database changes are applied safely , tracked properly ,
* The system ensures that database changes are applied safely , tracked properly ,
* and can handle edge cases gracefully .
* and can handle edge cases gracefully .
*
*
* # # Architecture Overview
* # # Architecture Overview
*
*
* The migration system follows these key principles :
* The migration system follows these key principles :
*
*
* 1 . * * Single Application * * : Each migration runs exactly once per database
* 1 . * * Single Application * * : Each migration runs exactly once per database
* 2 . * * Tracked Execution * * : All applied migrations are recorded in a migrations table
* 2 . * * Tracked Execution * * : All applied migrations are recorded in a migrations table
* 3 . * * Schema Validation * * : Actual database schema is validated before and after migrations
* 3 . * * Schema Validation * * : Actual database schema is validated before and after migrations
* 4 . * * Graceful Recovery * * : Handles cases where schema exists but tracking is missing
* 4 . * * Graceful Recovery * * : Handles cases where schema exists but tracking is missing
* 5 . * * Comprehensive Logging * * : Detailed logging for debugging and monitoring
* 5 . * * Comprehensive Logging * * : Detailed logging for debugging and monitoring
*
*
* # # Migration Flow
* # # Migration Flow
*
*
* ` ` `
* ` ` `
* 1 . Create migrations table ( if needed )
* 1 . Create migrations table ( if needed )
* 2 . Query existing applied migrations
* 2 . Query existing applied migrations
@ -30,28 +30,28 @@
* f . Record migration as applied
* f . Record migration as applied
* 4 . Final validation of all migrations
* 4 . Final validation of all migrations
* ` ` `
* ` ` `
*
*
* # # Usage Example
* # # Usage Example
*
*
* ` ` ` typescript
* ` ` ` typescript
* // Register migrations (typically in migration.ts)
* // Register migrations (typically in migration.ts)
* registerMigration ( {
* registerMigration ( {
* name : "001_initial" ,
* name : "001_initial" ,
* sql : "CREATE TABLE accounts (id INTEGER PRIMARY KEY, ...)"
* sql : "CREATE TABLE accounts (id INTEGER PRIMARY KEY, ...)"
* } ) ;
* } ) ;
*
*
* // Run migrations (typically in platform service)
* // Run migrations (typically in platform service)
* await runMigrations ( sqlExec , sqlQuery , extractMigrationNames ) ;
* await runMigrations ( sqlExec , sqlQuery , extractMigrationNames ) ;
* ` ` `
* ` ` `
*
*
* # # Error Handling
* # # Error Handling
*
*
* The system handles several error scenarios :
* The system handles several error scenarios :
* - Duplicate table / column errors ( schema already exists )
* - Duplicate table / column errors ( schema already exists )
* - Migration tracking inconsistencies
* - Migration tracking inconsistencies
* - Database connection issues
* - Database connection issues
* - Schema validation failures
* - Schema validation failures
*
*
* @author Matthew Raymer
* @author Matthew Raymer
* @version 1.0 . 0
* @version 1.0 . 0
* @since 2025 - 06 - 30
* @since 2025 - 06 - 30
@ -61,11 +61,11 @@ import { logger } from "../utils/logger";
/ * *
/ * *
* Migration interface for database schema migrations
* Migration interface for database schema migrations
*
*
* Represents a single database migration that can be applied to upgrade
* Represents a single database migration that can be applied to upgrade
* the database schema . Each migration should be idempotent and focused
* the database schema . Each migration should be idempotent and focused
* on a single schema change .
* on a single schema change .
*
*
* @interface Migration
* @interface Migration
* /
* /
interface Migration {
interface Migration {
@ -77,10 +77,10 @@ interface Migration {
/ * *
/ * *
* Migration validation result
* Migration validation result
*
*
* Contains the results of validating that a migration was successfully
* Contains the results of validating that a migration was successfully
* applied by checking the actual database schema .
* applied by checking the actual database schema .
*
*
* @interface MigrationValidation
* @interface MigrationValidation
* /
* /
interface MigrationValidation {
interface MigrationValidation {
@ -96,11 +96,11 @@ interface MigrationValidation {
/ * *
/ * *
* Migration registry to store and manage database migrations
* Migration registry to store and manage database migrations
*
*
* This class maintains a registry of all migrations that need to be applied
* This class maintains a registry of all migrations that need to be applied
* to the database . It uses the singleton pattern to ensure migrations are
* to the database . It uses the singleton pattern to ensure migrations are
* registered once and can be accessed globally .
* registered once and can be accessed globally .
*
*
* @class MigrationRegistry
* @class MigrationRegistry
* /
* /
class MigrationRegistry {
class MigrationRegistry {
@ -109,14 +109,14 @@ class MigrationRegistry {
/ * *
/ * *
* Register a migration with the registry
* Register a migration with the registry
*
*
* Adds a migration to the list of migrations that will be applied when
* Adds a migration to the list of migrations that will be applied when
* runMigrations ( ) is called . Migrations should be registered in order
* runMigrations ( ) is called . Migrations should be registered in order
* of their intended execution .
* of their intended execution .
*
*
* @param migration - The migration to register
* @param migration - The migration to register
* @throws { Error } If migration name is empty or already exists
* @throws { Error } If migration name is empty or already exists
*
*
* @example
* @example
* ` ` ` typescript
* ` ` ` typescript
* registry . registerMigration ( {
* registry . registerMigration ( {
@ -126,14 +126,14 @@ 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 ` ) ;
}
}
this . migrations . push ( migration ) ;
this . migrations . push ( migration ) ;
}
}
@ -142,7 +142,7 @@ class MigrationRegistry {
*
*
* Returns a copy of all migrations that have been registered with this
* Returns a copy of all migrations that have been registered with this
* registry . The migrations are returned in the order they were registered .
* registry . The migrations are returned in the order they were registered .
*
*
* @returns Array of registered migrations ( defensive copy )
* @returns Array of registered migrations ( defensive copy )
* /
* /
getMigrations ( ) : Migration [ ] {
getMigrations ( ) : Migration [ ] {
@ -151,10 +151,10 @@ class MigrationRegistry {
/ * *
/ * *
* Clear all registered migrations
* Clear all registered migrations
*
*
* Removes all migrations from the registry . This is primarily used for
* Removes all migrations from the registry . This is primarily used for
* testing purposes to ensure a clean state between test runs .
* testing purposes to ensure a clean state between test runs .
*
*
* @internal Used primarily for testing
* @internal Used primarily for testing
* /
* /
clearMigrations ( ) : void {
clearMigrations ( ) : void {
@ -163,7 +163,7 @@ class MigrationRegistry {
/ * *
/ * *
* Get the count of registered migrations
* Get the count of registered migrations
*
*
* @returns Number of migrations currently registered
* @returns Number of migrations currently registered
* /
* /
getCount ( ) : number {
getCount ( ) : number {
@ -183,7 +183,7 @@ const migrationRegistry = new MigrationRegistry();
*
*
* @param migration - The migration to register
* @param migration - The migration to register
* @throws { Error } If migration is invalid
* @throws { Error } If migration is invalid
*
*
* @example
* @example
* ` ` ` typescript
* ` ` ` typescript
* registerMigration ( {
* registerMigration ( {
@ -207,16 +207,16 @@ export function registerMigration(migration: Migration): void {
/ * *
/ * *
* Validate that a migration was successfully applied by checking schema
* Validate that a migration was successfully applied by checking schema
*
*
* This function performs post - migration validation to ensure that the
* This function performs post - migration validation to ensure that the
* expected database schema changes were actually applied . It checks for
* expected database schema changes were actually applied . It checks for
* the existence of tables , columns , and other schema elements that should
* the existence of tables , columns , and other schema elements that should
* have been created by the migration .
* have been created by the migration .
*
*
* @param migration - The migration to validate
* @param migration - The migration to validate
* @param sqlQuery - Function to execute SQL queries
* @param sqlQuery - Function to execute SQL queries
* @returns Promise resolving to validation results
* @returns Promise resolving to validation results
*
*
* @example
* @example
* ` ` ` typescript
* ` ` ` typescript
* const validation = await validateMigrationApplication ( migration , sqlQuery ) ;
* const validation = await validateMigrationApplication ( migration , sqlQuery ) ;
@ -233,48 +233,68 @@ 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 ,
) ;
}
}
}
}
// Add validation for future migrations here
// Add validation for future migrations here
// } 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 ;
@ -282,16 +302,16 @@ async function validateMigrationApplication<T>(
/ * *
/ * *
* Check if migration is already applied by examining actual schema
* Check if migration is already applied by examining actual schema
*
*
* This function performs schema introspection to determine if a migration
* This function performs schema introspection to determine if a migration
* has already been applied , even if it ' s not recorded in the migrations
* has already been applied , even if it ' s not recorded in the migrations
* table . This is useful for handling cases where the database schema exists
* table . This is useful for handling cases where the database schema exists
* but the migration tracking got out of sync .
* but the migration tracking got out of sync .
*
*
* @param migration - The migration to check
* @param migration - The migration to check
* @param sqlQuery - Function to execute SQL queries
* @param sqlQuery - Function to execute SQL queries
* @returns Promise resolving to true if schema already exists
* @returns Promise resolving to true if schema already exists
*
*
* @example
* @example
* ` ` ` typescript
* ` ` ` typescript
* const schemaExists = await isSchemaAlreadyPresent ( migration , sqlQuery ) ;
* const schemaExists = await isSchemaAlreadyPresent ( migration , sqlQuery ) ;
@ -307,33 +327,40 @@ 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 ;
}
}
}
}
// Add schema checks for future migrations here
// Add schema checks for future migrations here
// } 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 ;
}
}
return false ;
return false ;
}
}
@ -347,7 +374,7 @@ async function isSchemaAlreadyPresent<T>(
* 4 . Validates that migrations were applied correctly
* 4 . Validates that migrations were applied correctly
* 5 . Records successful migrations in the tracking table
* 5 . Records successful migrations in the tracking table
* 6 . Performs final validation of the migration state
* 6 . Performs final validation of the migration state
*
*
* The function is designed to be idempotent - it can be run multiple times
* The function is designed to be idempotent - it can be run multiple times
* safely without re - applying migrations that have already been completed .
* safely without re - applying migrations that have already been completed .
*
*
@ -357,22 +384,22 @@ async function isSchemaAlreadyPresent<T>(
* @param extractMigrationNames - Function to extract migration names from query results
* @param extractMigrationNames - Function to extract migration names from query results
* @returns Promise that resolves when all migrations are complete
* @returns Promise that resolves when all migrations are complete
* @throws { Error } If any migration fails to apply
* @throws { Error } If any migration fails to apply
*
*
* @example
* @example
* ` ` ` typescript
* ` ` ` typescript
* // Platform-specific implementation
* // Platform-specific implementation
* const sqlExec = async ( sql : string , params? : unknown [ ] ) = > {
* const sqlExec = async ( sql : string , params? : unknown [ ] ) = > {
* return await db . run ( sql , params ) ;
* return await db . run ( sql , params ) ;
* } ;
* } ;
*
*
* const sqlQuery = async ( sql : string , params? : unknown [ ] ) = > {
* const sqlQuery = async ( sql : string , params? : unknown [ ] ) = > {
* return await db . query ( sql , params ) ;
* return await db . query ( sql , params ) ;
* } ;
* } ;
*
*
* const extractNames = ( result : DBResult ) = > {
* const extractNames = ( result : DBResult ) = > {
* return new Set ( result . values . map ( row = > row [ 0 ] ) ) ;
* return new Set ( result . values . map ( row = > row [ 0 ] ) ) ;
* } ;
* } ;
*
*
* await runMigrations ( sqlExec , sqlQuery , extractNames ) ;
* await runMigrations ( sqlExec , sqlQuery , extractNames ) ;
* ` ` `
* ` ` `
* /
* /
@ -382,113 +409,146 @@ 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 ) ;
// 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 ;
}
}
}
}