SlimHajjami-Git

migration

É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.

SlimHajjami-Git 0 Updated 6d ago
GitHub

Install

npx skillscat add slimhajjami-git/gisv2/migration

Install via the SkillsCat registry.

SKILL.md

/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 par
bash 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 -3

Prendre le numéro suivant (dernier connu : 039_tarif_par_vehicule.sql040_…).
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.sql

En-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 …;
    Un NOT NULL sans DEFAULT échoue sur une table déjà remplie.
  • Table : CREATE TABLE IF NOT EXISTS … avec id 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 (…); — inclure
    company_id dans 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. Toujours updated_at = now().
  • Nommage des colonnes : suivre la convention de la table cible, qui est
    incohérente d'une table à l'autre (vehicles snake_case sauf échéances
    "InsuranceExpiry" ; notifications/audit_logs en PascalCase entre
    guillemets). Lister d'abord information_schema.columns (voir /prod-db).
    Une table NEUVE est en snake_case avec HasColumnName explicite côté EF.
  • Devise / montants : si la migration écrit des prix, ajouter le NB de 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/COLUMN sur un objet existant, pas de
    DELETE, pas de TRUNCATE. Un objet devenu inutile se documente, il ne se
    supprime pas (voir /prod-db pour 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)

  1. 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).

  2. 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,
      bloc modelBuilder.Entity<Entité>()… — pour SubscriptionType vers 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 -3

    Si 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.

  3. 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.cs
    • services/src/GisAPI.Application/Common/Interfaces/IGisDbContext.cs
    • services/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, le
    HasQueryFilter sur CompanyId comme ses voisines dans GisDbContext.cs.

  4. 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.

  5. Ne pas toucher GisDbContextModelSnapshot.cs ni le dossier
    Persistence/Migrations/. Si un dotnet ef migrations add a été lancé par
    erreur : supprimer les deux fichiers générés et git checkout du snapshot.

5. Tester depuis le WORKTREE (jamais depuis le checkout principal)

  1. Jouer le SQL deux fois sur la base locale (conteneur gisv2-postgres de
    docker-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.sql

    MigrateAsync ne sait pas créer une base de zéro (vérifié) : la base locale
    s'initialise par bash 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.

  2. Build + tests :

    cd <worktree>/services/GisAPI && dotnet build
    cd <worktree>/services/GisAPI && dotnet test ../tests/GisAPI.Tests/
  3. Si un endpoint lit la colonne : lancer l'API (dotnet run, port 5020) et
    l'appeler avec un jeton de POST /api/auth/login (compte seed local
    admin@belive.tn / Admin@2026).

6. Livrer dans le bon ordre

  1. Un seul commit avec le SQL + l'entité + les mappings + les consommateurs :
    feat(scope): … ou fix(scope): …, corps = constat + pourquoi, mention
    explicite « migration 0NN à jouer AVANT le pod API », ligne finale
    le trailer Co-Authored-By de Claude Code (modèle courant, ex. Claude Fable 5.1 <noreply@anthropic.com>).
  2. git push origin HEAD:master.
  3. /deploy dz api d'abord : la skill joue le SQL via
    kubectl 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 par information_schema.columns.
  4. 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 → 42703login
    cassé pour tous
    jusqu'au rollback.

Ne jamais

  • dotnet ef migrations add, dotnet ef database update contre 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, TRUNCATE sur un objet de prod existant.
  • Un NOT NULL sans DEFAULT, un CREATE/ALTER sans IF NOT EXISTS, un
    UPDATE sans WHERE qui le rend rejouable.
  • Ajouter le mapping dans un seul des deux fichiers EF (Configuration OU GisDbContext).
  • Oublier TestGisDbContext.cs pour un nouveau DbSet.
  • 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.

Categories