import assert from 'node:assert/strict' import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import test from 'node:test' import { buildPlan, check, createMigration, discoverMigrations, loadScopes, runManifest, } from './db-migrations.mjs' async function fixture() { const root = await mkdtemp(path.join(os.tmpdir(), 'db-migrations-')) const sqlRoot = path.join(root, 'sql') await mkdir(path.join(sqlRoot, 'migrations', 'cn', 'baiyue_auth'), { recursive: true }) await writeFile(path.join(sqlRoot, 'scopes.json'), JSON.stringify({ schemaVersion: 1, regions: { CN: { baseline: 'baseline/cn.sql', schemas: [ { name: 'baiyue_auth', services: ['auth'], order: 10 }, { name: 'baiyue_order', services: ['order'], order: 20 }, ], }, }, }), 'utf8') return { root, sqlRoot, scopesPath: path.join(sqlRoot, 'scopes.json') } } test('new 自动分配 schema 内下一个版本,并且可重复执行检查', async (t) => { const context = await fixture() t.after(() => rm(context.root, { recursive: true, force: true })) const scopes = await loadScopes(context.scopesPath) const first = await createMigration({ sqlRoot: context.sqlRoot, scopes, region: 'CN', schemaName: 'baiyue_auth', description: 'Add login audit' }) assert.equal(first.fileName, 'V1__add_login_audit.sql') const second = await createMigration({ sqlRoot: context.sqlRoot, scopes, region: 'CN', schemaName: 'baiyue_auth', description: 'add_session_index' }) assert.equal(second.fileName, 'V2__add_session_index.sql') assert.match(await readFile(path.join(context.sqlRoot, first.path), 'utf8'), /Schema: baiyue_auth/) assert.equal((await discoverMigrations(context.sqlRoot, 'CN', 'baiyue_auth')).length, 2) }) test('plan 只包含选中服务,不把同提交其他 schema 的 SQL 带入制品', async (t) => { const context = await fixture() t.after(() => rm(context.root, { recursive: true, force: true })) const scopes = await loadScopes(context.scopesPath) const created = await createMigration({ sqlRoot: context.sqlRoot, scopes, region: 'CN', schemaName: 'baiyue_auth', description: 'add_login_audit' }) const plan = await buildPlan({ sqlRoot: context.sqlRoot, scopes, regions: ['CN'], services: ['auth'], commit: 'abc1234' }) assert.equal(plan.sourceCommit, 'abc1234') assert.deepEqual(plan.regions[0].schemas.map((schema) => schema.schema), ['baiyue_auth']) assert.equal(plan.regions[0].schemas[0].target, '1') assert.equal(plan.regions[0].schemas[0].migrations[0].path, created.path) }) test('check 拒绝跨 schema 引用和跨库控制语句', async (t) => { const context = await fixture() t.after(() => rm(context.root, { recursive: true, force: true })) const scopes = await loadScopes(context.scopesPath) await writeFile(path.join(context.sqlRoot, 'migrations', 'cn', 'baiyue_auth', 'V1__bad.sql'), 'ALTER TABLE baiyue_order.t ADD c INT;\nUSE baiyue_order;\n', 'utf8') await assert.rejects(() => check({ sqlRoot: context.sqlRoot, scopes, regions: ['CN'] }), /其他 schema|跨库控制语句/) }) test('run 默认只能 dry-run,不能无意连接数据库', async (t) => { const context = await fixture() t.after(() => rm(context.root, { recursive: true, force: true })) const scopes = await loadScopes(context.scopesPath) await createMigration({ sqlRoot: context.sqlRoot, scopes, region: 'CN', schemaName: 'baiyue_auth', description: 'add_login_audit' }) const plan = await buildPlan({ sqlRoot: context.sqlRoot, scopes, regions: ['CN'], services: ['auth'] }) const manifestPath = path.join(context.root, 'release-manifest.json') await writeFile(manifestPath, JSON.stringify(plan), 'utf8') const dryRun = await runManifest({ sqlRoot: context.sqlRoot, manifestPath, dryRun: true }) assert.equal(dryRun.mode, 'dry-run') await assert.rejects(() => runManifest({ sqlRoot: context.sqlRoot, manifestPath }), /DB_MIGRATION_ALLOW/) })