db-migration-safe 是做什么的?
让 SQL Schema 迁移变得安全。该技能能够检测迁移中的锁/阻塞风险,将不安全的 DDL 重写为零停机多步骤迁移,生成回滚脚本,并在计划 → 验证 → 执行的工作流后进行实际执行门控。
Postgres 是强确定性的路径。MySQL 是一种诚实的混合方案(参见 references/mysql-catalog.md)—— 实质较弱,并已明确标注。
该技能外部调用了 squawk、eugene 等检查工具以及 gh-ost/pt-osc 等 OSC 工具。它不附带这些工具。如果缺少它们,请按照 references/tool-setup.md 进行安装。静态分析(squawk + eugene lint)不需要数据库;eugene trace 和 EXECUTE 则需要一个 Postgres。
何时使用 / 何时不使用
使用于任何 DDL:ALTER TABLE、CREATE INDEX、ADD/DROP COLUMN、类型变更、约束(NOT NULL、CHECK、UNIQUE、外键)、重命名和数据回填 —— 无论是手写 SQL 还是 ORM 生成的。不要用于纯 SELECT/DML 报告查询,或用于拥有自己应用步骤的声明式 Schema 引擎(例如 Atlas)—— 它们有自己的安全路径。
安全契约(请先阅读)
- PLAN 和 VALIDATE 永远不会接触用户的数据库。它们只是分析阶段,可以自由运行。
eugene trace针对一个临时的、用完即弃的 Postgres(或可丢弃的克隆)运行迁移,默认会回滚。它永远不会接触生产环境。- EXECUTE 将真实的 DDL 应用到真实数据库。除非用户明确要求执行/运行/部署迁移,否则永远不要运行 EXECUTE。 始终先进行 dry-run,始终包裹在
lock_timeout中,永远不要在事务内运行CONCURRENTLY。EXECUTE 是用户驱动的程序化指导——没有自动执行脚本。
关于自动调用的说明:这是一个单一技能,所以其前置元数据控制整个文件。PLAN/VALIDATE 是安全的自动触发操作,因此模型调用保持启用。EXECUTE 阶段通过上述规则行为上进行门控。如果您希望整个技能仅由用户触发,请在前置元数据中添加
disable-model-invocation: true——这样即使分析阶段也需要显式调用。
PLAN
对迁移文件运行静态分析器。不需要数据库。
python3 scripts/analyze.py path/to/migration.sql --dialect postgres
# 添加 --pg-version 13.0 以限制版本特定规则
# 添加 --json 以输出机器可读结果(CI 中使用)
analyze.py 运行 squawk 和 eugene lint,将两者规范化为一个综合裁决,如果存在任何错误级别的发现,则以非零退出码退出(因此它也可作为 CI 门控)。对于 --dialect mysql,它会应用内置的 InnoDB Online DDL 启发式分析替代(目前没有免费的静态 MySQL 锁检查工具;参见 references/mysql-catalog.md)。
阅读合并后的裁决:
- 发现项 —— 每项都有一个
source(squawk/eugene/heuristic)、规则 ID、level(error/warning)、行号、消息和解决方法。 - 解释 —— 通过
references/squawk-rules.md和references/eugene-hints.md将规则 ID 映射为通俗含义。 - 表大小 —— 静态检查工具无法区分 100 行和 10 万行。在判断一个“慢”操作是否真的是问题之前,使用
scripts/table_size.sql探测实际大小(需要数据库访问):psql "$DATABASE_URL" -v tablename=public.orders -f scripts/table_size.sql - 升级到 trace —— 当裁决涉及可能的表/索引重写(E5、E10)、没有覆盖索引的外键(E15)或任何实际持续时间取决于行数的锁时。
trace会观察实际的锁模式及其持续时长:python3 scripts/trace.py path/to/migration.sql # 临时服务器 python3 scripts/trace.py path/to/migration.sql --host db.staging --port 5432 \ --user app --database app # 可丢弃的克隆
然后为用户生成三个产物:
- 一个风险报告(裁决,通俗语言),
- 一个安全重写(下一节),以及
- 一个自动生成的回滚(
scripts/gen_rollback.py)。
安全重写
对于每个错误/警告发现项,将不安全的语句替换为目录中收录的零停机重写方案。完整的 11 种操作 → 重写表见 references/postgres-catalog.md(MySQL:references/mysql-catalog.md)。几乎所有重写背后的两个 Postgres 原语:
CONCURRENTLY—— 在无ACCESS EXCLUSIVE写锁的情况下构建/删除索引(必须在事务外运行)。NOT VALID→VALIDATE CONSTRAINT—— 分两阶段添加约束,使昂贵的全表验证使用较弱的SHARE UPDATE EXCLUSIVE锁,而不是阻塞写入。
快速参考(具体的多步骤 SQL 及 PG 版本差异详见目录):
| 不安全 | 安全重写 |
|---|---|
CREATE INDEX | CREATE INDEX CONCURRENTLY(在事务外) |
ADD COLUMN ... NOT NULL [DEFAULT] | 可空列 → 设置默认值 → 批量回填 → 通过 CHECK 实现 NOT NULL |
SET NOT NULL | ADD CHECK (c IS NOT NULL) NOT VALID → VALIDATE → SET NOT NULL → 删除检查 |
ADD FOREIGN KEY | ... NOT VALID → VALIDATE CONSTRAINT |
ADD CHECK | ... CHECK (...) NOT VALID → VALIDATE CONSTRAINT |
ADD UNIQUE | CREATE UNIQUE INDEX CONCURRENTLY → ADD CONSTRAINT ... USING INDEX |
ALTER COLUMN ... TYPE | 扩展-收缩(新列 → 双写 → 回填 → 交换 → 删除) |
| 重命名列/表 | 扩展-收缩,或兼容性 VIEW |
回填 UPDATE | 短事务中的有界批次,带节流 |
将重写后的语句写入新文件(例如 migration.safe.sql),以便可以重新验证。
回滚
从(重写后的)DDL 生成反向迁移:
python3 scripts/gen_rollback.py migration.safe.sql > migration.rollback.sql
gen_rollback.py 为每个可识别的目录操作发出逆操作(ADD COLUMN→DROP COLUMN, CREATE INDEX CONCURRENTLY→DROP INDEX CONCURRENTLY, …)。检查输出:
- 不可逆操作(
DROP COLUMN、DROP TABLE、类型缩小)无法仅从 DDL 恢复。生成器会标记它们,并发出备份表 SQL(正向迁移必须先运行该 SQL:<table>_<col>_backup_<date>)以及尽力而为的恢复。它绝不会发出一个静默错误的逆操作。 - 未知语句形状会产生显式的
manual rollback required: <reason>注释,而不是猜测。
VALIDATE
这是门控步骤。在重写后的迁移上重新运行分析器:
python3 scripts/analyze.py migration.safe.sql --dialect postgres
仅当它通过零错误级别发现项时才可以继续(警告可通过 .squawk.toml / -- eugene: ignore 配置)。如果仍然标记错误,请返回到安全重写阶段并循环。如果重写改变了获取的锁,请重新运行 trace。
EXECUTE(门控 —— 仅用户触发)
除非用户明确要求执行迁移,否则不要进入此阶段。
使用锁定防护措施执行已验证的迁移。先进行 dry-run。
- 限制每个锁。 包裹阻塞语句,使其快速失败,而不是在长事务后排队并阻塞所有流量:
SET lock_timeout = '5s'; SET statement_timeout = '0'; -- 或针对特定操作设置合理上限 CONCURRENTLY在事务外运行。 不要将CREATE/DROP INDEX CONCURRENTLY放在BEGIN/COMMIT块或事务性迁移运行器中。并发构建失败后,在重试前删除残留的INVALID索引。- 两阶段约束。 先运行
NOT VALID步骤,提交,然后在单独的语句中执行VALIDATE CONSTRAINT。 - 分批回填。 在独立的短事务中以有界块进行更新,批次之间短暂休眠,以便复制和自动清理跟上:
-- 重复直到影响 0 行 UPDATE t SET col = expr WHERE id IN (SELECT id FROM t WHERE col IS NULL ORDER BY id LIMIT 5000) - 最后对整个序列进行 dry-run:通过
scripts/trace.py对临时/克隆环境运行,确认passed_all_checks,然后应用到目标。
对于 MySQL,强制使用 ALGORITHM=…, LOCK=…,这样服务器会在静默回退到 COPY 重建时报错,并将大重写委托给 gh-ost/pt-osc——参见 references/mysql-catalog.md。
参考索引
references/postgres-catalog.md—— 11 种不安全操作 → 安全重写(核心知识)。references/mysql-catalog.md—— InnoDB Online DDL 矩阵 + OSC 委托(较弱路径)。references/squawk-rules.md—— squawk 规则 ID 及其捕获内容。references/eugene-hints.md—— eugene E1–E15 / W12–W14 及其解决方法。references/tool-setup.md—— 安装 squawk / eugene / gh-ost;临时 Postgres。
脚本
scripts/analyze.py—— 编排 squawk + eugene lint(或 MySQL 启发式)→ 一个综合裁决。存在错误级别发现时以非零退出。scripts/trace.py—— 运行eugene trace(临时服务器或克隆)→ 真实锁报告。scripts/gen_rollback.py—— 生成反向迁移;标记不可逆操作。scripts/table_size.sql——pg_class.reltuples大小探测。
所有脚本仅依赖 Python 3 标准库 —— 无需 pip install。