From c46f356c7cd148d84df2f52ddb99ed8835d69c31 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 1 May 2026 18:41:11 +0000 Subject: [PATCH] add migrate.php --- migrate.php | 273 +++++++++++++++++++++++++++++++++++++++++++++ run_migrations.php | 62 +--------- 2 files changed, 274 insertions(+), 61 deletions(-) create mode 100644 migrate.php diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..49e556c --- /dev/null +++ b/migrate.php @@ -0,0 +1,273 @@ +exec(<<query('SELECT migration FROM migrations'); + $rows = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: []; + return array_fill_keys($rows, true); +} + +function getMigrationFiles(): array +{ + $sqlFiles = glob(__DIR__ . '/db/migrations/*.sql') ?: []; + $phpFiles = glob(__DIR__ . '/db/migrations/*.php') ?: []; + $files = array_merge($sqlFiles, $phpFiles); + + usort($files, static function (string $left, string $right): int { + return strnatcasecmp(basename($left), basename($right)); + }); + + return $files; +} + +function splitSqlStatements(string $sql): array +{ + $sql = preg_replace('/^\xEF\xBB\xBF/', '', $sql) ?? $sql; + $sql = preg_replace('/\/\*.*?\*\//s', '', $sql) ?? $sql; + + $lines = preg_split('/\R/', $sql) ?: []; + $filteredLines = []; + + foreach ($lines as $line) { + $trimmed = ltrim($line); + if ($trimmed === '' || str_starts_with($trimmed, '--') || str_starts_with($trimmed, '#')) { + continue; + } + $filteredLines[] = $line; + } + + $cleanSql = implode("\n", $filteredLines); + $statements = []; + $buffer = ''; + $inSingleQuote = false; + $inDoubleQuote = false; + $length = strlen($cleanSql); + + for ($index = 0; $index < $length; $index++) { + $char = $cleanSql[$index]; + $previous = $index > 0 ? $cleanSql[$index - 1] : ''; + + if ($char === "'" && !$inDoubleQuote && $previous !== '\\') { + $inSingleQuote = !$inSingleQuote; + } elseif ($char === '"' && !$inSingleQuote && $previous !== '\\') { + $inDoubleQuote = !$inDoubleQuote; + } + + if ($char === ';' && !$inSingleQuote && !$inDoubleQuote) { + $statement = trim($buffer); + if ($statement !== '') { + $statements[] = $statement; + } + $buffer = ''; + continue; + } + + $buffer .= $char; + } + + $tail = trim($buffer); + if ($tail !== '') { + $statements[] = $tail; + } + + return $statements; +} + +function isIgnorableMigrationError(PDOException $exception): bool +{ + $driverCode = isset($exception->errorInfo[1]) ? (int) $exception->errorInfo[1] : null; + $message = strtolower($exception->getMessage()); + $ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826]; + $ignorableSnippets = [ + 'already exists', + 'duplicate column name', + 'duplicate key name', + 'duplicate entry', + 'duplicate foreign key constraint name', + 'check that column/key exists', + ]; + + if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) { + return true; + } + + foreach ($ignorableSnippets as $snippet) { + if (str_contains($message, $snippet)) { + return true; + } + } + + return false; +} + +function executeSqlMigration(PDO $pdo, string $filePath): void +{ + $sql = file_get_contents($filePath); + if ($sql === false) { + throw new RuntimeException('Unable to read SQL migration: ' . basename($filePath)); + } + + $statements = splitSqlStatements($sql); + if ($statements === []) { + migrationOutput(' - no executable SQL statements found'); + return; + } + + foreach ($statements as $number => $statement) { + try { + $pdo->exec($statement); + } catch (PDOException $exception) { + if (isIgnorableMigrationError($exception)) { + migrationOutput(' - skipped statement ' . ($number + 1) . ': ' . $exception->getMessage()); + continue; + } + + throw new RuntimeException( + 'SQL migration failed in ' . basename($filePath) . ' at statement ' . ($number + 1) . ': ' . $exception->getMessage(), + 0, + $exception + ); + } + } +} + +function executePhpMigration(string $filePath): void +{ + ob_start(); + + try { + $result = include $filePath; + } catch (Throwable $throwable) { + ob_end_clean(); + throw new RuntimeException('PHP migration failed in ' . basename($filePath) . ': ' . $throwable->getMessage(), 0, $throwable); + } + + $output = trim((string) ob_get_clean()); + + if ($result === false) { + throw new RuntimeException('PHP migration returned false: ' . basename($filePath)); + } + + if ($output !== '') { + foreach (preg_split('/\R/', $output) ?: [] as $line) { + migrationOutput(' ' . $line); + } + } +} + +function recordMigration(PDO $pdo, string $migrationName): void +{ + $stmt = $pdo->prepare('INSERT INTO migrations (migration) VALUES (:migration)'); + $stmt->execute(['migration' => $migrationName]); +} + +function runAllMigrations(): int +{ + if (!canRunMigrations()) { + if (PHP_SAPI !== 'cli') { + http_response_code(403); + } + + migrationOutput('Forbidden: run migrate.php from CLI, localhost, or while logged in as an Administrator.'); + return 1; + } + + try { + $pdo = db(); + ensureMigrationsTable($pdo); + $executed = getExecutedMigrations($pdo); + $files = getMigrationFiles(); + + if ($files === []) { + migrationOutput('No migration files found in db/migrations.'); + return 0; + } + + $applied = 0; + $skipped = 0; + + migrationOutput('Starting migration run...'); + + foreach ($files as $filePath) { + $migrationName = basename($filePath); + + if (isset($executed[$migrationName])) { + migrationOutput('SKIP ' . $migrationName); + $skipped++; + continue; + } + + migrationOutput('RUN ' . $migrationName); + $extension = strtolower((string) pathinfo($filePath, PATHINFO_EXTENSION)); + + if ($extension === 'sql') { + executeSqlMigration($pdo, $filePath); + } elseif ($extension === 'php') { + executePhpMigration($filePath); + } else { + throw new RuntimeException('Unsupported migration type: ' . $migrationName); + } + + recordMigration($pdo, $migrationName); + $executed[$migrationName] = true; + $applied++; + migrationOutput('OK ' . $migrationName); + } + + migrationOutput('Done. Applied ' . $applied . ' migration(s), skipped ' . $skipped . ' already executed.'); + return 0; + } catch (Throwable $throwable) { + if (PHP_SAPI !== 'cli') { + http_response_code(500); + } + + migrationOutput('ERROR: ' . $throwable->getMessage()); + return 1; + } +} + +exit(runAllMigrations()); diff --git a/run_migrations.php b/run_migrations.php index 20771cc..cbdeb07 100644 --- a/run_migrations.php +++ b/run_migrations.php @@ -1,62 +1,2 @@ query("SHOW TABLES LIKE 'migrations'"); - if ($stmt->rowCount() == 0) { - $pdo->exec("CREATE TABLE migrations ( - id INT AUTO_INCREMENT PRIMARY KEY, - migration VARCHAR(255) NOT NULL, - executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )"); - echo "Created migrations table.\n"; - } - - $stmt = $pdo->query("SELECT migration FROM migrations"); - $executed = $stmt->fetchAll(PDO::FETCH_COLUMN); - - $files = glob('db/migrations/*.sql'); - sort($files); - - foreach ($files as $file) { - $migrationName = basename($file); - if (!in_array($migrationName, $executed)) { - echo "Executing: $migrationName...\n"; - $sql = file_get_contents($file); - try { - $pdo->exec($sql); - $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); - $stmt->execute([$migrationName]); - echo "Success: $migrationName\n"; - } catch (PDOException $e) { - echo "Error executing $migrationName: " . $e->getMessage() . "\n"; - } - } - } - - - -} catch (Exception $e) { - echo "DB Error: " . $e->getMessage() . "\n"; -} - - $php_files = glob('db/migrations/*.php'); - sort($php_files); - foreach ($php_files as $pfile) { - $migrationName = basename($pfile); - if (!in_array($migrationName, $executed)) { - echo "Executing PHP: $migrationName...\n"; - try { - include $pfile; - $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); - $stmt->execute([$migrationName]); - echo "Success: $migrationName\n"; - } catch (Exception $e) { - echo "Error executing $migrationName: " . $e->getMessage() . "\n"; - } - } - } -echo "Done checking migrations.\n"; +require_once __DIR__ . '/migrate.php';