const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const database = require('../config/database'); const migrationsDir = path.join(__dirname); /** * Enterprise-grade migration runner with proper state tracking */ class MigrationRunner { constructor() { this.processId = `migration_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Calculate SHA-256 checksum of migration content */ calculateChecksum(content) { return crypto.createHash('sha256').update(content).digest('hex'); } /** * Parse migration version from filename */ parseVersion(filename) { const match = filename.match(/^(\d{3})_/); return match ? match[1] : null; } /** * Check if migration tracking system exists */ async ensureMigrationTrackingExists() { try { const result = await database.query(` SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_name = 'schema_migrations' AND table_schema = 'public' ) as exists `); return result.rows[0].exists; } catch (error) { console.error('Error checking migration tracking:', error); return false; } } /** * Initialize migration tracking system */ async initializeMigrationTracking() { console.log('šŸ”§ Initializing migration tracking system...'); const trackingMigrationPath = path.join(migrationsDir, '000_migration_tracking_system.sql'); if (!fs.existsSync(trackingMigrationPath)) { throw new Error('Migration tracking system file not found: 000_migration_tracking_system.sql'); } const trackingSQL = fs.readFileSync(trackingMigrationPath, 'utf8'); await database.query(trackingSQL); console.log('āœ… Migration tracking system initialized'); } /** * Acquire migration lock to prevent concurrent runs */ async acquireLock() { console.log(`šŸ”’ Acquiring migration lock (${this.processId})...`); const result = await database.query( 'SELECT acquire_migration_lock($1) as acquired', [this.processId] ); if (!result.rows[0].acquired) { throw new Error('Could not acquire migration lock. Another migration may be running.'); } console.log('āœ… Migration lock acquired'); } /** * Release migration lock */ async releaseLock() { try { await database.query('SELECT release_migration_lock($1)', [this.processId]); console.log('šŸ”“ Migration lock released'); } catch (error) { console.warn('āš ļø Error releasing migration lock:', error.message); } } /** * Check if migration has already been applied */ async isMigrationApplied(version) { const result = await database.query( 'SELECT migration_applied($1) as applied', [version] ); return result.rows[0].applied; } /** * Record migration execution */ async recordMigration(version, filename, checksum, executionTime, success, errorMessage = null) { await database.query( 'SELECT record_migration($1, $2, $3, $4, $5, $6)', [version, filename, checksum, executionTime, success, errorMessage] ); } /** * Get list of migration files to run */ getMigrationFiles() { return fs.readdirSync(migrationsDir) .filter(file => file.endsWith('.sql') && file !== '000_migration_tracking_system.sql') .sort(); } /** * Run a single migration */ async runSingleMigration(migrationFile) { const version = this.parseVersion(migrationFile); if (!version) { console.warn(`āš ļø Skipping file with invalid version format: ${migrationFile}`); return; } // Check if already applied if (await this.isMigrationApplied(version)) { console.log(`ā­ļø Skipping already applied migration: ${migrationFile}`); return; } console.log(`šŸš€ Running migration: ${migrationFile}`); const migrationPath = path.join(migrationsDir, migrationFile); const migrationSQL = fs.readFileSync(migrationPath, 'utf8'); const checksum = this.calculateChecksum(migrationSQL); const startTime = Date.now(); let success = false; let errorMessage = null; try { await database.query(migrationSQL); success = true; console.log(`āœ… Migration ${migrationFile} completed successfully!`); } catch (err) { errorMessage = err.message; console.error(`āŒ Migration ${migrationFile} failed:`, err.message); // Check if it's an idempotent error we can ignore const isIdempotentError = this.isIdempotentError(err); if (isIdempotentError) { console.warn(`āš ļø Treating as idempotent error, marking as successful`); success = true; errorMessage = `Idempotent: ${err.message}`; } else { throw err; // Re-throw non-idempotent errors } } finally { const executionTime = Date.now() - startTime; await this.recordMigration(version, migrationFile, checksum, executionTime, success, errorMessage); } } /** * Check if error is idempotent (safe to ignore) */ isIdempotentError(err) { const message = (err && err.message) ? err.message.toLowerCase() : ''; const code = err && err.code ? err.code : ''; return message.includes('already exists') || code === '42710' /* duplicate_object */ || code === '42P07' /* duplicate_table */ || code === '42701' /* duplicate_column */ || code === '42P06' /* duplicate_schema */ || code === '42723' /* duplicate_function */; } /** * Display migration status */ async displayStatus() { try { const result = await database.query('SELECT * FROM get_migration_history() LIMIT 10'); console.log('\nšŸ“Š Recent Migration History:'); console.log('Version | Filename | Applied At | Success | Time (ms)'); console.log('--------|----------|------------|---------|----------'); result.rows.forEach(row => { const status = row.success ? 'āœ…' : 'āŒ'; const time = row.execution_time_ms || 'N/A'; console.log(`${row.version.padEnd(7)} | ${row.filename.substring(0, 30).padEnd(30)} | ${row.applied_at.toISOString().substring(0, 19)} | ${status.padEnd(7)} | ${time}`); }); const versionResult = await database.query('SELECT get_current_schema_version() as version'); console.log(`\nšŸ·ļø Current Schema Version: ${versionResult.rows[0].version || 'None'}`); } catch (error) { console.warn('āš ļø Could not display migration status:', error.message); } } /** * Main migration runner */ async runMigrations() { console.log('šŸš€ Starting Enterprise Database Migration System...'); try { // Connect to database await database.testConnection(); console.log('āœ… Database connected successfully'); // Ensure migration tracking exists const trackingExists = await this.ensureMigrationTrackingExists(); if (!trackingExists) { await this.initializeMigrationTracking(); } // Acquire lock await this.acquireLock(); // Get migration files const migrationFiles = this.getMigrationFiles(); console.log(`šŸ“„ Found ${migrationFiles.length} migration files to process`); // Run migrations for (const migrationFile of migrationFiles) { await this.runSingleMigration(migrationFile); } // Display status await this.displayStatus(); console.log('šŸŽ‰ All migrations completed successfully!'); } catch (error) { console.error('āŒ Migration failed:', error); process.exit(1); } finally { await this.releaseLock(); await database.close(); console.log('šŸ”Œ Database connection closed'); } } } // Run migrations if this file is executed directly if (require.main === module) { const runner = new MigrationRunner(); runner.runMigrations(); } module.exports = { MigrationRunner };