feat(db): add production migration chain
This commit is contained in:
@@ -3,15 +3,24 @@ import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { buildPlan, loadScopes } from '../deploy/sql/scripts/db-migrations.mjs'
|
||||
|
||||
const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||
const migrations = {
|
||||
iam: 'deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql',
|
||||
iamForwardFix: 'deploy/sql/migrations/cn/baiyue_iam/V3__align_domestic_pda_mojia_sailing_permissions.sql',
|
||||
firstleg: 'deploy/sql/migrations/cn/baiyue_firstleg/V1__align_schema_baseline.sql',
|
||||
firstlegMeasurement: 'deploy/sql/migrations/cn/baiyue_firstleg/V2__align_measurement_and_fee_schema.sql',
|
||||
firstlegCustoms: 'deploy/sql/migrations/cn/baiyue_firstleg/V3__add_customs_receipt_declaration_schema.sql',
|
||||
firstlegSailing: 'deploy/sql/migrations/cn/baiyue_firstleg/V4__add_sailing_calendar_schema.sql',
|
||||
file: 'deploy/sql/migrations/cn/baiyue_file/V1__align_schema_baseline.sql',
|
||||
fileForwardFix: 'deploy/sql/migrations/cn/baiyue_file/V2__widen_business_type.sql',
|
||||
lastleg: 'deploy/sql/migrations/overseas/baiyue_lastleg/V2__align_schema_baseline.sql',
|
||||
lastlegForwardFix: 'deploy/sql/migrations/overseas/baiyue_lastleg/V3__relax_valid_nullable_fields.sql',
|
||||
lastlegRelabel: 'deploy/sql/migrations/overseas/baiyue_lastleg/V5__allow_multiple_relabel_applications.sql',
|
||||
}
|
||||
const destructiveMigrations = {
|
||||
overseasWarehouseLocation: 'deploy/sql/migrations/overseas/baiyue_lastlag_warehouse/V2__retire_location_zone_and_rebuild_query_index.sql',
|
||||
}
|
||||
|
||||
async function sources() {
|
||||
@@ -27,10 +36,44 @@ test('schema alignment migrations contain no destructive statements', async () =
|
||||
}
|
||||
})
|
||||
|
||||
test('warehouse location migration marks its production-only destructive step', async () => {
|
||||
const source = await readFile(path.join(repositoryRoot, destructiveMigrations.overseasWarehouseLocation), 'utf8')
|
||||
assert.match(source, /DROP COLUMN zone_code/i)
|
||||
assert.match(source, /生产执行前必须单独授权 DROP COLUMN/i)
|
||||
assert.match(source, /有意的破坏性结构变更/i)
|
||||
assert.doesNotMatch(source, /^\s*(?:DROP|DELETE|UPDATE|TRUNCATE)\b/im)
|
||||
})
|
||||
|
||||
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`))
|
||||
for (const field of [
|
||||
'domestic_warehouse_id', 'idx_membership_domestic_warehouse',
|
||||
'platform:mojia-sku-sync:list', 'platform:firstleg:preload:list',
|
||||
'platform:firstleg:pda:assign', 'platform:firstleg:sailing-calendar:list',
|
||||
'platform:firstleg:sailing-volume:exception-reassign',
|
||||
]) assert.match(source.iamForwardFix, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')))
|
||||
assert.match(source.firstleg, /\bpallet_no\b/)
|
||||
for (const field of [
|
||||
'inbound_code', 'measurement_kind', 'weight_kg', 'sync_status',
|
||||
'sync_failure_code', 'sync_failure_reason', 'lingxing_sku_id', 'revision',
|
||||
'match_failure_code', 'match_failure_reason', 'carrier_rate_id',
|
||||
'carrier_rate_version', 'rate_effective_from',
|
||||
]) assert.match(source.firstlegMeasurement, new RegExp(`\\b${field}\\b`))
|
||||
for (const table of [
|
||||
'fl_customs_broker_submission', 'fl_customs_completion_action_history',
|
||||
'fl_customs_final_document', 'fl_customs_declaration_batch', 'fl_customs_clearance_result',
|
||||
]) assert.match(source.firstlegCustoms, new RegExp(`\\b${table}\\b`))
|
||||
for (const field of [
|
||||
'broker_submitted_by', 'broker_submitted_by_name', 'broker_submitted_at',
|
||||
'customer_confirm_status', 'customer_confirmed_by', 'customer_confirmed_by_name',
|
||||
'customer_confirmed_at', 'customer_reject_reason',
|
||||
]) assert.match(source.firstlegCustoms, new RegExp(`\\b${field}\\b`))
|
||||
for (const table of [
|
||||
'fl_sailing_channel', 'fl_sailing_template', 'fl_sailing', 'fl_sailing_history',
|
||||
'fl_sailing_receipt_batch_fact', 'fl_sailing_inbound_volume_forecast',
|
||||
'fl_sailing_assignment', 'fl_sailing_booking_link',
|
||||
]) assert.match(source.firstlegSailing, new RegExp(`\\b${table}\\b`))
|
||||
for (const table of ['file_pda_release_channel', 'file_pda_release']) assert.match(source.file, new RegExp(`\\b${table}\\b`))
|
||||
assert.match(source.fileForwardFix, /business_type\s+varchar\(64\)/i)
|
||||
for (const field of [
|
||||
@@ -40,14 +83,38 @@ test('schema alignment migrations cover the verified drift', async () => {
|
||||
for (const field of ['inventory_position_id', 'new_sku_code', 'source_warehouse_id']) {
|
||||
assert.match(source.lastlegForwardFix, new RegExp(`\\b${field}\\b`))
|
||||
}
|
||||
assert.match(source.lastlegRelabel, /uk_ll_relabel_application_order/i)
|
||||
assert.match(source.lastlegRelabel, /idx_ll_relabel_application_order/i)
|
||||
})
|
||||
|
||||
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') {
|
||||
if (name === 'file' || name === 'iam' || name === 'firstleg' || name === 'lastleg' || name === 'firstlegMeasurement' || name === 'firstlegCustoms') {
|
||||
assert.ok(firstGuard >= 0 && firstGuard < firstAlter, `${name} 必须先验证存量数据再执行 DDL`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('数据库迁移计划包含本批四个 schema 的正式目标版本', async () => {
|
||||
const sqlRoot = path.join(repositoryRoot, 'deploy', 'sql')
|
||||
const scopes = await loadScopes(path.join(sqlRoot, 'migration-scopes.json'))
|
||||
const plan = await buildPlan({
|
||||
sqlRoot,
|
||||
scopes,
|
||||
regions: ['CN', 'OVERSEAS'],
|
||||
services: ['iam', 'firstleg', 'lastleg-service', 'warehouse-overseas-service'],
|
||||
commit: null,
|
||||
})
|
||||
const targets = Object.fromEntries(plan.regions.flatMap((region) => region.schemas.map((schema) => [schema.schema, schema])))
|
||||
assert.equal(targets.baiyue_iam.target, '3')
|
||||
assert.equal(targets.baiyue_firstleg.target, '4')
|
||||
assert.equal(targets.baiyue_lastleg.target, '5')
|
||||
assert.equal(targets.baiyue_lastlag_warehouse.target, '2')
|
||||
assert.deepEqual(targets.baiyue_firstleg.migrations.slice(-3).map((migration) => migration.path), [
|
||||
'migrations/cn/baiyue_firstleg/V2__align_measurement_and_fee_schema.sql',
|
||||
'migrations/cn/baiyue_firstleg/V3__add_customs_receipt_declaration_schema.sql',
|
||||
'migrations/cn/baiyue_firstleg/V4__add_sailing_calendar_schema.sql',
|
||||
])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user