Skip to content

ADR 0004 — yoyo-migrations pour la gestion des migrations SQL

Date : initial Statut : Accepté

Contexte

Le schéma travels repose lourdement sur des colonnes générées (GENERATED ALWAYS AS … STORED) qui contiennent du SQL non trivial : JSONPath, fonctions immutable, weighted tsvector. Le besoin :

  1. Versionner le schéma comme du code (SQL en clair, lu et reviewé en PR).
  2. Appliquer les migrations idempotemment au démarrage du conteneur.
  3. Tracer l'historique appliqué en DB.
  4. Permettre une exécution depuis un poste dev ou un job CI.

Décision

Utiliser yoyo-migrations :

  • SQL pur : un fichier 0001_baseline.sql, 0002_*.sql, etc., avec une directive -- depends: <previous> en haut.
  • Application au démarrage : entrypoint.sh exécute yoyo apply --database "$DATABASE_URL" ./migrations --batch avant de lancer FastAPI.
  • Registre côté DB : yoyo crée _yoyo_migration (et compagnons) qui tracent les migrations appliquées.
  • Config : yoyo.ini au root avec sources = migrations, batch_mode = on.
ini
[DEFAULT]
sources = migrations
batch_mode = on
verbosity = 1

Conséquences

Positives

  • SQL en clair, reviewable. Pas de DSL ORM à apprendre. Les colonnes générées et leurs JSONPath restent lisibles.
  • Application idempotente. yoyo skip les migrations déjà appliquées (tracé via _yoyo_migration). Le redémarrage d'un conteneur est sûr.
  • Léger. Une seule dépendance Python (yoyo-migrations~=9.0.0). Pas de génération de code.
  • --batch : ne demande pas de confirmation interactive — adapté à un démarrage Docker.
  • CI compatible : uv run yoyo apply --database "$TEST_DATABASE_URL" ./migrations --batch dans .gitlab-ci.yml.

Négatives

  • Pas de rollback automatique. Aujourd'hui aucune migration n'a de section __rollback__. En cas de migration cassée, c'est restauration depuis backup, pas yoyo rollback.
  • Ordre lexical strict via -- depends:. Si on saute un numéro ou si deux PRs introduisent 0005_* simultanément, conflit à reviewer manuellement (pas auto-mergeable comme Alembic).
  • Pas de migration auto-générée depuis les changements d'ORM. Pas un problème ici (pas d'ORM), mais limite la réutilisation pour d'autres projets.
  • yoyo mark nécessaire pour les bases existantes qui passent à yoyo après-coup. Documenté dans le commentaire de 0001_baseline.sql : yoyo mark 0001_baseline.

Alternatives écartées

  • Alembic (SQLAlchemy) : excellent mais demande SQLAlchemy / ORM. On est en SQL pur via psycopg, le couplage n'a pas de sens.
  • Migrations dans un script Python custom : ce qu'on aurait fait au début. Yoyo apporte le tracking côté DB et le --batch gratis.
  • Liquibase / Flyway : équivalents Java, surdimensionnés pour ce projet.
  • pgmigrate / tusker : alternatives Python valables. Yoyo a été retenu par simplicité de mise en route.

Workflow d'ajout d'une migration

  1. Créer migrations/000N_description.sql.
  2. Première ligne : -- depends: <previous_migration>.
  3. Écrire le SQL.
  4. Tester en local : yoyo apply --database "$DATABASE_URL" ./migrations --batch.
  5. Commit + PR. La CI applique la migration sur la DB de test avant de lancer pytest.

Voir modules/migrations.md pour le détail (filtre, embedding model, etc.).

Pièges fréquents

  • Migrations non-atomiques par défaut. Si une migration contient plusieurs statements et qu'un échoue à mi-chemin, la DB est dans un état partiel. Pour les migrations sensibles, wrapper en BEGIN; ... COMMIT; explicite ou utiliser plusieurs fichiers de migration.
  • -- depends: cassée. Si vous renommez un fichier de migration déjà appliqué côté prod, yoyo refusera (l'entrée existe en DB mais le fichier source a changé). Ne renommez jamais une migration appliquée.
  • DDL sur de grosses tables. PostgreSQL prend des locks. Pour ce projet (table petite), pas d'impact ; à surveiller si le catalogue grossit.

Contributors

No contributors

Changelog

No recent changes