Skip to content

Module — Migrations SQL

Source de vérité du schéma : les fichiers SQL sous migrations/. Gérés via yoyo-migrations. ADR : architecture/adr/0004-yoyo-migrations.md.

Inventaire

migrations/
├── 0001_baseline.sql                   — schéma initial, helpers, table, indexes, vue v1
├── 0002_add_next_departure_view.sql    — vue v2 (next_departure calculé)
├── 0003_update_travels_view.sql        — vue v3 (filtre WHERE next_departure > NOW)
└── 0004_add_seaside_filter.sql         — colonne is_seaside + index + vue v4

Chaque fichier commence par -- depends: <previous> (sauf 0001 qui a -- depends: vide). yoyo applique dans l'ordre des dépendances et trace en table _yoyo_migration.

Workflow d'application

En conteneur (entrypoint.sh)

sh
yoyo apply --database "$DATABASE_URL" ./migrations --batch
  • Tourne à chaque démarrage du conteneur. yoyo skip les migrations déjà appliquées.
  • --batch désactive l'interaction (indispensable hors TTY).
  • Si une migration échoue, l'entrypoint plante avec set -e → FastAPI ne démarre pas.

En local (dev)

sh
# Migration de la DB dev (sur port 5432)
yoyo apply --database "postgresql://better_search:better_search_pwd@localhost:5432/better_search" ./migrations --batch

# Migration de la DB test (sur port 5433)
yoyo apply --database "postgresql://better_search:better_search_pwd@localhost:5433/better_search_test" ./migrations --batch

Avec uv : uv run yoyo apply ….

En CI

.gitlab-ci.yml job test :

yaml
- uv run yoyo apply --database "$TEST_DATABASE_URL" ./migrations --batch

Avant pytest.

Ajouter une migration

  1. Choisir le numéro : 000N_*.sqlN est le prochain entier libre. Préfixe à 4 chiffres pour le tri lexical.
  2. Première ligne : -- depends: 000(N-1)_* (sans l'extension .sql). yoyo applique ce fichier seulement après le précédent.
  3. Écrire le SQL. Idempotent si possible (CREATE INDEX IF NOT EXISTS, DROP VIEW IF EXISTS, etc.) — facilite les rejouages sur des bases hétéroclites.
  4. Tester en local sur la DB dev :
    sh
    yoyo apply --database "$DATABASE_URL" ./migrations --batch
  5. Vérifier le résultat : \d travels côté psql doit montrer la nouvelle colonne / index / contrainte.
  6. Commit + PR. Le job test de CI applique la migration sur la DB de test puis lance pytest.

Exemple : ajouter un filtre ?promo=true

Hypothèse : un voyage est en promo si raw_data->'minPrice'->>'priceWithSpecialOffers' < raw_data->'minPrice'->>'price'.

1. Migration (migrations/0005_add_promo_filter.sql) :

sql
-- Add is_promo generated column + index
-- depends: 0004_add_seaside_filter

ALTER TABLE travels
    ADD COLUMN is_promo BOOLEAN
    GENERATED ALWAYS AS (
        CASE
            WHEN (raw_data->'minPrice'->>'priceWithSpecialOffers')::NUMERIC
                 < (raw_data->'minPrice'->>'price')::NUMERIC
            THEN TRUE ELSE FALSE
        END
    ) STORED;

CREATE INDEX IF NOT EXISTS idx_travels_is_promo ON travels (is_promo);

-- Recréer la vue pour exposer la nouvelle colonne (sinon SELECT * stocké au plan)
DROP VIEW IF EXISTS travels_view;
CREATE VIEW travels_view AS
WITH with_departure_dates AS (
    SELECT
        raw_data, embedding, id, name, destination,
        discount_club, is_valid, is_seaside, is_promo,
        travel_ranges, departure_dates, search_vector, updated_at,
        (SELECT MIN(d) FROM unnest(departure_dates) d
         WHERE d > CURRENT_DATE) AS next_departure
    FROM travels
)
SELECT * FROM with_departure_dates
WHERE next_departure > CURRENT_DATE;

2. Filtre côté src/search_query.py :

python
if promo is not None:
    where_parts.append("is_promo = %(promo)s")
    params["promo"] = promo

3. Param côté src/server.py :

python
promo: bool = None,
# ...
records, total_count = db.search(..., promo=promo)

Et db.search doit forwarder promo à build_search_query.

4. Test côté tests/test_search.py : nouvelle classe TestSearchByPromo avec fixtures qui setent priceWithSpecialOffers < price.

Recréer la vue ou pas ?

Quand vous ajoutez une colonne à travels, vous devez recréer travels_view si vous voulez que la nouvelle colonne soit exposée par SELECT * FROM travels_view. Postgres mémorise les colonnes du SELECT * au moment du CREATE VIEW.

Recommandé : DROP VIEW IF EXISTS travels_view; CREATE VIEW travels_view AS … dans la même migration. Faire un seul fichier qui contient les deux DDL.

Marquer une migration comme appliquée (rattrapage)

Pour les bases existantes pré-yoyo, marquer la baseline comme appliquée :

sh
yoyo mark 0001_baseline --database "$DATABASE_URL"

yoyo mark ajoute l'entrée dans _yoyo_migration sans exécuter le SQL. À utiliser quand le schéma est déjà en place.

Rollback

Aujourd'hui aucune migration n'a de section __rollback__ (ou from yoyo import step côté Python). Pour rollback :

  • Soit ajouter un rollback explicite à la migration (et tester en local).
  • Soit restaurer depuis un backup Postgres.

Pas de procédure « yoyo rollback-en-prod-au-prochain-incident » documentée.

Helpers SQL utilisés dans les migrations

Définis dans 0001_baseline.sql :

sql
CREATE OR REPLACE FUNCTION jsonb_to_text_array(j JSONB, path TEXT)
RETURNS TEXT[] AS $$ ... $$ LANGUAGE SQL IMMUTABLE;

CREATE OR REPLACE FUNCTION jsonb_to_timestamp_array(j JSONB, path TEXT)
RETURNS TIMESTAMP[] AS $$ ... $$ LANGUAGE SQL IMMUTABLE;
  • IMMUTABLE : prérequis pour usage dans une GENERATED ALWAYS AS … STORED.
  • Ne pas modifier ces fonctions une fois en prod. Postgres les caches dans la définition des colonnes ; une modif peut casser ces colonnes silencieusement. Pour les modifier, il faut : DROP des colonnes qui s'en servent → recréer la fonction → recréer les colonnes → reindex complet.

Anti-patterns

  • yoyo apply sur la mauvaise DB en prod. Toujours vérifier $DATABASE_URL avant.
  • Renommer une migration appliquée. yoyo refusera (l'entrée existe mais le fichier source a changé).
  • Sauter un numéro (passer de 0004 à 0006). Pas bloquant techniquement, mais déroutant pour la review.
  • Mettre du DML dans une migration (un INSERT INTO travels). Le reindex est le seul writer.
  • Ajouter un IF EXISTS à un ALTER TABLE (Postgres ne le supporte pas pour ALTER). Préférer un test côté pg_attribute ou utiliser des migrations idempotentes simples (ADD COLUMN IF NOT EXISTS).

Métadonnées yoyo

yoyo crée et maintient :

  • _yoyo_migration — registre des migrations appliquées.
  • _yoyo_log — historique des opérations.
  • _yoyo_lock — verrou (anti-race) pendant qu'une migration tourne.

Visible côté psql via \dt _yoyo_*. Ne pas modifier à la main.

Contributors

No contributors

Changelog

No recent changes