Écrire un changement de schéma PostgreSQL comme migration SQL numérotée et idempotente dans migrations/ (0NN_slug.sql, sur le modèle de 039), mettre à jour l'entité et les DEUX mappings EF (Configuration ET GisDbContext), et rappeler l'ordre SQL-avant-pod. Utiliser pour toute nouvelle colonne, table, index ou recalage de données ; jamais dotnet ef.
Install
npx skillscat add slimhajjami-git/gisv2/migration Install via the SkillsCat registry.
/migration <description du changement de schéma>
Changement demandé : $ARGUMENTS.
Le schéma de production évolue uniquement par les fichiers migrations/*.sql
appliqués à la main (psql). Le snapshot EF (GisDbContextModelSnapshot.cs) est en
dérive massive : une migration EF générée contre la prod a voulu DROP des tables
existantes. dotnet ef migrations add et dotnet ef database update sont donc
interdits contre toute base réelle. Au démarrage, l'API applique les migrations
EF en attente (Program.cs, context.Database.MigrateAsync()) — mais MigrateAsync
ne sait pas créer une base de zéro (vérifié) ; la base locale s'initialise parbash scripts/init-local-db.sh (schéma copié du serveur de test + migrations/*.sql)
et a donc un schéma proche de DZ. En prod, c'est le SQL qui fait foi.
1. Choisir le numéro et le nom
ls migrations/ | grep -E '^[0-9]{3}_' | sort | tail -3Prendre le numéro suivant (dernier connu : 039_tarif_par_vehicule.sql → 040_…).
Nom : NNN_slug_en_francais.sql, slug court en snake_case décrivant le POURQUOI
métier (040_kilometrage_echeances, pas 040_add_column). Vérifier qu'aucune
autre branche n'a pris le même numéro (git log origin/master --oneline -- migrations/ | head).
2. Relire le modèle de style
cat migrations/039_tarif_par_vehicule.sqlEn-tête de commentaires obligatoire, dans cet ordre :
-- NNN — Titre en une ligne (contexte : recette client JJ/MM/AAAA, ou ticket).
--
-- Constat : ce que l'écran / l'API montrait de faux, et pourquoi (la cause
-- réelle, chiffrée si possible : « 299 EUR affichés au lieu de 3 × 2 × 12 = 72 »).
--
-- Modèle : ce que représente la nouvelle colonne/table, comment l'API l'utilise
-- (recalcul à la lecture, cache, etc.), et ce qu'elle NE change pas.Puis le corps : DDL idempotent → UPDATE de recalage guidé par un WHERE →
commentaire NB sur la devise ou la calibration par serveur si des montants
sont écrits en dur.
3. Écrire un SQL idempotent (rejouable sans effet)
- Colonne :
ALTER TABLE t ADD COLUMN IF NOT EXISTS c type NOT NULL DEFAULT …;
UnNOT NULLsansDEFAULTéchoue sur une table déjà remplie. - Table :
CREATE TABLE IF NOT EXISTS …avecid INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,company_id INTEGER NOT NULL(multi-tenant),created_at/updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
(modèle :migrations/029_vehicle_load_periods.sql). - Index :
CREATE INDEX IF NOT EXISTS "IX_table_colonnes" ON t (…);— inclurecompany_iddans les index des tables interrogées par société. - Valeurs de référence :
INSERT … ON CONFLICT DO NOTHING. - Recalage de données :
UPDATE … WHERE <cible> AND <colonne> IS DISTINCT FROM <nouvelle valeur>
pour qu'un second passage touche 0 ligne. Toujoursupdated_at = now(). - Nommage des colonnes : suivre la convention de la table cible, qui est
incohérente d'une table à l'autre (vehiclessnake_case sauf échéances"InsuranceExpiry";notifications/audit_logsen PascalCase entre
guillemets). Lister d'abordinformation_schema.columns(voir/prod-db).
Une table NEUVE est en snake_case avecHasColumnNameexplicite côté EF. - Devise / montants : si la migration écrit des prix, ajouter le
NBde 039
(« tarifs calibrés EUR — adapter AVANT de jouer sur un serveur en monnaie
locale »). TN facture en TND (comptes GPA en EUR), DZ en DZD. - Ne rien détruire : pas de
DROP TABLE/COLUMNsur un objet existant, pas deDELETE, pas deTRUNCATE. Un objet devenu inutile se documente, il ne se
supprime pas (voir/prod-dbpour le protocole si Slim l'ordonne). - Une migration déjà appliquée quelque part ne se modifie plus : on en écrit une nouvelle.
4. Mettre à jour le code .NET (sinon 42703 ou colonne ignorée)
Entité :
services/src/GisAPI.Domain/Entities/<Entité>.cs— propriété avec
un commentaire XML<summary>qui explique le POURQUOI métier (modèle :SubscriptionType.PricePerVehicle).Mapping EF, aux DEUX endroits — le projet a des mappings dupliqués :
services/src/GisAPI.Infrastructure/Persistence/Configurations/<Entité>Configuration.cs:builder.Property(s => s.NouvelleProp).HasColumnName("nouvelle_colonne");services/src/GisAPI.Infrastructure/Persistence/GisDbContext.cs(OnModelCreating,
blocmodelBuilder.Entity<Entité>()…— pourSubscriptionTypevers la ligne 272) :modelBuilder.Entity<SubscriptionType>().Property(s => s.NouvelleProp).HasColumnName("nouvelle_colonne");
Vérifier si l'entité est mappée dans les deux fichiers :
grep -n "Entity<SubscriptionType>()" services/src/GisAPI.Infrastructure/Persistence/GisDbContext.cs | head -3 grep -n "HasColumnName" services/src/GisAPI.Infrastructure/Persistence/Configurations/SubscriptionTypeConfiguration.cs | head -3Si elle l'est, ajouter la ligne aux DEUX : un mapping manquant d'un côté
laisse EF partir sur le nom PascalCase par défaut →42703 column "NouvelleProp" does not exist
sur toutes les requêtes de l'entité (login compris si l'entité est chargée à la connexion).
Types décimaux :.HasPrecision(10, 2)ou.HasColumnType("decimal(10,2)")comme les voisins.Nouvelle table : ajouter le
DbSet<T>dans TROIS fichiers, sinon les
tests ne compilent plus (le contexte de test implémente l'interface à la main) :services/src/GisAPI.Infrastructure/Persistence/GisDbContext.csservices/src/GisAPI.Application/Common/Interfaces/IGisDbContext.csservices/tests/GisAPI.Tests/Common/TestGisDbContext.cs
Plus une
<Entité>Configuration.cs(ToTable("nom_table"),HasColumnName
pour chaque colonne, index) et, si l'entité est multi-tenant, leHasQueryFiltersurCompanyIdcomme ses voisines dansGisDbContext.cs.Consommateurs : DTO / handlers / seed. Le seed des plans dans
Program.cs
est create-only : il ne corrige jamais une ligne existante — c'est le rôle
de l'UPDATE de recalage de la migration.Ne pas toucher
GisDbContextModelSnapshot.csni le dossierPersistence/Migrations/. Si undotnet ef migrations adda été lancé par
erreur : supprimer les deux fichiers générés etgit checkoutdu snapshot.
5. Tester depuis le WORKTREE (jamais depuis le checkout principal)
Jouer le SQL deux fois sur la base locale (conteneur
gisv2-postgresdedocker-compose.dev.yml) ; le second passage doit être sans effet (0 ligne,
aucune erreur), ce qui prouve l'idempotence :docker exec -i gisv2-postgres psql -U postgres -d gis_v2 -v ON_ERROR_STOP=1 -f - < migrations/0NN_slug.sql docker exec -i gisv2-postgres psql -U postgres -d gis_v2 -v ON_ERROR_STOP=1 -f - < migrations/0NN_slug.sqlMigrateAsyncne sait pas créer une base de zéro (vérifié) : la base locale
s'initialise parbash scripts/init-local-db.sh(schéma copié du serveur de
test +migrations/*.sql) et a donc un schéma proche de DZ ; la vraie
répétition se fait sur DZ (/deploy dz), qui a un schéma proche de TN.Build + tests :
cd <worktree>/services/GisAPI && dotnet build cd <worktree>/services/GisAPI && dotnet test ../tests/GisAPI.Tests/Si un endpoint lit la colonne : lancer l'API (
dotnet run, port 5020) et
l'appeler avec un jeton dePOST /api/auth/login(compte seed localadmin@belive.tn/Admin@2026).
6. Livrer dans le bon ordre
- Un seul commit avec le SQL + l'entité + les mappings + les consommateurs :
feat(scope): …oufix(scope): …, corps = constat + pourquoi, mention
explicite « migration 0NN à jouer AVANT le pod API », ligne finale
le trailerCo-Authored-Byde Claude Code (modèle courant, ex.Claude Fable 5.1 <noreply@anthropic.com>). git push origin HEAD:master./deploy dz apid'abord : la skill joue le SQL viakubectl exec -i postgres-0 -n gisv2 -- psql -U postgres -d gis_v2 -v ON_ERROR_STOP=1 < migrations/0NN_slug.sql
PUIS bascule le pod. Vérifier la colonne parinformation_schema.columns.- TN seulement sur demande de Slim, même ordre : SQL → pod API → frontend.
Sans cela, le nouveau pod démarre contre l'ancien schéma →42703→ login
cassé pour tous jusqu'au rollback.
Ne jamais
dotnet ef migrations add,dotnet ef database updatecontre une base réelle
(TN, DZ, ou une base locale restaurée depuis un dump).- Modifier ou renuméroter une migration déjà appliquée sur un serveur.
DROP TABLE,DROP COLUMN,DELETE,TRUNCATEsur un objet de prod existant.- Un
NOT NULLsansDEFAULT, unCREATE/ALTERsansIF NOT EXISTS, unUPDATEsans WHERE qui le rend rejouable. - Ajouter le mapping dans un seul des deux fichiers EF (Configuration OU GisDbContext).
- Oublier
TestGisDbContext.cspour un nouveauDbSet. - Déployer le pod API avant d'avoir joué le SQL sur le serveur cible.
- Jouer sur DZ (DZD) ou TN (TND) une migration qui écrit des montants calibrés
EUR sans l'avoir adaptée. - Écrire un identifiant de prod, une IP ou un secret dans le fichier SQL ou le commit.