codenuk_backend_mine/services/git-integration/src/migrations/migrate_v2.js
2025-10-02 16:46:52 +05:30

266 lines
7.9 KiB
JavaScript

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 };