Files
rspade_system/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php
2025-12-26 02:44:39 +00:00

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