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:
+25
-31
@@ -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 {
|
||||
)
|
||||
}
|
||||
|
||||
// V7:PROC_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())",
|
||||
)
|
||||
|
||||
// V9:KAFKA: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 必须非空")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user