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.
---