chore(db): 压缩 Flyway 迁移链为单基线

原 V1–V10 的净结构全部合并进 V1__flight_state_baseline.sql,迁移链收敛为一条。
表集合、逐列类型/非空/默认值、表级约束、索引与种子行经重放比对与旧链条一致;
仅丢弃 5 条只对既有库有意义的存量数据语句(RECEIVED_AT/ENQUEUED_AT 回填、
schd 与开放请求去重、SENT_AT 回写)。

已按旧链迁移过的库版本链与校验和对不上,必须重建 schema 或删除数据卷后重跑,
禁止手工 repair 或改写 flyway_schema_history。

- 删除 V2–V10 共 9 个迁移文件
- 注释统一范式并改用稳定 ID(INV-x/C-x/G-x),修正 KAFKA_MSG/KAFKA_SCHD 与实际值不符
- 同步 docs/reference.md、docs/user-stories.md、oracle11g/README.md、README、application.yml、compose.yaml
- FlywayMigrationTest 改为断言单基线,并补 MSG_EVENT.SENT_AT 覆盖
This commit is contained in:
windyboy
2026-09-13 16:31:14 +08:00
parent 62f61ad9e4
commit 800d7617f2
17 changed files with 163 additions and 356 deletions
@@ -10,9 +10,10 @@ import org.junit.jupiter.api.Test
import java.sql.DriverManager
/**
* 在真实 PostgreSQL 上跑一遍迁移,确认结果符合预期:
* V1 基线、V2 信箱生命周期和 V3 稳定处理起点都能成功执行,迁移记录显示成功,该建的表和单行种子
* PIPELINE_LOCK、INBOX_CURSOR)都在,回填相关字段进了 PROC_STATE、BACKFILL_TODO 已下线。
* 在真实 PostgreSQL 上跑一遍迁移,确认结果符合预期:单基线 `V1__flight_state_baseline.sql`
* 执行成功且是唯一的迁移记录,该建的表和单行种子(PIPELINE_LOCK、INBOX_CURSOR)都在,
* 回填事实与收报水位都落在基线里,`BACKFILL_TODO`、`idx_evt_flid`、`PROC_STATE` 的处理开始
* 时间列都不复存在——这些正是原 V2–V10 合并后的净结构。
*
* 没有可用的 PostgreSQL 时跳过(不假装通过)。
*/
@@ -41,7 +42,7 @@ class FlywayMigrationTest {
DriverManager.getConnection(url, user, pass).use { conn ->
conn.createStatement().use { stmt ->
// 迁移记录:V1 基线 + V2 信箱生命周期 + V3 稳定处理起点 + V4 回填闭环 + V5 切流播种
// 单基线:原 V1–V10 已合并为一条迁移,历史里只应留下 V1
stmt.executeQuery(
"SELECT version, script, success FROM flyway_schema_history ORDER BY installed_rank ASC",
).use { rs ->
@@ -49,29 +50,13 @@ class FlywayMigrationTest {
while (rs.next()) {
records.add(Triple(rs.getString("version"), rs.getString("script"), rs.getBoolean("success")))
}
assertTrue(records.size >= 9, "flyway_schema_history must record all migrations")
assertEquals(1, records.size, "迁移链必须收敛为单基线,不得再有 V2+ 条目")
assertEquals("1", records[0].first)
assertEquals("V1__flight_state_baseline.sql", records[0].second)
assertEquals("2", records[1].first)
assertEquals("V2__inbox_lifecycle.sql", records[1].second)
assertEquals("3", records[2].first)
assertEquals("V3__stable_processing_start.sql", records[2].second)
assertEquals("4", records[3].first)
assertEquals("V4__backfill_closure.sql", records[3].second)
assertEquals("5", records[4].first)
assertEquals("V5__cutover_seed.sql", records[4].second)
assertEquals("6", records[5].first)
assertEquals("V6__enqueued_at.sql", records[5].second)
assertEquals("7", records[6].first)
assertEquals("V7__drop_processing_started_at.sql", records[6].second)
assertEquals("8", records[7].first)
assertEquals("V8__req_track_open_unique.sql", records[7].second)
assertEquals("9", records[8].first)
assertEquals("V9__schd_single_row.sql", records[8].second)
assertTrue(records.all { it.third })
}
// 全表就绪(V2 后 backfill_todo 下线
// 全表就绪(回填事实并入 PROC_STATE,基线不再建 BACKFILL_TODO
stmt.executeQuery(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",
).use { rs ->
@@ -110,20 +95,20 @@ class FlywayMigrationTest {
)
}
// V7PROC_STATE 不再保留处理开始时间(无判据消费
// 基线不含处理开始时间(原 V3 引入、V7 删除,合并后不得残留
stmt.executeQuery(
"SELECT count(*) FROM information_schema.columns WHERE table_name = 'proc_state' " +
"AND column_name = 'processing_started_at'",
).use { rs ->
assertTrue(rs.next())
assertEquals(0, rs.getInt(1), "V7 必须已删除 PROCESSING_STARTED_AT")
assertEquals(0, rs.getInt(1), "基线必须不含 PROCESSING_STARTED_AT")
}
// V8:REQ_TRACK 开放态部分唯一约束(仅 PENDING/SENT;关闭态可并存)
// 基线:REQ_TRACK 开放态部分唯一约束(仅 PENDING/SENT;关闭态可并存)
stmt.executeQuery(
"SELECT indexdef FROM pg_indexes WHERE tablename = 'req_track' AND indexname = 'uq_req_open'",
).use { rs ->
assertTrue(rs.next(), "V8 必须建立开放态唯一索引 uq_req_open")
assertTrue(rs.next(), "基线必须建立开放态唯一索引 uq_req_open")
val indexDef = rs.getString(1)
assertTrue(indexDef.contains("UNIQUE"), "uq_req_open 必须是唯一索引:$indexDef")
assertTrue(indexDef.contains("WHERE"), "uq_req_open 必须是仅约束开放态的部分索引:$indexDef")
@@ -150,11 +135,11 @@ class FlywayMigrationTest {
"VALUES ('RQFD-NONE', DATE '2026-09-12', 'RMS', 'PENDING', now())",
)
// V9KAFKA:schd 每个 FLID 单行(部分唯一索引),KAFKA:msg 仍可多行
// 基线KAFKA:schd 每个 FLID 单行(部分唯一索引),KAFKA:msg 仍可多行
stmt.executeQuery(
"SELECT indexdef FROM pg_indexes WHERE tablename = 'msg_event' AND indexname = 'uq_schd_event'",
).use { rs ->
assertTrue(rs.next(), "V9 必须建立 schd 单行唯一索引 uq_schd_event")
assertTrue(rs.next(), "基线必须建立 schd 单行唯一索引 uq_schd_event")
val indexDef = rs.getString(1)
assertTrue(indexDef.contains("UNIQUE"), "uq_schd_event 必须是唯一索引:$indexDef")
assertTrue(indexDef.contains("WHERE"), "uq_schd_event 必须是仅约束 schd 的部分索引:$indexDef")
@@ -181,15 +166,24 @@ class FlywayMigrationTest {
"SELECT count(*) FROM pg_indexes WHERE tablename = 'msg_event' AND indexname = 'idx_evt_flid'",
).use { rs ->
assertTrue(rs.next())
assertEquals(0, rs.getInt(1), "V9 必须删除已无消费者的 idx_evt_flid")
assertEquals(0, rs.getInt(1), "基线必须不含已无消费者的 idx_evt_flid")
}
// V6:入队时间是超期判据 R 的比较对象,必须非空(received_at 则允许为 NULL
// 基线:投递确认时间 SENT_AT 是保留期判定的唯一基准
stmt.executeQuery(
"SELECT count(*) FROM information_schema.columns WHERE table_name = 'msg_event' " +
"AND column_name = 'sent_at'",
).use { rs ->
assertTrue(rs.next())
assertEquals(1, rs.getInt(1), "基线必须含 MSG_EVENT.SENT_AT")
}
// 基线:入队时间是超期判据 R 的比较对象,必须非空(received_at 则允许为 NULL
stmt.executeQuery(
"SELECT is_nullable FROM information_schema.columns " +
"WHERE table_name = 'proc_state' AND column_name = 'enqueued_at'",
).use { rs ->
assertTrue(rs.next(), "V6 必须已加上 ENQUEUED_AT 列")
assertTrue(rs.next(), "基线必须已加上 ENQUEUED_AT 列")
assertEquals("NO", rs.getString("is_nullable"), "ENQUEUED_AT 必须非空")
}