option('framework-only'); // In development mode, use snapshot strategy (unless framework-only) $use_snapshot = $is_development && !$is_framework_only; if ($use_snapshot) { return $this->run_with_snapshot(); } else { return $this->run_without_snapshot(); } } /** * Run migrations with automatic snapshot protection (development mode) */ protected function run_with_snapshot(): int { // Check if running in Docker environment (required for snapshots) if (!file_exists('/.dockerenv')) { $this->error('[ERROR] Snapshot-based migrations require Docker environment!'); $this->info('In non-Docker development, set RSX_MODE=debug to run without snapshots.'); return 1; } $this->info(' Development mode: Using automatic snapshot protection'); $this->info(''); // Step 1: Create snapshot (migrate:begin logic) $this->info('[1/4] Creating database snapshot...'); if (!$this->create_snapshot()) { return 1; } // Step 2: Run migrations $this->info(''); $this->info('[2/4] Running migrations...'); $migration_result = $this->execute_migrations(); if ($migration_result !== 0) { // Migration failed - rollback and exit migration mode $this->error(''); $this->error('[ERROR] Migration failed!'); $this->warn(' Automatically rolling back to snapshot...'); $this->info(''); $this->rollback_snapshot(); $this->cleanup_migration_mode(); $this->info(''); $this->info('[OK] Database restored to pre-migration state.'); $this->info(''); $this->line('Fix your migration files and run "php artisan migrate" again.'); return 1; } // Step 3: Run schema quality check $this->info(''); $this->info('[3/4] Running schema quality check...'); $checker = new SchemaQualityChecker(); $checker->check(); if ($checker->has_violations()) { $this->error('[ERROR] Schema standards check failed with ' . $checker->get_violation_count() . ' violation(s):'); $this->info(''); // Display violations $grouped = $checker->get_violations_by_severity(); foreach (['critical', 'high', 'medium', 'low'] as $severity) { if (!empty($grouped[$severity])) { $this->line(strtoupper($severity) . ' VIOLATIONS:'); foreach ($grouped[$severity] as $violation) { $this->line($violation->format_output()); } } } $this->info(''); $this->warn(' Rolling back due to schema violations...'); $this->rollback_snapshot(); $this->cleanup_migration_mode(); $this->info(''); $this->warn('[WARNING] Migration has been rolled back. Fix the schema violations and try again.'); return 1; } $this->info('[OK] Schema quality check passed.'); // Step 4: Commit - cleanup snapshot and run post-migration tasks $this->info(''); $this->info('[4/4] Committing changes...'); $this->commit_snapshot(); // Run post-migration tasks (development only) $this->info(''); $this->info('Running post-migration tasks...'); // Regenerate model constants $this->call('rsx:constants:regenerate'); // Recompile bundles $this->newLine(); $this->info('Recompiling bundles...'); passthru('php artisan rsx:bundle:compile'); $this->info(''); $this->info('[OK] Migration completed successfully!'); return 0; } /** * Run migrations without snapshot protection (debug/production mode) */ protected function run_without_snapshot(): int { $mode_label = Rsx::get_mode_label(); $is_framework_only = $this->option('framework-only'); if ($is_framework_only) { $this->info(" Framework-only migrations (no snapshot protection)"); } else { $this->info(" {$mode_label} mode: Running without snapshot protection"); } $this->info(' Source code is read-only - constants/bundles will not be regenerated.'); $this->info(''); // Run migrations $migration_result = $this->execute_migrations(); if ($migration_result !== 0) { $this->error(''); $this->error('[ERROR] Migration failed!'); $this->warn('[WARNING] No snapshot available - database may be in inconsistent state.'); return 1; } // In debug/production mode, check manifest consistency with database if (!$is_framework_only) { AppServiceProvider::disable_query_echo(); $this->info(''); $consistency_check_exit = $this->call('rsx:migrate:check_consistency'); if ($consistency_check_exit !== 0) { $this->warn('[WARNING] Manifest-database consistency check failed.'); $this->warn('Source code may be out of sync with database schema.'); } } $this->info(''); $this->info('[OK] Migration completed!'); return 0; } /** * Execute the actual migration process */ protected function execute_migrations(): int { // Enable SQL query transformation for migrations SqlQueryTransformer::enable(); $this->register_query_transformer(); // Enable full query logging to stdout for migrations AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT); // Ensure migrations table exists (create it if needed) $this->ensure_migrations_table_exists(); $is_framework_only = $this->option('framework-only'); $is_development = Rsx::is_development(); // Get all the options $force = $this->option('force'); $seed = $this->option('seed'); $step = $this->option('step'); $paths = $this->option('path'); // Check if path option is used and throw exception if (!empty($paths)) { $this->error('[ERROR] Migration by path is disabled!'); $this->error(''); $this->line('This command enforces running all pending migrations in order.'); $this->line('Please run migrations without the --path option.'); SqlQueryTransformer::disable(); return 1; } // Determine which migration paths to use for whitelist check $paths_to_check = $is_framework_only ? [database_path('migrations')] : MigrationPaths::get_all_paths(); // Check migration whitelist if (!$this->checkMigrationWhitelist($paths_to_check)) { SqlQueryTransformer::disable(); return 1; } // Validate migration files for Schema builder usage (only in development) if ($is_development && !$this->_validate_schema_rules()) { SqlQueryTransformer::disable(); return 1; } // Run normalize_schema BEFORE migrations to fix existing tables $requiredColumnsArgs = $is_development ? [] : ['--production' => true]; $this->info("\n Pre-migration normalization (fixing existing tables)...\n"); $normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs); if ($normalizeExitCode !== 0) { $this->error('Pre-migration normalization failed'); SqlQueryTransformer::disable(); return $normalizeExitCode; } // Use a buffered output to capture migration output $bufferedOutput = new BufferedOutput(); try { // Run the standard migrations and capture the output $originalSqlMode = DB::selectOne('SELECT @@sql_mode as sql_mode')->sql_mode; DB::statement("SET sql_mode = REPLACE(@@sql_mode, 'NO_ZERO_DATE', '');"); // Run migrations directly via migrator to avoid recursion $migrator = app('migrator'); $migrator->setOutput($bufferedOutput); // Use all migration paths (framework + user), or just framework if --framework-only $migrationPaths = $is_framework_only ? [database_path('migrations')] : MigrationPaths::get_all_paths(); // Run migrations one-by-one with normalization after each $this->run_migrations_with_normalization($migrator, $migrationPaths, $step, $requiredColumnsArgs); $exitCode = 0; // Handle seeding if requested if ($seed) { $this->call('db:seed', ['--force' => $force]); } $migrationOutput = $bufferedOutput->fetch(); // Show the output from the migration command $this->output->write($migrationOutput); DB::statement('SET sql_mode = ?', [$originalSqlMode]); } catch (\Exception $e) { // Restore SQL mode try { if (isset($originalSqlMode)) { DB::statement('SET sql_mode = ?', [$originalSqlMode]); } } catch (\Exception $sqlEx) { // Ignore SQL mode restore errors } $this->error(''); $this->error('Error: ' . $e->getMessage()); // Disable query logging before returning AppServiceProvider::disable_query_echo(); SqlQueryTransformer::disable(); return 1; } // Run normalize_schema AFTER migrations to add framework columns to new tables $this->info("\n Post-migration normalization (adding framework columns to new tables)...\n"); // Switch to destructive-only query logging for normalize_schema AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT); $normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs); if ($normalizeExitCode !== 0) { $this->error('Post-migration normalization failed'); SqlQueryTransformer::disable(); return $normalizeExitCode; } // Disable query logging AppServiceProvider::disable_query_echo(); SqlQueryTransformer::disable(); return $exitCode; } /** * Create a database snapshot */ protected function create_snapshot(): bool { // Check if already in migration mode (shouldn't happen with new unified command) if (file_exists($this->flag_file)) { // Clean up stale migration mode $this->warn('[WARNING] Found stale migration mode flag. Cleaning up...'); $this->cleanup_migration_mode(); } try { // Stop MySQL $this->shell_exec_privileged('supervisorctl stop mysql 2>&1'); sleep(3); // Remove old backup if exists if (is_dir($this->backup_dir)) { $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); } // Copy MySQL data directory $this->run_privileged_command(['cp', '-r', $this->mysql_data_dir, $this->backup_dir]); // Start MySQL $this->shell_exec_privileged('mkdir -p /var/run/mysqld'); $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); // Wait for MySQL to be ready $this->wait_for_mysql_ready(); // Create migration flag file file_put_contents($this->flag_file, json_encode([ 'started_at' => now()->toIso8601String(), 'started_by' => get_current_user(), 'backup_dir' => $this->backup_dir, ], JSON_PRETTY_PRINT)); $this->info('[OK] Snapshot created successfully.'); return true; } catch (\Exception $e) { $this->error('[ERROR] Failed to create snapshot: ' . $e->getMessage()); // Try to restart MySQL $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); return false; } } /** * Rollback to snapshot */ protected function rollback_snapshot(): bool { if (!is_dir($this->backup_dir)) { $this->error('[ERROR] Backup directory not found!'); return false; } try { // Stop MySQL $this->shell_exec_privileged('supervisorctl stop mysql 2>&1'); sleep(3); // Clear current MySQL data $this->shell_exec_privileged("rm -rf {$this->mysql_data_dir}/* 2>/dev/null"); $this->shell_exec_privileged("rm -rf {$this->mysql_data_dir}/.* 2>/dev/null"); // Restore backup $this->shell_exec_privileged("cp -r {$this->backup_dir}/* {$this->mysql_data_dir}/"); $this->shell_exec_privileged("cp -r {$this->backup_dir}/.[^.]* {$this->mysql_data_dir}/ 2>/dev/null"); // Fix permissions $this->run_privileged_command(['chown', '-R', 'mysql:mysql', $this->mysql_data_dir]); // Start MySQL $this->shell_exec_privileged('mkdir -p /var/run/mysqld'); $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); // Wait for MySQL to be ready $this->wait_for_mysql_ready(); return true; } catch (\Exception $e) { $this->error('[ERROR] Rollback failed: ' . $e->getMessage()); // Try to restart MySQL $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); return false; } } /** * Commit snapshot (remove backup and flag) */ protected function commit_snapshot(): void { // Remove backup directory if (is_dir($this->backup_dir)) { $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); } // Remove migration flag if (file_exists($this->flag_file)) { unlink($this->flag_file); } $this->info('[OK] Snapshot committed - backup removed.'); } /** * Cleanup migration mode (remove flag and backup) */ protected function cleanup_migration_mode(): void { if (is_dir($this->backup_dir)) { $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); } if (file_exists($this->flag_file)) { unlink($this->flag_file); } } /** * Wait for MySQL to be ready for connections */ protected function wait_for_mysql_ready(): void { $max_attempts = 120; $attempt = 0; while ($attempt < $max_attempts) { $result = shell_exec("echo \"SELECT 'test';\" | mysql -urspade -prspadepass 2>/dev/null | grep test"); if ($result !== null && str_contains($result, 'test')) { return; } sleep(1); $attempt++; } throw new \Exception('MySQL did not start within 120 seconds'); } /** * Register query transformation listener */ protected function register_query_transformer(): void { $original_statement = \Closure::bind(function () { return $this->connection->statement(...func_get_args()); }, DB::getFacadeRoot(), DB::getFacadeRoot()); DB::macro('statement', function ($query, $bindings = []) use ($original_statement) { $transformed = SqlQueryTransformer::transform($query); return DB::connection()->statement($transformed, $bindings); }); } /** * Check if all pending migrations are whitelisted */ protected function checkMigrationWhitelist(array $paths): bool { $whitelistPaths = [ database_path('migrations/.migration_whitelist'), base_path('rsx/resource/migrations/.migration_whitelist'), ]; $whitelistedMigrations = []; $foundAtLeastOne = false; foreach ($whitelistPaths as $whitelistPath) { if (file_exists($whitelistPath)) { $foundAtLeastOne = true; $whitelist = json_decode(file_get_contents($whitelistPath), true); $migrations = array_keys($whitelist['migrations'] ?? []); $whitelistedMigrations = array_merge($whitelistedMigrations, $migrations); } } if (!$foundAtLeastOne) { $this->warn('[WARNING] No migration whitelist found. Creating one with existing migrations...'); $this->createInitialWhitelist(); return true; } $migrationFiles = []; foreach ($paths as $path) { if (is_dir($path)) { foreach (glob($path . '/*.php') as $file) { $migrationFiles[] = basename($file); } } } $unauthorizedMigrations = array_diff($migrationFiles, $whitelistedMigrations); if (!empty($unauthorizedMigrations)) { $this->error('[ERROR] Unauthorized migrations detected!'); $this->error(''); $this->line('The following migrations were not created via php artisan make:migration:'); foreach ($unauthorizedMigrations as $migration) { $this->line(' - ' . $migration); } $this->error(''); $this->warn('[WARNING] Manually created migrations can cause timestamp conflicts.'); $this->error(''); $this->line('To fix: Create migrations using "php artisan make:migration [name]"'); return false; } return true; } /** * Create initial whitelist with existing migrations */ protected function createInitialWhitelist(): void { $whitelistLocations = [ database_path('migrations/.migration_whitelist') => database_path('migrations'), base_path('rsx/resource/migrations/.migration_whitelist') => base_path('rsx/resource/migrations'), ]; $totalMigrations = 0; foreach ($whitelistLocations as $whitelistPath => $migrationPath) { if (!is_dir($migrationPath)) { continue; } $whitelist = [ 'description' => 'This file tracks migrations created via php artisan make:migration', 'purpose' => 'Prevents manually created migrations from running to avoid timestamp conflicts', 'migrations' => [], ]; foreach (glob($migrationPath . '/*.php') as $file) { $filename = basename($file); $whitelist['migrations'][$filename] = [ 'created_at' => 'pre-whitelist', 'created_by' => 'system', 'command' => 'existing migration', ]; } if (!empty($whitelist['migrations'])) { file_put_contents($whitelistPath, json_encode($whitelist, JSON_PRETTY_PRINT)); $count = count($whitelist['migrations']); $totalMigrations += $count; $location = str_replace(base_path() . '/', '', dirname($whitelistPath)); $this->info("[OK] Created whitelist in {$location} with {$count} migration(s)."); } } if ($totalMigrations === 0) { $this->info('[OK] No existing migrations found. Empty whitelists created.'); } } /** * Validate migration files for Schema builder usage */ protected function _validate_schema_rules(): bool { $repository = app('migration.repository'); $pending_migrations = MigrationValidator::get_pending_migrations($repository); if (empty($pending_migrations)) { return true; } $this->info('Validating migration files for Schema builder usage...'); $has_violations = false; foreach ($pending_migrations as $migration_path) { try { MigrationValidator::validate_migration_file($migration_path); $this->info(" [OK] " . basename($migration_path)); } catch (\RuntimeException $e) { $has_violations = true; break; } } if ($has_violations) { $this->error(''); $this->error('Migration validation failed!'); $this->info(''); $this->line('Please update your migration to use raw SQL instead of Schema builder.'); $this->line('See: php artisan rsx:man migrations'); return false; } // Remove down() methods $processed_migrations = []; foreach ($pending_migrations as $migration_path) { if (MigrationValidator::remove_down_method($migration_path)) { $processed_migrations[] = $migration_path; } } if (!empty($processed_migrations)) { $this->info(''); $this->warn('[WARNING] Modified migration files:'); foreach ($processed_migrations as $path) { $this->line(' - ' . basename($path) . ' - down() method removed'); } $this->info(''); } return true; } /** * Ensure the migrations table exists */ protected function ensure_migrations_table_exists(): void { try { $table = config('database.migrations', 'migrations'); DB::select("SELECT 1 FROM {$table} LIMIT 1"); } catch (\Exception $e) { $table = config('database.migrations', 'migrations'); $this->info('Creating migrations table...'); DB::statement(" CREATE TABLE IF NOT EXISTS {$table} ( id BIGINT AUTO_INCREMENT PRIMARY KEY, migration VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, batch BIGINT NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); $this->info('[OK] Migrations table created'); } } /** * Run migrations one-by-one with normalization after each */ protected function run_migrations_with_normalization($migrator, array $migrationPaths, bool $step, array $requiredColumnsArgs): void { $files = []; foreach ($migrationPaths as $path) { if (is_dir($path)) { $path_files = glob($path . '/*.php'); if ($path_files) { foreach ($path_files as $file) { $files[] = $file; } } } } sort($files); $repository = $migrator->getRepository(); $ran = $repository->getRan(); $pending = []; foreach ($files as $file) { $migrationName = $migrator->getMigrationName($file); if (!in_array($migrationName, $ran)) { $pending[] = $file; } } if (empty($pending)) { $this->info(' INFO Nothing to migrate.'); return; } $totalMigrations = count($pending); $currentMigration = 1; foreach ($pending as $file) { $migrationName = $migrator->getMigrationName($file); $this->info("\n[$currentMigration/$totalMigrations] Running: $migrationName"); $this->newLine(); $migrator->runPending([$file], [ 'pretend' => false, 'step' => $step ]); if ($currentMigration < $totalMigrations) { $this->info("\n Normalizing schema after migration...\n"); AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT); $normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs); AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT); if ($normalizeExitCode !== 0) { throw new \Exception("Normalization failed after migration: $migrationName"); } } $currentMigration++; } $this->newLine(); $this->info("[OK] All $totalMigrations migration" . ($totalMigrations > 1 ? 's' : '') . " completed successfully"); } }