DB-001. create_all() cree les tables manquantes et ne fait jamais d ALTER. Une colonne ajoutee a un modele il y a six mois est donc absente de toute base qui possedait deja la table, et rien ne le dit : l application demarre, et la premiere requete qui touche cette colonne echoue a l execution. C est la raison d etre de migrations/add_tryout_coaches.py, ecrit a la main pour rattraper un cas. Personne ne sait combien il y en a d autres. app/supporting_scripts/schema_report.py compare le catalogue d une base vivante aux modeles : tables, colonnes, types, nullabilite, contraintes d unicite, cles etrangeres, index. En **lecture seule** — il ouvre une connexion, lit, imprime, sort. Aucun DDL, aucun DML. Les constats sont classes par ce qu ils coutent, pas par ce qu ils sont : - BLOCKING : les modeles l attendent, la base ne l a pas. C est la derive ; - RISK : la base l a, aucun modele ne le decrit. Inoffensif tant que rien ne bouge — et **un alembic --autogenerate proposera de le supprimer**, avec ses donnees. C est la classe qu on lit en entier ; - DIFFERENCE : types, nullabilite, contraintes. Chacune demande un humain. Les types sont compares apres compilation vers le meme dialecte : opposer String(200) a VARCHAR(200) en chaines aurait signale chaque colonne comme differente, et un rapport qui crie partout ne se lit plus. --check-seed-accounts repond a la question de SEC-003 a laquelle le depot ne peut pas repondre : le compte admin/password seme par clear_db.py existe-t-il encore, et son mot de passe est-il toujours celui-la. 13 tests le pilotent contre des bases SQLite fabriquees pour diverger d une facon connue. Le cas qui compte le plus est la base propre : un rapport qui crie sur une base saine ne sera pas lu, et un rapport qui dit « aucun ecart » sur une base derivee est pire que pas de rapport — c est un feu vert pour laisser autogenerate ecrire la difference en DROP. docs/database-schema.md donne la suite, etape par etape, avec le piege de DB-002 en toutes lettres : la migration initiale doit decrire la base telle qu elle est, pas telle que les modeles la decrivent. Generer depuis les modeles puis estampiller revient a declarer que la derive n existe pas. Alembic n est pas ajoute aux dependances : rien ne l utilise encore, et une dependance que rien n utilise est exactement ce que l audit reprochait ailleurs. Le document dit a quelle etape l ajouter. 530 tests.
122 lines
6.0 KiB
Markdown
122 lines
6.0 KiB
Markdown
# Le schéma réel, et comment sortir de `create_all()`
|
|
|
|
> **État au 2026-08-11** : l'outil de relevé existe et est testé. 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 ce qui suit 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
|
|
```
|
|
|
|
## É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 <copie> # 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)` | Le plafond d'inscriptions est aujourd'hui un `count()` suivi d'un `add()` : deux requêtes simultanées passent toutes les deux |
|
|
| `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` | Identité Discord côté serveur, `unique=True` | La colonne doit être unique, donc dédoublonnée 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.
|