Ajouter une migration
Le schéma de la base est décrit par backend/prisma/schema.prisma et versionné en migrations SQL dans backend/prisma/migrations/. Ce guide crée une migration conforme à l'ADR 0003 (voir Migrations expand et contract) : appliquée une seule fois par un job dédié, compatible avec la version précédente du code, protégée par un lock_timeout, avec des contraintes ajoutées en NOT VALID puis validées.
L'exemple crée une table Refund qui enregistrera les remboursements réclamés sur le contrat après l'annulation d'un tour. Elle sert ensuite au guide Indexer un nouvel événement.
Fichiers à toucher
| Fichier | Modification |
|---|---|
backend/prisma/schema.prisma |
Nouveau modèle, relation inverse |
backend/prisma/migrations/<horodatage>_<nom>/migration.sql |
Généré par Prisma, puis relu et complété à la main |
backend/__tests__/__mocks__/prisma.mock.ts |
Doublure du nouveau modèle, si une route testée l'utilise |
documentation-officielle/content/_data/migrations.json |
Description de la migration |
documentation-officielle/content/_data/modele-donnees.json |
Description du modèle et de chaque champ |
Règles à respecter
| Règle | Pourquoi |
|---|---|
Première ligne : SET lock_timeout = '5s'; |
Une instruction qui attend un verrou plus de 5 secondes échoue au lieu de bloquer toutes les requêtes de l'application |
Contraintes CHECK et clés étrangères ajoutées en NOT VALID, puis VALIDATE CONSTRAINT |
L'ajout NOT VALID ne parcourt pas la table ; la validation prend un verrou plus léger, qui laisse passer les lectures et les écritures |
| Une colonne ajoutée sur une table existante est nullable ou a une valeur par défaut | L'ancienne version du code, encore en service pendant le déploiement, n'écrit pas cette colonne |
| Pas de renommage ni de suppression dans la même version que le code qui en dépend | Le code N-1 doit continuer de fonctionner (expand/contract) |
Pas de CREATE INDEX CONCURRENTLY dans une migration qui contient autre chose |
Prisma envoie le fichier dans une transaction implicite, où CONCURRENTLY est interdit |
| Les invariants critiques sont des contraintes en base | Un solde ou un stock négatif doit être impossible même si le code a un bogue |
| Une migration publiée n'est jamais modifiée | Prisma compare l'empreinte des fichiers déjà appliqués et refuserait de continuer |
1. Modifier le schéma
Dans schema.prisma, ajouter le modèle et la relation inverse dans LotteryRound :
model Refund {
id String @id @default(uuid(7))
lotteryId String
lottery LotteryRound @relation(fields: [lotteryId], references: [id])
walletAddress String
amountWei String
transactionHash String
logIndex Int
refundedAt DateTime @db.Timestamptz(3)
@@unique([transactionHash, logIndex])
@@index([lotteryId])
@@index([walletAddress])
}Dans le modèle LotteryRound existant, ajouter la relation inverse sous la ligne tickets Ticket[] :
refunds Refund[]Conventions du schéma existant :
| Convention | Exemple dans le schéma |
|---|---|
| Identifiant UUID v7, généré par Prisma | id String @id @default(uuid(7)) |
| Dates avec fuseau, précision milliseconde | @db.Timestamptz(3) |
| Montants en wei stockés en texte | prizeWei String?, ticketPriceWei String? : un uint256 ne tient pas dans un BIGINT |
| Adresses Ethereum en minuscules | walletAddress, winnerWallet, payerWallet |
| Unicité qui rend l'indexation idempotente | @@unique([lotteryId, ticketIndex]) sur Ticket |
| Index sur chaque clé étrangère | @@index([lotteryId]), @@index([ownerId]) |
La paire (transactionHash, logIndex) identifie de façon unique un événement de la chaîne : rejouer l'indexation ne crée pas de doublon.
2. Générer la migration sans l'appliquer
La base locale doit tourner (docker compose up -d --wait). Depuis backend/ :
npx prisma migrate dev --create-only --name refund_claimsPrisma crée prisma/migrations/<horodatage>_refund_claims/migration.sql sans l'appliquer. Le SQL généré, débarrassé des lignes de commentaire que Prisma insère (-- CreateTable, -- CreateIndex...) :
CREATE TABLE "Refund" (
"id" TEXT NOT NULL,
"lotteryId" TEXT NOT NULL,
"walletAddress" TEXT NOT NULL,
"amountWei" TEXT NOT NULL,
"transactionHash" TEXT NOT NULL,
"logIndex" INTEGER NOT NULL,
"refundedAt" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "Refund_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Refund_lotteryId_idx" ON "Refund"("lotteryId");
CREATE INDEX "Refund_walletAddress_idx" ON "Refund"("walletAddress");
CREATE UNIQUE INDEX "Refund_transactionHash_logIndex_key" ON "Refund"("transactionHash", "logIndex");
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_lotteryId_fkey" FOREIGN KEY ("lotteryId") REFERENCES "LotteryRound"("id") ON DELETE RESTRICT ON UPDATE CASCADE;3. Compléter le SQL à la main
Trois modifications, sur le modèle de 20261110004931_security_backup_codes_partner_owner et 20261109213843_infra_indexes_timestamptz_checks :
- ajouter
SET lock_timeout = '5s';en première ligne ; - passer la clé étrangère en
NOT VALIDet la valider ; - ajouter les contraintes
CHECKque Prisma ne sait pas exprimer.
SET lock_timeout = '5s';
CREATE TABLE "Refund" (
"id" TEXT NOT NULL,
"lotteryId" TEXT NOT NULL,
"walletAddress" TEXT NOT NULL,
"amountWei" TEXT NOT NULL,
"transactionHash" TEXT NOT NULL,
"logIndex" INTEGER NOT NULL,
"refundedAt" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "Refund_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Refund_lotteryId_idx" ON "Refund"("lotteryId");
CREATE INDEX "Refund_walletAddress_idx" ON "Refund"("walletAddress");
CREATE UNIQUE INDEX "Refund_transactionHash_logIndex_key" ON "Refund"("transactionHash", "logIndex");
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_lotteryId_fkey" FOREIGN KEY ("lotteryId") REFERENCES "LotteryRound"("id") ON DELETE RESTRICT ON UPDATE CASCADE NOT VALID;
ALTER TABLE "Refund" VALIDATE CONSTRAINT "Refund_lotteryId_fkey";
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_amountWei_digits" CHECK ("amountWei" ~ '^[0-9]+$') NOT VALID;
ALTER TABLE "Refund" VALIDATE CONSTRAINT "Refund_amountWei_digits";Les contraintes CHECK n'apparaissent pas dans schema.prisma. Le Dictionnaire de données les retrouve en lisant les migrations, à condition de garder la forme ALTER TABLE "Table" ADD CONSTRAINT "Nom" CHECK (...).
4. Appliquer et régénérer le client
npx prisma migrate devmigrate dev applique la migration à la base locale et régénère le client Prisma. Vérifier ensuite l'état et l'absence de dérive, comme le fait la CI :
npm run prisma:status
npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-codeLa seconde commande renvoie un code non nul si le schéma décrit un état que les migrations ne produisent pas : par exemple un champ ajouté dans schema.prisma après la génération du SQL.
Danger
npx prisma migrate dev et npm run prisma:reset sont réservés au poste de développement : ils peuvent proposer de réinitialiser la base. En production, seule npm run prisma:deploy (prisma migrate deploy) est lancée, par le service migrate de Compose.
5. Tests et documentation
| Étape | Commande ou fichier |
|---|---|
| Typecheck et tests | npm run typecheck && npm test |
| Doublure Prisma | Ajouter un objet refund avec ses méthodes dans prisma.mock.ts si une route testée l'utilise |
| Description de la migration | Entrée "<horodatage>_refund_claims": "..." dans migrations.json |
| Description du modèle | Entrée modeles.Refund avec description et champs dans modele-donnees.json |
| Régénération | npm run build dans documentation-officielle/, puis commiter site/ |
Exemple d'entrée pour la description du modèle :
{
"modeles": {
"Refund": {
"description": "Remboursement réclamé sur le contrat après l'annulation d'un tour, une ligne par événement RefundClaimed.",
"champs": {
"id": "Identifiant UUID v7.",
"lotteryId": "Tour annulé.",
"lottery": "Relation vers le tour.",
"walletAddress": "Portefeuille remboursé, en minuscules.",
"amountWei": "Montant remboursé en wei.",
"transactionHash": "Transaction qui a émis l'événement.",
"logIndex": "Position de l'événement dans le bloc.",
"refundedAt": "Date du bloc."
}
}
}
}Modifier une table existante
Une modification qui touche des données existantes se découpe en plusieurs pull requests, chacune déployée séparément.
Ajouter une colonne obligatoire
| Étape | Pull request | Contenu |
|---|---|---|
| Expansion | 1 | Colonne nullable ou avec défaut ; le nouveau code l'écrit, l'ancien l'ignore |
| Remplissage | 2 | Script de remplissage par lots, hors migration, pour ne pas tenir de verrou longtemps |
| Contraction | 3 | SET NOT NULL ou contrainte CHECK ... NOT VALID puis VALIDATE, une fois toutes les lignes remplies |
Renommer une colonne
Trois déploiements : ajout de la nouvelle colonne et double écriture, bascule des lectures, suppression de l'ancienne colonne. Un ALTER TABLE ... RENAME COLUMN direct casserait la version du code encore en service pendant le déploiement.
Créer un index sur une grosse table
Créer une migration qui ne contient que l'index, sans SET lock_timeout ni autre instruction, pour pouvoir utiliser CONCURRENTLY :
CREATE INDEX CONCURRENTLY "Ticket_payerWallet_idx" ON "Ticket"("payerWallet");Sur les volumes actuels (quelques milliers de lignes au plus), un CREATE INDEX simple suffit.
Revenir en arrière
Une migration appliquée ne se défait pas automatiquement : Prisma n'écrit pas de migration descendante. Le retour arrière passe par :
- le redéploiement de la version précédente du code, qui fonctionne avec le nouveau schéma grâce à l'expand/contract ;
- si nécessaire, une nouvelle migration qui annule la précédente ;
- en dernier recours, la restauration de la sauvegarde prise avant la migration (voir Sauvegardes et reprise).
Une migration qui échoue en cours de route en production se diagnostique avec npm run prisma:status, puis se marque résolue avec npx prisma migrate resolve --rolled-back <nom> ou --applied <nom> après correction manuelle.