Que fait db-migration-safe ?
Rendez les migrations de schéma SQL sans risque. Cette compétence détecte les dangers de verrouillage/blocage dans une migration, réécrit les DDL dangereuses en migrations multi-étapes sans temps d'arrêt, génère un rollback, et verrouille l'exécution réelle derrière un workflow planifier → valider → exécuter.
Postgres est le chemin fort et déterministe. MySQL est un hybride honnête (voir references/mysql-catalog.md) — matériellement plus faible, et étiqueté comme tel.
Cette compétence utilise des appels shell aux analyseurs externes (squawk, eugene) et aux outils OSC (gh-ost/pt-osc). Elle ne les inclut pas. S'ils manquent, installez-les selon references/tool-setup.md. L'analyse statique (squawk + eugene lint) ne nécessite pas de base de données ; eugene trace et EXECUTE nécessitent un Postgres.
Quand l'utiliser / quand ne PAS l'utiliser
À utiliser pour tout DDL : ALTER TABLE, CREATE INDEX, ADD/DROP COLUMN, changements de type, contraintes (NOT NULL, CHECK, UNIQUE, clés étrangères), renommages et backfills — que le SQL soit écrit à la main ou généré par un ORM. Ne pas utiliser pour les requêtes pures SELECT/DML de rapport, ou pour les moteurs de schéma déclaratifs qui gèrent leur propre étape d'application (ex. Atlas) — ceux-ci ont leur propre voie de sécurité.
Contrat de sécurité (lire d'abord)
- PLAN et VALIDATE ne touchent jamais à la base de données de l'utilisateur. Ils sont uniquement de l'analyse et peuvent être exécutés librement.
eugene traceexécute la migration contre un Postgres éphémère jetable (ou un clone jetable) et annule par défaut. Il ne touche jamais à la production.- EXECUTE applique du vrai DDL à une vraie base de données. N'exécutez jamais EXECUTE sauf si l'utilisateur demande explicitement d'appliquer/exécuter/envoyer la migration. Faites d'abord un essai à blanc, enveloppez toujours dans
lock_timeout, n'exécutez jamaisCONCURRENTLYdans une transaction. EXECUTE est un guide procédural que l'utilisateur pilote — il n'y a pas de script d'auto-exécution.
Note sur l'auto-invocation : il s'agit d'une compétence unique, donc son frontmatter gère tout le fichier. PLAN/VALIDATE sont sûrs à déclencher automatiquement, donc l'invocation par le modèle est laissée activée. La phase EXECUTE est verrouillée comportementalement par la règle ci-dessus. Si vous préférez que toute la compétence soit déclenchée uniquement par l'utilisateur, ajoutez
disable-model-invocation: truedans le frontmatter — cela nécessitera une invocation explicite même pour l'analyse.
PLAN
Exécutez l'analyseur statique sur le fichier de migration. Aucune base de données n'est requise.
python3 scripts/analyze.py chemin/vers/migration.sql --dialect postgres
# ajoutez --pg-version 13.0 pour filtrer les règles spécifiques à une version
# ajoutez --json pour une sortie lisible par machine (CI)
analyze.py exécute squawk et eugene lint, normalise les deux en un seul verdict, et se termine avec un code non nul si une constatation de niveau erreur est présente (ce qui en fait aussi une porte CI). Pour --dialect mysql, il applique les heuristiques InnoDB Online DDL intégrées à la place (aucun analyseur de verrouillage MySQL statique gratuit n'existe ; voir references/mysql-catalog.md).
Lisez le verdict fusionné :
- Constatations — chacune a une
source(squawk/eugene/heuristic), un identifiant de règle, unniveau(error/warning), la ligne, le message et une solution de contournement. - Interprétation — mappez les identifiants de règle à leur sens simple via
references/squawk-rules.mdetreferences/eugene-hints.md. - Taille de la table — les analyseurs statiques ne peuvent pas distinguer 100 lignes de 100k. Sondez la taille réelle avec
scripts/table_size.sql(nécessite un accès à la base) avant de décider si une opération « lente » est réellement un problème :psql "$DATABASE_URL" -v tablename=public.orders -f scripts/table_size.sql - Passez à la trace lorsque le verdict implique une possible réécriture de table/index (E5, E10), une FK sans index de couverture (E15), ou tout verrou dont la durée réelle dépend du nombre de lignes.
traceobserve le mode de verrouillage réel et combien de temps il est maintenu :python3 scripts/trace.py chemin/vers/migration.sql # serveur temporaire éphémère python3 scripts/trace.py chemin/vers/migration.sql --host db.staging --port 5432 \ --user app --database app # clone jetable
Produisez ensuite trois artefacts pour l'utilisateur :
- un RAPPORT DE RISQUE (le verdict, en langage clair),
- une RÉÉCRITURE SÛRE (section suivante), et
- un ROLLBACK généré (
scripts/gen_rollback.py).
Réécritures sûres
Pour chaque constatation d'erreur/avertissement, remplacez l'instruction dangereuse par la réécriture sans temps d'arrêt cataloguée. La table complète des 11 opérations → réécritures se trouve dans references/postgres-catalog.md (MySQL : references/mysql-catalog.md). Les deux primitives Postgres derrière presque toutes les réécritures :
CONCURRENTLY— construire/supprimer des index sans verrouACCESS EXCLUSIVEd'écriture (doit être exécuté en dehors d'une transaction).NOT VALID→VALIDATE CONSTRAINT— ajouter une contrainte en deux phases afin que la validation coûteuse de toute la table prenne un verrou faibleSHARE UPDATE EXCLUSIVEau lieu de bloquer les écritures.
Référence rapide (voir le catalogue pour le SQL exact multi-étapes et les nuances de version PG) :
| Dangereux | Réécriture sûre |
|---|---|
CREATE INDEX | CREATE INDEX CONCURRENTLY (hors txn) |
ADD COLUMN ... NOT NULL [DEFAULT] | colonne nullable → définir défaut → backfill par lots → NOT NULL via CHECK |
SET NOT NULL | ADD CHECK (c IS NOT NULL) NOT VALID → VALIDATE → SET NOT NULL → drop 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 | expand-contract (nouvelle colonne → double écriture → backfill → échange → drop) |
| renommer colonne/table | expand-contract, ou VIEW de compatibilité |
backfill UPDATE | lots bornés dans des transactions courtes, limités |
Écrivez les instructions réécrites dans un nouveau fichier (par ex. migration.safe.sql) afin qu'il puisse être revalidé.
Rollback
Générez la migration inverse à partir du DDL (réécrit) :
python3 scripts/gen_rollback.py migration.safe.sql > migration.rollback.sql
gen_rollback.py émet l'inverse de chaque opération cataloguée reconnue (ADD COLUMN→DROP COLUMN, CREATE INDEX CONCURRENTLY→DROP INDEX CONCURRENTLY, …). Vérifiez la sortie :
- Les opérations irréversibles (
DROP COLUMN,DROP TABLE, rétrécissement de type) ne peuvent pas être restaurées à partir du seul DDL. Le générateur les marque et émet le SQL de sauvegarde de table que la migration forward doit exécuter en premier (<table>_<col>_backup_<date>) plus une restauration au mieux. Il n'émet jamais d'inverse silencieusement erroné. - Les formes d'instruction inconnues produisent un commentaire explicite
rollback manuel requis : <raison>, pas une supposition.
VALIDER
C'est la porte. Réexécutez l'analyseur sur la migration réécrite :
python3 scripts/analyze.py migration.safe.sql --dialect postgres
Ne procédez que lorsqu'elle passe avec zéro constatation de niveau erreur (les avertissements sont configurables via .squawk.toml / -- eugene: ignore). Si elle signale encore des erreurs, retournez à Réécritures sûres et bouclez. Réexécutez trace si la réécriture a changé les verrous pris.
EXÉCUTER (verrouillé — déclenché uniquement par l'utilisateur)
N'entrez pas dans cette phase sauf si l'utilisateur a explicitement demandé d'appliquer la migration.
Appliquez la migration validée avec des garde-fous de verrouillage. Faites d'abord un essai à blanc.
- Limitez chaque verrou. Enveloppez les instructions bloquantes pour qu'elles échouent rapidement au lieu de faire la queue derrière une longue transaction et de bloquer tout le trafic :
SET lock_timeout = '5s'; SET statement_timeout = '0'; -- ou une limite raisonnable pour l'opération spécifique CONCURRENTLYs'exécute en dehors d'une transaction. Ne mettez pasCREATE/DROP INDEX CONCURRENTLYdans un blocBEGIN/COMMITou dans un exécuteur de migration transactionnel. Après une construction concurrente échouée, supprimez l'indexINVALIDrestant avant de réessayer.- Contraintes en deux phases. Exécutez l'étape
NOT VALID, laissez-la valider, puisVALIDATE CONSTRAINTdans une instruction séparée. - Backfills par lots. Mettez à jour par morceaux bornés dans des transactions courtes séparées, avec un court sommeil entre les lots pour laisser la réplication et l'autovacuum suivre :
-- répéter jusqu'à 0 lignes affectées UPDATE t SET col = expr WHERE id IN (SELECT id FROM t WHERE col IS NULL ORDER BY id LIMIT 5000) - Essai à blanc de la séquence entière contre l'éphémère/clone via
scripts/trace.pyune dernière fois, confirmezpassed_all_checks, puis appliquez à la cible.
Pour MySQL, forcez ALGORITHM=…, LOCK=… afin que le serveur génère une erreur au lieu de se rabattre silencieusement sur une reconstruction COPY, et déléguez les grandes réécritures à gh-ost/pt-osc — voir references/mysql-catalog.md.
Index des références
references/postgres-catalog.md— les 11 opérations dangereuses → réécritures sûres (connaissances de base).references/mysql-catalog.md— matrice InnoDB Online DDL + délégation OSC (voie plus faible).references/squawk-rules.md— identifiants de règles squawk et ce que chacune détecte.references/eugene-hints.md— eugene E1–E15 / W12–W14 et leurs solutions de contournement.references/tool-setup.md— installation de squawk / eugene / gh-ost ; Postgres éphémère.
Scripts
scripts/analyze.py— orchestre squawk + eugene lint (ou heuristiques MySQL) → un seul verdict. Se termine avec un code non nul en cas de constatations de niveau erreur.scripts/trace.py— exécuteeugene trace(serveur temporaire éphémère ou clone) → rapport de verrouillage réel.scripts/gen_rollback.py— génère la migration inverse ; signale les opérations irréversibles.scripts/table_size.sql— sonde de taillepg_class.reltuples.
Tous les scripts sont en bibliothèque standard Python 3 uniquement — pas de pip install.