# Le schéma réel, et comment sortir de `create_all()` > **État au 2026-08-16** : l'outil de relevé existe et est testé, y compris > pour les collisions d'identité Discord. Le relevé lui-même n'a pas été > exécuté — il demande un accès à la base de production, qui ne peut pas venir > du dépôt. Tout changement de schéma ci-dessous attend cette exécution. ## Pourquoi c'est le nœud `db.create_all()` crée les tables manquantes et **ne fait jamais d'`ALTER`**. Une colonne ajoutée à un modèle il y a six mois est donc absente de toute base qui possédait déjà la table, et rien ne le dit : l'application démarre normalement, et la première requête qui touche cette colonne échoue à l'exécution. C'est la raison d'être de `migrations/add_tryout_coaches.py`, un script écrit à la main pour rattraper un cas. Personne ne sait combien il y en a d'autres. C'est ce que `DB-001` mesure, et c'est pourquoi **huit tâches en dépendent** : `DB-002` à `DB-009`, plus `ARCH-001` (fusion coach/équipe) et `SEC-012` (identité Discord avec `unique=True`). ## Étape 1 — sauvegarder, et vérifier la sauvegarde **Rien de ce qui suit ne se fait avant qu'une sauvegarde ait été restaurée avec succès.** Pas « prise » : *restaurée*. Une sauvegarde qu'on n'a jamais restaurée est une hypothèse. Procédure dans `docs/database-restore.md`. La copie restaurée sert aussi de terrain pour les étapes 2 et 3. ## Étape 2 — relever l'écart ```bash # D'abord sur la copie restaurée, jamais directement sur la production python app/supporting_scripts/schema_report.py \ --url postgresql://user:pass@host:5432/copie_restauree ``` L'outil est **en lecture seule** : il ouvre une connexion, lit le catalogue, imprime et sort. Aucun DDL, aucun DML. Codes de sortie : `0` aucun écart, `1` écart trouvé, `2` connexion impossible. Le rapport classe ce qu'il trouve par ce que ça coûte : | Classe | Ce que c'est | Ce que ça coûte | |---|---|---| | `BLOCKING` | Les modèles l'attendent, la base ne l'a pas | L'application échoue à l'exécution. C'est la dérive de `create_all()` | | `RISK` | La base l'a, aucun modèle ne le décrit | Inoffensif tant que rien ne bouge. **Un `alembic --autogenerate` proposera de le supprimer**, avec ses données | | `DIFFERENCE` | Types, nullabilité, contraintes qui divergent | Chacune demande un humain : certaines sont de l'orthographe de dialecte, d'autres sont réelles | **La classe `RISK` est celle qu'on lit en entier.** C'est par là qu'une migration corrective détruit une colonne dont quelqu'un se servait encore. Pendant qu'on y est, la question que l'audit posait et à laquelle le dépôt ne peut pas répondre — le compte `admin`/`password` semé par `clear_db.py` existe-t-il encore ? (`SEC-003`) : ```bash python app/supporting_scripts/schema_report.py --check-seed-accounts ``` L'identité Discord est désormais conservée côté serveur et toute nouvelle collision est refusée par l'application. Les doublons historiques restent à identifier avant d'ajouter la contrainte `UNIQUE` de `SEC-012` : ```bash python app/supporting_scripts/schema_report.py --check-discord-identities ``` ## Étape 3 — Alembic, décrivant le schéma **réel** (`DB-002`) Le piège de cette étape tient en une phrase : **la migration initiale doit décrire la base telle qu'elle est, pas telle que les modèles la décrivent.** Générer la migration initiale depuis les modèles puis estampiller la production revient à déclarer que la dérive n'existe pas. Elle reste là, invisible, et la première migration suivante s'appuie sur un état faux. 1. Ajouter `alembic` à `requirements.txt` — et seulement à ce moment : une dépendance que rien n'utilise est exactement ce que l'audit reprochait ailleurs. 2. `alembic init migrations/alembic`, en pointant `sqlalchemy.url` sur `DATABASE_URL` plutôt qu'en le codant en dur. 3. Générer la révision initiale **contre la copie restaurée** : `alembic revision --autogenerate -m "schéma existant"`. 4. **Relire la révision ligne par ligne** contre le rapport de l'étape 2. Tout `op.drop_*` correspond à une ligne `RISK` : ou bien on l'assume, ou bien on le retire de la migration. 5. Estampiller : `alembic stamp head`. La révision initiale ne s'exécute jamais ; elle décrit le point de départ. ## Étape 4 — la migration corrective (`DB-003`) Une seconde révision qui rattrape les écarts relevés. À exécuter d'abord sur la copie restaurée, et à vérifier avec : ```bash alembic upgrade head && alembic downgrade -1 && alembic upgrade head python app/supporting_scripts/schema_report.py --url # doit sortir 0 ``` Le critère d'acceptation est celui-là : le relevé ne trouve plus rien. Attention aux colonnes `BLOCKING` déclarées `NOT NULL` : les ajouter à une table peuplée échoue sans valeur par défaut ni remplissage. Le rapport le signale dans la conséquence. ## Étape 5 — ce que la migration débloque Dans cet ordre, parce qu'ils dépendent tous de `DB-002` : | Tâche | Ce qu'elle fait | Pourquoi ça attendait | |---|---|---| | `DB-004` | Retirer `create_all()` de `create_app()` | Tant qu'il est là, deux mécanismes décrivent le schéma | | `DB-005` | Cascades de suppression au niveau base | Les cascades ORM sont en place ; PostgreSQL ne les connaît pas | | `DB-006` | Unicité sur `TryoutRegistration(tryout_id, player_id)` | Les deux routes verrouillent désormais la ligne `Tryout` avant le contrôle de doublon, le `count()` et l'`add()` : PostgreSQL sérialise donc leurs décisions de capacité. La contrainte reste nécessaire pour les scripts, imports et futurs chemins d'écriture qui ne passent pas par ces routes | | `DB-007` | Index, `CheckConstraint` sur les statuts, `server_default` | — | | `DB-008` | Trancher `attendance_confirmed` côté tryout | `discord_bot.py` écrit un attribut fantôme ; aujourd'hui journalisé en avertissement | | `DB-009` | Horodatages avec fuseau | `datetime.utcnow` partout, déprécié en 3.12 | | `ARCH-001` | Fusionner coach/équipe sur la relation m2m | Migration de données ; `app/permissions.py` rend la duplication inoffensive **en lecture** seulement, l'écriture crée toujours les deux | | `SEC-012` | Ajouter `unique=True` sur l'identité Discord | La valeur OAuth reste côté serveur et les nouvelles collisions sont refusées ; les lignes historiques doivent être dédoublonnées d'abord | ## Ce qu'on ne fait pas Générer la migration initiale depuis les modèles « pour avancer en attendant ». Ça produit un dépôt qui a l'air d'avoir des migrations, une production estampillée sur un état qu'elle n'a pas, et la dérive préservée sous une couche de plus.