db-migration-safe은(는) 무엇을 하나요?
SQL 스키마 마이그레이션을 안전하게 만듭니다. 이 스킬은 마이그레이션에서 잠금/블로킹 위험을 감지하고, 안전하지 않은 DDL을 제로 다운타임 다단계 마이그레이션으로 재작성하며, 롤백을 생성하고, 실제 실행을 계획 → 검증 → 실행 워크플로우 뒤에 게이트합니다.
Postgres는 강력하고 결정적인 경로입니다. MySQL은 정직한 하이브리드입니다(references/mysql-catalog.md 참조) — 실질적으로 약하며, 그렇게 표시됩니다.
이 스킬은 외부 린터(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, 외래 키), 이름 변경, 백필(backfill) — 직접 작성한 SQL이든 ORM에서 생성되었든 관계없습니다. 사용하지 마십시오: 순수 SELECT/DML 보고 쿼리, 또는 자체 적용 단계를 소유하는 선언적 스키마 엔진(예: Atlas) — 해당 도구는 자체 안전 경로가 있습니다.
안전 계약 (먼저 읽으십시오)
- PLAN과 VALIDATE는 사용자 데이터베이스에 절대 접촉하지 않습니다. 분석 전용이며 자유롭게 실행될 수 있습니다.
- **
eugene trace**는 임시 폐기용 Postgres(또는 disposable clone)에 대해 마이그레이션을 실행하고 기본적으로 롤백합니다. 프로덕션에 절대 접촉하지 않습니다. - 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를 실행하고, 둘을 하나의 판정으로 정규화하며, 오류 수준의 발견이 있을 경우 0이 아닌 값으로 종료합니다(따라서 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행과 100k행을 구분할 수 없습니다.
scripts/table_size.sql로 실제 크기를 확인하십시오(데이터베이스 접근 필요):psql "$DATABASE_URL" -v tablename=public.orders -f scripts/table_size.sql - trace로 에스컬레이션 — 판정에 가능한 테이블/인덱스 재작성(E5, E10), 커버링 인덱스가 없는 FK(E15), 또는 행 수에 따라 실제 잠금 지속 시간이 달라지는 잠금이 포함된 경우.
trace는 실제 잠금 모드와 유지 시간을 관찰합니다:python3 scripts/trace.py path/to/migration.sql # 임시 temp 서버 python3 scripts/trace.py path/to/migration.sql --host db.staging --port 5432 \ --user app --database app # disposable clone
그런 다음 사용자를 위해 세 가지 산출물을 생성합니다:
- 위험 보고서(판정, 일반 언어로),
- 안전한 재작성(다음 섹션), 및
- 생성된 롤백(
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] | nullable 컬럼 → 기본값 설정 → 배치 백필 → 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>)과 최선의 복원을 출력합니다. 조용히 잘못된 역을 생성하지 않습니다. - 알 수 없는 문 형태는 추측 대신 명시적인
수동 롤백 필요: <reason>주석을 생성합니다.
VALIDATE
이것이 게이트입니다. 재작성된 마이그레이션에 대해 분석기를 다시 실행합니다:
python3 scripts/analyze.py migration.safe.sql --dialect postgres
오류 수준의 발견이 0개로 통과할 때만 진행하십시오(경고는 .squawk.toml / -- eugene: ignore로 구성 가능). 여전히 오류를 플래그하면 안전한 재작성으로 돌아가 반복하십시오. 재작성이 취해지는 잠금을 변경한 경우 trace를 다시 실행하십시오.
EXECUTE (게이트됨 — 사용자 트리거 전용)
사용자가 명시적으로 마이그레이션을 적용하도록 요청하지 않는 한 이 단계에 진입하지 마십시오.
잠금 가드레일과 함께 검증된 마이그레이션을 적용합니다. 먼저 dry-run하십시오.
- 모든 잠금에 바운드를 설정하십시오. 블로킹 문을 감싸서 긴 트랜잭션 뒤에서 대기열에 쌓이고 모든 트래픽을 지연시키는 대신 빠르게 실패하도록 만듭니다:
SET lock_timeout = '5s'; SET statement_timeout = '0'; -- 또는 특정 작업에 대한 적절한 한도 CONCURRENTLY는 트랜잭션 외부에서 실행됩니다.BEGIN/COMMIT블록이나 트랜잭션 마이그레이션 실행기에CREATE/DROP INDEX CONCURRENTLY를 넣지 마십시오. 동시 빌드가 실패한 후에는 재시도하기 전에 남은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) - 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 휴리스틱)를 조정하여 하나의 판정 생성. 오류 수준 발견 시 0이 아닌 값으로 종료.scripts/trace.py—eugene trace실행(임시 temp 서버 또는 clone) → 실제 잠금 보고서.scripts/gen_rollback.py— 역 마이그레이션 생성; 되돌릴 수 없는 작업 플래그.scripts/table_size.sql—pg_class.reltuples크기 프로브.
모든 스크립트는 Python 3 표준 라이브러리만 사용 — pip install 필요 없음.