exec("CREATE TABLE IF NOT EXISTS schema_migrations (filename VARCHAR(255) NOT NULL PRIMARY KEY, sha256 CHAR(64) NOT NULL, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $applied = Database::select('SELECT filename, sha256, applied_at FROM schema_migrations ORDER BY filename'); $known = array_column($applied, null, 'filename'); foreach ($files as $file) { $name = basename($file); $sha = hash_file('sha256', $file); if (isset($known[$name])) { if (!hash_equals((string)$known[$name]['sha256'], $sha)) { fwrite(STDERR, "REFUSE {$name}: applied migration checksum changed.\n"); exit(2); } echo "APPLIED {$name} {$known[$name]['applied_at']}\n"; continue; } if ($mode === 'status') { echo "PENDING {$name}\n"; continue; } if ($mode === 'dry_run') { echo "WOULD APPLY {$name}\n"; continue; } $sql = file_get_contents($file); if ($sql === false || trim($sql) === '') { fwrite(STDERR, "REFUSE {$name}: unreadable or empty.\n"); exit(2); } echo "APPLYING {$name}\n"; try { // MySQL DDL may commit implicitly. The migration is recorded only after // its complete SQL succeeds; repair any partial DDL manually before retry. $pdo->exec($sql); Database::insert('INSERT INTO schema_migrations (filename, sha256) VALUES (?, ?)', [$name, $sha]); echo "APPLIED {$name}\n"; } catch (Throwable $e) { fwrite(STDERR, "FAILED {$name}: {$e->getMessage()}\n"); exit(1); } }