diff --git a/deploy/dmig.py b/deploy/dmig.py index 438854a..2b4cc1d 100755 --- a/deploy/dmig.py +++ b/deploy/dmig.py @@ -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 table(MySQL/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 中止)。 + # ⚠️ 正则组数是 2(table, col)——原写法带可选 `(COLUMN\s+)?` 组时列名是 + # group(3),收紧为强制 COLUMN 关键字后列名变成 group(2),索引必须同步改, + # 否则 IndexError(2026-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)): diff --git a/deploy/migrations/m0038_pipeline_user_secrets_unique_index.json b/deploy/migrations/m0038_pipeline_user_secrets_unique_index.json new file mode 100644 index 0000000..9d6a860 --- /dev/null +++ b/deploy/migrations/m0038_pipeline_user_secrets_unique_index.json @@ -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)" + } + ] +}