← [ BACK_TO_NOTES ]

语义不可逆 Schema Downgrade 的 Fail-Closed 策略

当新 schema 的业务语义无法被旧 schema 无损表示时,down migration 应拒绝降级而不是用伪默认值回填;区分“SQL 可逆”与“业务语义可逆”。

#concept #growing #backend #database #bodysense

[!info] related notes

语义不可逆 Schema Downgrade 的 Fail-Closed 策略

一句话定义

当新版本数据已经表达了旧 Schema 无法表示的业务语义时,Down Migration 不应该通过伪造默认值来“强行成功”,而应该 fail closed:拒绝降级并要求先处理不兼容数据或采用 forward-fix。

核心判断:

SQL can run backward
≠ business meaning can be restored backward

为什么普通的 up/down 心智不够

数据库迁移常被画成:

v59
  ↓ up
v60
  ↓ down
v59

这很容易让人误以为:

只要 down.sql 能执行
→ 迁移就是可逆的

但真正需要问的是:

v60 已经写入的数据,在 v59 的字段和约束里还有没有忠实表示?

如果没有,就不是单纯的 DDL 问题,而是 semantic downgrade。

BodySense 的典型例子

旧 Assessment v1 要求:

health_grade NOT NULL
value ∈ {A,B,C,D}

新 Assessment v2 明确表达:

没有临床评分 rubric
→ 不生成 health_grade
→ health_grade = NULL

这时如果降级到旧 schema,最简单的技术做法可能是:

UPDATE assessment_reports
SET health_grade = 'D'
WHERE health_grade IS NULL;

DDL 随后确实可以恢复:

ALTER COLUMN health_grade SET NOT NULL;

但业务语义被破坏了:

NULL = 系统明确没有做健康等级判断
D    = 系统做出了 D 级健康判断

它们不是同一件事。

因此“让 migration 成功”反而制造了新的伪事实。

Fail-Closed Downgrade

正确做法可以是:

IF incompatible-v2-data exists:
    refuse downgrade
ELSE:
    allow schema downgrade

BodySense 中最终采用的语义就是:

没有 v2 report
→ 60 → 59 可以回滚

已经存在 v2 report
→ down migration 明确失败
→ 不伪造 D / fake score

这同时保留:

  • 空库 migration history 的可验证性;
  • 真实数据的语义完整性。

“不可逆”到底不可逆在哪里

常见有四类。

1. 新状态比旧状态更丰富

例如:

old: status = active | inactive
new: active | inactive | rejected | superseded

旧 schema 无法忠实容纳新增状态。

2. 新版本区分了旧版本混在一起的概念

例如:

old: injury_safety
new: health_report + injury_symptoms

回滚时若重新合并,信息会丢失。

3. 新版本允许“未知/未判断”,旧版本强制一个值

例如:

new: grade = NULL  # not assessed
old: grade NOT NULL

此时默认值很容易伪造业务事实。

4. 数据已经经过不可逆转换

例如:

raw plaintext deleted after irreversible hash/tokenization

旧结构即使能建回来,也恢复不了原数据。

Down Migration 的三个选择

A. 真正无损 Down

如果新旧语义可以一一映射:

safe down

正常提供 down.sql。

B. 条件式 Fail-Closed Down

Schema 本身可以回滚,但只有在不存在新语义数据时才安全:

if incompatible rows == 0:
    down
else:
    abort

这是很多真实系统更合适的方案。

C. 明确不可逆 / Forward Fix Only

如果业务上根本不应该后退:

no down migration
or
explicit irreversible guard

通过新 migration 修复,而不是假装能恢复旧世界。

为什么“备份了就可以强行 down”仍不成立

备份可以降低数据丢失风险,但它不能让两个语义自动等价。

backup exists
≠ downgrade meaning is valid

备份解决的是:

能否恢复数据字节

Fail-closed 解决的是:

旧版本是否能正确解释这些数据

这是不同层的问题。

Migration Test 应该测什么

只测:

empty DB: up → down → up

不够。

对于 semantic downgrade,至少要再测:

A. empty/no-new-data
60 → 59 → 60
PASS

B. insert new-semantic row
60 → 59
MUST FAIL CLOSED

C. failed down
schema/data remains valid

D. no fabricated legacy values

这类测试证明的不是 SQL 语法,而是回滚语义。

与 Expand/Contract 的关系

Expand/Contract 主要解决多版本应用在发布过程中的 schema 兼容。

Fail-Closed Downgrade 解决另一件事:

当新业务语义已经真实产生后,还能不能把整个系统退回旧世界?

两者可以同时需要:

expand/contract
→ rollout compatibility

fail-closed downgrade
→ rollback semantic safety

一个设计检查表

写 down.sql 前问:

1. 新版本有没有新状态/新含义?
2. 旧 schema 能无损表示吗?
3. 需要默认值才能通过 NOT NULL 吗?
4. 这个默认值是真的业务事实,还是只是让 SQL 过?
5. 已存在新数据时回滚会不会制造错误含义?
6. 是否应该在检测到不兼容数据时直接 abort?

尤其第 4 个问题非常重要:

不要把 schema compatibility default 当成 domain truth。

常见误区

  • down.sql 能跑 = 可逆:只能证明语法/结构可执行。
  • 填一个默认值就行:默认值可能制造伪业务事实。
  • NULL 改成 D/0/false 没关系:unknown/not-assessed 与真实 D/0/false 不等价。
  • 生产必须支持任意 down:很多业务变更更安全的是 forward-fix 或条件式拒绝。
  • 备份解决所有 rollback 问题:备份不能解决语义解释不兼容。

最短记忆

Schema 回得去
≠ 业务世界回得去

如果旧版本无法忠实表达已经产生的新事实,宁可拒绝降级,也不要为了让 migration 变绿而伪造历史。

创建于 2026/9/1 更新于 2026/9/1
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本