validate-migrations.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/env node
  2. /**
  3. * 迁移文件幂等性校验脚本
  4. *
  5. * 检查所有 Drizzle 迁移文件,确保:
  6. * 1. CREATE TABLE 使用 IF NOT EXISTS
  7. * 2. CREATE INDEX 使用 IF NOT EXISTS
  8. *
  9. * 防止迁移在重复执行或数据库状态不一致时失败
  10. */
  11. import fs from "node:fs";
  12. import path from "node:path";
  13. import { fileURLToPath } from "node:url";
  14. const __filename = fileURLToPath(import.meta.url);
  15. const __dirname = path.dirname(__filename);
  16. // 迁移文件目录
  17. const MIGRATIONS_DIR = path.join(__dirname, "../drizzle");
  18. // 豁免列表:历史迁移文件(在问题修复前已发布的版本)
  19. // 这些文件虽然缺少 IF NOT EXISTS,但已在生产环境稳定运行
  20. // 不建议修改以避免引入新问题
  21. const EXEMPT_FILES = [
  22. "0000_legal_brother_voodoo.sql", // 初始迁移
  23. "0001_ambiguous_bromley.sql", // 历史迁移
  24. "0002_fancy_preak.sql", // 历史迁移
  25. "0003_outstanding_centennial.sql", // 历史迁移
  26. "0004_dazzling_starbolt.sql", // 历史迁移
  27. "0005_true_raza.sql", // 历史迁移
  28. "0006_lame_matthew_murdock.sql", // 历史迁移
  29. "0007_lazy_post.sql", // 历史迁移
  30. "0008_talented_molten_man.sql", // 历史迁移
  31. "0009_many_amazoness.sql", // 历史迁移
  32. "0010_unusual_bloodscream.sql", // 历史迁移
  33. "0011_charming_ben_parker.sql", // 历史迁移
  34. "0012_elite_iron_patriot.sql", // 历史迁移
  35. "0014_overconfident_mongu.sql", // 历史迁移(0013 已修复)
  36. ];
  37. // 颜色输出
  38. const colors = {
  39. reset: "\x1b[0m",
  40. red: "\x1b[31m",
  41. green: "\x1b[32m",
  42. yellow: "\x1b[33m",
  43. blue: "\x1b[34m",
  44. };
  45. function log(color, prefix, message) {
  46. console.log(`${color}${prefix}${colors.reset} ${message}`);
  47. }
  48. function info(message) {
  49. log(colors.blue, "[INFO]", message);
  50. }
  51. function warn(message) {
  52. log(colors.yellow, "[WARN]", message);
  53. }
  54. function error(message) {
  55. log(colors.red, "[ERROR]", message);
  56. }
  57. function success(message) {
  58. log(colors.green, "[SUCCESS]", message);
  59. }
  60. /**
  61. * 检查 SQL 文件的幂等性
  62. */
  63. function validateMigrationFile(filePath) {
  64. const fileName = path.basename(filePath);
  65. const content = fs.readFileSync(filePath, "utf-8");
  66. const lines = content.split("\n");
  67. const issues = [];
  68. // 检查 CREATE TABLE 语句
  69. const createTableRegex = /CREATE\s+TABLE\s+"[^"]+"/gi;
  70. const createTableIfNotExistsRegex = /CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+"[^"]+"/gi;
  71. const createTables = content.match(createTableRegex) || [];
  72. const createTablesIfNotExists = content.match(createTableIfNotExistsRegex) || [];
  73. const missingIfNotExistsTables = createTables.length - createTablesIfNotExists.length;
  74. if (missingIfNotExistsTables > 0) {
  75. createTables.forEach((match) => {
  76. if (!/IF\s+NOT\s+EXISTS/i.test(match)) {
  77. const lineNumber = lines.findIndex((line) => line.includes(match.split('"')[1])) + 1;
  78. issues.push({
  79. type: "CREATE TABLE",
  80. line: lineNumber,
  81. statement: match,
  82. suggestion: match.replace(/CREATE\s+TABLE\s+/i, "CREATE TABLE IF NOT EXISTS "),
  83. });
  84. }
  85. });
  86. }
  87. // 检查 CREATE INDEX 语句
  88. const createIndexRegex = /CREATE\s+(?:UNIQUE\s+)?INDEX\s+"[^"]+"/gi;
  89. const createIndexIfNotExistsRegex =
  90. /CREATE\s+(?:UNIQUE\s+)?INDEX\s+IF\s+NOT\s+EXISTS\s+"[^"]+"/gi;
  91. const createIndexes = content.match(createIndexRegex) || [];
  92. const createIndexesIfNotExists = content.match(createIndexIfNotExistsRegex) || [];
  93. const missingIfNotExistsIndexes = createIndexes.length - createIndexesIfNotExists.length;
  94. if (missingIfNotExistsIndexes > 0) {
  95. createIndexes.forEach((match) => {
  96. if (!/IF\s+NOT\s+EXISTS/i.test(match)) {
  97. const lineNumber = lines.findIndex((line) => line.includes(match)) + 1;
  98. issues.push({
  99. type: "CREATE INDEX",
  100. line: lineNumber,
  101. statement: match,
  102. suggestion: match.replace(
  103. /CREATE\s+(UNIQUE\s+)?INDEX\s+/i,
  104. "CREATE $1INDEX IF NOT EXISTS "
  105. ),
  106. });
  107. }
  108. });
  109. }
  110. return { fileName, issues };
  111. }
  112. /**
  113. * 主函数
  114. */
  115. function main() {
  116. info("开始检查迁移文件的幂等性...\n");
  117. // 获取所有 .sql 迁移文件
  118. const files = fs
  119. .readdirSync(MIGRATIONS_DIR)
  120. .filter((file) => file.endsWith(".sql") && /^\d{4}_/.test(file))
  121. .sort()
  122. .map((file) => path.join(MIGRATIONS_DIR, file));
  123. if (files.length === 0) {
  124. warn("未找到任何迁移文件");
  125. process.exit(0);
  126. }
  127. info(`找到 ${files.length} 个迁移文件\n`);
  128. let totalIssues = 0;
  129. const filesWithIssues = [];
  130. // 检查每个文件
  131. files.forEach((filePath) => {
  132. const result = validateMigrationFile(filePath);
  133. const isExempt = EXEMPT_FILES.includes(result.fileName);
  134. if (result.issues.length > 0) {
  135. if (isExempt) {
  136. // 豁免文件:只显示警告,不计入失败
  137. warn(`${result.fileName} - 发现 ${result.issues.length} 个问题(已豁免)`);
  138. return;
  139. }
  140. totalIssues += result.issues.length;
  141. filesWithIssues.push(result);
  142. error(`${result.fileName} - 发现 ${result.issues.length} 个问题:`);
  143. result.issues.forEach((issue, index) => {
  144. console.log(`\n ${index + 1}. ${issue.type} (第 ${issue.line} 行)`);
  145. console.log(` ${colors.red}✗${colors.reset} ${issue.statement}`);
  146. console.log(` ${colors.green}✓${colors.reset} ${issue.suggestion}`);
  147. });
  148. console.log("");
  149. }
  150. });
  151. // 输出总结
  152. console.log("─".repeat(60));
  153. if (totalIssues === 0) {
  154. success(`所有 ${files.length} 个迁移文件都通过了幂等性检查 ✓`);
  155. process.exit(0);
  156. } else {
  157. error(`检查完成: 发现 ${totalIssues} 个问题,涉及 ${filesWithIssues.length} 个文件`);
  158. console.log("");
  159. warn("建议修复上述问题以确保迁移的幂等性");
  160. warn("所有 CREATE TABLE 和 CREATE INDEX 语句都应该使用 IF NOT EXISTS");
  161. process.exit(1);
  162. }
  163. }
  164. // 执行
  165. try {
  166. main();
  167. } catch (err) {
  168. error(`脚本执行失败: ${err.message}`);
  169. console.error(err);
  170. process.exit(1);
  171. }