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(); // Check if we're in production mode (either via flag or environment) $is_production = $this->option('production') || app()->environment('production'); // Check if we're in framework-only mode $is_framework_only = $this->option('framework-only'); // Only enforce snapshot protection in development mode without --production or --framework-only flag $require_snapshot = !$is_production && !$is_framework_only; // Check for migration mode if we require snapshot if ($require_snapshot) { if (!file_exists($this->flag_file)) { $this->error('[ERROR] Migration mode not active!'); $this->error(''); $this->line('In development mode, you must create a database snapshot before running migrations.'); $this->line('This prevents partial migrations from corrupting your database.'); $this->info(''); $this->line('To begin a migration session:'); $this->line(' php artisan migrate:begin'); return 1; } $this->info('[OK] Migration mode active - snapshot available for rollback'); $this->info(''); } elseif ($is_production) { $this->info(' Running in production mode (no snapshot protection)'); } elseif ($is_framework_only) { $this->info(' Running framework-only migrations (no snapshot protection)'); } $mode_desc = $is_production ? ' (production mode)' : ($is_framework_only ? ' (framework-only)' : ' with maintenance commands'); $this->info('Running migrations' . $mode_desc . '...'); // 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:'); $this->info(' php artisan migrate'); $this->error(''); $this->line('If you need to run specific migrations:'); $this->line(' 1. Place them in the standard migrations directory'); $this->line(' 2. Use migration timestamps to control execution order'); 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)) { return 1; } // Validate migration files for Schema builder usage (only in non-production) if (!$is_production && !$this->_validate_schema_rules()) { return 1; } // Build command arguments $migrateArgs = []; if ($force) { $migrateArgs['--force'] = true; } if ($seed) { $migrateArgs['--seed'] = true; } if ($step) { $migrateArgs['--step'] = true; } // Run normalize_schema BEFORE migrations to fix existing tables // Pass --production flag to skip snapshot in both production and framework-only modes $requiredColumnsArgs = ($is_production || $is_framework_only) ? ['--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]); // Check if migration failed if ($exitCode !== 0) { throw new \Exception("Migration command failed with exit code: $exitCode"); } } catch (\Exception $e) { // Restore SQL mode try { DB::statement('SET sql_mode = ?', [$originalSqlMode]); } catch (\Exception $sqlEx) { // Ignore SQL mode restore errors } $this->error(''); $this->error('[ERROR] Migration failed!'); $this->error('Error: ' . $e->getMessage()); // If in development mode with snapshot, automatically rollback if ($require_snapshot && file_exists($this->flag_file)) { $this->error(''); $this->warn(' Automatically rolling back to snapshot due to migration failure...'); $this->info(''); // Call rollback command with output $rollback_result = Artisan::call('migrate:rollback', [], $this->output); if ($rollback_result === 0) { $this->info(''); $this->info('[OK] Database rolled back successfully!'); $this->info(''); $this->warn('[WARNING] You are still in migration mode.'); $this->line('Your options:'); $this->line(' 1. Fix your migration files'); $this->line(' 2. Run "php artisan migrate" to try again'); $this->line(' 3. Run "php artisan migrate:commit" to exit migration mode'); } else { $this->error('[ERROR] Automatic rollback failed!'); $this->error('Run "php artisan migrate:rollback" manually.'); $this->warn('[WARNING] The database may be in an inconsistent state!'); } } // Disable query logging before returning AppServiceProvider::disable_query_echo(); // Disable SQL query transformation 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'); return $normalizeExitCode; } // Run regenerate constants if not in production mode (framework-only still runs it) if (!$is_production) { // Disable query logging for regenerate_constants AppServiceProvider::disable_query_echo(); $maintenanceExitCode = $this->runMaintenanceCommand('rsx:constants:regenerate'); if ($maintenanceExitCode !== 0) { $this->error('Regenerate constants maintenance failed'); return $maintenanceExitCode; } } else { // In production mode, check manifest consistency with database // Disable query logging for consistency check AppServiceProvider::disable_query_echo(); $this->info("\n"); $consistency_check_exit = $this->call('rsx:migrate:check_consistency'); if ($consistency_check_exit !== 0) { $exitCode = 1; } } $this->info("\nAll tasks completed!"); // Remind user to commit snapshot in development mode if ($require_snapshot) { $this->newLine(); $this->line('Please run php artisan migrate:commit to finish the migration process.'); } // Disable query logging AppServiceProvider::disable_query_echo(); // Disable SQL query transformation SqlQueryTransformer::disable(); return $exitCode; } /** * Register query transformation listener * * Intercepts all DB::statement() calls and transforms CREATE/ALTER statements * to enforce framework column type conventions. */ protected function register_query_transformer(): void { // Store the original statement method $original_statement = \Closure::bind(function () { return $this->connection->statement(...func_get_args()); }, DB::getFacadeRoot(), DB::getFacadeRoot()); // Override DB::statement to transform queries DB::macro('statement', function ($query, $bindings = []) use ($original_statement) { // Transform the query before execution $transformed = SqlQueryTransformer::transform($query); // Call the original statement method with transformed query return DB::connection()->statement($transformed, $bindings); }); } /** * Check if all pending migrations are whitelisted * * @param array $paths Array of migration paths to check */ protected function checkMigrationWhitelist(array $paths): bool { // Load whitelists from both locations and merge them $whitelistPaths = [ database_path('migrations/.migration_whitelist'), // Framework migrations base_path('rsx/resource/migrations/.migration_whitelist'), // Application migrations ]; $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 no whitelist exists yet, create one with existing migrations if (!$foundAtLeastOne) { $this->warn('[WARNING] No migration whitelist found. Creating one with existing migrations...'); $this->createInitialWhitelist(); return true; } // Get all migration files from specified paths $migrationFiles = []; foreach ($paths as $path) { if (is_dir($path)) { foreach (glob($path . '/*.php') as $file) { $migrationFiles[] = basename($file); } } } // Check for unauthorized migrations $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 and ordering issues.'); $this->error(''); $this->line('To fix this:'); $this->line('1. Create a new migration using: php artisan make:migration [name]'); $this->line('2. Copy the migration code from the unauthorized file to the new file'); $this->line('3. Delete the unauthorized migration file'); $this->line('4. Run migrations again'); $this->error(''); $this->line('This ensures proper timestamp generation and prevents LLM timestamp confusion.'); return false; } return true; } /** * Create initial whitelist with existing migrations * Creates separate whitelist files for framework and application 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) { // Skip if migration directory doesn't exist 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' => [], ]; // Add all existing migrations from this specific path foreach (glob($migrationPath . '/*.php') as $file) { $filename = basename($file); $whitelist['migrations'][$filename] = [ 'created_at' => 'pre-whitelist', 'created_by' => 'system', 'command' => 'existing migration', ]; } // Only create whitelist if there are migrations in this location 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.'); } } protected function runMaintenanceCommand($command, $arguments = []) { $this->info("\nRunning $command..."); $output = new BufferedOutput(); $exitCode = Artisan::call($command, $arguments, $output); $commandOutput = $output->fetch(); // Only show concise output if successful if ($exitCode === 0) { $this->info("[OK] Command $command completed successfully."); } else { $this->error("[FAIL] Command $command failed with exit code $exitCode"); $this->output->write($commandOutput); } return $exitCode; } /** * Validate migration files for Schema builder usage * Only enforced in non-production mode */ protected function _validate_schema_rules(): bool { $repository = app('migration.repository'); $pending_migrations = MigrationValidator::get_pending_migrations($repository); if (empty($pending_migrations)) { // No pending migrations to validate return true; } $this->info('Validating migration files for Schema builder usage...'); $has_violations = false; foreach ($pending_migrations as $migration_path) { try { // Validate the migration file for Schema builder usage MigrationValidator::validate_migration_file($migration_path); $this->info(" [OK] " . basename($migration_path)); } catch (\RuntimeException $e) { // MigrationValidator already printed colored error output $has_violations = true; break; // Stop at first violation } } 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; } // All validations passed - now remove down() methods $processed_migrations = []; foreach ($pending_migrations as $migration_path) { if (MigrationValidator::remove_down_method($migration_path)) { $processed_migrations[] = $migration_path; } } // If we processed any migrations (removed down methods), notify user 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(''); $this->line('These migrations have been updated to follow framework standards.'); } return true; } /** * Ensure the migrations table exists in the database * Creates it using raw SQL if it doesn't exist */ protected function ensure_migrations_table_exists(): void { try { // Try to query the migrations table DB::select('SELECT 1 FROM migrations LIMIT 1'); } catch (\Exception $e) { // Table doesn't exist, create it $this->info('Creating migrations table...'); DB::statement(" CREATE TABLE IF NOT EXISTS migrations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, migration VARCHAR(255) NOT NULL, batch INT 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 * * Runs pending migrations individually, normalizing schema after each successful * migration. This ensures type consistency before subsequent migrations reference * newly-created tables. * * @param \Illuminate\Database\Migrations\Migrator $migrator * @param array $migrationPaths * @param bool $step * @param array $requiredColumnsArgs * @return void */ protected function run_migrations_with_normalization($migrator, array $migrationPaths, bool $step, array $requiredColumnsArgs): void { // Get all migration files $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 chronologically by filename sort($files); // Get already-run migrations $repository = $migrator->getRepository(); $ran = $repository->getRan(); // Filter to only pending migrations $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; // Run each migration individually with normalization after foreach ($pending as $file) { $migrationName = $migrator->getMigrationName($file); $this->info("\n[$currentMigration/$totalMigrations] Running: $migrationName"); $this->newLine(); // Run this single migration $migrator->runPending([$file], [ 'pretend' => false, 'step' => $step ]); // Run normalization after this migration (if not the last) if ($currentMigration < $totalMigrations) { $this->info("\n Normalizing schema after migration...\n"); // Switch to destructive-only query logging AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT); $normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs); // Restore full query logging 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"); } }