Migrations
Le schéma de la base évolue uniquement par des migrations Prisma Migrate versionnées dans backend/prisma/migrations/. Chaque migration est un dossier <horodatage>_<nom> contenant un fichier migration.sql. Le fichier migration_lock.toml fixe le fournisseur à postgresql. Prisma enregistre les migrations appliquées dans la table _prisma_migrations de la base.
Les règles de cette page reprennent la décision d'architecture du 10 novembre 2026 sur les migrations : migrations exécutées une seule fois par un job dédié, compatibles avec la version précédente du code, et invariants garantis par la base.
Historique
Le tableau suivant est généré depuis le contenu du dossier des migrations.
| Migration | Date | Contenu |
|---|---|---|
20261102182348_init |
02/11/2026 18:23 | Schéma initial : énumérations Role, TwoFactorMethod, TokenType et Status ; tables User, Token, LotteryRound, Ticket, PointTransaction, Partner, RewardCatalog, ClaimedReward et Analytics avec horodatages TIMESTAMP(3) ; index uniques sur l'adresse, l'email et le code de parrainage du compte, le jeton, le numéro de tour, le hachage de transaction du ticket et le code promo ; clés étrangères (cascade pour Token, SET NULL pour le parrain et Analytics, RESTRICT ailleurs). |
20261109213843_infra_indexes_timestamptz_checks |
09/11/2026 21:38 | Durcissement : lock_timeout de 5 s ; conversion de toutes les colonnes de date en TIMESTAMPTZ(3) en les interprétant comme UTC ; onze index sur les clés étrangères et les colonnes de tri ou de filtre ; cinq contraintes CHECK (solde d'EtherPoints, stocks et cagnotte non négatifs, coût en points positif) ajoutées en NOT VALID puis validées. |
20261110004931_security_backup_codes_partner_owner |
10/11/2026 00:49 | Ajout de Partner.ownerId (compte gestionnaire du commerce, index et clé étrangère SET NULL ajoutée en NOT VALID puis validée) et de User.twoFactorBackupCodes (tableau de textes, vide par défaut). |
20261111150000_onchain_wallets_tickets_indexer |
11/11/2026 15:00 | Passage au mode onchain : portefeuille intégré (walletEncryptedKey, welcomeGrantTxHash) sur User ; colonnes de projection du contrat sur LotteryRound (ticketPriceWei, endsAt, drawRequestedAt, winningTicket, prizeWei, prizeClaimedAt) ; ticketIndex et payerWallet sur Ticket ; table ChainCursor ; l'index unique sur Ticket.transactionHash devient un index simple et l'unicité passe sur (lotteryId, ticketIndex). |
20261112090000_password_accounts_identity_verification |
12/11/2026 09:00 | Comptes avec mot de passe et vérification d'âge : lock_timeout de 5 s ; énumération AgeVerificationStatus ; colonnes passwordHash, ageVerificationStatus (NONE par défaut), ageVerifiedAt et ageVerificationNote sur User ; table IdentityDocument (images chiffrées en BYTEA, index unique sur userId, index sur (status, submittedAt) et reviewerId, clés étrangères en cascade vers le compte et SET NULL vers l'agent qui a statué). Ajout pur, sans réécriture des lignes existantes. |
20261112150000_user_names |
12/11/2026 15:00 | Prénom et nom du compte : lock_timeout de 5 s ; colonnes facultatives firstName et lastName (TEXT, NULL par défaut) sur User, renseignées depuis l'espace Paramètres de l'application. Ajout pur, sans réécriture des lignes existantes. |
Le détail de ce que chaque migration crée (index, contraintes) est sur la page Index et contraintes.
Procédure
Créer une migration en développement
Depuis backend/, avec la base locale démarrée (docker compose up -d --wait à la racine) :
- modifier
backend/prisma/schema.prisma; - générer et appliquer la migration :
npm run prisma:migrate -- --name ajout_colonne_exempleLe script lance prisma migrate dev, qui compare le schéma à la base, écrit prisma/migrations/<horodatage>_ajout_colonne_exemple/migration.sql, l'applique et régénère le client Prisma.
- relire le SQL produit et l'adapter aux règles ci-dessous : ajouter
SET lock_timeout = '5s';en tête, transformer un ajout de contrainte enNOT VALIDsuivi deVALIDATE CONSTRAINT, ajouter les contraintes CHECK que Prisma ne sait pas générer ; - mettre à jour les descriptions de la documentation : nouvelle entrée dans
documentation-officielle/content/_data/migrations.jsonet nouveaux champs dansmodele-donnees.json(voir Dictionnaire de données) ; - lancer
npm run typechecketnpm test.
Pour une contrainte CHECK seule, sans changement du schéma Prisma, on crée une migration vide avec npm run prisma:migrate -- --create-only --name … et on écrit le SQL à la main.
Appliquer les migrations
| Contexte | Commande | Effet |
|---|---|---|
| Développement, base existante | npx prisma migrate deploy ou npm run prisma:deploy |
Applique les migrations manquantes, sans rien générer |
| État des migrations | npm run prisma:status |
Liste les migrations appliquées et en attente |
| Réinitialisation complète | npm run prisma:reset |
Supprime toutes les données, réapplique toutes les migrations ; destructif. Le seed n'est pas lancé : il faut ensuite npm run prisma:seed (voir Seed) |
| Docker Compose | service migrate |
Lancé avant backend et worker |
| CI | npx prisma migrate deploy |
Sur une base PostgreSQL vierge |
L'API n'applique jamais les migrations elle-même. Dans docker-compose.yml, le service migrate (profil app) construit la cible migrator de backend/Dockerfile, dont la commande est npx prisma migrate deploy. Les services backend et worker attendent qu'il se termine avec succès (condition: service_completed_successfully). Avec plusieurs instances d'API, appliquer les migrations au démarrage de chacune provoquerait des exécutions concurrentes ; le job unique l'évite.
prisma migrate reset et prisma migrate dev ne doivent jamais être lancés sur une base de production : le premier efface les données, le second peut proposer de le faire en cas de dérive.
Règle expand et contract
Pendant un déploiement, l'ancienne version du code tourne encore quelques instants avec le nouveau schéma. Chaque migration doit donc rester compatible avec la version N-1 du code. Un changement cassant est découpé en étapes déployées séparément :
| Étape | Exemple pour renommer prizePool en prizePoolEth |
|---|---|
| Expansion | Migration qui ajoute prizePoolEth, nullable ou avec un défaut |
| Double écriture | Version du code qui écrit les deux colonnes et lit l'ancienne |
| Rattrapage | Copie des valeurs existantes par lots, hors migration |
| Bascule | Version du code qui lit la nouvelle colonne |
| Contraction | Migration ultérieure qui supprime prizePool |
Un renommage demande ainsi trois déploiements au minimum. Opérations sûres en une seule migration : ajout de table, ajout de colonne nullable ou avec défaut, ajout d'index, ajout de contrainte que les données existantes respectent.
Application aux migrations existantes :
20261112150000_user_namesn'ajoute que les colonnes nullablesUser.firstNameetUser.lastName: l'ancien code les ignore, et PostgreSQL ajoute une colonne sans défaut sans réécrire la table ;20261110004931et20261111150000n'ajoutent que des colonnes nullables (ou à défaut constant), une table et des index : compatibles avec la version précédente ;20261111150000supprime aussi l'index uniqueTicket_transactionHash_key. C'est une contraction, faite dans la même migration que l'expansion. Elle est sans risque ici : l'ancien code vérifie l'existence d'un ticket parfindFirstsur le hachage avant d'écrire, et l'index simple créé juste avant garde cette recherche rapide ;20261109213843change le type des colonnes de date deTIMESTAMP(3)versTIMESTAMPTZ(3). Le code lit les deux types de la même façon ; le changement réécrit en revanche les tables concernées (voir plus bas).
Verrous
lock_timeout
Toutes les migrations postérieures à init commencent par :
SET lock_timeout = '5s';Une instruction ALTER TABLE demande un verrou exclusif sur la table. Si une longue transaction de l'application tient déjà un verrou sur cette table, l'ALTER TABLE attend, et toutes les requêtes suivantes sur la table attendent derrière lui : l'application se fige. Avec lock_timeout, la migration abandonne au bout de 5 s au lieu de bloquer, et peut être relancée à un moment plus calme. L'application elle-même se connecte avec lock_timeout à 5 s (voir Performance et sauvegardes).
Contraintes ajoutées sans bloquer
Les clés étrangères et contraintes CHECK ajoutées après init suivent le motif :
ALTER TABLE "User" ADD CONSTRAINT "User_pointsBalance_non_negative" CHECK ("pointsBalance" >= 0) NOT VALID;
ALTER TABLE "User" VALIDATE CONSTRAINT "User_pointsBalance_non_negative";NOT VALID ajoute la contrainte pour les nouvelles écritures sans vérifier les lignes existantes, ce qui est immédiat. VALIDATE CONSTRAINT vérifie ensuite les lignes existantes avec un verrou plus faible, qui laisse passer les lectures et les écritures.
À savoir
Prisma envoie chaque fichier migration.sql en un seul bloc, exécuté par PostgreSQL dans une transaction implicite. Le verrou exclusif pris par ADD CONSTRAINT … NOT VALID est alors conservé jusqu'à la fin de la transaction, donc pendant VALIDATE CONSTRAINT. Dans les migrations actuelles, le motif n'apporte donc pas le gain attendu. Pour qu'il soit effectif sur une grosse table, la validation doit être placée dans une migration séparée. Aux volumes du projet, la différence n'est pas mesurable.
Index sans CONCURRENTLY
CREATE INDEX CONCURRENTLY construit un index sans bloquer les écritures, mais il est interdit dans une transaction. Pour la même raison de transaction implicite, les index des migrations sont créés sans CONCURRENTLY et bloquent les écritures sur la table pendant leur construction. Sur une grosse table, il faudra une migration ne contenant que l'instruction CREATE INDEX CONCURRENTLY.
Réécriture de table
La conversion ALTER COLUMN … SET DATA TYPE TIMESTAMPTZ(3) USING … AT TIME ZONE 'UTC' de la migration 20261109213843 réécrit chaque table concernée sous verrou exclusif. Aux volumes du projet, c'est instantané. Une conversion de ce type sur une table volumineuse passerait par une nouvelle colonne et un rattrapage par lots.
Contrôle de dérive en CI
Le job backend de .github/workflows/ci.yml contient deux étapes sur une base PostgreSQL 15 vierge :
npx prisma migrate deploy
npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --exit-code| Étape | Ce qu'elle détecte |
|---|---|
| « Migrations appliquées sur une base vierge » | Une migration qui ne s'applique pas : erreur SQL, ordre incorrect, dépendance à des données |
| « Absence de dérive entre le schéma Prisma et les migrations » | Un schéma modifié sans migration, ou une migration modifiée à la main de façon incompatible avec le schéma |
migrate diff compare la base obtenue (lue par la source de données de backend/prisma.config.ts, donc DATABASE_URL) au fichier schema.prisma. Avec --exit-code, la commande renvoie un code non nul s'il existe une différence, ce qui fait échouer le job. Les contraintes CHECK écrites à la main ne déclenchent pas de dérive, car Prisma ne les représente pas dans le schéma.
Le job chain applique aussi toutes les migrations sur une base vierge avant le test d'intégration blockchain (voir Tests sur une vraie chaîne).
Revenir en arrière
Prisma Migrate ne génère pas de migration inverse. Revenir à une version précédente du code n'est sûr que si la dernière migration est compatible avec cette version, ce que garantit la règle expand et contract. Si ce n'est pas le cas, il faut écrire une migration corrective, ou restaurer une sauvegarde ; voir Performance et sauvegardes et les runbooks.
Une migration déjà appliquée quelque part ne doit jamais être modifiée : Prisma vérifie la somme de contrôle de chaque fichier et signale toute modification.