🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
8.6 KiB
PHP
Executable File
196 lines
8.6 KiB
PHP
Executable File
<?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
|
||
{
|
||
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...');
|
||
shell_exec('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_command(['rm', '-rf', $this->backup_dir]);
|
||
}
|
||
|
||
// Step 3: Copy MySQL data directory
|
||
$this->info('3️⃣ Creating snapshot of MySQL data...');
|
||
$this->run_command(['cp', '-r', $this->mysql_data_dir, $this->backup_dir]);
|
||
|
||
// Step 4: Start MySQL again using supervisorctl
|
||
$this->info('4️⃣ Starting MySQL server...');
|
||
shell_exec('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...');
|
||
shell_exec('supervisorctl start mysql 2>&1');
|
||
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Run a shell command and check for errors
|
||
*/
|
||
protected function run_command(array $command, bool $throw_on_error = true): string
|
||
{
|
||
$process = new Process($command);
|
||
$process->setTimeout(60);
|
||
$process->run();
|
||
|
||
if ($throw_on_error && !$process->isSuccessful()) {
|
||
throw new ProcessFailedException($process);
|
||
}
|
||
|
||
return $process->getOutput();
|
||
}
|
||
|
||
/**
|
||
* Wait for MySQL to be ready for connections
|
||
*/
|
||
protected function wait_for_mysql_ready(): void
|
||
{
|
||
$max_attempts = 30;
|
||
$attempt = 0;
|
||
|
||
while ($attempt < $max_attempts) {
|
||
// Use mysql client to test connection (similar to user's approach)
|
||
$result = shell_exec("echo \"SELECT 'test';\" | mysql -uroot 2>/dev/null | grep test");
|
||
|
||
if ($result !== null && str_contains($result, 'test')) {
|
||
return;
|
||
}
|
||
|
||
sleep(1);
|
||
$attempt++;
|
||
}
|
||
|
||
throw new \Exception('MySQL did not start within 30 seconds');
|
||
}
|
||
} |