139 lines
4.3 KiB
PHP
139 lines
4.3 KiB
PHP
<?php
|
||
/**
|
||
* SQL 结构迁移执行器(轻量版)
|
||
*
|
||
* 目标:让数据库结构变更以 "V{n}__xxx.sql" 脚本形式沉淀(仅结构,不推业务数据)。
|
||
* 使用:php scripts/sql_migrate.php
|
||
*
|
||
* 约定:
|
||
* - 脚本目录:database/migrations
|
||
* - 脚本命名:V1__xxx.sql, V2__xxx.sql ...(按数字升序执行)
|
||
* - 记录表:schema_sql_migrations(记录已执行版本)
|
||
*/
|
||
|
||
$baseDir = realpath(__DIR__ . '/../database/migrations');
|
||
if ($baseDir === false) {
|
||
fwrite(STDERR, "SQL migrations dir not found.\n");
|
||
exit(1);
|
||
}
|
||
|
||
// 读取 .env(尽量少依赖 Laravel 容器,便于 CI/部署直接跑)
|
||
$envPath = realpath(__DIR__ . '/../.env');
|
||
$env = [];
|
||
if ($envPath && file_exists($envPath)) {
|
||
foreach (file($envPath, FILE_IGNORE_NEW_LINES) as $line) {
|
||
$line = trim($line);
|
||
if ($line === '' || str_starts_with($line, '#')) {
|
||
continue;
|
||
}
|
||
if (!str_contains($line, '=')) {
|
||
continue;
|
||
}
|
||
[$k, $v] = explode('=', $line, 2);
|
||
$k = trim($k);
|
||
$v = trim($v);
|
||
$v = trim($v, "\"'");
|
||
$env[$k] = $v;
|
||
}
|
||
}
|
||
|
||
// env 变量优先级:系统环境变量 > .env
|
||
$getEnv = function (string $key, string $default = '') use ($env): string {
|
||
$v = getenv($key);
|
||
if ($v !== false && $v !== '') {
|
||
return (string) $v;
|
||
}
|
||
return (string) ($env[$key] ?? $default);
|
||
};
|
||
|
||
$driver = $getEnv('DB_CONNECTION', 'mysql');
|
||
$host = $getEnv('DB_HOST', '127.0.0.1');
|
||
$port = $getEnv('DB_PORT', $driver === 'pgsql' ? '5432' : ($driver === 'sqlite' ? '' : '3306'));
|
||
$db = $getEnv('DB_DATABASE', '');
|
||
$user = $getEnv('DB_USERNAME', '');
|
||
$pass = $getEnv('DB_PASSWORD', '');
|
||
|
||
if ($driver === 'sqlite') {
|
||
// sqlite: DB_DATABASE 可能是绝对路径
|
||
$dsn = $db !== '' ? "sqlite:" . $db : 'sqlite::memory:';
|
||
} elseif ($driver === 'pgsql') {
|
||
$dsn = "pgsql:host={$host};port={$port};dbname={$db}";
|
||
} else {
|
||
// mysql
|
||
$charset = $getEnv('DB_CHARSET', 'utf8mb4');
|
||
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset={$charset}";
|
||
}
|
||
|
||
try {
|
||
$pdo = new PDO($dsn, $user, $pass, [
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||
]);
|
||
} catch (Throwable $e) {
|
||
fwrite(STDERR, "DB connect failed: {$e->getMessage()}\n");
|
||
exit(2);
|
||
}
|
||
|
||
// 确保迁移记录表存在
|
||
// applied_at 使用 TEXT,确保 sqlite/mysql 下都可用(避免 DATETIME 在部分方言下不兼容)
|
||
$pdo->exec("CREATE TABLE IF NOT EXISTS schema_sql_migrations (\n version VARCHAR(50) PRIMARY KEY,\n description VARCHAR(255) NULL,\n applied_at TEXT NOT NULL\n)");
|
||
|
||
$applied = [];
|
||
foreach ($pdo->query('SELECT version FROM schema_sql_migrations') as $row) {
|
||
$applied[(string) $row['version']] = true;
|
||
}
|
||
|
||
$files = glob($baseDir . '/V*__*.sql');
|
||
sort($files);
|
||
|
||
$pending = [];
|
||
foreach ($files as $file) {
|
||
$name = basename($file);
|
||
if (!preg_match('/^(V\d+)__.+\.sql$/', $name, $m)) {
|
||
continue;
|
||
}
|
||
$version = $m[1];
|
||
if (isset($applied[$version])) {
|
||
continue;
|
||
}
|
||
$pending[] = ['version' => $version, 'file' => $file, 'name' => $name];
|
||
}
|
||
|
||
if (count($pending) === 0) {
|
||
fwrite(STDOUT, "No pending SQL migrations.\n");
|
||
exit(0);
|
||
}
|
||
|
||
foreach ($pending as $item) {
|
||
$sql = file_get_contents($item['file']);
|
||
$sql = $sql === false ? '' : $sql;
|
||
|
||
fwrite(STDOUT, "Applying {$item['name']} ...\n");
|
||
|
||
$pdo->beginTransaction();
|
||
try {
|
||
// 简单处理:按分号执行(对于包含存储过程等复杂语法的脚本,需拆分策略升级)
|
||
$statements = array_filter(array_map('trim', preg_split('/;\s*\n/', $sql)));
|
||
foreach ($statements as $stmt) {
|
||
if ($stmt === '' || str_starts_with(ltrim($stmt), '--')) {
|
||
continue;
|
||
}
|
||
$pdo->exec($stmt);
|
||
}
|
||
|
||
$desc = preg_replace('/^V\d+__/', '', $item['name']);
|
||
$desc = preg_replace('/\.sql$/', '', $desc);
|
||
|
||
$stmt = $pdo->prepare('INSERT INTO schema_sql_migrations(version, description, applied_at) VALUES(?, ?, ?)');
|
||
$stmt->execute([$item['version'], $desc, date('Y-m-d H:i:s')]);
|
||
|
||
$pdo->commit();
|
||
} catch (Throwable $e) {
|
||
$pdo->rollBack();
|
||
fwrite(STDERR, "Failed {$item['name']}: {$e->getMessage()}\n");
|
||
exit(3);
|
||
}
|
||
}
|
||
|
||
fwrite(STDOUT, "SQL migrations applied: " . count($pending) . "\n");
|