fix(dmig): DROP INDEX 被 DROP COLUMN 守卫误解析致静默跳过 + m0038 唯一索引修正

dmig.py 两处(2026-09-17 m0038 部署预演时本地测试抓到):
① `ALTER TABLE t DROP INDEX idx` 会命中 DROP COLUMN 守卫的可选 `(COLUMN\s+)?`
   分支,group(3) 捕获到字面词 'INDEX' 而非索引名 → column_exists(t,'INDEX')
   恒 False → 整步被静默跳过(索引删不掉、无任何报错)。修: DROP INDEX 守卫
   (ALTER 形态 + 独立 `DROP INDEX idx ON t` 形态,历史迁移 m0025/m0031 等
   13 条 down 段用的就是独立形态、此前同样无守卫)放在 DROP COLUMN 之前,
   幂等判据用 index_exists。
② DROP COLUMN 正则收紧为强制 COLUMN 关键字后列名组号 3→2,调用点同步
   (历史 22 条 DROP COLUMN 全部显式带 COLUMN、bare 形态 0 条,无回归)。
本地守卫测试 18/18 全绿(含 m0038 up/down/重复down 完整生命周期)。

m0038: m0037 把 models 声明的 uk_org_user_name(unique) 误写成普通 CREATE
INDEX → 实测建出 KEY 而非 UNIQUE KEY,失去并发去重保护(同名可插重复行、
save_secret 幂等判定失效)。本迁移删误建索引并按 models JSON 权威定义重建
UNIQUE。backup_tables 照旧。不改已应用的 m0037(台账完整性)。
This commit is contained in:
yumoqing 2026-09-17 16:02:33 +08:00
parent 9d668843f1
commit 66996c693d
2 changed files with 54 additions and 5 deletions

View File

@ -168,14 +168,38 @@ def render_step(conf, step, direction):
if m:
if column_exists(conf, m.group(1), m.group(3)):
return [], "列已存在: %s.%s" % (m.group(1), m.group(3))
# DROP COLUMN 幂等守卫:列已不存在则跳过(生产表由 create_tables.py 按最新
# models 建出、天然没有旧列m0009/m0010 类删列迁移直跑会 1091 中止)
m = re.match(r"ALTER\s+TABLE\s+`?(\w+)`?\s+DROP\s+(COLUMN\s+)?`?(\w+)`?", sql, re.I)
# DROP INDEX 幂等守卫2026-09-17 m0038 部署实抓的解析缺陷):
# 必须放在 DROP COLUMN 守卫**之前**——后者的可选 `(COLUMN\s+)?` 分支对
# `ALTER TABLE t DROP INDEX idx` 同样匹配,且 group(3) 捕获到的是字面词
# 'INDEX' 而非索引名 → column_exists(t,'INDEX') 恒 False → 整步被静默跳过
# (索引永远删不掉、也不报错)。删索引的正确幂等判据是 index_exists。
m = re.match(r"ALTER\s+TABLE\s+`?(\w+)`?\s+DROP\s+INDEX\s+`?(\w+)`?", sql, re.I)
if m:
if not table_exists(conf, m.group(1)):
return [], "表不存在: %s" % m.group(1)
if not column_exists(conf, m.group(1), m.group(3)):
return [], "列已不存在: %s.%s" % (m.group(1), m.group(3))
if not index_exists(conf, m.group(1), m.group(2)):
return [], "索引已不存在: %s.%s" % (m.group(1), m.group(2))
# 独立形态 DROP INDEX idx ON tableMySQL/MariaDB 同样合法,历史迁移
# m0025/m0031 等 13 条 down 段用的就是它)——同款幂等守卫,否则重复
# rollback 会 1091 中止。
m = re.match(r"DROP\s+INDEX\s+`?(\w+)`?\s+ON\s+`?(\w+)`?", sql, re.I)
if m:
if not table_exists(conf, m.group(2)):
return [], "表不存在: %s" % m.group(2)
if not index_exists(conf, m.group(2), m.group(1)):
return [], "索引已不存在: %s.%s" % (m.group(2), m.group(1))
# DROP COLUMN 幂等守卫:列已不存在则跳过(生产表由 create_tables.py 按最新
# models 建出、天然没有旧列m0009/m0010 类删列迁移直跑会 1091 中止)。
# ⚠️ 正则组数是 2table, col——原写法带可选 `(COLUMN\s+)?` 组时列名是
# group(3),收紧为强制 COLUMN 关键字后列名变成 group(2),索引必须同步改,
# 否则 IndexError2026-09-17 本地测试抓到)。历史迁移 22 条 DROP COLUMN
# 全部显式带 COLUMN 关键字、bare 形态 0 条,收紧无回归。
m = re.match(r"ALTER\s+TABLE\s+`?(\w+)`?\s+DROP\s+COLUMN\s+`?(\w+)`?", sql, re.I)
if m:
if not table_exists(conf, m.group(1)):
return [], "表不存在: %s" % m.group(1)
if not column_exists(conf, m.group(1), m.group(2)):
return [], "列已不存在: %s.%s" % (m.group(1), m.group(2))
m = re.match(r"CREATE\s+(UNIQUE\s+)?INDEX\s+`?(\w+)`?\s+ON\s+`?(\w+)`?", sql, re.I)
if m:
if index_exists(conf, m.group(3), m.group(2)):

View File

@ -0,0 +1,25 @@
{
"id": "m0038",
"title": "修正 pipeline_user_secrets 唯一索引(2026-09-17): m0037 把 models 声明的 uk_org_user_name(unique) 误写成普通 CREATE INDEX → 实测建出 KEY 而非 UNIQUE KEY失去并发去重保护(同名可插重复行、幂等判定失效)。本迁移删除误建的普通索引并按 models JSON 权威定义重建为 UNIQUE。DROP INDEX 用不带 IF EXISTS 的形态(MariaDB 10.6 对 DROP INDEX IF EXISTS 支持不确定,不赌语法)dmig 的 index_exists 守卫已覆盖幂等",
"backup_tables": ["pipeline_user_secrets"],
"up": [
{
"op": "sql",
"sql": "ALTER TABLE pipeline_user_secrets DROP INDEX pipeline_user_secrets_idx_org_user_name"
},
{
"op": "sql",
"sql": "CREATE UNIQUE INDEX uk_org_user_name ON pipeline_user_secrets(org_id, user_id, name)"
}
],
"down": [
{
"op": "sql",
"sql": "ALTER TABLE pipeline_user_secrets DROP INDEX uk_org_user_name"
},
{
"op": "sql",
"sql": "CREATE INDEX pipeline_user_secrets_idx_org_user_name ON pipeline_user_secrets(org_id, user_id, name)"
}
]
}