| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- #!/usr/bin/env node
- /**
- * 迁移文件幂等性校验脚本
- *
- * 检查所有 Drizzle 迁移文件,确保:
- * 1. CREATE TABLE 使用 IF NOT EXISTS
- * 2. CREATE INDEX 使用 IF NOT EXISTS
- *
- * 防止迁移在重复执行或数据库状态不一致时失败
- */
- import fs from "node:fs";
- import path from "node:path";
- import { fileURLToPath } from "node:url";
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = path.dirname(__filename);
- // 迁移文件目录
- const MIGRATIONS_DIR = path.join(__dirname, "../drizzle");
- // 豁免列表:历史迁移文件(在问题修复前已发布的版本)
- // 这些文件虽然缺少 IF NOT EXISTS,但已在生产环境稳定运行
- // 不建议修改以避免引入新问题
- const EXEMPT_FILES = [
- "0000_legal_brother_voodoo.sql", // 初始迁移
- "0001_ambiguous_bromley.sql", // 历史迁移
- "0002_fancy_preak.sql", // 历史迁移
- "0003_outstanding_centennial.sql", // 历史迁移
- "0004_dazzling_starbolt.sql", // 历史迁移
- "0005_true_raza.sql", // 历史迁移
- "0006_lame_matthew_murdock.sql", // 历史迁移
- "0007_lazy_post.sql", // 历史迁移
- "0008_talented_molten_man.sql", // 历史迁移
- "0009_many_amazoness.sql", // 历史迁移
- "0010_unusual_bloodscream.sql", // 历史迁移
- "0011_charming_ben_parker.sql", // 历史迁移
- "0012_elite_iron_patriot.sql", // 历史迁移
- "0014_overconfident_mongu.sql", // 历史迁移(0013 已修复)
- ];
- // 颜色输出
- const colors = {
- reset: "\x1b[0m",
- red: "\x1b[31m",
- green: "\x1b[32m",
- yellow: "\x1b[33m",
- blue: "\x1b[34m",
- };
- function log(color, prefix, message) {
- console.log(`${color}${prefix}${colors.reset} ${message}`);
- }
- function info(message) {
- log(colors.blue, "[INFO]", message);
- }
- function warn(message) {
- log(colors.yellow, "[WARN]", message);
- }
- function error(message) {
- log(colors.red, "[ERROR]", message);
- }
- function success(message) {
- log(colors.green, "[SUCCESS]", message);
- }
- /**
- * 检查 SQL 文件的幂等性
- */
- function validateMigrationFile(filePath) {
- const fileName = path.basename(filePath);
- const content = fs.readFileSync(filePath, "utf-8");
- const lines = content.split("\n");
- const issues = [];
- // 检查 CREATE TABLE 语句
- const createTableRegex = /CREATE\s+TABLE\s+"[^"]+"/gi;
- const createTableIfNotExistsRegex = /CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+"[^"]+"/gi;
- const createTables = content.match(createTableRegex) || [];
- const createTablesIfNotExists = content.match(createTableIfNotExistsRegex) || [];
- const missingIfNotExistsTables = createTables.length - createTablesIfNotExists.length;
- if (missingIfNotExistsTables > 0) {
- createTables.forEach((match) => {
- if (!/IF\s+NOT\s+EXISTS/i.test(match)) {
- const lineNumber = lines.findIndex((line) => line.includes(match.split('"')[1])) + 1;
- issues.push({
- type: "CREATE TABLE",
- line: lineNumber,
- statement: match,
- suggestion: match.replace(/CREATE\s+TABLE\s+/i, "CREATE TABLE IF NOT EXISTS "),
- });
- }
- });
- }
- // 检查 CREATE INDEX 语句
- const createIndexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+"[^"]+"/gi;
- const createIndexIfNotExistsRegex =
- /CREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\s+"[^"]+"/gi;
- const createIndexes = content.match(createIndexRegex) || [];
- const createIndexesIfNotExists = content.match(createIndexIfNotExistsRegex) || [];
- const missingIfNotExistsIndexes = createIndexes.length - createIndexesIfNotExists.length;
- if (missingIfNotExistsIndexes > 0) {
- createIndexes.forEach((match) => {
- if (!/IF\s+NOT\s+EXISTS/i.test(match)) {
- const lineNumber = lines.findIndex((line) => line.includes(match)) + 1;
- issues.push({
- type: "CREATE INDEX",
- line: lineNumber,
- statement: match,
- suggestion: match.replace(
- /CREATE\s+(UNIQUE\s+)?INDEX\s+/i,
- "CREATE $1INDEX IF NOT EXISTS "
- ),
- });
- }
- });
- }
- return { fileName, issues };
- }
- /**
- * 主函数
- */
- function main() {
- info("开始检查迁移文件的幂等性...\n");
- // 获取所有 .sql 迁移文件
- const files = fs
- .readdirSync(MIGRATIONS_DIR)
- .filter((file) => file.endsWith(".sql") && /^\d{4}_/.test(file))
- .sort()
- .map((file) => path.join(MIGRATIONS_DIR, file));
- if (files.length === 0) {
- warn("未找到任何迁移文件");
- process.exit(0);
- }
- info(`找到 ${files.length} 个迁移文件\n`);
- let totalIssues = 0;
- const filesWithIssues = [];
- // 检查每个文件
- files.forEach((filePath) => {
- const result = validateMigrationFile(filePath);
- const isExempt = EXEMPT_FILES.includes(result.fileName);
- if (result.issues.length > 0) {
- if (isExempt) {
- // 豁免文件:只显示警告,不计入失败
- warn(`${result.fileName} - 发现 ${result.issues.length} 个问题(已豁免)`);
- return;
- }
- totalIssues += result.issues.length;
- filesWithIssues.push(result);
- error(`${result.fileName} - 发现 ${result.issues.length} 个问题:`);
- result.issues.forEach((issue, index) => {
- console.log(`\n ${index + 1}. ${issue.type} (第 ${issue.line} 行)`);
- console.log(` ${colors.red}✗${colors.reset} ${issue.statement}`);
- console.log(` ${colors.green}✓${colors.reset} ${issue.suggestion}`);
- });
- console.log("");
- }
- });
- // 输出总结
- console.log("─".repeat(60));
- if (totalIssues === 0) {
- success(`所有 ${files.length} 个迁移文件都通过了幂等性检查 ✓`);
- process.exit(0);
- } else {
- error(`检查完成: 发现 ${totalIssues} 个问题,涉及 ${filesWithIssues.length} 个文件`);
- console.log("");
- warn("建议修复上述问题以确保迁移的幂等性");
- warn("所有 CREATE TABLE 和 CREATE INDEX 语句都应该使用 IF NOT EXISTS");
- process.exit(1);
- }
- }
- // 执行
- try {
- main();
- } catch (err) {
- error(`脚本执行失败: ${err.message}`);
- console.error(err);
- process.exit(1);
- }
|