一句话说明
#3696 / #3717 把遗留的全局唯一索引改成了租户内复合唯一,迁移逻辑落在 schema sync 时就地收敛。结果是:一次 DROP + CREATE 索引的 DDL 在启动时静默发生,只留一行日志,os migrate plan 完全看不到它。想在 DDL 落库前审查的运维没有任何预检手段。
原 issue #3696 明确建议「进 os migrate 的 plan/apply」,本条是那部分未落地的残留。
现状
SqlDriver.syncTableIndexes() → dropLegacyGlobalUniques() 在 initObjects 期间执行:
DROP CONSTRAINT / DROP INDEX <table>_<col>_unique ← knex 旧命名
DROP INDEX uniq_<table>_<col> ← 旧 rebuild 路径命名
CREATE UNIQUE INDEX uniq_<table>_<tenant>_<col>
唯一的可观测信号是一行 log:
[sql-driver] dropped legacy global unique index '<name>' on '<table>' —
<table>.<col> is now unique per '<tenantField>' (#3696).
而 os migrate plan 输出为空 —— detectManagedDrift() 只做列级差异(ManagedDriftEntry 带 column 字段,ManagedDriftOp 的联合类型只有 relax_not_null / tighten_not_null / drop_column / varchar 宽窄),根本没有索引维度。
为什么当初选就地收敛(以及为什么它仍然不够)
选择的理由是实在的:没跑过 os migrate 的部署会一直坏着,而故障形态是多租户下插入被拒 —— 用户什么都没做错却写不进数据。等一个需要人主动触发的迁移,等于让这批部署无限期带病运行。
而且这次迁移是纯放松:旧的全局约束严格强于新的租户内约束,存量数据天然满足替换约束,不可能失败。这让"自动执行"变得安全。
但"安全"不等于"该隐形"。 缺的是:
- 无预检。生产 DBA 想在 DDL 执行前看一眼改什么,现在做不到。
os migrate plan 的存在意义就是这个,而它对此沉默。 - 无事后清单。跑完之后也没有结构化记录说明哪些表被改了 —— 只有散落的日志行。
- 与
os migrate 的心智模型冲突。ADR / CLI 都把 os migrate plan 呈现为"物理 schema 与元数据的差异全集"。现在它有一类差异永远不出现在里面,因为在被观察前就自愈了。
建议处置
给 ManagedDriftOp 加索引维度的 op(例如 replace_unique_index,携带 { table, dropIndexNames, createColumns }),归类为 safe(纯放松),并让:
detectManagedDrift() 把「物理上存在遗留单列 unique、而元数据要求复合」检出为一条 drift;os migrate plan 渲染它;os migrate apply 能执行它。
就地收敛保留还是移除,是个需要决定的取舍:
- 保留 + 同时可见:drift 检出与就地收敛并存。启动时仍自愈(不回退到"带病运行"),但
plan 在收敛发生前的窗口里能显示它,且新部署/新表接入时可见。缺点是 plan 里的条目可能在你去 apply 之前就已经被下次启动消化掉,读起来会困惑。 - 移除就地收敛,只走 migrate:语义干净、完全可审查,但退回原来的问题——没跑 migrate 的部署持续撞号。除非同时加一条启动时的显式告警(检出但不修,WARN 并指向
os migrate),否则不建议。
倾向后者 + 启动告警:plan 恢复成完整的差异全集,运维保有控制权,而告警确保没人在不知情的情况下带病运行。但这是个产品取舍,需要 owner 拍板。
附带发现
detectManagedDrift() 没有任何索引维度,这不只影响 unique 迁移 —— 声明式 indexes[] 的漂移(元数据声明了索引但物理库里没有、或物理库有多余索引)同样不可见。syncDeclaredIndexes 一直是只增不减、且不上报。本 issue 若要做,可以顺带把这块补齐。
关联
一句话说明
#3696 / #3717 把遗留的全局唯一索引改成了租户内复合唯一,迁移逻辑落在 schema sync 时就地收敛。结果是:一次 DROP + CREATE 索引的 DDL 在启动时静默发生,只留一行日志,
os migrate plan完全看不到它。想在 DDL 落库前审查的运维没有任何预检手段。原 issue #3696 明确建议「进
os migrate的 plan/apply」,本条是那部分未落地的残留。现状
SqlDriver.syncTableIndexes()→dropLegacyGlobalUniques()在initObjects期间执行:唯一的可观测信号是一行 log:
而
os migrate plan输出为空 ——detectManagedDrift()只做列级差异(ManagedDriftEntry带column字段,ManagedDriftOp的联合类型只有relax_not_null/tighten_not_null/drop_column/ varchar 宽窄),根本没有索引维度。为什么当初选就地收敛(以及为什么它仍然不够)
选择的理由是实在的:没跑过
os migrate的部署会一直坏着,而故障形态是多租户下插入被拒 —— 用户什么都没做错却写不进数据。等一个需要人主动触发的迁移,等于让这批部署无限期带病运行。而且这次迁移是纯放松:旧的全局约束严格强于新的租户内约束,存量数据天然满足替换约束,不可能失败。这让"自动执行"变得安全。
但"安全"不等于"该隐形"。 缺的是:
os migrate plan的存在意义就是这个,而它对此沉默。os migrate的心智模型冲突。ADR / CLI 都把os migrate plan呈现为"物理 schema 与元数据的差异全集"。现在它有一类差异永远不出现在里面,因为在被观察前就自愈了。建议处置
给
ManagedDriftOp加索引维度的 op(例如replace_unique_index,携带{ table, dropIndexNames, createColumns }),归类为safe(纯放松),并让:detectManagedDrift()把「物理上存在遗留单列 unique、而元数据要求复合」检出为一条 drift;os migrate plan渲染它;os migrate apply能执行它。就地收敛保留还是移除,是个需要决定的取舍:
plan在收敛发生前的窗口里能显示它,且新部署/新表接入时可见。缺点是 plan 里的条目可能在你去 apply 之前就已经被下次启动消化掉,读起来会困惑。os migrate),否则不建议。倾向后者 + 启动告警:
plan恢复成完整的差异全集,运维保有控制权,而告警确保没人在不知情的情况下带病运行。但这是个产品取舍,需要 owner 拍板。附带发现
detectManagedDrift()没有任何索引维度,这不只影响 unique 迁移 —— 声明式indexes[]的漂移(元数据声明了索引但物理库里没有、或物理库有多余索引)同样不可见。syncDeclaredIndexes一直是只增不减、且不上报。本 issue 若要做,可以顺带把这块补齐。关联
os migrate的 plan/apply」)