Files

89 lines
4.3 KiB
Python
Raw Permalink Normal View History

"""撤下配置发布的"复核人必须不同于创建人"数据库约束(幂等收敛)。
修订原因(产品决策,非技术缺陷):`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})")