Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
In development mode, `migrate` automatically creates a database snapshot and rolls back on failure. No manual rollback command exists - forward-only migrations are enforced.
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. **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
|
||||
|
||||
### Migration Flow
|
||||
|
||||
2. **Migration Session Flow**
|
||||
```bash
|
||||
# 1. Start migration session (creates snapshot)
|
||||
php artisan migrate:begin
|
||||
# Development - just one command does everything
|
||||
$ php artisan migrate
|
||||
Development mode: Using automatic snapshot protection
|
||||
|
||||
# 2. Run migrations (with safety net)
|
||||
php artisan migrate
|
||||
[1/4] Creating database snapshot...
|
||||
[OK] Snapshot created successfully.
|
||||
|
||||
# 3a. If successful, commit changes
|
||||
php artisan migrate:commit
|
||||
[2/4] Running migrations...
|
||||
[OK] All 3 migrations completed successfully
|
||||
|
||||
# 3b. If failed, rollback to snapshot
|
||||
php artisan migrate:rollback
|
||||
[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!
|
||||
```
|
||||
|
||||
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
|
||||
### 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.
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user