Bibliothèque de playbooks
Migrations de base et changements de schéma
Une migration est le seul type de diff où du code correct peut quand même provoquer une panne. Les migrations générées sont syntaxiquement parfaites et opérationnellement naïves : elles ajoutent un `NOT NULL` sur une table remplie, construisent un index dans une transaction et suppriment une colonne que l'ancien process, toujours en vie, continue d'écrire. Ces règles traduisent expand-migrate-contract en énoncés vérifiables en revue.
- Écrit pour
- Les équipes qui déploient en rolling update sur une base Postgres ou MySQL en production.
- Non couvert
- Écrit pour la sémantique Postgres ; quelques règles (`CONCURRENTLY`, `timestamptz`) demandent une transposition sous MySQL. Rien ici ne contrôle la performance des requêtes — cela relève d'un test de charge, pas d'une revue.
11 règles6 bloquantes3 warning2 info
01 Une migration par PR, avec son rollback
errorUne pull request contient au plus une migration de schéma, et le chemin de retour arrière est livré dans la même PR. Des migrations regroupées ne peuvent pas être annulées indépendamment — et on découvre qu'il fallait le rollback à 2 h du matin, pas au moment de la revue, où l'écrire prend quatre minutes.
Bloque : sans rollback dans le diff, le plan de réponse à incident se résume à improviser.
Portéemigrations/**db/migrations/****/drizzle/**
Tagsdatabasesafetyprocess
02 Une migration prend un verrou borné
errorUne migration qui modifie une table existante fixe un `lock_timeout` court (et un `statement_timeout`) en tête de fichier, pour qu'un DDL incapable d'obtenir son verrou échoue vite au lieu d'attendre. Un `ALTER` en attente se place derrière la transaction qu'il attend, et toute requête ultérieure sur cette table se place derrière l'`ALTER` — un verrou que la migration n'obtient jamais met donc la table à l'arrêt aussi longtemps qu'elle persiste.
Bloque : sans cela, une seule instruction bloquée fige toutes les lectures et écritures sur la table — une panne, et non un déploiement raté.
Portéemigrations/**db/migrations/****/drizzle/**
Tagsdatabasezero-downtimelocking
03 Les changements destructifs viennent une release plus tard
errorSupprimer ou renommer une colonne ou une table se fait dans une PR distincte et postérieure au changement de code qui a cessé de s'en servir — expand, migrate, contract. Pendant un rolling update, anciens et nouveaux process tournent en même temps : une colonne supprimée dans la même release casse toutes les requêtes encore servies par l'ancienne version.
Bloque : c'est de loin la première cause de 500 au moment du déploiement sur une codebase saine.
Portéemigrations/**db/migrations/****/drizzle/**
Tagsdatabasezero-downtime
04 Une nouvelle colonne est nullable ou pourvue d'un défaut, puis remplie
errorAjouter une colonne `NOT NULL` sans valeur par défaut à une table remplie est refusé. Ajoutez-la nullable (ou avec un défaut), remplissez-la par lots, puis resserrez la contrainte dans une migration ultérieure. La version en une seule instruction réécrit la table, ou la verrouille en exclusif, pendant toute la durée de la réécriture.
Bloque : cela prend un verrou d'écriture sur une table en production — une panne que rien n'annonce dans le diff.
Portéemigrations/**db/migrations/****/drizzle/**
Tagsdatabasezero-downtimelocking
05 Construisez les index en concurrent, hors transaction
error`CREATE INDEX` et `DROP INDEX` sur une table au volume de production utilisent `CONCURRENTLY`, et la migration est marquée comme non transactionnelle. Un `CREATE INDEX` ordinaire tient un verrou qui bloque toutes les écritures sur la table jusqu'à la fin — soit, sur une grosse table, des minutes et non des secondes.
Bloque : même raisonnement — un verrou sur une table en production, invisible pour qui ne sait pas où regarder.
Portéemigrations/**db/migrations/****/drizzle/**
Tagsdatabasezero-downtimelocking
06 Les colonnes monétaires et de précision ne sont jamais flottantes
errorLes colonnes monétaires ou de quantité précise utilisent `numeric`/`decimal`, ou une colonne entière en unité mineure. `float`, `real` et `double precision` sont refusés pour ces usages. Le flottant ne représente pas exactement les fractions décimales : les sommes cessent de se réconcilier, et l'écart grandit avec le volume.
Bloque : changer le type d'une colonne plus tard impose une réécriture complète de la table et un audit des données.
Portéemigrations/**db/migrations/****/schema*.ts**/models/**
Tagsdatabasecorrectnessbilling
07 Les backfills tournent par lots bornés
warningUne migration qui touche des lignes existantes les traite par lots, avec une borne explicite et une clé de reprise, ou bien tourne comme un job séparé plutôt qu'à l'intérieur de la migration. Un unique `UPDATE` sur toute la table tient une longue transaction, bloque l'autovacuum et génère du WAL plus vite que les réplicas ne peuvent le consommer.
Avertit : sur une petite table, c'est sans danger — c'est au reviewer de trancher au vu du nombre réel de lignes.
Portéemigrations/**db/migrations/**
Tagsdatabaseperformance
08 Les clés étrangères ont un index sur la colonne enfant
warningChaque colonne de clé étrangère porte un index, créé dans la même migration. Sans lui, une suppression ou une mise à jour sur la ligne parente déclenche un parcours séquentiel de la table enfant pour chaque ligne concernée — rapide en développement avec cent lignes, incident à dix millions.
Avertit : c'est un décrochage de performance, et l'index s'ajoute facilement après coup.
Portéemigrations/**db/migrations/****/schema*.ts
Tagsdatabaseperformance
09 Les horodatages portent leur fuseau
warningLes colonnes de temps utilisent `timestamptz` (ou l'équivalent) et prennent par défaut le `now()` UTC de la base. `timestamp without time zone` est refusé. Un horodatage naïf signifie en silence « le fuseau qu'avait le process qui a écrit », ce qui change dès qu'un serveur change de région ou qu'un worker s'exécute ailleurs.
Avertit : le dommage est une ambiguïté progressive des données, pas une casse immédiate.
Portéemigrations/**db/migrations/****/schema*.ts
Tagsdatabasetime
10 Gardez les changements de données hors des migrations de schéma
infoLes seeds, les corrections et les reprises de données ponctuelles vivent dans leur propre migration ou dans un script, jamais mêlés au DDL du même fichier. DDL et DML n'ont ni le même profil de verrouillage ni la même histoire de rollback — les mélanger, c'est ne plus pouvoir annuler la moitié sûre sans la moitié risquée.
Indicatif : une règle d'hygiène qui ne paie que le jour où quelque chose tourne mal.
Portéemigrations/**db/migrations/**
Tagsdatabaseprocess
11 Le nommage suit la convention de schéma
infoLes tables sont en snake_case et au pluriel, les clés étrangères s'appellent `<singulier>_id`, les booléens se lisent comme des prédicats (`is_active`, `has_access`), et les mots réservés sont évités. C'est cette cohérence qui permet à n'importe qui — humain ou agent — de deviner correctement un nom de colonne au lieu d'aller lire le schéma.
Indicatif : c'est du goût, mais c'est un goût constant qui rend un schéma navigable.
Portéemigrations/**db/migrations/****/schema*.ts
Tagsdatabaseconventions
Avant de tout importer
La sévérité est le levier qui décide si l'équipe garde la gate activée. Seul error bloque un merge — importez d'abord ces règles en warning, regardez ce qui se déclenche réellement sur quelques semaines de vraies pull requests, et promouvez celles qui l'ont mérité. Et réécrivez les globs de portée : les chemins ci-dessus sont des exemples, et une règle ne s'applique qu'aux fichiers correspondant à ses globs.
Les règles entrent dans Normik une par une — via le formulaire du playbook, ou en laissant un agent de code les proposer par MCP à partir d'un repo existant. Il n'y a pas d'import markdown en masse : le bouton de copie ci-dessus sert à votre AGENTS.md, à une PR où l'équipe débat de la liste, et à garder les règles sous les yeux pendant que vous saisissez celles que vous avez retenues.
Chaque règle ici est jugée comme Normik la juge : sur les lignes que la pull request ajoute. Rien dans ce playbook ne dépend de la description de la PR, d'un fichier que le diff ne touche pas, ni de l'historique d'un fichier — ce sont de vrais standards de revue, mais une gate qui n'en voit pas la preuve ne pourrait que rester muette ou deviner.
L'anatomie d'une règle, le comportement des sévérités et des globs, le versionnage et l'export AGENTS.md sont détaillés dans la documentation du playbook.