Was macht db-migration-safe?
SQL-Schema-Migrationen sicher machen. Diese Fähigkeit erkennt Sperr-/Blockierrisiken in einer Migration, schreibt unsicheres DDL in ausfallsichere mehrschrittige Migrationen um, generiert ein Rollback und steuert die tatsächliche Ausführung über einen Plan → Prüfen → Ausführen-Workflow.
Postgres ist der starke, deterministische Pfad. MySQL ist ein ehrlicher Hybrid (siehe
references/mysql-catalog.md) — materiell schwächer und als solcher gekennzeichnet.
Diese Fähigkeit greift auf externe Linter (squawk, eugene) und OSC-Tools
(gh-ost/pt-osc) zurück. Sie bindet sie nicht ein. Fehlen sie, installiere sie gemäß
references/tool-setup.md. Statische Analyse (squawk + eugene lint) benötigt keine DB;
eugene trace und EXECUTE benötigen Postgres.
Wann verwenden / wann NICHT verwenden
Verwende für jedes DDL: ALTER TABLE, CREATE INDEX, ADD/DROP COLUMN, Typänderungen,
Constraints (NOT NULL, CHECK, UNIQUE, Fremdschlüssel), Umbenennungen und Backfills —
ob handgeschriebenes SQL oder von einem ORM generiert. Nicht für reine SELECT/DML-
Reporting-Queries oder für deklarative Schema-Engines, die ihren eigenen Anwendungsschritt
besitzen (z.B. Atlas) — diese haben ihren eigenen Sicherheitspfad.
Sicherheitsvertrag (zuerst lesen)
- PLAN und VALIDATE berühren nie die Datenbank des Benutzers. Sie sind reine Analyse und können frei ausgeführt werden.
eugene traceführt die Migration gegen eine flüchtige Wegwerf-Postgres (oder einen Wegwerf-Klon) aus und führt standardmäßig einen Rollback durch. Es berührt niemals die Produktion.- EXECUTE wendet echtes DDL auf eine echte Datenbank an. Führe EXECUTE niemals aus, es sei denn, der Benutzer bittet explizit darum, die Migration anzuwenden / auszuführen / zu versenden. Führe immer zuerst einen Trockenlauf durch, umschließe immer mit
lock_timeout, führeCONCURRENTLYniemals innerhalb einer Transaktion aus. EXECUTE ist eine verfahrenslenkende Anleitung, die der Benutzer steuert – es gibt kein Auto-Ausführ-Skript.
Hinweis zur Autoinvocation: Dies ist eine einzelne Fähigkeit, daher regelt ihr Frontmatter die gesamte Datei. PLAN/VALIDATE sind sicher als Autotrigger, daher ist die Modell-Invocation aktiviert. Die EXECUTE-Phase wird verhaltensorientiert durch die obige Regel geschützt. Falls du die gesamte Fähigkeit nur benutzergesteuert ausführen möchtest, füge
disable-model-invocation: truezum Frontmatter hinzu – dann ist auch für die Analyse eine explizite Aufforderung erforderlich.
PLAN
Führe den statischen Analyse auf der Migrationsdatei aus. Keine Datenbank erforderlich.
python3 scripts/analyze.py path/to/migration.sql --dialect postgres
# füge --pg-version 13.0 hinzu, um versionsspezifische Regeln zu aktivieren
# füge --json für maschinenlesbare Ausgabe (CI) hinzu
analyze.py führt squawk und eugene lint aus, normalisiert beide zu einem Urteil und
beendet sich mit einem Exit-Code ungleich Null, wenn ein Fehler-Finding vorliegt (dient daher
auch als CI-Gate). Für --dialect mysql werden stattdessen die integrierten InnoDB Online-DDL-
Heuristiken angewendet (es gibt keinen kostenlosen statischen MySQL-Lock-Linter; siehe
references/mysql-catalog.md).
Lese das zusammengeführte Urteil:
- Findings — jedes hat eine
source(squawk/eugene/heuristic), eine Regel-ID, einlevel(error/warning), die Zeile, die Meldung und einen Workaround. - Interpretation — ordne Regel-IDs über
references/squawk-rules.mdundreferences/eugene-hints.mdeiner verständlichen Bedeutung zu. - Tabellengröße — statische Linter können 100 Zeilen nicht von 100k unterscheiden. Überprüfe die
tatsächliche Größe mit
scripts/table_size.sql(benötigt DB-Zugriff), bevor du entscheidest, ob eine „langsame“ Operation tatsächlich ein Problem darstellt:psql "$DATABASE_URL" -v tablename=public.orders -f scripts/table_size.sql - Eskalation zum Trace, wenn das Urteil eine mögliche Tabellen-/Index-Neuschreibung (E5,
E10), einen FK ohne Abdeckungsindex (E15) oder einen Lock betrifft, dessen tatsächliche Dauer
von der Zeilenanzahl abhängt.
tracebeobachtet den tatsächlichen Lock-Modus und wie lange er gehalten wird:python3 scripts/trace.py path/to/migration.sql # flüchtiger Temp-Server python3 scripts/trace.py path/to/migration.sql --host db.staging --port 5432 \ --user app --database app # Wegwerf-Klon
Erstelle dann drei Artefakte für den Benutzer:
- einen RISIKOBERICHT (das Urteil, in verständlicher Sprache),
- eine SICHERE NEUSCHREIBUNG (nächster Abschnitt) und
- ein generiertes ROLLBACK (
scripts/gen_rollback.py).
Sichere Neuschreibungen
Ersetze für jedes Fehler-/Warnungs-Finding die unsichere Anweisung durch die katalogisierte
ausfallsichere Neuschreibung. Die vollständige Tabelle der 11 Operationen → Neuschreibungen
befindet sich in references/postgres-catalog.md (MySQL: references/mysql-catalog.md). Die beiden
Postgres-Primitive, die hinter fast jeder Neuschreibung stehen:
CONCURRENTLY— erstelle/lösche Indizes ohne einenACCESS EXCLUSIVE-Schreib-Lock (muss außerhalb einer Transaktion ausgeführt werden).NOT VALID→VALIDATE CONSTRAINT— füge eine Einschränkung in zwei Phasen hinzu, sodass die teure vollständige Tabellenvalidierung nur einen schwachenSHARE UPDATE EXCLUSIVE-Lock benötigt anstatt Schreibvorgänge zu blockieren.
Kurzreferenz (das genaue mehrschrittige SQL und die PG-Versionsnuancen findest du im Katalog):
| Unsicher | Sichere Neuschreibung |
|---|---|
CREATE INDEX | CREATE INDEX CONCURRENTLY (außerhalb einer Txn) |
ADD COLUMN ... NOT NULL [DEFAULT] | nullable Spalte → Standard setzen → Batch-Backfill → NOT NULL per CHECK |
SET NOT NULL | ADD CHECK (c IS NOT NULL) NOT VALID → VALIDATE → SET NOT NULL → Check löschen |
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 | Expand-Contract (neue Spalte → Dual-Write → Backfill → Tausch → Löschen) |
| Spalten-/Tabellenumbenennung | Expand-Contract oder Kompatibilitäts-VIEW |
Backfill UPDATE | Begrenzte Batches in kurzen Transaktionen, gedrosselt |
Schreibe die neugeschriebenen Anweisungen in eine neue Datei (z.B. migration.safe.sql), damit sie
erneut validiert werden kann.
Rollback
Generiere die umgekehrte Migration aus dem (neugeschriebenen) DDL:
python3 scripts/gen_rollback.py migration.safe.sql > migration.rollback.sql
gen_rollback.py gibt die Umkehrung jeder erkannten katalogisierten Operation aus
(ADD COLUMN→DROP COLUMN, CREATE INDEX CONCURRENTLY→DROP INDEX CONCURRENTLY, …).
Überprüfe die Ausgabe:
- Nicht umkehrbare Operationen (
DROP COLUMN,DROP TABLE, Typeinschränkung) können nicht allein aus DDL wiederhergestellt werden. Der Generator markiert sie und gibt das Backup-Tabellen-SQL aus, das die Vorwärts-Migration zuerst ausführen muss (<tabelle>_<spalte>_backup_<datum>), plus eine bestmögliche Wiederherstellung. Er gibt niemals eine stillschweigend falsche Umkehrung aus. - Unbekannte Anweisungsformen erzeugen einen expliziten Kommentar
manual rollback required: <grund>, keine Vermutung.
VALIDATE
Dies ist das Gate. Führe die Analyse erneut auf der neugeschriebenen Migration aus:
python3 scripts/analyze.py migration.safe.sql --dialect postgres
Fahre nur fort, wenn sie mit null Fehler-Level-Findings durchläuft (Warnungen sind über
.squawk.toml / -- eugene: ignore konfigurierbar). Wenn immer noch Fehler gemeldet werden, kehre
zu Sichere Neuschreibungen zurück und wiederhole den Vorgang. Führe trace erneut aus, wenn die
Neuschreibung geändert hat, welche Locks verwendet werden.
EXECUTE (geschützt — nur benutzergesteuert)
Beginne diese Phase nur, wenn der Benutzer explizit darum gebeten hat, die Migration anzuwenden.
Wende die validierte Migration mit Locking-Schutzmechanismen an. Führe zuerst einen Trockenlauf durch.
- Begrenze jeden Lock. Umschließe blockierende Anweisungen, sodass sie schnell fehlschlagen,
anstatt sich hinter einer langen Transaktion anzustellen und den gesamten Verkehr zu blockieren:
SET lock_timeout = '5s'; SET statement_timeout = '0'; -- oder eine sinnvolle Obergrenze für die spezifische Operation CONCURRENTLYläuft außerhalb einer Transaktion. SetzeCREATE/DROP INDEX CONCURRENTLYnicht in einenBEGIN/COMMIT-Block oder einen transaktionalen Migrations-Runner. Lösche nach einem fehlgeschlagenen Concurrent-Build den übriggebliebenenINVALID-Index, bevor du es erneut versuchst.- Zweiphasige Constraints. Führe den
NOT VALID-Schritt aus, lass ihn committen, und führe dannVALIDATE CONSTRAINTin einer separaten Anweisung aus. - Batch-Backfills. Aktualisiere in begrenzten Batches in separaten kurzen Transaktionen, mit
einer kurzen Pause zwischen den Batches, damit Replikation und Autovacuum mithalten können:
-- wiederholen, bis 0 Zeilen betroffen UPDATE t SET col = expr WHERE id IN (SELECT id FROM t WHERE col IS NULL ORDER BY id LIMIT 5000) - Führe einen Trockenlauf der gesamten Sequenz gegen den flüchtigen/Klon über
scripts/trace.pyein letztes Mal durch, bestätigepassed_all_checks, und wende dann auf das Ziel an.
Für MySQL erzwinge ALGORITHM=…, LOCK=…, damit der Server einen Fehler ausgibt, anstatt
stillschweigend auf einen COPY-Rebuild zurückzufallen, und delegiere große Neuschreibungen an
gh-ost/pt-osc — siehe references/mysql-catalog.md.
Referenzindex
references/postgres-catalog.md— die 11 unsicheren Operationen → sichere Neuschreibungen (Kernwissen).references/mysql-catalog.md— InnoDB Online-DDL-Matrix + OSC-Delegation (schwächerer Pfad).references/squawk-rules.md— Squawk-Regel-IDs und was jede abfängt.references/eugene-hints.md— Eugene E1–E15 / W12–W14 und ihre Workarounds.references/tool-setup.md— Installation von squawk / eugene / gh-ost; flüchtige Postgres.
Skripte
scripts/analyze.py— orchestriert squawk + eugene lint (oder MySQL-Heuristiken) → ein Urteil. Exit-Code ungleich Null bei Fehler-Level-Findings.scripts/trace.py— führteugene traceaus (flüchtiger Temp-Server oder Klon) → echter Lock-Bericht.scripts/gen_rollback.py— generiert die umgekehrte Migration; kennzeichnet nicht umkehrbare Operationen.scripts/table_size.sql—pg_class.reltuples-Größenprüfung.
Alle Skripte verwenden nur die Python 3-Standardbibliothek — kein pip install.