/** * 创建下一份渐进式数据库迁移文件。 * * 用法: * npm run db:migrate:create -- add_order_note * npm run db:migrate:create -- "add order note" * * 规则: * - 001_init.sql 是基线,只用于空库首次建表 * - 后续结构变更一律新增 002_xxx.sql / 003_xxx.sql ... * - 已应用到任何环境的迁移文件禁止修改内容 */ import fs from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)) const MIGRATIONS_DIR = path.join(CURRENT_DIR, '../src/db/migrations') const MIGRATION_FILE_PATTERN = /^(\d{3})_(.+)\.sql$/i function slugify(input: string): string { return input .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '_') .replace(/^_+|_+$/g, '') .replace(/_+/g, '_') } function listMigrationFiles(): string[] { if (!fs.existsSync(MIGRATIONS_DIR)) { throw new Error(`迁移目录不存在: ${MIGRATIONS_DIR}`) } return fs .readdirSync(MIGRATIONS_DIR) .filter((name) => MIGRATION_FILE_PATTERN.test(name)) .sort((a, b) => a.localeCompare(b, 'en')) } function resolveNextVersion(files: string[]): number { let maxVersion = 0 for (const file of files) { const match = file.match(MIGRATION_FILE_PATTERN) if (!match) { continue } maxVersion = Math.max(maxVersion, Number(match[1])) } return maxVersion + 1 } function buildTemplate(version: string, name: string): string { const stamp = new Date().toISOString() return `-- ${version}_${name}.sql -- 创建时间: ${stamp} -- 说明: TODO 填写本次结构变更目的 -- -- 渐进迁移约定: -- 1. 只写增量 SQL(ALTER / CREATE INDEX / 数据回填等) -- 2. 禁止修改已经提交并应用过的历史迁移文件 -- 3. 空库会按文件名顺序执行 001_init.sql → 后续增量迁移 -- 4. 线上/开发库启动时自动应用尚未记录在 schema_migrations 中的文件 -- 在此编写增量 SQL ` } function main() { const rawName = process.argv.slice(2).join(' ').trim() if (!rawName || rawName.startsWith('-')) { console.error('用法: npm run db:migrate:create -- ') console.error('示例: npm run db:migrate:create -- add_task_priority') process.exitCode = 1 return } const name = slugify(rawName) if (!name) { console.error('迁移名称无效,请使用英文/数字/下划线描述,例如 add_task_priority') process.exitCode = 1 return } const existing = listMigrationFiles() const nextVersion = resolveNextVersion(existing) if (nextVersion > 999) { console.error('迁移序号已超过 999,请调整命名策略') process.exitCode = 1 return } const version = String(nextVersion).padStart(3, '0') const fileName = `${version}_${name}.sql` const filePath = path.join(MIGRATIONS_DIR, fileName) if (fs.existsSync(filePath)) { console.error(`迁移文件已存在: ${fileName}`) process.exitCode = 1 return } const conflict = existing.find((item) => item.startsWith(`${version}_`)) if (conflict) { console.error(`迁移序号冲突: ${version} 已被 ${conflict} 占用`) process.exitCode = 1 return } fs.writeFileSync(filePath, buildTemplate(version, name), 'utf8') console.info(`[db:migrate:create] 已创建 ${path.relative(process.cwd(), filePath)}`) console.info('[db:migrate:create] 编辑 SQL 后提交;后端启动或 npm run db:migrate 会自动应用') } main()