db-migration-safe 是做什麼的?
讓 SQL 結構變更遷移變得更安全。此技能能偵測遷移中的鎖定/阻塞風險,將不安全的 DDL 改寫為零停機的多步驟遷移,產生回退腳本,並將實際執行流程閘控在「規劃 → 驗證 → 執行」的工作流程中。
Postgres 是強確定性路徑。MySQL 為誠實的混合路徑(參閱 references/mysql-catalog.md)——實際上較弱,並已明確標示。
此技能會 shell out 至外部 lint 工具(squawk, eugene)與 OSC 工具(gh-ost/pt-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 報表查詢,或擁有自有套用步驟的宣告式結構引擎(例如 Atlas)——這些已有自己的安全路徑。
安全合約(請先閱讀)
- PLAN 和 VALIDATE 絕不觸及使用者的資料庫。它們僅進行分析,可自由執行。
eugene trace會對一個臨時拋棄式 Postgres(或一個可丟棄的複本)執行遷移並預設回滾。它絕不觸碰正式環境。- EXECUTE 會對真實資料庫套用真正的 DDL。除非使用者明確要求套用/執行/發布遷移,否則絕不執行 EXECUTE。務必先進行空跑(dry-run),務必包裝在
lock_timeout中,絕不在交易內執行CONCURRENTLY。EXECUTE 是引導使用者的程序性指導——沒有自動執行腳本。
關於自動觸發:這是一個單一技能,因此其 frontmatter 會規範整個檔案。PLAN/VALIDATE 可安全地自動觸發,因此保留模型呼叫的啟用狀態。EXECUTE 階段是由上述規則行為上閘控的。如果您更傾向讓整個技能僅由使用者觸發,請在 frontmatter 中加入
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 鎖定 lint 工具;請參閱 references/mysql-catalog.md)。
閱讀合併後的判決:
- 發現列表 — 每一項都有
source(squawk/eugene/heuristic)、規則 ID、level(error/warning)、行號、訊息及因應方式。 - 解讀 — 透過
references/squawk-rules.md與references/eugene-hints.md將規則 ID 對應為白話解釋。 - 資料表大小 — 靜態 lint 工具無法分辨 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] | 可為 Null 的資料行 → 設定預設值 → 批次回填 → 透過 CHECK 設定 NOT NULL |
SET NOT NULL | ADD CHECK (c IS NOT NULL) NOT VALID → VALIDATE → SET NOT NULL → 刪除 check |
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(<table>_<col>_backup_<date>)以及盡力還原的腳本。它絕不發出無聲錯誤的反向操作。 - 未知的陳述式形式會產生明確的
需要手動回退:<原因>註解,而非猜測。
VALIDATE
這是閘關。對改寫後的遷移重新執行分析器:
python3 scripts/analyze.py migration.safe.sql --dialect postgres
僅在零錯誤等級發現(警告可透過 .squawk.toml / -- eugene: ignore 設定)時才繼續。如果仍標記錯誤,則返回安全改寫並循環。如果改寫改變了鎖定模式,請重新執行 trace。
EXECUTE(閘控 — 僅限使用者觸發)
除非使用者明確要求套用遷移,否則不要進入此階段。
在鎖定防護下套用通過驗證的遷移。先進行空跑。
- 約束每個鎖定。 將阻塞陳述式包裝起來,使其快速失敗,而不是在長時間交易後排隊等候並阻斷所有流量:
SET lock_timeout = '5s'; SET statement_timeout = '0'; -- 或針對特定操作設定一個合理的上限 CONCURRENTLY必須在交易外執行。 不要將CREATE/DROP INDEX CONCURRENTLY放在BEGIN/COMMIT區塊或交易型遷移執行工具中。在並行建立失敗後,請先刪除殘留的INVALID索引再重試。- 兩階段限制條件。 先執行
NOT VALID步驟,讓它提交,然後在獨立的陳述式中執行VALIDATE CONSTRAINT。 - 批次回填。 在各自獨立的短交易中以有限區塊進行更新,批次之間加入短暫停頓,讓複製與 autovacuum 能跟上:
-- 重複直到影響 0 行 UPDATE t SET col = expr WHERE id IN (SELECT id FROM t WHERE col IS NULL ORDER BY id LIMIT 5000) - 空跑透過
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。