"""修 `account_events.source_ref` 的可空性(模型与迁移文件早已是 NULL,但库里不是) ## 问题 `source_ref` 是**可选**的幂等键。设计上明确可空(模型与迁移 `0041` 都写着 `nullable=True`,注释也说「MySQL 允许多个 NULL,便于『没有幂等键』的事件共存」)。 `emit()` 对**没有天然幂等键**的事件(状态变迁 / 分配 / 标签 / 备注 / CK 清除) 就是传 `None` 的。 但有的库里这一列是 **NOT NULL** —— 原因是我早期把迁移从 `20261002_0040` 重编号成 `20261003_0041` 并顺手修正了可空性,而**已经用旧版本建过表的库不会跟着改** (Alembic 只记录版本号,不会重放已应用过的脚本)。 ## 危害:静默丢事件 `emit()` 按设计**吞掉任何异常**(事件是旁路,不能影响主流程),只记一条 warning。 于是这些事件在那种库上**一条都写不进去**,而业务完全正常、没有任何报错: - `status_changed`(S3 生命周期状态变迁) - `assigned` / `tag_changed` / `remark_changed`(S4 人工操作) - `ck_cleared`(S4 清 CK) 而 `ck_invalid` / `relogin_*` / 业务事实等**带幂等键**的事件照常写入 —— 所以时间线看起来「有数据」,只是**恰好缺了没有幂等键的那几类**,极难发现。 ## 修法 改成可空。用 `MODIFY` 而不是重建列,**唯一索引会保留**(索引与列定义是两回事), 而且重复执行是幂等的 —— 无论那种库此前是什么状态,跑完都对齐模型。 `downgrade` 刻意什么都不做:把可空改回 NOT NULL 会让上述事件重新丢失, 而这不是「回退」该有的行为。 """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision: str = "20261004_0045" down_revision: str | None = "20261004_0044" branch_labels: str | None = None depends_on: str | None = None TABLE = "account_events" COLUMN = "source_ref" def upgrade() -> None: op.execute(sa.text(f"ALTER TABLE {TABLE} MODIFY {COLUMN} VARCHAR(96) NULL")) def downgrade() -> None: # 见模块 docstring:改回 NOT NULL 会重新丢事件,不做。 pass