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';
|
$started_at = $session_info['started_at'] ?? 'unknown';
|
||||||
|
|
||||||
// Create a detailed error message
|
// Create a detailed error message
|
||||||
$message = "🚧 Database Migration in Progress\n\n";
|
$message = "Database Migration in Progress\n\n";
|
||||||
$message .= "A database migration session is currently active.\n";
|
$message .= "A database migration is currently running.\n";
|
||||||
$message .= "Started at: {$started_at}\n\n";
|
$message .= "Started at: {$started_at}\n\n";
|
||||||
$message .= "The application is temporarily unavailable to ensure data integrity.\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 .= "Please wait for the migration to complete.\n";
|
||||||
$message .= " • php artisan migrate:commit - Keep the changes\n";
|
$message .= "If this message persists, the migration may have been interrupted.\n";
|
||||||
$message .= " • php artisan migrate:rollback - Revert to snapshot\n\n";
|
$message .= "Check the terminal running 'php artisan migrate' for status.";
|
||||||
$message .= "For status: php artisan migrate:status";
|
|
||||||
|
|
||||||
// Throw service unavailable exception
|
// Throw service unavailable exception
|
||||||
throw new ServiceUnavailableHttpException(
|
throw new ServiceUnavailableHttpException(
|
||||||
|
|||||||
@@ -15,31 +15,203 @@ use Illuminate\Database\Console\Migrations\MigrateCommand;
|
|||||||
use App\Providers\AppServiceProvider;
|
use App\Providers\AppServiceProvider;
|
||||||
use App\RSpade\Core\Database\MigrationValidator;
|
use App\RSpade\Core\Database\MigrationValidator;
|
||||||
use App\RSpade\Core\Database\SqlQueryTransformer;
|
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
|
* In DEVELOPMENT mode:
|
||||||
* maintenance commands after migrations complete:
|
* - 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)
|
* In DEBUG/PRODUCTION mode:
|
||||||
* 2. migrate:regenerate_constants - Updates model constants and docblocks, exports to JavaScript
|
* - Runs migrations without snapshot protection
|
||||||
*
|
* - Runs schema normalization
|
||||||
* The command preserves and displays all output from the standard migration command,
|
* - Does NOT update source code (constants, bundles) - source is read-only
|
||||||
* then runs the maintenance commands.
|
|
||||||
*
|
*
|
||||||
* This command is automatically used when running 'php artisan migrate' due to
|
* This command is automatically used when running 'php artisan migrate' due to
|
||||||
* a modification in the artisan script.
|
* a modification in the artisan script.
|
||||||
*/
|
*/
|
||||||
class Maint_Migrate extends Command
|
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 $flag_file = '/var/www/html/.migrating';
|
||||||
|
protected $mysql_data_dir = '/var/lib/mysql';
|
||||||
|
protected $backup_dir = '/var/lib/mysql_backup';
|
||||||
|
|
||||||
public function handle()
|
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
|
// Enable SQL query transformation for migrations
|
||||||
SqlQueryTransformer::enable();
|
SqlQueryTransformer::enable();
|
||||||
@@ -51,39 +223,8 @@ class Maint_Migrate extends Command
|
|||||||
// Ensure migrations table exists (create it if needed)
|
// Ensure migrations table exists (create it if needed)
|
||||||
$this->ensure_migrations_table_exists();
|
$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');
|
$is_framework_only = $this->option('framework-only');
|
||||||
|
$is_development = Rsx::is_development();
|
||||||
// Only enforce snapshot protection in development mode without --production or --framework-only flag
|
|
||||||
$require_snapshot = !$is_production && !$is_framework_only;
|
|
||||||
|
|
||||||
// Check for migration mode if we require snapshot
|
|
||||||
if ($require_snapshot) {
|
|
||||||
if (!file_exists($this->flag_file)) {
|
|
||||||
$this->error('[ERROR] Migration mode not active!');
|
|
||||||
$this->error('');
|
|
||||||
$this->line('In development mode, you must create a database snapshot before running migrations.');
|
|
||||||
$this->line('This prevents partial migrations from corrupting your database.');
|
|
||||||
$this->info('');
|
|
||||||
$this->line('To begin a migration session:');
|
|
||||||
$this->line(' php artisan migrate:begin');
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('[OK] Migration mode active - snapshot available for rollback');
|
|
||||||
$this->info('');
|
|
||||||
} elseif ($is_production) {
|
|
||||||
$this->info(' Running in production mode (no snapshot protection)');
|
|
||||||
} elseif ($is_framework_only) {
|
|
||||||
$this->info(' Running framework-only migrations (no snapshot protection)');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mode_desc = $is_production ? ' (production mode)' : ($is_framework_only ? ' (framework-only)' : ' with maintenance commands');
|
|
||||||
$this->info('Running migrations' . $mode_desc . '...');
|
|
||||||
|
|
||||||
// Get all the options
|
// Get all the options
|
||||||
$force = $this->option('force');
|
$force = $this->option('force');
|
||||||
@@ -96,12 +237,8 @@ class Maint_Migrate extends Command
|
|||||||
$this->error('[ERROR] Migration by path is disabled!');
|
$this->error('[ERROR] Migration by path is disabled!');
|
||||||
$this->error('');
|
$this->error('');
|
||||||
$this->line('This command enforces running all pending migrations in order.');
|
$this->line('This command enforces running all pending migrations in order.');
|
||||||
$this->line('Please run migrations without the --path option:');
|
$this->line('Please run migrations without the --path option.');
|
||||||
$this->info(' php artisan migrate');
|
SqlQueryTransformer::disable();
|
||||||
$this->error('');
|
|
||||||
$this->line('If you need to run specific migrations:');
|
|
||||||
$this->line(' 1. Place them in the standard migrations directory');
|
|
||||||
$this->line(' 2. Use migration timestamps to control execution order');
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,29 +249,18 @@ class Maint_Migrate extends Command
|
|||||||
|
|
||||||
// Check migration whitelist
|
// Check migration whitelist
|
||||||
if (!$this->checkMigrationWhitelist($paths_to_check)) {
|
if (!$this->checkMigrationWhitelist($paths_to_check)) {
|
||||||
|
SqlQueryTransformer::disable();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate migration files for Schema builder usage (only in non-production)
|
// Validate migration files for Schema builder usage (only in development)
|
||||||
if (!$is_production && !$this->_validate_schema_rules()) {
|
if ($is_development && !$this->_validate_schema_rules()) {
|
||||||
|
SqlQueryTransformer::disable();
|
||||||
return 1;
|
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
|
// Run normalize_schema BEFORE migrations to fix existing tables
|
||||||
// Pass --production flag to skip snapshot in both production and framework-only modes
|
$requiredColumnsArgs = $is_development ? [] : ['--production' => true];
|
||||||
$requiredColumnsArgs = ($is_production || $is_framework_only) ? ['--production' => true] : [];
|
|
||||||
|
|
||||||
$this->info("\n Pre-migration normalization (fixing existing tables)...\n");
|
$this->info("\n Pre-migration normalization (fixing existing tables)...\n");
|
||||||
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
||||||
@@ -178,51 +304,21 @@ class Maint_Migrate extends Command
|
|||||||
|
|
||||||
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
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) {
|
} catch (\Exception $e) {
|
||||||
// Restore SQL mode
|
// Restore SQL mode
|
||||||
try {
|
try {
|
||||||
|
if (isset($originalSqlMode)) {
|
||||||
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
||||||
|
}
|
||||||
} catch (\Exception $sqlEx) {
|
} catch (\Exception $sqlEx) {
|
||||||
// Ignore SQL mode restore errors
|
// Ignore SQL mode restore errors
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->error('');
|
$this->error('');
|
||||||
$this->error('[ERROR] Migration failed!');
|
|
||||||
$this->error('Error: ' . $e->getMessage());
|
$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
|
// Disable query logging before returning
|
||||||
AppServiceProvider::disable_query_echo();
|
AppServiceProvider::disable_query_echo();
|
||||||
|
|
||||||
// Disable SQL query transformation
|
|
||||||
SqlQueryTransformer::disable();
|
SqlQueryTransformer::disable();
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
@@ -237,82 +333,191 @@ class Maint_Migrate extends Command
|
|||||||
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
||||||
if ($normalizeExitCode !== 0) {
|
if ($normalizeExitCode !== 0) {
|
||||||
$this->error('Post-migration normalization failed');
|
$this->error('Post-migration normalization failed');
|
||||||
|
SqlQueryTransformer::disable();
|
||||||
return $normalizeExitCode;
|
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
|
// Disable query logging
|
||||||
AppServiceProvider::disable_query_echo();
|
AppServiceProvider::disable_query_echo();
|
||||||
|
|
||||||
// Disable SQL query transformation
|
|
||||||
SqlQueryTransformer::disable();
|
SqlQueryTransformer::disable();
|
||||||
|
|
||||||
return $exitCode;
|
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
|
* 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
|
protected function register_query_transformer(): void
|
||||||
{
|
{
|
||||||
// Store the original statement method
|
|
||||||
$original_statement = \Closure::bind(function () {
|
$original_statement = \Closure::bind(function () {
|
||||||
return $this->connection->statement(...func_get_args());
|
return $this->connection->statement(...func_get_args());
|
||||||
}, DB::getFacadeRoot(), DB::getFacadeRoot());
|
}, DB::getFacadeRoot(), DB::getFacadeRoot());
|
||||||
|
|
||||||
// Override DB::statement to transform queries
|
|
||||||
DB::macro('statement', function ($query, $bindings = []) use ($original_statement) {
|
DB::macro('statement', function ($query, $bindings = []) use ($original_statement) {
|
||||||
// Transform the query before execution
|
|
||||||
$transformed = SqlQueryTransformer::transform($query);
|
$transformed = SqlQueryTransformer::transform($query);
|
||||||
|
|
||||||
// Call the original statement method with transformed query
|
|
||||||
return DB::connection()->statement($transformed, $bindings);
|
return DB::connection()->statement($transformed, $bindings);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if all pending migrations are whitelisted
|
* Check if all pending migrations are whitelisted
|
||||||
*
|
|
||||||
* @param array $paths Array of migration paths to check
|
|
||||||
*/
|
*/
|
||||||
protected function checkMigrationWhitelist(array $paths): bool
|
protected function checkMigrationWhitelist(array $paths): bool
|
||||||
{
|
{
|
||||||
// Load whitelists from both locations and merge them
|
|
||||||
$whitelistPaths = [
|
$whitelistPaths = [
|
||||||
database_path('migrations/.migration_whitelist'), // Framework migrations
|
database_path('migrations/.migration_whitelist'),
|
||||||
base_path('rsx/resource/migrations/.migration_whitelist'), // Application migrations
|
base_path('rsx/resource/migrations/.migration_whitelist'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$whitelistedMigrations = [];
|
$whitelistedMigrations = [];
|
||||||
@@ -327,14 +532,12 @@ class Maint_Migrate extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no whitelist exists yet, create one with existing migrations
|
|
||||||
if (!$foundAtLeastOne) {
|
if (!$foundAtLeastOne) {
|
||||||
$this->warn('[WARNING] No migration whitelist found. Creating one with existing migrations...');
|
$this->warn('[WARNING] No migration whitelist found. Creating one with existing migrations...');
|
||||||
$this->createInitialWhitelist();
|
$this->createInitialWhitelist();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all migration files from specified paths
|
|
||||||
$migrationFiles = [];
|
$migrationFiles = [];
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
if (is_dir($path)) {
|
if (is_dir($path)) {
|
||||||
@@ -344,7 +547,6 @@ class Maint_Migrate extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for unauthorized migrations
|
|
||||||
$unauthorizedMigrations = array_diff($migrationFiles, $whitelistedMigrations);
|
$unauthorizedMigrations = array_diff($migrationFiles, $whitelistedMigrations);
|
||||||
|
|
||||||
if (!empty($unauthorizedMigrations)) {
|
if (!empty($unauthorizedMigrations)) {
|
||||||
@@ -352,18 +554,12 @@ class Maint_Migrate extends Command
|
|||||||
$this->error('');
|
$this->error('');
|
||||||
$this->line('The following migrations were not created via php artisan make:migration:');
|
$this->line('The following migrations were not created via php artisan make:migration:');
|
||||||
foreach ($unauthorizedMigrations as $migration) {
|
foreach ($unauthorizedMigrations as $migration) {
|
||||||
$this->line(' • ' . $migration);
|
$this->line(' - ' . $migration);
|
||||||
}
|
}
|
||||||
$this->error('');
|
$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->error('');
|
||||||
$this->line('To fix this:');
|
$this->line('To fix: Create migrations using "php artisan make:migration [name]"');
|
||||||
$this->line('1. Create a new migration using: php artisan make:migration [name]');
|
|
||||||
$this->line('2. Copy the migration code from the unauthorized file to the new file');
|
|
||||||
$this->line('3. Delete the unauthorized migration file');
|
|
||||||
$this->line('4. Run migrations again');
|
|
||||||
$this->error('');
|
|
||||||
$this->line('This ensures proper timestamp generation and prevents LLM timestamp confusion.');
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -373,7 +569,6 @@ class Maint_Migrate extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create initial whitelist with existing migrations
|
* Create initial whitelist with existing migrations
|
||||||
* Creates separate whitelist files for framework and application migrations
|
|
||||||
*/
|
*/
|
||||||
protected function createInitialWhitelist(): void
|
protected function createInitialWhitelist(): void
|
||||||
{
|
{
|
||||||
@@ -385,7 +580,6 @@ class Maint_Migrate extends Command
|
|||||||
$totalMigrations = 0;
|
$totalMigrations = 0;
|
||||||
|
|
||||||
foreach ($whitelistLocations as $whitelistPath => $migrationPath) {
|
foreach ($whitelistLocations as $whitelistPath => $migrationPath) {
|
||||||
// Skip if migration directory doesn't exist
|
|
||||||
if (!is_dir($migrationPath)) {
|
if (!is_dir($migrationPath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -396,7 +590,6 @@ class Maint_Migrate extends Command
|
|||||||
'migrations' => [],
|
'migrations' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add all existing migrations from this specific path
|
|
||||||
foreach (glob($migrationPath . '/*.php') as $file) {
|
foreach (glob($migrationPath . '/*.php') as $file) {
|
||||||
$filename = basename($file);
|
$filename = basename($file);
|
||||||
$whitelist['migrations'][$filename] = [
|
$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'])) {
|
if (!empty($whitelist['migrations'])) {
|
||||||
file_put_contents($whitelistPath, json_encode($whitelist, JSON_PRETTY_PRINT));
|
file_put_contents($whitelistPath, json_encode($whitelist, JSON_PRETTY_PRINT));
|
||||||
$count = count($whitelist['migrations']);
|
$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
|
* Validate migration files for Schema builder usage
|
||||||
* Only enforced in non-production mode
|
|
||||||
*/
|
*/
|
||||||
protected function _validate_schema_rules(): bool
|
protected function _validate_schema_rules(): bool
|
||||||
{
|
{
|
||||||
@@ -449,7 +622,6 @@ class Maint_Migrate extends Command
|
|||||||
$pending_migrations = MigrationValidator::get_pending_migrations($repository);
|
$pending_migrations = MigrationValidator::get_pending_migrations($repository);
|
||||||
|
|
||||||
if (empty($pending_migrations)) {
|
if (empty($pending_migrations)) {
|
||||||
// No pending migrations to validate
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,13 +631,11 @@ class Maint_Migrate extends Command
|
|||||||
|
|
||||||
foreach ($pending_migrations as $migration_path) {
|
foreach ($pending_migrations as $migration_path) {
|
||||||
try {
|
try {
|
||||||
// Validate the migration file for Schema builder usage
|
|
||||||
MigrationValidator::validate_migration_file($migration_path);
|
MigrationValidator::validate_migration_file($migration_path);
|
||||||
$this->info(" [OK] " . basename($migration_path));
|
$this->info(" [OK] " . basename($migration_path));
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
// MigrationValidator already printed colored error output
|
|
||||||
$has_violations = true;
|
$has_violations = true;
|
||||||
break; // Stop at first violation
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +648,7 @@ class Maint_Migrate extends Command
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All validations passed - now remove down() methods
|
// Remove down() methods
|
||||||
$processed_migrations = [];
|
$processed_migrations = [];
|
||||||
foreach ($pending_migrations as $migration_path) {
|
foreach ($pending_migrations as $migration_path) {
|
||||||
if (MigrationValidator::remove_down_method($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)) {
|
if (!empty($processed_migrations)) {
|
||||||
$this->info('');
|
$this->info('');
|
||||||
$this->warn('[WARNING] Modified migration files:');
|
$this->warn('[WARNING] Modified migration files:');
|
||||||
foreach ($processed_migrations as $path) {
|
foreach ($processed_migrations as $path) {
|
||||||
$this->line(' • ' . basename($path) . ' - down() method removed');
|
$this->line(' - ' . basename($path) . ' - down() method removed');
|
||||||
}
|
}
|
||||||
$this->info('');
|
$this->info('');
|
||||||
$this->line('These migrations have been updated to follow framework standards.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the migrations table exists in the database
|
* Ensure the migrations table exists
|
||||||
* Creates it using raw SQL if it doesn't exist
|
|
||||||
*/
|
*/
|
||||||
protected function ensure_migrations_table_exists(): void
|
protected function ensure_migrations_table_exists(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Try to query the migrations table
|
|
||||||
$table = config('database.migrations', 'migrations');
|
$table = config('database.migrations', 'migrations');
|
||||||
DB::select("SELECT 1 FROM {$table} LIMIT 1");
|
DB::select("SELECT 1 FROM {$table} LIMIT 1");
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Table doesn't exist, create it
|
|
||||||
$table = config('database.migrations', 'migrations');
|
$table = config('database.migrations', 'migrations');
|
||||||
$this->info('Creating migrations table...');
|
$this->info('Creating migrations table...');
|
||||||
|
|
||||||
@@ -529,20 +694,9 @@ class Maint_Migrate extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Run migrations one-by-one with normalization after each
|
* 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
|
protected function run_migrations_with_normalization($migrator, array $migrationPaths, bool $step, array $requiredColumnsArgs): void
|
||||||
{
|
{
|
||||||
// Get all migration files
|
|
||||||
$files = [];
|
$files = [];
|
||||||
foreach ($migrationPaths as $path) {
|
foreach ($migrationPaths as $path) {
|
||||||
if (is_dir($path)) {
|
if (is_dir($path)) {
|
||||||
@@ -555,14 +709,11 @@ class Maint_Migrate extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort files chronologically by filename
|
|
||||||
sort($files);
|
sort($files);
|
||||||
|
|
||||||
// Get already-run migrations
|
|
||||||
$repository = $migrator->getRepository();
|
$repository = $migrator->getRepository();
|
||||||
$ran = $repository->getRan();
|
$ran = $repository->getRan();
|
||||||
|
|
||||||
// Filter to only pending migrations
|
|
||||||
$pending = [];
|
$pending = [];
|
||||||
foreach ($files as $file) {
|
foreach ($files as $file) {
|
||||||
$migrationName = $migrator->getMigrationName($file);
|
$migrationName = $migrator->getMigrationName($file);
|
||||||
@@ -579,29 +730,24 @@ class Maint_Migrate extends Command
|
|||||||
$totalMigrations = count($pending);
|
$totalMigrations = count($pending);
|
||||||
$currentMigration = 1;
|
$currentMigration = 1;
|
||||||
|
|
||||||
// Run each migration individually with normalization after
|
|
||||||
foreach ($pending as $file) {
|
foreach ($pending as $file) {
|
||||||
$migrationName = $migrator->getMigrationName($file);
|
$migrationName = $migrator->getMigrationName($file);
|
||||||
|
|
||||||
$this->info("\n[$currentMigration/$totalMigrations] Running: $migrationName");
|
$this->info("\n[$currentMigration/$totalMigrations] Running: $migrationName");
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
|
|
||||||
// Run this single migration
|
|
||||||
$migrator->runPending([$file], [
|
$migrator->runPending([$file], [
|
||||||
'pretend' => false,
|
'pretend' => false,
|
||||||
'step' => $step
|
'step' => $step
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Run normalization after this migration (if not the last)
|
|
||||||
if ($currentMigration < $totalMigrations) {
|
if ($currentMigration < $totalMigrations) {
|
||||||
$this->info("\n Normalizing schema after migration...\n");
|
$this->info("\n Normalizing schema after migration...\n");
|
||||||
|
|
||||||
// Switch to destructive-only query logging
|
|
||||||
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT);
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT);
|
||||||
|
|
||||||
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
||||||
|
|
||||||
// Restore full query logging
|
|
||||||
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT);
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT);
|
||||||
|
|
||||||
if ($normalizeExitCode !== 0) {
|
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
|
## Alternative Approaches
|
||||||
|
|
||||||
Instead of using restricted commands:
|
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
|
- 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
|
- Maintain separate development databases for testing destructive operations
|
||||||
|
|
||||||
## Directive for AI Agents
|
## Directive for AI Agents
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Migration Policy
|
## Migration Policy
|
||||||
|
|
||||||
- **Forward-only migrations** - No rollbacks, no down() methods
|
- **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
|
- **All migrations must be created via artisan** - Manual creation is blocked by whitelist
|
||||||
- Use `php artisan make:migration:safe` to create whitelisted migrations
|
- Use `php artisan make:migration:safe` to create whitelisted migrations
|
||||||
- **Sequential execution only** - No --path option allowed
|
- **Sequential execution only** - No --path option allowed
|
||||||
@@ -38,8 +38,18 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
php artisan make:migration:safe # Create whitelisted migration
|
php artisan make:migration:safe # Create whitelisted migration
|
||||||
php artisan migrate:begin # Start migration snapshot session
|
php artisan migrate # Run migrations (auto-snapshot in dev mode)
|
||||||
php artisan migrate # Run migrations with safety checks
|
php artisan migrate:status # View pending migrations
|
||||||
php artisan migrate:commit # Commit migration changes
|
|
||||||
php artisan migrate:rollback # Rollback to snapshot (stays in session)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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()
|
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
|
// 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
|
// Merge framework config defaults
|
||||||
$package_config = base_path('config/rsx.php');
|
$package_config = base_path('config/rsx.php');
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ use Illuminate\Support\Facades\Event;
|
|||||||
* PHILOSOPHY:
|
* PHILOSOPHY:
|
||||||
* - Production databases should never be rolled back
|
* - Production databases should never be rolled back
|
||||||
* - If a migration is bad, write a new migration to fix it
|
* - If a migration is bad, write a new migration to fix it
|
||||||
* - Rollbacks are unreliable and dangerous in production
|
* - In development mode, `php artisan migrate` auto-creates snapshots
|
||||||
* - Use migration snapshots (migrate:begin/commit/rollback) for local dev
|
* - Failed migrations automatically rollback to pre-migration state
|
||||||
*/
|
*/
|
||||||
#[Instantiatable]
|
#[Instantiatable]
|
||||||
class Rsx_Migration_Notice_Provider extends ServiceProvider
|
class Rsx_Migration_Notice_Provider extends ServiceProvider
|
||||||
|
|||||||
@@ -5,34 +5,35 @@ NAME
|
|||||||
|
|
||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
php artisan make:migration:safe <name>
|
php artisan make:migration:safe <name>
|
||||||
php artisan migrate:begin
|
php artisan migrate
|
||||||
php artisan migrate [--production]
|
php artisan migrate:status
|
||||||
php artisan migrate:commit
|
|
||||||
php artisan migrate:rollback
|
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
The RSX framework enforces a forward-only migration strategy using raw SQL
|
The RSX framework enforces a forward-only migration strategy using raw SQL
|
||||||
statements. Laravel's Schema builder is prohibited to ensure clarity,
|
statements. Laravel's Schema builder is prohibited to ensure clarity,
|
||||||
auditability, and prevent hidden behaviors.
|
auditability, and prevent hidden behaviors.
|
||||||
|
|
||||||
|
The migrate command automatically handles snapshot protection in development
|
||||||
|
mode - no manual steps required.
|
||||||
|
|
||||||
PHILOSOPHY
|
PHILOSOPHY
|
||||||
1. Forward-only migrations - No rollbacks, no down() methods
|
1. Forward-only migrations - No rollbacks, no down() methods
|
||||||
2. Raw SQL only - Direct MySQL statements, no abstractions
|
2. Raw SQL only - Direct MySQL statements, no abstractions
|
||||||
3. Fail loud - Migrations must succeed or fail with clear errors
|
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
|
MIGRATION RULES
|
||||||
|
|
||||||
Schema Builder Prohibition
|
Schema Builder Prohibition
|
||||||
All migrations MUST use DB::statement() with raw SQL. The following are prohibited:
|
All migrations MUST use DB::statement() with raw SQL. The following are prohibited:
|
||||||
|
|
||||||
• Schema::create()
|
- Schema::create()
|
||||||
• Schema::table()
|
- Schema::table()
|
||||||
• Schema::drop()
|
- Schema::drop()
|
||||||
• Schema::dropIfExists()
|
- Schema::dropIfExists()
|
||||||
• Schema::rename()
|
- Schema::rename()
|
||||||
• Blueprint class usage
|
- Blueprint class usage
|
||||||
• $table-> method chains
|
- $table-> method chains
|
||||||
|
|
||||||
The migration validator automatically checks for these patterns and will prevent
|
The migration validator automatically checks for these patterns and will prevent
|
||||||
migrations from running if violations are found.
|
migrations from running if violations are found.
|
||||||
@@ -50,26 +51,26 @@ MIGRATION RULES
|
|||||||
use simpler types and let the system handle optimization:
|
use simpler types and let the system handle optimization:
|
||||||
|
|
||||||
What You Can Use (System Auto-Converts):
|
What You Can Use (System Auto-Converts):
|
||||||
• INT → automatically becomes BIGINT
|
- INT -> automatically becomes BIGINT
|
||||||
• TEXT → automatically becomes LONGTEXT
|
- TEXT -> automatically becomes LONGTEXT
|
||||||
• FLOAT → automatically becomes DOUBLE
|
- FLOAT -> automatically becomes DOUBLE
|
||||||
• Any charset → automatically becomes UTF8MB4
|
- Any charset -> automatically becomes UTF8MB4
|
||||||
• created_at/updated_at → automatically added with proper defaults
|
- created_at/updated_at -> automatically added with proper defaults
|
||||||
• created_by/updated_by → automatically added
|
- created_by/updated_by -> automatically added
|
||||||
• deleted_by → automatically added for soft-delete tables
|
- deleted_by -> automatically added for soft-delete tables
|
||||||
|
|
||||||
What You MUST Be Careful About:
|
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
|
Example: If users.id is BIGINT, then orders.user_id must be BIGINT
|
||||||
• TINYINT(1) - Preserved for boolean values, won't be converted
|
- TINYINT(1) - Preserved for boolean values, won't be converted
|
||||||
• Column names ending in _id are assumed to be foreign keys
|
- Column names ending in _id are assumed to be foreign keys
|
||||||
|
|
||||||
Recommended for Simplicity:
|
Recommended for Simplicity:
|
||||||
• Just use INT for integers (becomes BIGINT automatically)
|
- Just use INT for integers (becomes BIGINT automatically)
|
||||||
• Just use TEXT for long content (becomes LONGTEXT automatically)
|
- Just use TEXT for long content (becomes LONGTEXT automatically)
|
||||||
• Just use FLOAT for decimals (becomes DOUBLE automatically)
|
- Just use FLOAT for decimals (becomes DOUBLE automatically)
|
||||||
• Don't add created_at/updated_at (added automatically)
|
- Don't add created_at/updated_at (added automatically)
|
||||||
• Don't add created_by/updated_by (added automatically)
|
- Don't add created_by/updated_by (added automatically)
|
||||||
|
|
||||||
down() Method Removal
|
down() Method Removal
|
||||||
The migration system automatically removes down() methods from migration files.
|
The migration system automatically removes down() methods from migration files.
|
||||||
@@ -81,31 +82,31 @@ AUTOMATIC NORMALIZATION
|
|||||||
After migrations run, the normalize_schema command automatically:
|
After migrations run, the normalize_schema command automatically:
|
||||||
|
|
||||||
1. Type Conversions:
|
1. Type Conversions:
|
||||||
• INT columns → BIGINT (except TINYINT(1) for booleans)
|
- INT columns -> BIGINT (except TINYINT(1) for booleans)
|
||||||
• BIGINT UNSIGNED → BIGINT SIGNED
|
- BIGINT UNSIGNED -> BIGINT SIGNED
|
||||||
• TEXT → LONGTEXT
|
- TEXT -> LONGTEXT
|
||||||
• FLOAT → DOUBLE
|
- FLOAT -> DOUBLE
|
||||||
• All text columns → UTF8MB4 character set
|
- All text columns -> UTF8MB4 character set
|
||||||
|
|
||||||
2. Required Columns Added:
|
2. Required Columns Added:
|
||||||
• created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)
|
- created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3)
|
||||||
• updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE
|
- updated_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE
|
||||||
• created_by INT(11) NULL
|
- created_by INT(11) NULL
|
||||||
• updated_by INT(11) NULL
|
- updated_by INT(11) NULL
|
||||||
• deleted_by INT(11) NULL (only for soft-delete tables)
|
- deleted_by INT(11) NULL (only for soft-delete tables)
|
||||||
|
|
||||||
3. Indexes Added:
|
3. Indexes Added:
|
||||||
• INDEX on created_at
|
- INDEX on created_at
|
||||||
• INDEX on updated_at
|
- INDEX on updated_at
|
||||||
• INDEX on site_id (for models extending Rsx_Site_Model_Abstract)
|
- INDEX on site_id (for models extending Rsx_Site_Model_Abstract)
|
||||||
• INDEX on id+version (for Versionable models)
|
- INDEX on id+version (for Versionable models)
|
||||||
|
|
||||||
4. Model-Specific Columns:
|
4. Model-Specific Columns:
|
||||||
• site_id BIGINT - for models extending Rsx_Site_Model_Abstract
|
- site_id BIGINT - for models extending Rsx_Site_Model_Abstract
|
||||||
• version INT(11) DEFAULT 1 - for Versionable/Ajaxable models
|
- version INT(11) DEFAULT 1 - for Versionable/Ajaxable models
|
||||||
|
|
||||||
5. Precision Upgrades:
|
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
|
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
|
optimization and standardization. The only time you need to be explicit about
|
||||||
@@ -115,7 +116,7 @@ AUTOMATIC NORMALIZATION
|
|||||||
VALIDATION SYSTEM
|
VALIDATION SYSTEM
|
||||||
|
|
||||||
Automatic Validation
|
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
|
1. Validates all pending migrations for Schema builder usage
|
||||||
2. Removes down() methods if present
|
2. Removes down() methods if present
|
||||||
@@ -125,7 +126,7 @@ VALIDATION SYSTEM
|
|||||||
Validation Output
|
Validation Output
|
||||||
When a violation is detected, you'll see:
|
When a violation is detected, you'll see:
|
||||||
|
|
||||||
❌ Migration Validation Failed
|
[ERROR] Migration Validation Failed
|
||||||
|
|
||||||
File: 2025_09_30_create_example_table.php
|
File: 2025_09_30_create_example_table.php
|
||||||
Line: 28
|
Line: 28
|
||||||
@@ -133,12 +134,12 @@ VALIDATION SYSTEM
|
|||||||
Violation: Found forbidden Schema builder usage: Schema::create
|
Violation: Found forbidden Schema builder usage: Schema::create
|
||||||
|
|
||||||
Code Preview:
|
Code Preview:
|
||||||
────────────────────────────────────────
|
----------------------------------------
|
||||||
Schema::create('users', function (Blueprint $table) {
|
Schema::create('users', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
});
|
});
|
||||||
────────────────────────────────────────
|
----------------------------------------
|
||||||
|
|
||||||
Remediation: Use DB::statement("CREATE TABLE...") instead
|
Remediation: Use DB::statement("CREATE TABLE...") instead
|
||||||
|
|
||||||
@@ -149,28 +150,43 @@ VALIDATION SYSTEM
|
|||||||
created_at TIMESTAMP NULL DEFAULT NULL
|
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
|
MIGRATION WORKFLOW
|
||||||
|
|
||||||
Development Workflow
|
Development Mode (RSX_MODE=development)
|
||||||
1. Create snapshot: php artisan migrate:begin
|
Simply run:
|
||||||
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
|
|
||||||
|
|
||||||
Production Workflow
|
php artisan migrate
|
||||||
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
|
|
||||||
|
|
||||||
Note: No snapshot protection in production mode. Ensure migrations are
|
The command automatically:
|
||||||
thoroughly tested before running in production.
|
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
|
MIGRATION EXAMPLES
|
||||||
|
|
||||||
@@ -295,30 +311,40 @@ ERROR MESSAGES
|
|||||||
Your migration uses Laravel's Schema builder. Rewrite using DB::statement()
|
Your migration uses Laravel's Schema builder. Rewrite using DB::statement()
|
||||||
with raw SQL.
|
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!"
|
"Unauthorized migrations detected!"
|
||||||
Migration files exist that weren't created via make:migration:safe.
|
Migration files exist that weren't created via make:migration:safe.
|
||||||
Recreate them using the proper command.
|
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
|
SECURITY CONSIDERATIONS
|
||||||
|
|
||||||
SQL Injection
|
SQL Injection
|
||||||
When using dynamic values in migrations, always use parameter binding:
|
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']);
|
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'");
|
DB::statement("UPDATE users SET status = '$status' WHERE created_at < '$date'");
|
||||||
|
|
||||||
Production Safety
|
Production Safety
|
||||||
• Always test migrations in development/staging first
|
- Always test migrations in development first
|
||||||
• Keep migrations small and focused
|
- Keep migrations small and focused
|
||||||
• Never reference models or services in migrations
|
- Never reference models or services in migrations
|
||||||
• Migrations must be self-contained and idempotent where possible
|
- Migrations must be self-contained and idempotent where possible
|
||||||
|
|
||||||
DEBUGGING
|
DEBUGGING
|
||||||
|
|
||||||
@@ -326,17 +352,17 @@ DEBUGGING
|
|||||||
php artisan migrate:status
|
php artisan migrate:status
|
||||||
|
|
||||||
Testing a Migration
|
Testing a Migration
|
||||||
1. Create snapshot: php artisan migrate:begin
|
In development mode, just run:
|
||||||
2. Run migration: php artisan migrate
|
php artisan migrate
|
||||||
3. If it fails, automatic rollback occurs
|
|
||||||
4. Fix the migration file
|
If it fails, the database is automatically restored. Fix the migration
|
||||||
5. Try again: php artisan migrate
|
and try again.
|
||||||
|
|
||||||
Common Issues
|
Common Issues
|
||||||
• "Class not found" - Don't reference models in migrations
|
- "Class not found" - Don't reference models in migrations
|
||||||
• "Syntax error" - Check your SQL syntax, test in MySQL client first
|
- "Syntax error" - Check your SQL syntax, test in MySQL client first
|
||||||
• "Foreign key constraint" - Ensure referenced table/column exists
|
- "Foreign key constraint" - Ensure referenced table/column exists
|
||||||
• "Duplicate column" - Check if column already exists before adding
|
- "Duplicate column" - Check if column already exists before adding
|
||||||
|
|
||||||
SEE ALSO
|
SEE ALSO
|
||||||
rsx:man database - Database system overview
|
rsx:man database - Database system overview
|
||||||
@@ -346,4 +372,4 @@ SEE ALSO
|
|||||||
AUTHORS
|
AUTHORS
|
||||||
RSX Framework Team
|
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
|
## Working with Migrations
|
||||||
|
|
||||||
1. To run migrations: `php artisan migrate`
|
1. To create a new migration: `php artisan make:migration:safe name_of_migration`
|
||||||
2. To create a new migration: `php artisan make:migration name_of_migration`
|
2. To run migrations: `php artisan migrate`
|
||||||
3. To roll back the last batch: `php artisan migrate:rollback`
|
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
|
```bash
|
||||||
php artisan make:migration:safe create_users_table
|
php artisan make:migration:safe create_users_table
|
||||||
php artisan migrate:begin
|
|
||||||
php artisan migrate
|
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:**
|
**NO defensive coding in migrations:**
|
||||||
```php
|
```php
|
||||||
// ❌ WRONG - conditional logic
|
// ❌ WRONG - conditional logic
|
||||||
@@ -838,7 +838,7 @@ DB::statement("ALTER TABLE foo DROP FOREIGN KEY bar");
|
|||||||
DB::statement("ALTER TABLE foo DROP COLUMN baz");
|
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
|
- **Implementation**: Environment configuration
|
||||||
- **Date**: 2025-05-15
|
- **Date**: 2025-05-15
|
||||||
|
|
||||||
### Snapshot-Protected Migrations (Development)
|
### Automatic Snapshot-Protected Migrations (Development)
|
||||||
- **Change**: Migrations require database snapshot in development environments
|
- **Change**: `php artisan migrate` automatically handles snapshots in development
|
||||||
- **Affected**: `php artisan migrate` command in development
|
- **Affected**: `php artisan migrate` command
|
||||||
- **Reason**: Prevents partial migrations from corrupting database, LLM-friendly error recovery
|
- **Reason**: Prevents partial migrations from corrupting database, LLM-friendly error recovery
|
||||||
- **Implementation**: Enhanced `Maint_Migrate` command with snapshot commands
|
- **Implementation**: Unified `Maint_Migrate` command with automatic snapshot/rollback
|
||||||
- **Commands**: `migrate:begin`, `migrate:commit`, `migrate:rollback`, `migrate:status`
|
- **Behavior**: Creates snapshot, runs migrations, commits on success, auto-rollbacks on failure
|
||||||
- **Bypass**: Use `--production` flag to skip snapshot requirement
|
- **Note**: Only in development mode with Docker; debug/production runs without snapshots
|
||||||
- **Note**: Only enforced in development with Docker, production runs normally
|
- **Date**: 2025-01-15
|
||||||
- **Date**: 2025-08-13
|
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# LLM-Friendly Migration System - Snapshot Implementation
|
# LLM-Friendly Migration System - Automatic Snapshot Protection
|
||||||
|
|
||||||
## Problem Statement
|
## Problem Statement
|
||||||
LLMs struggle with partial migration failures because:
|
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
|
- LLMs get confused about what has/hasn't been applied
|
||||||
- Recovery requires manual intervention or database reset
|
- Recovery requires manual intervention or database reset
|
||||||
|
|
||||||
## Implemented Solution: Docker Snapshot System
|
## Implemented Solution: Automatic Docker Snapshot System
|
||||||
|
|
||||||
### Core Design
|
### 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
|
### How It Works
|
||||||
|
|
||||||
1. **Development Mode Only**
|
1. **Development Mode** (`RSX_MODE=development`)
|
||||||
- System only enforces snapshots in development environments
|
- Automatically creates MySQL snapshot before migrations
|
||||||
- Production migrations run normally without snapshots
|
- Runs all pending migrations with validation
|
||||||
- Detects environment via `APP_ENV` and Docker presence
|
- 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
|
```bash
|
||||||
# 1. Start migration session (creates snapshot)
|
# Development - just one command does everything
|
||||||
php artisan migrate:begin
|
$ php artisan migrate
|
||||||
|
Development mode: Using automatic snapshot protection
|
||||||
|
|
||||||
# 2. Run migrations (with safety net)
|
[1/4] Creating database snapshot...
|
||||||
php artisan migrate
|
[OK] Snapshot created successfully.
|
||||||
|
|
||||||
# 3a. If successful, commit changes
|
[2/4] Running migrations...
|
||||||
php artisan migrate:commit
|
[OK] All 3 migrations completed successfully
|
||||||
|
|
||||||
# 3b. If failed, rollback to snapshot
|
[3/4] Running schema quality check...
|
||||||
php artisan migrate:rollback
|
[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**
|
### Automatic Recovery on Failure
|
||||||
- When migrations fail in development, system offers automatic rollback
|
|
||||||
- LLM gets clear instructions on how to proceed
|
```bash
|
||||||
- Database restored to exact pre-migration state
|
$ 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
|
### Implementation Details
|
||||||
|
|
||||||
#### Commands
|
The unified `migrate` command in development mode:
|
||||||
|
1. Stops MySQL via supervisord
|
||||||
**migrate:begin**
|
2. Creates backup of `/var/lib/mysql` to `/var/lib/mysql_backup`
|
||||||
- Stops MySQL via supervisord
|
3. Restarts MySQL and runs migrations
|
||||||
- Creates backup of `/var/lib/mysql` to `/var/lib/mysql_backup`
|
4. On success: removes backup, regenerates constants, recompiles bundles
|
||||||
- Restarts MySQL
|
5. On failure: restores backup, removes flag file
|
||||||
- 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...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Detection
|
### Environment Detection
|
||||||
|
|
||||||
The system automatically detects the environment:
|
The system automatically detects the environment:
|
||||||
|
|
||||||
1. **Development Mode** (snapshots required):
|
1. **Development Mode** (snapshots enabled):
|
||||||
- `APP_ENV` is not 'production'
|
- `RSX_MODE=development` in `.env`
|
||||||
- Docker environment detected (`/.dockerenv` exists)
|
- Docker environment detected (`/.dockerenv` exists)
|
||||||
- No `--production` flag passed
|
|
||||||
|
|
||||||
2. **Production Mode** (no snapshots):
|
2. **Debug/Production Mode** (no snapshots):
|
||||||
- `APP_ENV` is 'production' OR
|
- `RSX_MODE=debug` or `RSX_MODE=production`
|
||||||
- `--production` flag is passed OR
|
- OR not running in Docker
|
||||||
- Not running in Docker
|
|
||||||
|
|
||||||
### Benefits for LLMs
|
### Benefits for LLMs
|
||||||
|
|
||||||
1. **Clear Error Recovery**
|
1. **Zero-Step Recovery**
|
||||||
- Failed migrations automatically offer rollback
|
- Failed migrations automatically restore database
|
||||||
- No partial state confusion
|
- No commands to remember
|
||||||
- Explicit instructions on next steps
|
- Just fix migration files and run `migrate` again
|
||||||
|
|
||||||
2. **Safe Experimentation**
|
2. **Safe Experimentation**
|
||||||
- LLMs can try migrations without fear
|
- LLMs can try migrations without fear
|
||||||
@@ -163,48 +102,32 @@ The system automatically detects the environment:
|
|||||||
- Learn from failures without consequences
|
- Learn from failures without consequences
|
||||||
|
|
||||||
3. **Simple Mental Model**
|
3. **Simple Mental Model**
|
||||||
- Migration session is atomic: all or nothing
|
- One command: `php artisan migrate`
|
||||||
- Clear session lifecycle: begin → migrate → commit/rollback
|
- System handles all the complexity
|
||||||
- Status command provides current state
|
- Clear success/failure messaging
|
||||||
|
|
||||||
4. **Production Safety**
|
4. **Production Safety**
|
||||||
- System automatically adapts to environment
|
- 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
|
- 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
|
### Limitations
|
||||||
|
|
||||||
1. **Docker Only**: Snapshots require Docker environment with supervisord
|
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
|
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:
|
- `/app/RSpade/Commands/Migrate/Maint_Migrate.php` - Unified migrate command
|
||||||
- Incremental snapshots for large databases
|
- `/app/Http/Middleware/CheckMigrationMode.php` - Web UI protection during migration
|
||||||
- Migration preview/dry-run mode
|
|
||||||
- Automatic migration file fixes for common errors
|
|
||||||
- Integration with version control for migration rollback
|
|
||||||
|
|
||||||
## Why This Approach
|
## Why This Approach
|
||||||
|
|
||||||
This implementation was chosen over alternatives because:
|
This implementation was chosen because:
|
||||||
|
|
||||||
1. **Complete Safety**: Filesystem snapshots guarantee perfect rollback
|
1. **Complete Safety**: Filesystem snapshots guarantee perfect rollback
|
||||||
2. **Simple Implementation**: Uses existing tools (supervisord, cp, rm)
|
2. **Zero Friction**: Single command handles everything
|
||||||
3. **LLM-Friendly**: Clear session model with explicit states
|
3. **LLM-Friendly**: No multi-step workflow to remember
|
||||||
4. **Production Compatible**: Gracefully degrades in production
|
4. **Production Compatible**: Gracefully adapts to environment
|
||||||
5. **Laravel Integration**: Works with existing migration system
|
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
|
## Development Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Create snapshot (required)
|
# 1. Create migration
|
||||||
php artisan migrate:begin
|
|
||||||
|
|
||||||
# 2. Create migration
|
|
||||||
php artisan make:migration:safe create_products_table
|
php artisan make:migration:safe create_products_table
|
||||||
|
|
||||||
# 3. Write migration with raw SQL
|
# 2. Write migration with raw SQL
|
||||||
# 4. Run migrations
|
|
||||||
|
# 3. Run migrations (auto-snapshot in development)
|
||||||
php artisan migrate
|
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
|
## Automatic Normalization
|
||||||
@@ -163,14 +161,19 @@ user_id BIGINT NULL -- ✅ Matches
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Production Workflow
|
## Debug/Production Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# No snapshot protection in production
|
# In debug or production mode (RSX_MODE=debug or production)
|
||||||
php artisan migrate --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