Files
rspade_system/app/RSpade/Commands/Migrate/Migrate_Begin_Command.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

196 lines
8.6 KiB
PHP
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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('✅ 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. ✅ 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('⚠️ 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('⚠️ 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('⚠️ 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('✅ 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('⚠️ MIGRATION GUIDELINES FOR LLMs:');
$this->line('');
$this->line(' 🔹 Use RAW MySQL queries, not Laravel schema builder:');
$this->line(' ✅ DB::statement("ALTER TABLE users ADD COLUMN age INT")');
$this->line(' ❌ Schema::table("users", function($table) { $table->integer("age"); })');
$this->line('');
$this->line(' 🔹 ALL tables MUST have BIGINT ID primary key:');
$this->line(' ✅ id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY');
$this->line(' ❌ No exceptions - every table needs this exact ID column (SIGNED for easier migrations)');
$this->line('');
$this->line(' 🔹 Integer column types:');
$this->line(' ✅ BIGINT for all integers (IDs, counts, foreign keys, etc.)');
$this->line(' ✅ TINYINT(1) for boolean values (true/false only)');
$this->line(' ❌ NEVER use unsigned - always use signed integers');
$this->line(' ❌ 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('⚠️ The web UI is disabled during migration mode.');
return 0;
} catch (\Exception $e) {
$this->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');
}
}