.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");