dependencyGraph = new MigrationDependencyGraph(); $effectiveTableConfig = $tableConfig ?? MigrationTableConfig::default(); // Initialize service classes $this->databaseManager = new MigrationDatabaseManager( $this->connection, $this->platform, $this->clock, $effectiveTableConfig ); $this->performanceTracker = new MigrationPerformanceTracker( $operationTracker, $memoryMonitor, $performanceMetricsRepository, $logger, $memoryThresholds ); $this->migrationLogger = new MigrationLogger($logger); $this->validator = new MigrationValidator( $this->connection, $this->platform ); $this->errorAnalyzer = new MigrationErrorAnalyzer(); } /** * Run migrations */ public function migrate(MigrationCollection $migrations, bool $skipPreflightChecks = false): array { $this->databaseManager->ensureMigrationsTable(); $executedMigrations = []; $appliedVersions = MigrationVersionCollection::fromStrings($this->databaseManager->getAppliedVersions()); // Pre-flight checks if (! $skipPreflightChecks) { $this->runPreFlightChecks($migrations, $appliedVersions); } // Filter and order migrations $pendingMigrations = $migrations->filterByNotApplied($appliedVersions); if ($pendingMigrations->isEmpty()) { $this->migrationLogger->logMigration('none', 'No pending migrations', 'Skipped'); return []; } $this->dependencyGraph->buildGraph($pendingMigrations); $orderedMigrations = $this->dependencyGraph->getExecutionOrder(); $totalMigrations = $orderedMigrations->count(); // Start batch tracking $batchOperationId = 'migration_batch_' . $this->ulidGenerator->generate(); $this->performanceTracker->startBatchOperation($batchOperationId, $totalMigrations); $currentPosition = 0; foreach ($orderedMigrations as $migration) { $version = $migration->getVersion()->toString(); $currentPosition++; if ($appliedVersions->contains($migration->getVersion())) { continue; } try { // Start individual migration tracking $migrationOperationId = "migration_{$version}"; $this->performanceTracker->startMigrationOperation( $migrationOperationId, $version, $migration->getDescription(), $currentPosition, $totalMigrations, $batchOperationId ); // Check memory thresholds before migration $this->performanceTracker->checkMemoryThresholds($version, $currentPosition - 1, $totalMigrations); // Execute migration in transaction $this->connection->beginTransaction(); try { $migration->up($this->connection); $this->databaseManager->recordMigrationExecution($migration, $version); if ($this->connection->inTransaction()) { $this->connection->commit(); } // Update operation with successful execution $this->performanceTracker->updateBatchOperation($batchOperationId, [ 'items_processed' => $currentPosition, ]); } catch (\Throwable $e) { if ($this->connection->inTransaction()) { $this->connection->rollback(); } throw $e; } $executedMigrations[] = $version; // Complete individual migration tracking $migrationSnapshot = $this->performanceTracker->completeMigrationOperation( $migrationOperationId, $version, $migration->getDescription(), $currentPosition, $totalMigrations, $batchOperationId ); $this->migrationLogger->logMigration($version, $migration->getDescription(), 'Migrated'); $this->migrationLogger->logMigrationProgress($version, $currentPosition, $totalMigrations, 'completed', [ 'execution_time' => $migrationSnapshot?->duration?->toSeconds(), 'memory_used' => $migrationSnapshot?->memoryDelta?->toHumanReadable(), ]); // Post-migration memory check $this->performanceTracker->logPostMigrationMemory($version, $migrationSnapshot); } catch (\Throwable $e) { // Track failed individual migration $failedSnapshot = $this->performanceTracker->failMigrationOperation( $migrationOperationId, $version, $migration->getDescription(), $currentPosition, $totalMigrations, $batchOperationId, $e ); // Track failed batch $this->performanceTracker->failBatchOperation($batchOperationId, $e); // Enhanced error reporting with recovery hints $recoveryHints = $this->errorAnalyzer->generateMigrationRecoveryHints($migration, $e, $executedMigrations); $migrationContext = $this->errorAnalyzer->analyzeMigrationContext($migration, $version, $e); throw FrameworkException::create( DatabaseErrorCode::MIGRATION_FAILED, "Migration {$version} failed: {$e->getMessage()}" )->withContext( ExceptionContext::forOperation('migration.execute', 'MigrationRunner') ->withData([ 'migration_version' => $version, 'migration_description' => $migration->getDescription(), 'executed_migrations' => $executedMigrations, 'total_migrations' => $totalMigrations, 'current_position' => $currentPosition, ]) ->withDebug($migrationContext) ->withMetadata([ 'recovery_hints' => $recoveryHints, 'can_retry' => $this->errorAnalyzer->canRetryMigration($e), 'rollback_recommended' => $this->errorAnalyzer->isRollbackRecommended($e, $executedMigrations), ]) ); } } // Complete batch tracking $batchSnapshot = $this->performanceTracker->completeBatchOperation($batchOperationId); $this->migrationLogger->logMigrationBatchSummary($executedMigrations, $totalMigrations, $batchSnapshot); return $executedMigrations; } /** * Rollback migrations (only SafelyReversible migrations) * * This method will ONLY rollback migrations that implement the SafelyReversible interface, * ensuring no data loss occurs during rollback operations. * * @param MigrationCollection $migrations Available migrations * @param int $steps Number of migrations to rollback * @return array Successfully rolled back migrations * @throws FrameworkException If attempting to rollback unsafe migration */ public function rollback(MigrationCollection $migrations, int $steps = 1): array { $appliedVersions = MigrationVersionCollection::fromStrings($this->databaseManager->getAppliedVersions()); if ($appliedVersions->isEmpty()) { $this->migrationLogger->logMigration('none', 'No migrations to rollback', 'Skipped'); return []; } $versionsToRollback = array_slice(array_reverse($appliedVersions->toArray()), 0, $steps); $rolledBackMigrations = []; $totalRollbacks = count($versionsToRollback); // Start rollback batch tracking $rollbackBatchId = 'rollback_batch_' . $this->ulidGenerator->generate(); $this->performanceTracker->startBatchOperation($rollbackBatchId, $totalRollbacks); $currentPosition = 0; foreach ($versionsToRollback as $version) { $currentPosition++; $migration = $migrations->findByVersion(MigrationVersion::fromTimestamp($version)); if (! $migration) { $this->migrationLogger->logRollbackSkipped($version, ['reason' => 'Migration class not found']); continue; } // CRITICAL SAFETY CHECK: Ensure migration supports safe rollback if (! $migration instanceof SafelyReversible) { throw FrameworkException::create( DatabaseErrorCode::MIGRATION_NOT_REVERSIBLE, "Migration {$version} does not support safe rollback" )->withContext( ExceptionContext::forOperation('migration.rollback', 'MigrationRunner') ->withData([ 'migration_version' => $version, 'migration_class' => get_class($migration), 'migration_description' => $migration->getDescription(), 'requested_rollback_steps' => $steps, 'current_position' => $currentPosition, ]) ->withMetadata([ 'reason' => 'Migration does not implement SafelyReversible interface', 'recommendation' => 'Create a new forward migration to undo the changes instead of rolling back', 'safe_rollback_guide' => 'See SafelyReversible interface documentation for guidelines', ]) ); } try { // Validate rollback safety $this->validator->validateRollbackSafety($migration, $version); // Real-time rollback progress tracking $this->migrationLogger->logRollbackProgress($version, $currentPosition, $totalRollbacks, 'starting'); // Start individual rollback tracking $rollbackOperationId = "rollback_{$version}"; $this->performanceTracker->startRollbackOperation( $rollbackOperationId, $version, $migration->getDescription(), $currentPosition, $totalRollbacks, $rollbackBatchId, $steps ); // Execute rollback in transaction $this->connection->beginTransaction(); try { $migration->down($this->connection); $this->databaseManager->recordMigrationRollback($version); if ($this->connection->inTransaction()) { $this->connection->commit(); } } catch (\Throwable $e) { if ($this->connection->inTransaction()) { $this->connection->rollback(); } throw $e; } $rolledBackMigrations[] = $migration; // Complete individual rollback tracking $rollbackSnapshot = $this->performanceTracker->completeRollbackOperation( $rollbackOperationId, $version, $migration->getDescription(), $currentPosition, $totalRollbacks, $rollbackBatchId, $steps ); $this->migrationLogger->logMigration($version, $migration->getDescription(), 'Rolled back'); $this->migrationLogger->logRollbackProgress($version, $currentPosition, $totalRollbacks, 'completed', [ 'execution_time' => $rollbackSnapshot?->duration?->toSeconds(), 'memory_used' => $rollbackSnapshot?->memoryDelta?->toHumanReadable(), ]); // Post-rollback memory check $this->performanceTracker->logPostRollbackMemory($version, $rollbackSnapshot); } catch (\Throwable $e) { // Generate rollback recovery hints $remainingRollbacks = array_slice($versionsToRollback, $currentPosition); $recoveryHints = $this->errorAnalyzer->generateRollbackRecoveryHints($migration, $e, $remainingRollbacks); throw FrameworkException::create( DatabaseErrorCode::MIGRATION_ROLLBACK_FAILED, "Rollback failed for migration {$version}: {$e->getMessage()}" )->withContext( ExceptionContext::forOperation('migration.rollback', 'MigrationRunner') ->withData([ 'migration_version' => $version, 'rollback_steps' => $steps, 'completed_rollbacks' => count($rolledBackMigrations), 'failed_migration' => $version, ]) ->withMetadata([ 'recovery_hints' => $recoveryHints, 'can_continue' => $this->errorAnalyzer->canContinueRollback($remainingRollbacks), ]) ); } } // Complete rollback batch tracking $rollbackSnapshot = $this->performanceTracker->completeBatchOperation($rollbackBatchId); $this->migrationLogger->logRollbackBatchSummary($rolledBackMigrations, $totalRollbacks, $rollbackSnapshot); return $rolledBackMigrations; } /** * Get applied migration versions */ public function getAppliedVersions(): array { return $this->databaseManager->getAppliedVersions(); } /** * Get status information for all migrations * * @return array */ public function getStatus(MigrationCollection $migrations): array { $appliedVersions = MigrationVersionCollection::fromStrings($this->databaseManager->getAppliedVersions()); $statuses = []; foreach ($migrations as $migration) { $version = $migration->getVersion(); $applied = $appliedVersions->contains($version); $statuses[] = new MigrationStatus( version: $version, description: $migration->getDescription(), applied: $applied ); } return $statuses; } private function runPreFlightChecks(MigrationCollection $migrations, MigrationVersionCollection $appliedVersions): void { $results = $this->validator->runPreFlightChecks($migrations, $appliedVersions); $dependencyResults = $this->validator->validateMigrationDependencies($migrations, $appliedVersions); // Extract critical issues and warnings $criticalIssues = []; $warnings = []; foreach ($results as $check => $result) { if ($result['status'] === 'fail' && ($result['severity'] ?? 'info') === 'critical') { $criticalIssues[] = $check . ': ' . $result['message']; } elseif ($result['status'] === 'warning' || ($result['severity'] ?? 'info') === 'warning') { $warnings[] = $check . ': ' . $result['message']; } } $this->migrationLogger->logPreFlightResults($results, $criticalIssues, $warnings); $this->migrationLogger->logDependencyValidationIssues( $dependencyResults['orphaned_dependencies'], $dependencyResults['partial_chains'], $dependencyResults['ordering_issues'] ); // Throw exception if critical issues found if (! empty($criticalIssues)) { throw FrameworkException::create( DatabaseErrorCode::MIGRATION_PREFLIGHT_FAILED, 'Pre-flight checks failed with critical issues' )->withData([ 'critical_issues' => $criticalIssues, 'warnings' => $warnings, 'full_results' => $results, ]); } } }