2026-09-20 19:08:42 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
|
|
|
|
|
const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
|
|
|
const migrations = {
|
|
|
|
|
iam: 'deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql',
|
|
|
|
|
firstleg: 'deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql',
|
|
|
|
|
file: 'deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql',
|
2026-09-20 19:57:47 +08:00
|
|
|
fileForwardFix: 'deploy/sql/migrations/cn/baiyue_file/V2__widen_business_type.sql',
|
2026-09-20 19:08:42 +08:00
|
|
|
lastleg: 'deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql',
|
2026-09-20 19:57:47 +08:00
|
|
|
lastlegForwardFix: 'deploy/sql/migrations/overseas/baiyue_lastleg/V3__relax_valid_nullable_fields.sql',
|
2026-09-20 19:08:42 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sources() {
|
|
|
|
|
return Object.fromEntries(await Promise.all(Object.entries(migrations).map(async ([name, relative]) => (
|
|
|
|
|
[name, await readFile(path.join(repositoryRoot, relative), 'utf8')]
|
|
|
|
|
))))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('schema alignment migrations contain no destructive statements', async () => {
|
|
|
|
|
for (const [name, source] of Object.entries(await sources())) {
|
|
|
|
|
assert.doesNotMatch(source, /^\s*(?:DROP|DELETE|UPDATE|TRUNCATE)\b/im, `${name} 迁移不得删除、清理或回填数据`)
|
|
|
|
|
assert.doesNotMatch(source, /\bSIGNAL\s+SQLSTATE\b/i, `${name} 迁移不得使用 MySQL 不支持的动态 SIGNAL`)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('schema alignment migrations cover the verified drift', async () => {
|
|
|
|
|
const source = await sources()
|
|
|
|
|
for (const field of ['tenant_code', 'customer_nickname', 'original_file_name']) assert.match(source.iam, new RegExp(`\\b${field}\\b`))
|
|
|
|
|
assert.match(source.firstleg, /\bpallet_no\b/)
|
|
|
|
|
for (const table of ['file_pda_release_channel', 'file_pda_release']) assert.match(source.file, new RegExp(`\\b${table}\\b`))
|
2026-09-20 19:57:47 +08:00
|
|
|
assert.match(source.fileForwardFix, /business_type\s+varchar\(64\)/i)
|
2026-09-20 19:08:42 +08:00
|
|
|
for (const field of [
|
|
|
|
|
'actual_discard_packages', 'pda_status', 'pda_operator_id', 'pda_completed_at',
|
|
|
|
|
'quantity', 'pallet_code', 'discrepancy_reason', 'manually_completed_at',
|
|
|
|
|
]) assert.match(source.lastleg, new RegExp(`\\b${field}\\b`))
|
2026-09-20 19:57:47 +08:00
|
|
|
for (const field of ['inventory_position_id', 'new_sku_code', 'source_warehouse_id']) {
|
|
|
|
|
assert.match(source.lastlegForwardFix, new RegExp(`\\b${field}\\b`))
|
|
|
|
|
}
|
2026-09-20 19:08:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('constraint tightening is guarded before any alter table', async () => {
|
|
|
|
|
for (const [name, source] of Object.entries(await sources())) {
|
|
|
|
|
const firstAlter = source.search(/\bALTER TABLE\b/i)
|
|
|
|
|
const firstGuard = source.search(/SET @guard\s*=\s*IF/i)
|
|
|
|
|
if (name === 'file' || name === 'iam' || name === 'firstleg' || name === 'lastleg') {
|
|
|
|
|
assert.ok(firstGuard >= 0 && firstGuard < firstAlter, `${name} 必须先验证存量数据再执行 DDL`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|