"""撤下配置发布的"复核人必须不同于创建人"数据库约束(幂等收敛)。 修订原因(产品决策,非技术缺陷):`config_release` 上原有 `CONSTRAINT chk_config_release_separation CHECK (reviewer_id IS NULL OR reviewer_id <> created_by)`, 强制"双人复核"。当前平台是**单管理员部署**(唯一的 admin 身份是 `sys_user.id=9003`), 该约束使配置发布流程在数据库层无法走通:应用层已按决策放行自审,但提交时 MySQL 仍以 错误 3819 (`Check constraint 'chk_config_release_separation' is violated`) 拒绝,表现为 `POST /config-releases/{id}/approve` 必然失败。因此约束必须与代码同步撤下,否则保留的 是一个"永远不可能被满足"的状态。 保留了 `chk_config_release_status`:状态机取值集合未被放宽,本次只撤"复核人身份"限制。 "审核"这一流程节点本身仍然存在(`pending_review` → `approved` 仍要求调用审核接口并写入 `reviewer_id`/`reviewed_at`),取消的只是"必须由另一个人操作"。 基线合规(`AGENTS.md` 第 3/4 条):本迁移**不重命名、不删除任何表或字段,也不改变任何 已有字段的类型、可空性与业务含义**——只删除一个表级 `CHECK` 约束,且 `reviewer_id` 列 本身及其可空性完全保持原样。`docs/00-新数据库基线设计.md` 未修改(该约束本就不在基线 文档中,只存在于 `docs/02` 建表设计与旧的 `20260909_agent_platform_v31` 建表语句里)。 幂等性:先查 `information_schema.CHECK_CONSTRAINTS` 再决定是否 `ALTER`。因此 - 既有库(约束存在):撤下; - 空库重建(`baseline_schema` + `agent_platform_v31` 会先建出该约束,再到本迁移):同样撤下, 两条路径最终收敛到同一结构,重复执行也不会报 1091/3820。 不采用"直接改历史迁移 `20260909_agent_platform_v31`"的做法:已发布的迁移文件应保持其 曾被应用过的事实,收敛交给新迁移完成。 影响面:1 张表(`config_release`)、删除 1 个约束、不涉及任何数据行改写。当时表内 5 行 配置批次(其中 2 行为 `pending_review` 且 `reviewer_id IS NULL`,不违反约束)。 """ from sqlalchemy import text from sqlalchemy.engine import Connection from alembic import op revision = "20260910_drop_review_separation" down_revision = "20260909_memory_active_key" branch_labels = None depends_on = None TABLE = "config_release" CONSTRAINT = "chk_config_release_separation" SEPARATION_CLAUSE = "reviewer_id IS NULL OR reviewer_id <> created_by" def _constraint_exists(bind: Connection, table: str, name: str) -> bool: """MySQL 的 CHECK 约束在 information_schema 里挂在表上,按表名+约束名判定。""" found = bind.execute( text( """ SELECT 1 FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = :table AND CONSTRAINT_NAME = :name AND CONSTRAINT_TYPE = 'CHECK' """ ), {"table": table, "name": name}, ).first() return found is not None def upgrade() -> None: bind = op.get_bind() if _constraint_exists(bind, TABLE, CONSTRAINT): op.execute(f"ALTER TABLE `{TABLE}` DROP CHECK `{CONSTRAINT}`") def downgrade() -> None: """恢复双人复核约束。 恢复约束前必须先把"自己审自己"的既有行清掉:MySQL 在 `ADD CHECK` 时会立即校验现有 数据,只要存在一行 `reviewer_id = created_by` 就会报 3819,导致回退失败。这里把这类 行的 `reviewer_id` 置回 `NULL`(即回到"尚未复核"状态),而不是删除任何行——`created_at`、 `reviewed_at`、状态与配置内容都不动。回退会丢失"谁审的"这一条信息,这是恢复旧语义的 必然代价,仅在整体回退到本迁移之前的状态时使用。 """ bind = op.get_bind() if _constraint_exists(bind, TABLE, CONSTRAINT): return bind.execute( text( f"UPDATE `{TABLE}` SET `reviewer_id` = NULL " # noqa: S608 - 表名/列名为本模块常量 f"WHERE `reviewer_id` IS NOT NULL AND `reviewer_id` = `created_by`" ) ) op.execute(f"ALTER TABLE `{TABLE}` ADD CONSTRAINT `{CONSTRAINT}` CHECK ({SEPARATION_CLAUSE})")