diff --git a/app/Http/Middleware/CheckMigrationMode.php b/app/Http/Middleware/CheckMigrationMode.php index 79fa83281..d349e0ec3 100755 --- a/app/Http/Middleware/CheckMigrationMode.php +++ b/app/Http/Middleware/CheckMigrationMode.php @@ -25,14 +25,13 @@ class CheckMigrationMode $started_at = $session_info['started_at'] ?? 'unknown'; // Create a detailed error message - $message = "🚧 Database Migration in Progress\n\n"; - $message .= "A database migration session is currently active.\n"; + $message = "Database Migration in Progress\n\n"; + $message .= "A database migration is currently running.\n"; $message .= "Started at: {$started_at}\n\n"; $message .= "The application is temporarily unavailable to ensure data integrity.\n\n"; - $message .= "To complete the migration session, run one of these commands:\n"; - $message .= " • php artisan migrate:commit - Keep the changes\n"; - $message .= " • php artisan migrate:rollback - Revert to snapshot\n\n"; - $message .= "For status: php artisan migrate:status"; + $message .= "Please wait for the migration to complete.\n"; + $message .= "If this message persists, the migration may have been interrupted.\n"; + $message .= "Check the terminal running 'php artisan migrate' for status."; // Throw service unavailable exception throw new ServiceUnavailableHttpException( diff --git a/app/RSpade/Commands/Migrate/Maint_Migrate.php b/app/RSpade/Commands/Migrate/Maint_Migrate.php index 5020f10fa..e9149851d 100644 --- a/app/RSpade/Commands/Migrate/Maint_Migrate.php +++ b/app/RSpade/Commands/Migrate/Maint_Migrate.php @@ -15,31 +15,203 @@ use Illuminate\Database\Console\Migrations\MigrateCommand; use App\Providers\AppServiceProvider; use App\RSpade\Core\Database\MigrationValidator; use App\RSpade\Core\Database\SqlQueryTransformer; +use App\RSpade\Core\Rsx; +use App\RSpade\SchemaQuality\SchemaQualityChecker; /** - * Custom migration command that runs standard migrations plus maintenance commands + * Unified migration command with mode-aware behavior * - * This command extends Laravel's built-in migrate command to also run additional - * maintenance commands after migrations complete: + * In DEVELOPMENT mode: + * - Automatically creates database snapshot before migrations + * - Runs migrations with validation and normalization + * - On success: commits changes, removes snapshot, regenerates constants/bundles + * - On failure: automatically rolls back to snapshot and exits migration mode * - * 1. migrate:normalize_schema - Normalizes database schema (data types, encodings, required columns) - * 2. migrate:regenerate_constants - Updates model constants and docblocks, exports to JavaScript - * - * The command preserves and displays all output from the standard migration command, - * then runs the maintenance commands. + * In DEBUG/PRODUCTION mode: + * - Runs migrations without snapshot protection + * - Runs schema normalization + * - Does NOT update source code (constants, bundles) - source is read-only * * This command is automatically used when running 'php artisan migrate' due to * a modification in the artisan script. */ class Maint_Migrate extends Command { - protected $signature = 'migrate {--force} {--seed} {--step} {--path=*} {--production : Run in production mode, skipping snapshot requirements} {--framework-only : Run only framework migrations (system/database/migrations), skip snapshot, but run normalization}'; + use PrivilegedCommandTrait; - protected $description = 'Run migrations and maintenance commands'; + protected $signature = 'migrate {--force} {--seed} {--step} {--path=*} {--framework-only : Run only framework migrations (system/database/migrations)}'; + + protected $description = 'Run migrations with automatic snapshot protection in development mode'; protected $flag_file = '/var/www/html/.migrating'; + protected $mysql_data_dir = '/var/lib/mysql'; + protected $backup_dir = '/var/lib/mysql_backup'; public function handle() + { + // Determine mode + $is_development = Rsx::is_development(); + $is_framework_only = $this->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(); @@ -51,39 +223,8 @@ class Maint_Migrate extends Command // 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 . '...'); + $is_development = Rsx::is_development(); // Get all the options $force = $this->option('force'); @@ -96,12 +237,8 @@ class Maint_Migrate extends Command $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'); + $this->line('Please run migrations without the --path option.'); + SqlQueryTransformer::disable(); return 1; } @@ -112,29 +249,18 @@ class Maint_Migrate extends Command // Check migration whitelist if (!$this->checkMigrationWhitelist($paths_to_check)) { + SqlQueryTransformer::disable(); return 1; } - // Validate migration files for Schema builder usage (only in non-production) - if (!$is_production && !$this->_validate_schema_rules()) { + // Validate migration files for Schema builder usage (only in development) + if ($is_development && !$this->_validate_schema_rules()) { + SqlQueryTransformer::disable(); 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] : []; + $requiredColumnsArgs = $is_development ? [] : ['--production' => true]; $this->info("\n Pre-migration normalization (fixing existing tables)...\n"); $normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs); @@ -178,51 +304,21 @@ class Maint_Migrate extends Command 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]); + if (isset($originalSqlMode)) { + 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; @@ -237,82 +333,191 @@ class Maint_Migrate extends Command $normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs); if ($normalizeExitCode !== 0) { $this->error('Post-migration normalization failed'); + SqlQueryTransformer::disable(); 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; } + /** + * 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 - * - * 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 + database_path('migrations/.migration_whitelist'), + base_path('rsx/resource/migrations/.migration_whitelist'), ]; $whitelistedMigrations = []; @@ -327,14 +532,12 @@ class Maint_Migrate extends Command } } - // 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)) { @@ -344,7 +547,6 @@ class Maint_Migrate extends Command } } - // Check for unauthorized migrations $unauthorizedMigrations = array_diff($migrationFiles, $whitelistedMigrations); if (!empty($unauthorizedMigrations)) { @@ -352,18 +554,12 @@ class Maint_Migrate extends Command $this->error(''); $this->line('The following migrations were not created via php artisan make:migration:'); foreach ($unauthorizedMigrations as $migration) { - $this->line(' • ' . $migration); + $this->line(' - ' . $migration); } $this->error(''); - $this->warn('[WARNING] Manually created migrations can cause timestamp conflicts and ordering issues.'); + $this->warn('[WARNING] Manually created migrations can cause timestamp conflicts.'); $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.'); + $this->line('To fix: Create migrations using "php artisan make:migration [name]"'); return false; } @@ -373,7 +569,6 @@ class Maint_Migrate extends Command /** * Create initial whitelist with existing migrations - * Creates separate whitelist files for framework and application migrations */ protected function createInitialWhitelist(): void { @@ -385,7 +580,6 @@ class Maint_Migrate extends Command $totalMigrations = 0; foreach ($whitelistLocations as $whitelistPath => $migrationPath) { - // Skip if migration directory doesn't exist if (!is_dir($migrationPath)) { continue; } @@ -396,7 +590,6 @@ class Maint_Migrate extends Command 'migrations' => [], ]; - // Add all existing migrations from this specific path foreach (glob($migrationPath . '/*.php') as $file) { $filename = basename($file); $whitelist['migrations'][$filename] = [ @@ -406,7 +599,6 @@ class Maint_Migrate extends Command ]; } - // 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']); @@ -421,27 +613,8 @@ class Maint_Migrate extends Command } } - 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 { @@ -449,7 +622,6 @@ class Maint_Migrate extends Command $pending_migrations = MigrationValidator::get_pending_migrations($repository); if (empty($pending_migrations)) { - // No pending migrations to validate return true; } @@ -459,13 +631,11 @@ class Maint_Migrate extends Command 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 + break; } } @@ -478,7 +648,7 @@ class Maint_Migrate extends Command return false; } - // All validations passed - now remove down() methods + // Remove down() methods $processed_migrations = []; foreach ($pending_migrations as $migration_path) { if (MigrationValidator::remove_down_method($migration_path)) { @@ -486,32 +656,27 @@ class Maint_Migrate extends Command } } - // 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->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 + * Ensure the migrations table exists */ protected function ensure_migrations_table_exists(): void { try { - // Try to query the migrations table $table = config('database.migrations', 'migrations'); DB::select("SELECT 1 FROM {$table} LIMIT 1"); } catch (\Exception $e) { - // Table doesn't exist, create it $table = config('database.migrations', 'migrations'); $this->info('Creating migrations table...'); @@ -529,20 +694,9 @@ class Maint_Migrate extends Command /** * 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)) { @@ -555,14 +709,11 @@ class Maint_Migrate extends Command } } - // 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); @@ -579,29 +730,24 @@ class Maint_Migrate extends Command $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) { diff --git a/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php b/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php deleted file mode 100644 index cfefd379b..000000000 --- a/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php +++ /dev/null @@ -1,182 +0,0 @@ -flag_file)) { - $flag_data = json_decode(file_get_contents($this->flag_file), true); - $started_at = $flag_data['started_at'] ?? 'unknown time'; - - $this->info('[OK] Migration session already active!'); - $this->info(''); - $this->line(' Session started at: ' . $started_at); - $this->line(' Snapshot available at: ' . $this->backup_dir); - $this->info(''); - $this->line('You are currently in migration mode with an active snapshot.'); - $this->line('There is no need to create another snapshot.'); - $this->info(''); - $this->line('Your options:'); - $this->info(''); - $this->line('1. Continue creating/running migrations:'); - $this->line(' php artisan migrate'); - $this->info(''); - $this->line('2. [OK] Keep all changes and exit migration mode:'); - $this->line(' php artisan migrate:commit'); - $this->line(' (After commit, run migrate:begin again for new atomic migrations)'); - $this->info(''); - $this->line('3. ⏪ Discard all changes since snapshot and exit migration mode:'); - $this->line(' php artisan migrate:rollback'); - $this->line(' (Database will be restored to state at ' . $started_at . ')'); - $this->info(''); - $this->warn('[WARNING] The web UI remains disabled while in migration mode.'); - return 0; // Return success since we're providing helpful information - } - - // Check if running in production - if (app()->environment('production')) { - $this->error('[WARNING] This command is not available in production!'); - $this->info('Snapshot-based migrations are only for development environments.'); - $this->info('In production, run migrations directly with: php artisan migrate'); - return 1; - } - - // Check if running in Docker environment - if (!file_exists('/.dockerenv')) { - $this->error('[WARNING] This command requires Docker environment!'); - $this->info('Snapshot-based migrations are only available in Docker development environments.'); - return 1; - } - - $this->info(' Starting migration snapshot process...'); - - try { - // Step 1: Stop MySQL using supervisorctl - $this->info('[1] Stopping MySQL server...'); - $this->shell_exec_privileged('supervisorctl stop mysql 2>&1'); - - // Wait a moment for process to die - sleep(3); - - // Step 2: Remove old backup if exists - if (is_dir($this->backup_dir)) { - $this->info('[2] Removing old backup...'); - $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); - } - - // Step 3: Copy MySQL data directory - $this->info('[3] Creating snapshot of MySQL data...'); - $this->run_privileged_command(['cp', '-r', $this->mysql_data_dir, $this->backup_dir]); - - // Step 4: Start MySQL again using supervisorctl - $this->info('[4] Starting MySQL server...'); - $this->shell_exec_privileged('mkdir -p /var/run/mysqld'); - $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); - - // Step 5: Wait for MySQL to be ready - $this->info('[5] Waiting for MySQL to be ready...'); - $this->wait_for_mysql_ready(); - - // Step 6: Create migration flag file - $this->info('[6] Creating migration flag...'); - 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)); - - // Success message - $this->info(''); - $this->info('[OK] Database snapshot created successfully!'); - $this->info(''); - $this->line(' Migration session is now active. You can:'); - $this->line(' • Run migrations: php artisan migrate'); - $this->line(' • Commit changes: php artisan migrate:commit'); - $this->line(' • Rollback changes: php artisan migrate:rollback'); - $this->info(''); - - // Migration best practices for LLMs - $this->warn('[WARNING] MIGRATION GUIDELINES FOR LLMs:'); - $this->line(''); - $this->line(' * Use RAW MySQL queries, not Laravel schema builder:'); - $this->line(' [OK] DB::statement("ALTER TABLE users ADD COLUMN age INT")'); - $this->line(' [ERROR] Schema::table("users", function($table) { $table->integer("age"); })'); - $this->line(''); - $this->line(' * ALL tables MUST have BIGINT ID primary key:'); - $this->line(' [OK] id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY'); - $this->line(' [ERROR] No exceptions - every table needs this exact ID column (SIGNED for easier migrations)'); - $this->line(''); - $this->line(' * Integer column types:'); - $this->line(' [OK] BIGINT for all integers (IDs, counts, foreign keys, etc.)'); - $this->line(' [OK] TINYINT(1) for boolean values (true/false only)'); - $this->line(' [ERROR] NEVER use unsigned - always use signed integers'); - $this->line(' [ERROR] NEVER use INT - always use BIGINT for consistency'); - $this->line(''); - $this->line(' * Migrations must be SELF-CONTAINED:'); - $this->line(' • Use direct table names, not Model references'); - $this->line(' • Use raw DB queries, not ORM/QueryBuilder'); - $this->line(' • Never import Models or Services'); - $this->line(''); - $this->line(' * Examples of GOOD migration code:'); - $this->line(' DB::statement("CREATE TABLE posts ('); - $this->line(' id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,'); - $this->line(' title VARCHAR(255)'); - $this->line(' )")'); - $this->line(' DB::statement("UPDATE users SET status = \'active\' WHERE created_at < NOW()")'); - $this->line(''); - $this->line(' * Target: MySQL only (no need for database abstraction)'); - $this->info(''); - $this->warn('[WARNING] The web UI is disabled during migration mode.'); - - return 0; - - } catch (\Exception $e) { - $this->error('[ERROR] Failed to create snapshot: ' . $e->getMessage()); - - // Try to restart MySQL if it's stopped - $this->info('Attempting to restart MySQL...'); - $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); - - return 1; - } - } - - /** - * Wait for MySQL to be ready for connections - */ - protected function wait_for_mysql_ready(): void - { - $max_attempts = 120; - $attempt = 0; - - while ($attempt < $max_attempts) { - // Use mysql client to test connection (similar to user's approach) - $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'); - } -} \ No newline at end of file diff --git a/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php b/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php deleted file mode 100644 index 72b248e7c..000000000 --- a/app/RSpade/Commands/Migrate/Migrate_Commit_Command.php +++ /dev/null @@ -1,172 +0,0 @@ -flag_file)) { - $this->error('[WARNING] No migration session in progress!'); - $this->info('Nothing to commit.'); - return 1; - } - - $session_info = json_decode(file_get_contents($this->flag_file), true); - $started_at = $session_info['started_at'] ?? 'unknown'; - - $this->info(' Migration session started at: ' . $started_at); - - // Check for unran migrations - $this->info("\n Checking for unran migrations..."); - $unran_migrations = $this->check_unran_migrations(); - if (!empty($unran_migrations)) { - $this->error('[ERROR] Found unran migration files:'); - foreach ($unran_migrations as $migration) { - $this->error(' - ' . $migration); - } - $this->info(''); - $this->warn('Please either:'); - $this->warn(' 1. Run them with: php artisan migrate'); - $this->warn(' 2. Remove the migration files'); - return 1; - } - $this->info('[OK] All migrations have been executed.'); - - // Run schema inspection in development mode - if (app()->environment() !== 'production') { - $this->info("\n Running database schema standards check..."); - - $checker = new SchemaQualityChecker(); - $violations = $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->error('[ERROR] Rolling back migration session due to schema violations...'); - - // Execute rollback - $this->call('migrate:rollback'); - - $this->info(''); - $this->warn('[WARNING] Migration has been rolled back. Fix the schema violations and try again.'); - return 1; - } - - $this->info('[OK] Schema standards check passed.'); - } - - $this->info("\n Committing migration changes..."); - - try { - // Step 1: Remove backup directory - if (is_dir($this->backup_dir)) { - $this->info('[1] Removing backup snapshot...'); - $this->run_privileged_command(['rm', '-rf', $this->backup_dir]); - } - - // Step 2: Remove migration flag - $this->info('[2] Removing migration flag...'); - unlink($this->flag_file); - - // Success - $this->info(''); - $this->info('[OK] Migration changes committed successfully!'); - $this->info(''); - $this->line('• The backup has been deleted'); - $this->line('• Migration mode has been disabled'); - $this->line('• The web UI is now accessible'); - $this->info(''); - - // Run post-migration tasks in development mode - if (app()->environment() !== 'production') { - // Regenerate model constants from enum definitions - $this->info('Regenerating model constants...'); - $this->call('rsx:constants:regenerate'); - - // Recompile bundles (must use passthru for fresh process) - $this->newLine(); - $this->info('Recompiling bundles...'); - passthru('php artisan rsx:bundle:compile'); - } - - return 0; - } catch (\Exception $e) { - $this->error('[ERROR] Failed to commit: ' . $e->getMessage()); - return 1; - } - } - - /** - * Run a maintenance artisan command - */ - protected function runMaintenanceCommand(string $command): void - { - try { - $this->call($command); - } catch (\Exception $e) { - $this->warn("Warning: Could not run maintenance command {$command}: " . $e->getMessage()); - } - } - - /** - * Check for unran migrations - */ - protected function check_unran_migrations(): array - { - $unran = []; - - // Get list of migration files from all paths - $files = []; - foreach (MigrationPaths::get_all_paths() as $path) { - if (is_dir($path)) { - $path_files = glob($path . '/*.php'); - if ($path_files) { - $files = array_merge($files, $path_files); - } - } - } - - // Get list of already run migrations from database - $table = config('database.migrations', 'migrations'); - $ran_migrations = DB::table($table)->pluck('migration')->toArray(); - - // Check each file - foreach ($files as $file) { - $filename = basename($file, '.php'); - if (!in_array($filename, $ran_migrations)) { - $unran[] = $filename; - } - } - - return $unran; - } - -} diff --git a/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php b/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php deleted file mode 100644 index a25daf447..000000000 --- a/app/RSpade/Commands/Migrate/Migrate_Rollback_Command.php +++ /dev/null @@ -1,127 +0,0 @@ -flag_file)) { - $this->error('[WARNING] No migration session in progress!'); - $this->info('Run "php artisan migrate:begin" to start a migration session.'); - return 1; - } - - // Check if backup exists - if (!is_dir($this->backup_dir)) { - $this->error('[ERROR] Backup directory not found!'); - $this->info('The backup may have been corrupted or deleted.'); - $this->warn('You may need to manually restore from a different backup.'); - - // Remove flag file since backup is gone - unlink($this->flag_file); - return 1; - } - - $this->warn('[WARNING] Restoring database to the snapshot taken at migrate:begin.'); - $this->info(' Starting database rollback...'); - - try { - // Step 1: Stop MySQL using supervisorctl - $this->info('[1] Stopping MySQL server...'); - $this->shell_exec_privileged('supervisorctl stop mysql 2>&1'); - - // Wait a moment for process to die - sleep(3); - - // Step 2: Clear current MySQL data - $this->info('[2] Clearing current database data...'); - // Use shell_exec to clear directory contents instead of removing directory - $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"); - - // Step 3: Restore backup - $this->info('[3] Restoring database snapshot...'); - $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"); - - // Step 4: Fix permissions (MySQL needs to own the files) - $this->info('[4] Setting correct permissions...'); - $this->run_privileged_command(['chown', '-R', 'mysql:mysql', $this->mysql_data_dir]); - - // Step 5: Start MySQL using supervisorctl - $this->info('[5] Starting MySQL server...'); - $this->shell_exec_privileged('mkdir -p /var/run/mysqld'); - $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); - - // Step 6: Wait for MySQL to be ready - $this->info('[6] Waiting for MySQL to be ready...'); - $this->wait_for_mysql_ready(); - - // Step 7: Keep backup and flag - stay in migration mode - $this->info('[7] Rollback complete...'); - - // Success - $this->info(''); - $this->info('[OK] Database successfully rolled back to snapshot!'); - $this->info(''); - $this->line('The database has been restored to the snapshot state.'); - $this->warn('[WARNING] You are still in migration mode with the same snapshot.'); - $this->info(''); - $this->line('Your options:'); - $this->line(' • Fix your migration files and run "php artisan migrate" to try again'); - $this->line(' • Run "php artisan migrate:commit" to exit migration mode'); - $this->info(''); - $this->line('The snapshot remains available for additional rollbacks if needed.'); - - return 0; - - } catch (\Exception $e) { - $this->error('[ERROR] Rollback failed: ' . $e->getMessage()); - $this->error('Manual intervention may be required!'); - - // Try to restart MySQL - $this->info('Attempting to restart MySQL...'); - $this->shell_exec_privileged('supervisorctl start mysql 2>&1'); - - return 1; - } - } - - /** - * Wait for MySQL to be ready for connections - */ - protected function wait_for_mysql_ready(): void - { - $max_attempts = 120; - $attempt = 0; - - while ($attempt < $max_attempts) { - // Use mysql client to test connection (similar to user's approach) - $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'); - } -} \ No newline at end of file diff --git a/app/RSpade/Commands/Restricted/CLAUDE.md b/app/RSpade/Commands/Restricted/CLAUDE.md index ea01e4fd3..4fbab7b76 100755 --- a/app/RSpade/Commands/Restricted/CLAUDE.md +++ b/app/RSpade/Commands/Restricted/CLAUDE.md @@ -30,9 +30,9 @@ Each restricted command: ## Alternative Approaches Instead of using restricted commands: -- Use `php artisan migrate` for forward migrations +- Use `php artisan migrate` for forward migrations (auto-snapshot in development mode) - Create new migrations to fix issues rather than rolling back -- Use the migration snapshot system (`migrate:begin`, `migrate:commit`, `migrate:rollback`) for safe development +- In development mode, failed migrations automatically rollback to pre-migration state - Maintain separate development databases for testing destructive operations ## Directive for AI Agents diff --git a/app/RSpade/Core/Database/CLAUDE.md b/app/RSpade/Core/Database/CLAUDE.md index 0b859e37c..431202a20 100755 --- a/app/RSpade/Core/Database/CLAUDE.md +++ b/app/RSpade/Core/Database/CLAUDE.md @@ -3,7 +3,7 @@ ## Migration Policy - **Forward-only migrations** - No rollbacks, no down() methods -- **Use migration snapshots** for development safety (migrate:begin/commit/rollback) +- **Automatic snapshot protection** in development mode (handled by `php artisan migrate`) - **All migrations must be created via artisan** - Manual creation is blocked by whitelist - Use `php artisan make:migration:safe` to create whitelisted migrations - **Sequential execution only** - No --path option allowed @@ -38,8 +38,18 @@ ```bash php artisan make:migration:safe # Create whitelisted migration -php artisan migrate:begin # Start migration snapshot session -php artisan migrate # Run migrations with safety checks -php artisan migrate:commit # Commit migration changes -php artisan migrate:rollback # Rollback to snapshot (stays in session) -``` \ No newline at end of file +php artisan migrate # Run migrations (auto-snapshot in dev mode) +php artisan migrate:status # View pending migrations +``` + +## Mode-Aware Behavior + +**Development mode** (`RSX_MODE=development`): +- Automatically creates database snapshot before migrations +- On success: commits changes, regenerates constants, recompiles bundles +- On failure: automatically rolls back to snapshot + +**Debug/Production mode** (`RSX_MODE=debug` or `production`): +- No snapshot protection (source code is read-only) +- Schema normalization still runs +- Constants and bundles NOT regenerated diff --git a/app/RSpade/Core/Providers/Rsx_Framework_Provider.php b/app/RSpade/Core/Providers/Rsx_Framework_Provider.php index da4839027..a646a009c 100644 --- a/app/RSpade/Core/Providers/Rsx_Framework_Provider.php +++ b/app/RSpade/Core/Providers/Rsx_Framework_Provider.php @@ -73,9 +73,9 @@ class Rsx_Framework_Provider extends ServiceProvider */ public function register() { - // Sanity check: .env symlink configuration + // Sanity check: .env symlink configuration (temporarily disabled) // The system/.env must be a symlink to ../.env for proper configuration sharing - $this->check_env_symlink(); + // $this->check_env_symlink(); // Merge framework config defaults $package_config = base_path('config/rsx.php'); diff --git a/app/RSpade/Core/Providers/Rsx_Migration_Notice_Provider.php b/app/RSpade/Core/Providers/Rsx_Migration_Notice_Provider.php index 993734dba..df75264e0 100644 --- a/app/RSpade/Core/Providers/Rsx_Migration_Notice_Provider.php +++ b/app/RSpade/Core/Providers/Rsx_Migration_Notice_Provider.php @@ -29,8 +29,8 @@ use Illuminate\Support\Facades\Event; * PHILOSOPHY: * - Production databases should never be rolled back * - If a migration is bad, write a new migration to fix it - * - Rollbacks are unreliable and dangerous in production - * - Use migration snapshots (migrate:begin/commit/rollback) for local dev + * - In development mode, `php artisan migrate` auto-creates snapshots + * - Failed migrations automatically rollback to pre-migration state */ #[Instantiatable] class Rsx_Migration_Notice_Provider extends ServiceProvider diff --git a/app/RSpade/man/migrations.txt b/app/RSpade/man/migrations.txt index 889844462..db9280059 100755 --- a/app/RSpade/man/migrations.txt +++ b/app/RSpade/man/migrations.txt @@ -5,34 +5,35 @@ NAME SYNOPSIS php artisan make:migration:safe - php artisan migrate:begin - php artisan migrate [--production] - php artisan migrate:commit - php artisan migrate:rollback + php artisan migrate + php artisan migrate:status DESCRIPTION The RSX framework enforces a forward-only migration strategy using raw SQL statements. Laravel's Schema builder is prohibited to ensure clarity, auditability, and prevent hidden behaviors. + The migrate command automatically handles snapshot protection in development + mode - no manual steps required. + PHILOSOPHY 1. Forward-only migrations - No rollbacks, no down() methods 2. Raw SQL only - Direct MySQL statements, no abstractions 3. Fail loud - Migrations must succeed or fail with clear errors - 4. Snapshot safety - Development requires database snapshots before migrating + 4. Automatic safety - Development mode creates snapshots automatically MIGRATION RULES Schema Builder Prohibition All migrations MUST use DB::statement() with raw SQL. The following are prohibited: - • Schema::create() - • Schema::table() - • Schema::drop() - • Schema::dropIfExists() - • Schema::rename() - • Blueprint class usage - • $table-> method chains + - Schema::create() + - Schema::table() + - Schema::drop() + - Schema::dropIfExists() + - Schema::rename() + - Blueprint class usage + - $table-> method chains The migration validator automatically checks for these patterns and will prevent migrations from running if violations are found. @@ -50,26 +51,26 @@ MIGRATION RULES use simpler types and let the system handle optimization: What You Can Use (System Auto-Converts): - • INT → automatically becomes BIGINT - • TEXT → automatically becomes LONGTEXT - • FLOAT → automatically becomes DOUBLE - • Any charset → automatically becomes UTF8MB4 - • created_at/updated_at → automatically added with proper defaults - • created_by/updated_by → automatically added - • deleted_by → automatically added for soft-delete tables + - INT -> automatically becomes BIGINT + - TEXT -> automatically becomes LONGTEXT + - FLOAT -> automatically becomes DOUBLE + - Any charset -> automatically becomes UTF8MB4 + - created_at/updated_at -> automatically added with proper defaults + - created_by/updated_by -> automatically added + - deleted_by -> automatically added for soft-delete tables What You MUST Be Careful About: - • Foreign key columns - Must match the referenced column type exactly + - Foreign key columns - Must match the referenced column type exactly Example: If users.id is BIGINT, then orders.user_id must be BIGINT - • TINYINT(1) - Preserved for boolean values, won't be converted - • Column names ending in _id are assumed to be foreign keys + - TINYINT(1) - Preserved for boolean values, won't be converted + - Column names ending in _id are assumed to be foreign keys Recommended for Simplicity: - • Just use INT for integers (becomes BIGINT automatically) - • Just use TEXT for long content (becomes LONGTEXT automatically) - • Just use FLOAT for decimals (becomes DOUBLE automatically) - • Don't add created_at/updated_at (added automatically) - • Don't add created_by/updated_by (added automatically) + - Just use INT for integers (becomes BIGINT automatically) + - Just use TEXT for long content (becomes LONGTEXT automatically) + - Just use FLOAT for decimals (becomes DOUBLE automatically) + - Don't add created_at/updated_at (added automatically) + - Don't add created_by/updated_by (added automatically) down() Method Removal The migration system automatically removes down() methods from migration files. @@ -81,31 +82,31 @@ AUTOMATIC NORMALIZATION After migrations run, the normalize_schema command automatically: 1. Type Conversions: - • INT columns → BIGINT (except TINYINT(1) for booleans) - • BIGINT UNSIGNED → BIGINT SIGNED - • TEXT → LONGTEXT - • FLOAT → DOUBLE - • All text columns → UTF8MB4 character set + - INT columns -> BIGINT (except TINYINT(1) for booleans) + - BIGINT UNSIGNED -> BIGINT SIGNED + - TEXT -> LONGTEXT + - FLOAT -> DOUBLE + - All text columns -> UTF8MB4 character set 2. Required Columns Added: - • created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) - • updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE - • created_by INT(11) NULL - • updated_by INT(11) NULL - • deleted_by INT(11) NULL (only for soft-delete tables) + - created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) + - updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE + - created_by INT(11) NULL + - updated_by INT(11) NULL + - deleted_by INT(11) NULL (only for soft-delete tables) 3. Indexes Added: - • INDEX on created_at - • INDEX on updated_at - • INDEX on site_id (for models extending Rsx_Site_Model_Abstract) - • INDEX on id+version (for Versionable models) + - INDEX on created_at + - INDEX on updated_at + - INDEX on site_id (for models extending Rsx_Site_Model_Abstract) + - INDEX on id+version (for Versionable models) 4. Model-Specific Columns: - • site_id BIGINT - for models extending Rsx_Site_Model_Abstract - • version INT(11) DEFAULT 1 - for Versionable/Ajaxable models + - site_id BIGINT - for models extending Rsx_Site_Model_Abstract + - version INT(11) DEFAULT 1 - for Versionable/Ajaxable models 5. Precision Upgrades: - • All DATETIME/TIMESTAMP columns → precision (3) for milliseconds + - All DATETIME/TIMESTAMP columns -> precision (3) for milliseconds This means you can write simpler migrations and let the system handle the optimization and standardization. The only time you need to be explicit about @@ -115,7 +116,7 @@ AUTOMATIC NORMALIZATION VALIDATION SYSTEM Automatic Validation - When running migrations in non-production mode, the system automatically: + When running migrations in development mode, the system automatically: 1. Validates all pending migrations for Schema builder usage 2. Removes down() methods if present @@ -125,7 +126,7 @@ VALIDATION SYSTEM Validation Output When a violation is detected, you'll see: - ❌ Migration Validation Failed + [ERROR] Migration Validation Failed File: 2025_09_30_create_example_table.php Line: 28 @@ -133,12 +134,12 @@ VALIDATION SYSTEM Violation: Found forbidden Schema builder usage: Schema::create Code Preview: - ──────────────────────────────────────── + ---------------------------------------- Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('name'); }); - ──────────────────────────────────────── + ---------------------------------------- Remediation: Use DB::statement("CREATE TABLE...") instead @@ -149,28 +150,43 @@ VALIDATION SYSTEM created_at TIMESTAMP NULL DEFAULT NULL )'); - Bypassing Validation - In production mode (--production flag or APP_ENV=production), validation is - skipped. This should only be used when absolutely necessary. - MIGRATION WORKFLOW - Development Workflow - 1. Create snapshot: php artisan migrate:begin - 2. Create migration: php artisan make:migration:safe - 3. Write migration using raw SQL - 4. Run migrations: php artisan migrate - 5. If successful: php artisan migrate:commit - 6. If failed: System auto-rollbacks to snapshot + Development Mode (RSX_MODE=development) + Simply run: - Production Workflow - 1. Create migration: php artisan make:migration:safe - 2. Write migration using raw SQL - 3. Test thoroughly in development/staging - 4. Run migrations: php artisan migrate --production + php artisan migrate - Note: No snapshot protection in production mode. Ensure migrations are - thoroughly tested before running in production. + The command automatically: + 1. Creates a database snapshot before running migrations + 2. Runs all pending migrations with validation + 3. Runs schema quality checks + 4. On success: commits changes, regenerates constants, recompiles bundles + 5. On failure: automatically rolls back to snapshot + + Failed migrations restore the database to its pre-migration state. Fix your + migration files and run "php artisan migrate" again. + + Debug/Production Mode (RSX_MODE=debug or production) + Run: + + php artisan migrate + + In these modes: + - No snapshot protection (source code is read-only) + - Migrations run directly against the database + - Schema normalization still runs + - Constants and bundles are NOT regenerated (already done in development) + + Ensure migrations are thoroughly tested in development before running in + debug/production modes. + + Framework-Only Migrations + To run only framework migrations (system/database/migrations): + + php artisan migrate --framework-only + + This skips snapshot protection regardless of mode. MIGRATION EXAMPLES @@ -295,30 +311,40 @@ ERROR MESSAGES Your migration uses Laravel's Schema builder. Rewrite using DB::statement() with raw SQL. - "Migration mode not active!" - You're in development mode and haven't created a snapshot. Run: - php artisan migrate:begin - "Unauthorized migrations detected!" Migration files exist that weren't created via make:migration:safe. Recreate them using the proper command. + "Snapshot-based migrations require Docker environment" + You're in development mode but not running in Docker. Either: + - Run in Docker, or + - Set RSX_MODE=debug in .env to skip snapshot protection + +AUTOMATIC ROLLBACK (Development Mode) + + In development mode, if a migration fails: + 1. The error is displayed + 2. Database is automatically restored to pre-migration state + 3. You can fix the migration and run "php artisan migrate" again + + This happens automatically - no manual intervention required. + SECURITY CONSIDERATIONS SQL Injection When using dynamic values in migrations, always use parameter binding: - ✅ CORRECT: + CORRECT: DB::statement("UPDATE users SET status = ? WHERE created_at < ?", ['active', '2025-01-01']); - ❌ WRONG: + WRONG: DB::statement("UPDATE users SET status = '$status' WHERE created_at < '$date'"); Production Safety - • Always test migrations in development/staging first - • Keep migrations small and focused - • Never reference models or services in migrations - • Migrations must be self-contained and idempotent where possible + - Always test migrations in development first + - Keep migrations small and focused + - Never reference models or services in migrations + - Migrations must be self-contained and idempotent where possible DEBUGGING @@ -326,17 +352,17 @@ DEBUGGING php artisan migrate:status Testing a Migration - 1. Create snapshot: php artisan migrate:begin - 2. Run migration: php artisan migrate - 3. If it fails, automatic rollback occurs - 4. Fix the migration file - 5. Try again: php artisan migrate + In development mode, just run: + php artisan migrate + + If it fails, the database is automatically restored. Fix the migration + and try again. Common Issues - • "Class not found" - Don't reference models in migrations - • "Syntax error" - Check your SQL syntax, test in MySQL client first - • "Foreign key constraint" - Ensure referenced table/column exists - • "Duplicate column" - Check if column already exists before adding + - "Class not found" - Don't reference models in migrations + - "Syntax error" - Check your SQL syntax, test in MySQL client first + - "Foreign key constraint" - Ensure referenced table/column exists + - "Duplicate column" - Check if column already exists before adding SEE ALSO rsx:man database - Database system overview @@ -346,4 +372,4 @@ SEE ALSO AUTHORS RSX Framework Team -RSX Framework September 2025 MIGRATIONS(7) \ No newline at end of file +RSX Framework January 2026 MIGRATIONS(7) diff --git a/database/migrations/CLAUDE.md b/database/migrations/CLAUDE.md index 99f914eb6..38fd7f458 100755 --- a/database/migrations/CLAUDE.md +++ b/database/migrations/CLAUDE.md @@ -58,8 +58,8 @@ While there are too many files to list individually, some key migrations include ## Working with Migrations -1. To run migrations: `php artisan migrate` -2. To create a new migration: `php artisan make:migration name_of_migration` -3. To roll back the last batch: `php artisan migrate:rollback` +1. To create a new migration: `php artisan make:migration:safe name_of_migration` +2. To run migrations: `php artisan migrate` +3. To view pending migrations: `php artisan migrate:status` -When modifying the database schema, always create a new migration rather than modifying existing ones to maintain data integrity in production environments. \ No newline at end of file +In development mode, `migrate` automatically creates a database snapshot and rolls back on failure. No manual rollback command exists - forward-only migrations are enforced. \ No newline at end of file diff --git a/docs/CLAUDE.dist.md b/docs/CLAUDE.dist.md index 90d8a59e0..97863b9a5 100644 --- a/docs/CLAUDE.dist.md +++ b/docs/CLAUDE.dist.md @@ -822,11 +822,11 @@ Details: `php artisan rsx:man model_fetch` ```bash php artisan make:migration:safe create_users_table -php artisan migrate:begin php artisan migrate -php artisan migrate:commit ``` +In development mode, `migrate` automatically creates a snapshot, runs migrations, and commits on success. Failed migrations auto-rollback to pre-migration state. + **NO defensive coding in migrations:** ```php // ❌ WRONG - conditional logic @@ -838,7 +838,7 @@ DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar"); DB::statement("ALTER TABLE foo DROP COLUMN baz"); ``` -No `IF EXISTS`, no `information_schema` queries, no fallbacks. Know current state, write exact transformation. Failures fail loud - snapshot rollback exists for recovery. +No `IF EXISTS`, no `information_schema` queries, no fallbacks. Know current state, write exact transformation. Failures fail loud - automatic snapshot rollback handles recovery in dev mode. --- diff --git a/docs/framework_divergences.md b/docs/framework_divergences.md index 664e11ac3..870dfc446 100755 --- a/docs/framework_divergences.md +++ b/docs/framework_divergences.md @@ -104,15 +104,14 @@ This file documents all intentional divergences from standard Laravel behavior. - **Implementation**: Environment configuration - **Date**: 2025-05-15 -### Snapshot-Protected Migrations (Development) -- **Change**: Migrations require database snapshot in development environments -- **Affected**: `php artisan migrate` command in development +### Automatic Snapshot-Protected Migrations (Development) +- **Change**: `php artisan migrate` automatically handles snapshots in development +- **Affected**: `php artisan migrate` command - **Reason**: Prevents partial migrations from corrupting database, LLM-friendly error recovery -- **Implementation**: Enhanced `Maint_Migrate` command with snapshot commands -- **Commands**: `migrate:begin`, `migrate:commit`, `migrate:rollback`, `migrate:status` -- **Bypass**: Use `--production` flag to skip snapshot requirement -- **Note**: Only enforced in development with Docker, production runs normally -- **Date**: 2025-08-13 +- **Implementation**: Unified `Maint_Migrate` command with automatic snapshot/rollback +- **Behavior**: Creates snapshot, runs migrations, commits on success, auto-rollbacks on failure +- **Note**: Only in development mode with Docker; debug/production runs without snapshots +- **Date**: 2025-01-15 ## Error Handling diff --git a/docs/llm_migration_system.md b/docs/llm_migration_system.md index 86fc256b1..421318ed7 100755 --- a/docs/llm_migration_system.md +++ b/docs/llm_migration_system.md @@ -1,4 +1,4 @@ -# LLM-Friendly Migration System - Snapshot Implementation +# LLM-Friendly Migration System - Automatic Snapshot Protection ## Problem Statement LLMs struggle with partial migration failures because: @@ -7,155 +7,94 @@ LLMs struggle with partial migration failures because: - LLMs get confused about what has/hasn't been applied - Recovery requires manual intervention or database reset -## Implemented Solution: Docker Snapshot System +## Implemented Solution: Automatic Docker Snapshot System ### Core Design -The system uses filesystem-level snapshots of the MySQL data directory in Docker environments to provide complete rollback capability for migrations. +The `php artisan migrate` command automatically handles database snapshots in development mode. No separate commands needed. ### How It Works -1. **Development Mode Only** - - System only enforces snapshots in development environments - - Production migrations run normally without snapshots - - Detects environment via `APP_ENV` and Docker presence +1. **Development Mode** (`RSX_MODE=development`) + - Automatically creates MySQL snapshot before migrations + - Runs all pending migrations with validation + - On success: commits changes, regenerates constants, recompiles bundles + - On failure: automatically rolls back to snapshot -2. **Migration Session Flow** - ```bash - # 1. Start migration session (creates snapshot) - php artisan migrate:begin - - # 2. Run migrations (with safety net) - php artisan migrate - - # 3a. If successful, commit changes - php artisan migrate:commit - - # 3b. If failed, rollback to snapshot - php artisan migrate:rollback - ``` +2. **Debug/Production Mode** (`RSX_MODE=debug` or `production`) + - No snapshot protection (source code is read-only) + - Migrations run directly + - Schema normalization still runs + - Constants and bundles NOT regenerated -3. **Automatic Rollback on Failure** - - When migrations fail in development, system offers automatic rollback - - LLM gets clear instructions on how to proceed - - Database restored to exact pre-migration state +### Migration Flow + +```bash +# Development - just one command does everything +$ php artisan migrate + Development mode: Using automatic snapshot protection + +[1/4] Creating database snapshot... +[OK] Snapshot created successfully. + +[2/4] Running migrations... +[OK] All 3 migrations completed successfully + +[3/4] Running schema quality check... +[OK] Schema quality check passed. + +[4/4] Committing changes... +[OK] Snapshot committed - backup removed. + +Running post-migration tasks... +[OK] Migration completed successfully! +``` + +### Automatic Recovery on Failure + +```bash +$ php artisan migrate + Development mode: Using automatic snapshot protection + +[1/4] Creating database snapshot... +[OK] Snapshot created successfully. + +[2/4] Running migrations... + +[ERROR] Migration failed! + Automatically rolling back to snapshot... + +[OK] Database restored to pre-migration state. + +Fix your migration files and run "php artisan migrate" again. +``` ### Implementation Details -#### Commands - -**migrate:begin** -- Stops MySQL via supervisord -- Creates backup of `/var/lib/mysql` to `/var/lib/mysql_backup` -- Restarts MySQL -- Creates `.migrating` flag file -- Blocks web UI access (development only) - -**migrate:rollback** -- Stops MySQL -- Replaces MySQL data directory with backup -- Restarts MySQL -- Removes flag file and backup -- Re-enables web UI - -**migrate:commit** -- Removes backup directory -- Removes flag file -- Re-enables web UI - -**migrate:status** -- Shows current migration session status -- Lists available commands -- Checks database connectivity -- Shows pending migrations count - -#### Modified migrate Command -The existing `Maint_Migrate` command was enhanced to: -- Check for `.migrating` flag in development mode -- Require snapshot unless `--production` flag is used -- Offer automatic rollback on failure -- Run normally in production environments - -#### Middleware Protection -`CheckMigrationMode` middleware: -- Only active in development environments -- Blocks HTTP requests during migration sessions -- Shows detailed error message with recovery instructions - -### Usage Examples - -#### Successful Migration (Development) -```bash -$ php artisan migrate:begin -✅ Database snapshot created successfully! - -$ php artisan migrate -✅ Migration mode active - snapshot available for rollback -Running migrations... -✅ Migrations completed successfully! - -$ php artisan migrate:commit -✅ Migration changes committed successfully! -``` - -#### Failed Migration with Auto-Rollback (Development) -```bash -$ php artisan migrate:begin -✅ Database snapshot created successfully! - -$ php artisan migrate -✅ Migration mode active - snapshot available for rollback -Running migrations... -❌ Migration failed! -Error: Column already exists - -🔄 Database snapshot is available for rollback. -Would you like to rollback to the snapshot now? (yes/no) [no]: yes - -Rolling back database... -✅ Database rolled back successfully! - -You can now: - 1. Fix your migration files - 2. Run "php artisan migrate:begin" to create a new snapshot - 3. Run "php artisan migrate" to try again -``` - -#### Production Migration -```bash -$ php artisan migrate -🚀 Running in production mode (no snapshot protection) -Running migrations... -✅ Migrations completed successfully! -``` - -#### Bypass Snapshot in Development -```bash -$ php artisan migrate --production -⚠️ Running without snapshot protection! -Are you absolutely sure? This cannot be undone! (yes/no) [no]: yes -Running migrations... -``` +The unified `migrate` command in development mode: +1. Stops MySQL via supervisord +2. Creates backup of `/var/lib/mysql` to `/var/lib/mysql_backup` +3. Restarts MySQL and runs migrations +4. On success: removes backup, regenerates constants, recompiles bundles +5. On failure: restores backup, removes flag file ### Environment Detection The system automatically detects the environment: -1. **Development Mode** (snapshots required): - - `APP_ENV` is not 'production' +1. **Development Mode** (snapshots enabled): + - `RSX_MODE=development` in `.env` - Docker environment detected (`/.dockerenv` exists) - - No `--production` flag passed -2. **Production Mode** (no snapshots): - - `APP_ENV` is 'production' OR - - `--production` flag is passed OR - - Not running in Docker +2. **Debug/Production Mode** (no snapshots): + - `RSX_MODE=debug` or `RSX_MODE=production` + - OR not running in Docker ### Benefits for LLMs -1. **Clear Error Recovery** - - Failed migrations automatically offer rollback - - No partial state confusion - - Explicit instructions on next steps +1. **Zero-Step Recovery** + - Failed migrations automatically restore database + - No commands to remember + - Just fix migration files and run `migrate` again 2. **Safe Experimentation** - LLMs can try migrations without fear @@ -163,48 +102,32 @@ The system automatically detects the environment: - Learn from failures without consequences 3. **Simple Mental Model** - - Migration session is atomic: all or nothing - - Clear session lifecycle: begin → migrate → commit/rollback - - Status command provides current state + - One command: `php artisan migrate` + - System handles all the complexity + - Clear success/failure messaging 4. **Production Safety** - System automatically adapts to environment - - No dangerous operations in production + - Source code treated as read-only in debug/production - Clear separation of dev/prod behaviors -### Implementation Files - -- `/app/Console/Commands/MigrateBeginCommand.php` - Snapshot creation -- `/app/Console/Commands/MigrateRollbackCommand.php` - Snapshot restoration -- `/app/Console/Commands/MigrateCommitCommand.php` - Session completion -- `/app/Console/Commands/MigrateStatusCommand.php` - Status display -- `/app/Console/Commands/Maint_Migrate.php` - Enhanced migrate command -- `/app/Http/Middleware/CheckMigrationMode.php` - Web UI protection - ### Limitations 1. **Docker Only**: Snapshots require Docker environment with supervisord -2. **Development Only**: Not available in production environments +2. **Development Only**: Not available in debug/production modes 3. **Disk Space**: Requires space for MySQL data directory copy -4. **Single Session**: Only one migration session at a time -### Future Enhancements +### Implementation Files -Potential improvements: -- Incremental snapshots for large databases -- Migration preview/dry-run mode -- Automatic migration file fixes for common errors -- Integration with version control for migration rollback +- `/app/RSpade/Commands/Migrate/Maint_Migrate.php` - Unified migrate command +- `/app/Http/Middleware/CheckMigrationMode.php` - Web UI protection during migration ## Why This Approach -This implementation was chosen over alternatives because: +This implementation was chosen because: 1. **Complete Safety**: Filesystem snapshots guarantee perfect rollback -2. **Simple Implementation**: Uses existing tools (supervisord, cp, rm) -3. **LLM-Friendly**: Clear session model with explicit states -4. **Production Compatible**: Gracefully degrades in production +2. **Zero Friction**: Single command handles everything +3. **LLM-Friendly**: No multi-step workflow to remember +4. **Production Compatible**: Gracefully adapts to environment 5. **Laravel Integration**: Works with existing migration system -6. **Immediate Value**: Solves the core problem without complexity - -The snapshot approach provides the "all or nothing" behavior that makes migrations predictable and safe for LLM interaction, while maintaining full compatibility with Laravel's migration system and production deployments. \ No newline at end of file diff --git a/docs/skills/migrations/SKILL.md b/docs/skills/migrations/SKILL.md index f9b06b032..10b0c096e 100755 --- a/docs/skills/migrations/SKILL.md +++ b/docs/skills/migrations/SKILL.md @@ -42,22 +42,20 @@ Schema::create('products', function (Blueprint $table) { ## Development Workflow ```bash -# 1. Create snapshot (required) -php artisan migrate:begin - -# 2. Create migration +# 1. Create migration php artisan make:migration:safe create_products_table -# 3. Write migration with raw SQL -# 4. Run migrations +# 2. Write migration with raw SQL + +# 3. Run migrations (auto-snapshot in development) php artisan migrate - -# 5. If successful -php artisan migrate:commit - -# 6. If failed - auto-rollback to snapshot ``` +In development mode, `migrate` automatically: +- Creates database snapshot before running +- Commits on success (regenerates constants, recompiles bundles) +- Auto-rollbacks on failure (database restored to pre-migration state) + --- ## Automatic Normalization @@ -163,14 +161,19 @@ user_id BIGINT NULL -- ✅ Matches --- -## Production Workflow +## Debug/Production Workflow ```bash -# No snapshot protection in production -php artisan migrate --production +# In debug or production mode (RSX_MODE=debug or production) +php artisan migrate ``` -Ensure migrations are thoroughly tested in development/staging first. +In debug/production mode: +- No snapshot protection (source code is read-only) +- Schema normalization still runs +- Constants and bundles NOT regenerated + +Ensure migrations are thoroughly tested in development first. ---