项目概述
本项目实现数据库迁移执行器:按数字前缀排序应用 SQL 迁移且只应用一次,记录每个文件的校验和,并在已应用文件被改动时检测漂移。执行器刻意保持简单——纯 SQL 文件、一张账本表与一把锁——因为迁移触及每个环境,其失败代价最高。
架构与关键决策
发现文件、按前缀排序并对账账本得出待执行清单;账本保存 id、校验和与应用时间,是执行器唯一信任的状态。应用前重算所有已应用文件的校验和,一旦不符即中止,避免把一个漂移叠加到新迁移上。每个待执行迁移在事务中应用并同事务写入账本行,失败则两者一起回滚;由顾问锁串行化并发部署,使账本检查与应用不构成竞态。
实施与测试
对全新数据库应用一组迁移并断言账本正确,再次运行断言不重复应用;改动已应用文件断言检测到漂移并中止;模拟失败迁移断言不留部分改动与账本行;并发运行两个执行器断言只有一个生效。
限制与后续
- 只应用纯 SQL,不生成或比较 schema。
- 回滚依赖手写 down 脚本,缺失时拒绝执行。
- 事务性 DDL 依赖具体数据库引擎,非事务引擎仍可能留部分状态。
代码示例
pnpm init
pnpm add pg
pnpm add -D typescript tsx vitest
migration-runner/
├── bin/migrate.mjs
├── src/plan.mjs
├── src/ledger.mjs
├── src/apply.mjs
├── migrations/0001_init.sql
├── tests/migrate.test.ts
└── package.json
const plan = files.sort((a, b) => num(a) - num(b));
if (hasDuplicateIds(plan)) throw new Error('duplicate migration id');
CREATE TABLE IF NOT EXISTS schema_migrations (
id text PRIMARY KEY, checksum text NOT NULL, applied_at timestamptz DEFAULT now());
if (applied.get(id) && applied.get(id) !== checksum(file)) throw new Error('checksum drift: ' + id);
await client.query('BEGIN');
await client.query(sql);
await client.query('INSERT INTO schema_migrations(id, checksum) VALUES ($1,$2)', [id, hash]);
await client.query('COMMIT');
await client.query('BEGIN'); await client.query(downSql); await client.query('DELETE FROM schema_migrations WHERE id = $1', [id]); await client.query('COMMIT');
pnpm vitest run