🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
182 lines
8.3 KiB
PHP
182 lines
8.3 KiB
PHP
<?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');
|
|
}
|
|
} |