fix(db): 归一化寄卖迁移旧命名,修复部署迁移顺序校验

This commit is contained in:
2026-11-09 14:09:42 +08:00
parent e5d6e92966
commit 6867431a72
2 changed files with 159 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { planLegacyConsignmentMigrationCleanup } from './migrate.js'
const MAIN_MIGRATIONS = [
'147_internal_sku_product_routing',
'148_fulfillment_execution_truth',
'149_finance_derived_scope',
'150_voucher_consume_order_received_scope',
'151_sales_daily_facts_on_account_channel',
]
test('迁移归一化:生产旧命名 147–152 全部清理,152 未登记时不额外删除', () => {
const stale = planLegacyConsignmentMigrationCleanup([
'001_init',
'146_voucher_consume_fallback_channel',
'147_consignment_platform',
'148_consignment_sms_logs',
'149_consignment_inventory_category_backfill',
'150_work_order_consignment_original_reward',
'151_work_order_internal_matching_deadline_guard',
'152_consignment_internal_match_cleanup',
])
assert.deepEqual(stale, [
'147_consignment_platform',
'148_consignment_sms_logs',
'149_consignment_inventory_category_backfill',
'150_work_order_consignment_original_reward',
'151_work_order_internal_matching_deadline_guard',
'152_consignment_internal_match_cleanup',
])
})
test('迁移归一化:dev 顺延后的 152–158 一并清理(含已登记的 152)', () => {
const stale = planLegacyConsignmentMigrationCleanup([
'001_init',
'146_voucher_consume_fallback_channel',
'152_consignment_platform',
'153_consignment_sms_logs',
'154_consignment_inventory_category_backfill',
'155_work_order_consignment_original_reward',
'156_work_order_internal_matching_deadline_guard',
'157_consignment_internal_match_cleanup',
'158_consignment_match_log_snapshot',
])
assert.deepEqual(stale, [
'153_consignment_sms_logs',
'154_consignment_inventory_category_backfill',
'155_work_order_consignment_original_reward',
'156_work_order_internal_matching_deadline_guard',
'157_consignment_internal_match_cleanup',
'158_consignment_match_log_snapshot',
'152_consignment_platform',
])
})
test('迁移归一化:main 147–151 与 152 都已登记时不做任何清理', () => {
const stale = planLegacyConsignmentMigrationCleanup([
'001_init',
'146_voucher_consume_fallback_channel',
...MAIN_MIGRATIONS,
'152_consignment_platform',
])
assert.deepEqual(stale, [])
})
test('迁移归一化:main 已执行、只剩旧寄卖记录时只清理旧名字', () => {
const stale = planLegacyConsignmentMigrationCleanup([
'001_init',
...MAIN_MIGRATIONS,
'147_consignment_platform',
'148_consignment_sms_logs',
])
assert.deepEqual(stale, ['147_consignment_platform', '148_consignment_sms_logs'])
})
test('迁移归一化:152 已登记但 main 147–151 缺失时,152 也要重跑', () => {
const stale = planLegacyConsignmentMigrationCleanup([
'001_init',
'146_voucher_consume_fallback_channel',
'152_consignment_platform',
])
assert.deepEqual(stale, ['152_consignment_platform'])
})
+69
View File
@@ -25,9 +25,42 @@ const MIGRATIONS_TABLE = 'schema_migrations'
const SHORT_NUMERIC_PREFIX_MESSAGE = /^Can't determine timestamp for \d+$/
const MIGRATION_FILE_PATTERN = /^(\d{3})_.+\.sql$/i
/** 合并后的寄卖迁移文件名(不含 .sql);main 147–151 保持原编号。 */
const CONSOLIDATED_CONSIGNMENT_MIGRATION = '152_consignment_platform'
const MAIN_MIGRATIONS_BEFORE_CONSIGNMENT = [
'147_internal_sku_product_routing',
'148_fulfillment_execution_truth',
'149_finance_derived_scope',
'150_voucher_consume_order_received_scope',
'151_sales_daily_facts_on_account_channel',
] as const
/**
* 历史上寄卖迁移用过两套编号,且都已被部分环境登记过:
* - 147_consignment_platform … 152_consignment_internal_match_cleanup(生产当前状态)
* - 152_consignment_platform … 158_consignment_match_log_snapshot(dev 顺延后、合并前)
* 合并后统一为 152_consignment_platform,这些旧名字必须从 schema_migrations 清掉。
*/
const LEGACY_CONSIGNMENT_MIGRATION_NAMES = [
'147_consignment_platform',
'148_consignment_sms_logs',
'149_consignment_inventory_category_backfill',
'150_work_order_consignment_original_reward',
'151_work_order_internal_matching_deadline_guard',
'152_consignment_internal_match_cleanup',
'153_consignment_match_log_snapshot',
'153_consignment_sms_logs',
'154_consignment_inventory_category_backfill',
'155_work_order_consignment_original_reward',
'156_work_order_internal_matching_deadline_guard',
'157_consignment_internal_match_cleanup',
'158_consignment_match_log_snapshot',
] as const
export async function runDatabaseMigrations() {
assertMigrationFilesOrdered()
await ensureNodePgMigrateMetadataCompatibility()
await normalizeLegacyConsignmentMigrationNames()
console.info(`[db:migrate] 迁移目录: ${MIGRATIONS_DIR}`)
@@ -129,6 +162,42 @@ function assertMigrationFilesOrdered() {
}
}
/**
* 计算需要从 schema_migrations 清理的旧寄卖迁移记录(纯函数,便于单测)。
*
* node-pg-migrate 的 checkOrder 是按位置逐项比对「已执行列表(ORDER BY run_on, id)」和
* 「本地迁移文件列表」,因此只改名/改 run_on 都不够:只要 152 已登记而 main 147–151 未登记,
* 152 就会占据第 147 个位置,和 147_internal_sku_product_routing 比对失败。
* 唯一稳妥的做法是删掉旧记录,让 147–152 按新文件顺序重新执行;152 内容幂等。
*/
export function planLegacyConsignmentMigrationCleanup(appliedNames: readonly string[]): string[] {
const applied = new Set(appliedNames)
const staleNames: string[] = LEGACY_CONSIGNMENT_MIGRATION_NAMES.filter((name) =>
applied.has(name),
)
const mainMissing = MAIN_MIGRATIONS_BEFORE_CONSIGNMENT.some((name) => !applied.has(name))
if (mainMissing && applied.has(CONSOLIDATED_CONSIGNMENT_MIGRATION)) {
staleNames.push(CONSOLIDATED_CONSIGNMENT_MIGRATION)
}
return staleNames
}
async function normalizeLegacyConsignmentMigrationNames() {
const { rows } = await query<{ name: string }>(`SELECT name FROM ${MIGRATIONS_TABLE}`)
const staleNames = planLegacyConsignmentMigrationCleanup(rows.map((row) => row.name))
if (staleNames.length === 0) {
return
}
await query(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ANY($1::text[])`, [staleNames])
console.warn(
`[db:migrate] 检测到旧寄卖迁移命名,已清理 ${staleNames.length} 条历史记录: ${staleNames.join(', ')};` +
'将按 147–152 的新顺序重新执行(152 内容幂等,不会重复建表/加列)',
)
}
async function ensureNodePgMigrateMetadataCompatibility() {
await query(`
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (