Files
saasshop/scripts/sql_migrate.php

139 lines
4.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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");