Framework updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-01-19 04:33:43 +00:00
parent 1594502cb2
commit a5e1c604ab
15 changed files with 606 additions and 981 deletions

View File

@@ -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(

View File

@@ -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 <info>php artisan migrate:commit</info> 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) {

View File

@@ -1,182 +0,0 @@
<?php
namespace App\RSpade\Commands\Migrate;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
class Migrate_Begin_Command extends Command
{
use PrivilegedCommandTrait;
protected $signature = 'migrate:begin';
protected $description = 'Begin a migration session by creating a MySQL snapshot';
protected $mysql_data_dir = '/var/lib/mysql';
protected $backup_dir = '/var/lib/mysql_backup';
protected $flag_file = '/var/www/html/.migrating';
public function handle()
{
// Check if already in migration mode
if (file_exists($this->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');
}
}

View File

@@ -1,172 +0,0 @@
<?php
namespace App\RSpade\Commands\Migrate;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
use App\RSpade\SchemaQuality\SchemaQualityChecker;
use Illuminate\Support\Facades\DB;
use App\RSpade\Core\Database\MigrationPaths;
class Migrate_Commit_Command extends Command
{
use PrivilegedCommandTrait;
protected $signature = 'migrate:commit';
protected $description = 'Commit the migration changes and end the migration session';
protected $backup_dir = '/var/lib/mysql_backup';
protected $flag_file = '/var/www/html/.migrating';
public function handle()
{
// Check if in migration mode
if (!file_exists($this->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;
}
}

View File

@@ -1,127 +0,0 @@
<?php
namespace App\RSpade\Commands\Migrate;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
class Migrate_Rollback_Command extends Command
{
use PrivilegedCommandTrait;
protected $signature = 'migrate:rollback';
protected $description = 'Rollback the database to the snapshot taken at migrate:begin';
protected $mysql_data_dir = '/var/lib/mysql';
protected $backup_dir = '/var/lib/mysql_backup';
protected $flag_file = '/var/www/html/.migrating';
public function handle()
{
// Check if in migration mode
if (!file_exists($this->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');
}
}

View File

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

View File

@@ -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)
```
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

View File

@@ -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');

View File

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

View File

@@ -5,34 +5,35 @@ NAME
SYNOPSIS
php artisan make:migration:safe <name>
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 <name>
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 <name>
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)
RSX Framework January 2026 MIGRATIONS(7)