🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
765 lines
26 KiB
PHP
765 lines
26 KiB
PHP
<?php
|
|
/**
|
|
* CODING CONVENTION:
|
|
* This file follows the coding convention where variable_names and function_names
|
|
* use snake_case (underscore_wherever_possible).
|
|
*/
|
|
|
|
namespace App\RSpade\Commands\Migrate;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\Console\Output\BufferedOutput;
|
|
use Illuminate\Database\Console\Migrations\MigrateCommand;
|
|
use App\Providers\AppServiceProvider;
|
|
use App\RSpade\Core\Database\MigrationValidator;
|
|
use App\RSpade\Core\Database\SqlQueryTransformer;
|
|
use App\RSpade\Core\Rsx;
|
|
use App\RSpade\SchemaQuality\SchemaQualityChecker;
|
|
|
|
/**
|
|
* Unified migration command with mode-aware behavior
|
|
*
|
|
* In DEVELOPMENT mode:
|
|
* - 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
|
|
*
|
|
* In DEBUG/PRODUCTION mode:
|
|
* - Runs migrations without snapshot protection
|
|
* - Runs schema normalization
|
|
* - Does NOT update source code (constants, bundles) - source is read-only
|
|
*
|
|
* This command is automatically used when running 'php artisan migrate' due to
|
|
* a modification in the artisan script.
|
|
*/
|
|
class Maint_Migrate extends Command
|
|
{
|
|
use PrivilegedCommandTrait;
|
|
|
|
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 $mysql_data_dir = '/var/lib/mysql';
|
|
protected $backup_dir = '/var/lib/mysql_backup';
|
|
|
|
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
|
|
SqlQueryTransformer::enable();
|
|
$this->register_query_transformer();
|
|
|
|
// Enable full query logging to stdout for migrations
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT);
|
|
|
|
// Ensure migrations table exists (create it if needed)
|
|
$this->ensure_migrations_table_exists();
|
|
|
|
$is_framework_only = $this->option('framework-only');
|
|
$is_development = Rsx::is_development();
|
|
|
|
// Get all the options
|
|
$force = $this->option('force');
|
|
$seed = $this->option('seed');
|
|
$step = $this->option('step');
|
|
$paths = $this->option('path');
|
|
|
|
// Check if path option is used and throw exception
|
|
if (!empty($paths)) {
|
|
$this->error('[ERROR] Migration by path is disabled!');
|
|
$this->error('');
|
|
$this->line('This command enforces running all pending migrations in order.');
|
|
$this->line('Please run migrations without the --path option.');
|
|
SqlQueryTransformer::disable();
|
|
return 1;
|
|
}
|
|
|
|
// Determine which migration paths to use for whitelist check
|
|
$paths_to_check = $is_framework_only
|
|
? [database_path('migrations')]
|
|
: MigrationPaths::get_all_paths();
|
|
|
|
// Check migration whitelist
|
|
if (!$this->checkMigrationWhitelist($paths_to_check)) {
|
|
SqlQueryTransformer::disable();
|
|
return 1;
|
|
}
|
|
|
|
// Validate migration files for Schema builder usage (only in development)
|
|
if ($is_development && !$this->_validate_schema_rules()) {
|
|
SqlQueryTransformer::disable();
|
|
return 1;
|
|
}
|
|
|
|
// Run normalize_schema BEFORE migrations to fix existing tables
|
|
$requiredColumnsArgs = $is_development ? [] : ['--production' => true];
|
|
|
|
$this->info("\n Pre-migration normalization (fixing existing tables)...\n");
|
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
|
if ($normalizeExitCode !== 0) {
|
|
$this->error('Pre-migration normalization failed');
|
|
SqlQueryTransformer::disable();
|
|
return $normalizeExitCode;
|
|
}
|
|
|
|
// Use a buffered output to capture migration output
|
|
$bufferedOutput = new BufferedOutput();
|
|
|
|
try {
|
|
// Run the standard migrations and capture the output
|
|
$originalSqlMode = DB::selectOne('SELECT @@sql_mode as sql_mode')->sql_mode;
|
|
DB::statement("SET sql_mode = REPLACE(@@sql_mode, 'NO_ZERO_DATE', '');");
|
|
|
|
// Run migrations directly via migrator to avoid recursion
|
|
$migrator = app('migrator');
|
|
$migrator->setOutput($bufferedOutput);
|
|
|
|
// Use all migration paths (framework + user), or just framework if --framework-only
|
|
$migrationPaths = $is_framework_only
|
|
? [database_path('migrations')]
|
|
: MigrationPaths::get_all_paths();
|
|
|
|
// Run migrations one-by-one with normalization after each
|
|
$this->run_migrations_with_normalization($migrator, $migrationPaths, $step, $requiredColumnsArgs);
|
|
|
|
$exitCode = 0;
|
|
|
|
// Handle seeding if requested
|
|
if ($seed) {
|
|
$this->call('db:seed', ['--force' => $force]);
|
|
}
|
|
|
|
$migrationOutput = $bufferedOutput->fetch();
|
|
|
|
// Show the output from the migration command
|
|
$this->output->write($migrationOutput);
|
|
|
|
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
|
|
|
} catch (\Exception $e) {
|
|
// Restore SQL mode
|
|
try {
|
|
if (isset($originalSqlMode)) {
|
|
DB::statement('SET sql_mode = ?', [$originalSqlMode]);
|
|
}
|
|
} catch (\Exception $sqlEx) {
|
|
// Ignore SQL mode restore errors
|
|
}
|
|
|
|
$this->error('');
|
|
$this->error('Error: ' . $e->getMessage());
|
|
|
|
// Disable query logging before returning
|
|
AppServiceProvider::disable_query_echo();
|
|
SqlQueryTransformer::disable();
|
|
|
|
return 1;
|
|
}
|
|
|
|
// Run normalize_schema AFTER migrations to add framework columns to new tables
|
|
$this->info("\n Post-migration normalization (adding framework columns to new tables)...\n");
|
|
|
|
// Switch to destructive-only query logging for normalize_schema
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT);
|
|
|
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
|
if ($normalizeExitCode !== 0) {
|
|
$this->error('Post-migration normalization failed');
|
|
SqlQueryTransformer::disable();
|
|
return $normalizeExitCode;
|
|
}
|
|
|
|
// Disable query logging
|
|
AppServiceProvider::disable_query_echo();
|
|
SqlQueryTransformer::disable();
|
|
|
|
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
|
|
*/
|
|
protected function register_query_transformer(): void
|
|
{
|
|
$original_statement = \Closure::bind(function () {
|
|
return $this->connection->statement(...func_get_args());
|
|
}, DB::getFacadeRoot(), DB::getFacadeRoot());
|
|
|
|
DB::macro('statement', function ($query, $bindings = []) use ($original_statement) {
|
|
$transformed = SqlQueryTransformer::transform($query);
|
|
return DB::connection()->statement($transformed, $bindings);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if all pending migrations are whitelisted
|
|
*/
|
|
protected function checkMigrationWhitelist(array $paths): bool
|
|
{
|
|
$whitelistPaths = [
|
|
database_path('migrations/.migration_whitelist'),
|
|
base_path('rsx/resource/migrations/.migration_whitelist'),
|
|
];
|
|
|
|
$whitelistedMigrations = [];
|
|
$foundAtLeastOne = false;
|
|
|
|
foreach ($whitelistPaths as $whitelistPath) {
|
|
if (file_exists($whitelistPath)) {
|
|
$foundAtLeastOne = true;
|
|
$whitelist = json_decode(file_get_contents($whitelistPath), true);
|
|
$migrations = array_keys($whitelist['migrations'] ?? []);
|
|
$whitelistedMigrations = array_merge($whitelistedMigrations, $migrations);
|
|
}
|
|
}
|
|
|
|
if (!$foundAtLeastOne) {
|
|
$this->warn('[WARNING] No migration whitelist found. Creating one with existing migrations...');
|
|
$this->createInitialWhitelist();
|
|
return true;
|
|
}
|
|
|
|
$migrationFiles = [];
|
|
foreach ($paths as $path) {
|
|
if (is_dir($path)) {
|
|
foreach (glob($path . '/*.php') as $file) {
|
|
$migrationFiles[] = basename($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
$unauthorizedMigrations = array_diff($migrationFiles, $whitelistedMigrations);
|
|
|
|
if (!empty($unauthorizedMigrations)) {
|
|
$this->error('[ERROR] Unauthorized migrations detected!');
|
|
$this->error('');
|
|
$this->line('The following migrations were not created via php artisan make:migration:');
|
|
foreach ($unauthorizedMigrations as $migration) {
|
|
$this->line(' - ' . $migration);
|
|
}
|
|
$this->error('');
|
|
$this->warn('[WARNING] Manually created migrations can cause timestamp conflicts.');
|
|
$this->error('');
|
|
$this->line('To fix: Create migrations using "php artisan make:migration [name]"');
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create initial whitelist with existing migrations
|
|
*/
|
|
protected function createInitialWhitelist(): void
|
|
{
|
|
$whitelistLocations = [
|
|
database_path('migrations/.migration_whitelist') => database_path('migrations'),
|
|
base_path('rsx/resource/migrations/.migration_whitelist') => base_path('rsx/resource/migrations'),
|
|
];
|
|
|
|
$totalMigrations = 0;
|
|
|
|
foreach ($whitelistLocations as $whitelistPath => $migrationPath) {
|
|
if (!is_dir($migrationPath)) {
|
|
continue;
|
|
}
|
|
|
|
$whitelist = [
|
|
'description' => 'This file tracks migrations created via php artisan make:migration',
|
|
'purpose' => 'Prevents manually created migrations from running to avoid timestamp conflicts',
|
|
'migrations' => [],
|
|
];
|
|
|
|
foreach (glob($migrationPath . '/*.php') as $file) {
|
|
$filename = basename($file);
|
|
$whitelist['migrations'][$filename] = [
|
|
'created_at' => 'pre-whitelist',
|
|
'created_by' => 'system',
|
|
'command' => 'existing migration',
|
|
];
|
|
}
|
|
|
|
if (!empty($whitelist['migrations'])) {
|
|
file_put_contents($whitelistPath, json_encode($whitelist, JSON_PRETTY_PRINT));
|
|
$count = count($whitelist['migrations']);
|
|
$totalMigrations += $count;
|
|
$location = str_replace(base_path() . '/', '', dirname($whitelistPath));
|
|
$this->info("[OK] Created whitelist in {$location} with {$count} migration(s).");
|
|
}
|
|
}
|
|
|
|
if ($totalMigrations === 0) {
|
|
$this->info('[OK] No existing migrations found. Empty whitelists created.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate migration files for Schema builder usage
|
|
*/
|
|
protected function _validate_schema_rules(): bool
|
|
{
|
|
$repository = app('migration.repository');
|
|
$pending_migrations = MigrationValidator::get_pending_migrations($repository);
|
|
|
|
if (empty($pending_migrations)) {
|
|
return true;
|
|
}
|
|
|
|
$this->info('Validating migration files for Schema builder usage...');
|
|
|
|
$has_violations = false;
|
|
|
|
foreach ($pending_migrations as $migration_path) {
|
|
try {
|
|
MigrationValidator::validate_migration_file($migration_path);
|
|
$this->info(" [OK] " . basename($migration_path));
|
|
} catch (\RuntimeException $e) {
|
|
$has_violations = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($has_violations) {
|
|
$this->error('');
|
|
$this->error('Migration validation failed!');
|
|
$this->info('');
|
|
$this->line('Please update your migration to use raw SQL instead of Schema builder.');
|
|
$this->line('See: php artisan rsx:man migrations');
|
|
return false;
|
|
}
|
|
|
|
// Remove down() methods
|
|
$processed_migrations = [];
|
|
foreach ($pending_migrations as $migration_path) {
|
|
if (MigrationValidator::remove_down_method($migration_path)) {
|
|
$processed_migrations[] = $migration_path;
|
|
}
|
|
}
|
|
|
|
if (!empty($processed_migrations)) {
|
|
$this->info('');
|
|
$this->warn('[WARNING] Modified migration files:');
|
|
foreach ($processed_migrations as $path) {
|
|
$this->line(' - ' . basename($path) . ' - down() method removed');
|
|
}
|
|
$this->info('');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Ensure the migrations table exists
|
|
*/
|
|
protected function ensure_migrations_table_exists(): void
|
|
{
|
|
try {
|
|
$table = config('database.migrations', 'migrations');
|
|
DB::select("SELECT 1 FROM {$table} LIMIT 1");
|
|
} catch (\Exception $e) {
|
|
$table = config('database.migrations', 'migrations');
|
|
$this->info('Creating migrations table...');
|
|
|
|
DB::statement("
|
|
CREATE TABLE IF NOT EXISTS {$table} (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
migration VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
batch BIGINT NOT NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
|
|
$this->info('[OK] Migrations table created');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run migrations one-by-one with normalization after each
|
|
*/
|
|
protected function run_migrations_with_normalization($migrator, array $migrationPaths, bool $step, array $requiredColumnsArgs): void
|
|
{
|
|
$files = [];
|
|
foreach ($migrationPaths as $path) {
|
|
if (is_dir($path)) {
|
|
$path_files = glob($path . '/*.php');
|
|
if ($path_files) {
|
|
foreach ($path_files as $file) {
|
|
$files[] = $file;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort($files);
|
|
|
|
$repository = $migrator->getRepository();
|
|
$ran = $repository->getRan();
|
|
|
|
$pending = [];
|
|
foreach ($files as $file) {
|
|
$migrationName = $migrator->getMigrationName($file);
|
|
if (!in_array($migrationName, $ran)) {
|
|
$pending[] = $file;
|
|
}
|
|
}
|
|
|
|
if (empty($pending)) {
|
|
$this->info(' INFO Nothing to migrate.');
|
|
return;
|
|
}
|
|
|
|
$totalMigrations = count($pending);
|
|
$currentMigration = 1;
|
|
|
|
foreach ($pending as $file) {
|
|
$migrationName = $migrator->getMigrationName($file);
|
|
|
|
$this->info("\n[$currentMigration/$totalMigrations] Running: $migrationName");
|
|
$this->newLine();
|
|
|
|
$migrator->runPending([$file], [
|
|
'pretend' => false,
|
|
'step' => $step
|
|
]);
|
|
|
|
if ($currentMigration < $totalMigrations) {
|
|
$this->info("\n Normalizing schema after migration...\n");
|
|
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_DESTRUCTIVE_STDOUT);
|
|
|
|
$normalizeExitCode = $this->call('migrate:normalize_schema', $requiredColumnsArgs);
|
|
|
|
AppServiceProvider::set_query_log_mode(AppServiceProvider::QUERY_LOG_ALL_STDOUT);
|
|
|
|
if ($normalizeExitCode !== 0) {
|
|
throw new \Exception("Normalization failed after migration: $migrationName");
|
|
}
|
|
}
|
|
|
|
$currentMigration++;
|
|
}
|
|
|
|
$this->newLine();
|
|
$this->info("[OK] All $totalMigrations migration" . ($totalMigrations > 1 ? 's' : '') . " completed successfully");
|
|
}
|
|
}
|