Migrations de Base de Données
Contexte
Le projet utilise le client Supabase natif pour toutes les opérations de données — pas SQLAlchemy ORM. Alembic gère uniquement les migrations de schéma.
Cette approche hybride signifie :
- Les modèles SQLAlchemy sont créés progressivement, uniquement pour les tables qu'on veut modifier
target_metadata = Nonedansalembic/env.py— pas de détection automatique des changements- L'autogenerate ne fonctionne que si un modèle SQLAlchemy existe pour la table modifiée
Workflow Complet : Comment faire une migration
Étape 1 : Créer le modèle SQLAlchemy pour la table concernée
L'autogenerate d'Alembic nécessite un modèle SQLAlchemy qui reflète la table existante.
Créer le modèle dans un fichier de modèles (ex: backend/app/models/schema.py) :
from sqlalchemy import Column, String, Boolean, DateTime
from sqlalchemy.dialects.postgresql import UUID
import uuid
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class University(Base):
__tablename__ = 'universities'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(255), nullable=False)
university_code = Column(String(50), unique=True, nullable=False)
short_name = Column(String(100))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default="NOW()")
updated_at = Column(DateTime(timezone=True), server_default="NOW()")
Important : Ce modèle n'est nécessaire que pour la table modifiée, pas pour toutes les tables du projet.
Étape 2 : Générer la migration
alembic revision --autogenerate -m "add_is_verified_to_universities"
Convention de nommage pour les descriptions :
add_<colonne>_to_<table>— ajout de colonnecreate_<table>— création de tabledrop_<table>— suppression de tablealter_<table>_constraints— modification de contraintes
Étape 3 : Vérifier le fichier généré (OBLIGATOIRE)
Jamais appliquer une migration sans l'avoir lue.
Le fichier généré se trouve dans backend/alembic/versions/.
Vérifications à faire :
- La table modifiée est bien la bonne (pas de table système Supabase)
- Les colonnes ajoutées/modifiées correspondent à ce qui est prévu
- Pas de suppression accidentelle de données
- La fonction
downgrade()est correcte (permettra le rollback)
Exemple de fichier généré :
"""add_is_verified_to_universities
Revision ID: abc123
Revises: 61e524cfcce2
Create Date: 2026-02-21 10:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = 'abc123'
down_revision: Union[str, Sequence[str], None] = '61e524cfcce2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('universities', sa.column('is_verified', sa.Boolean(), server_default='false'))
def downgrade() -> None:
op.drop_column('universities', 'is_verified')
Étape 4 : Appliquer la migration
alembic upgrade head
Vérifier que la migration s'est appliquée :
alembic current
Étape 5 : Commiter
Le fichier de migration doit être commité avec le code qui l'utilise dans la même PR. Pas de migration orpheline.
Commandes de Référence
| Commande | Description |
|---|---|
alembic current | Affiche la migration actuellement appliquée |
alembic history | Affiche l'historique complet des migrations |
alembic upgrade head | Applique toutes les migrations en attente |
alembic downgrade -1 | Annule la dernière migration appliquée |
alembic revision --autogenerate -m "description" | Génère une migration automatique |
alembic revision -m "description" | Crée une migration vide (manuelle) |
alembic stamp head | Marque une migration comme appliquée sans l'exécuter |
Règles Absolues
-
Jamais de modification directe du schéma Supabase sans passer par Alembic
- Pas de console Supabase pour ajouter des colonnes en prod
- Toutes les modifications passent par migration
-
Toujours lire le fichier généré avant d'appliquer
- L'autogenerate peut faire des erreurs
- Vérifier le SQL généré manuellement
-
Le fichier de migration = commité dans la même PR que le code
- Pas de migration séparément du code qui l'utilise
- reviewers doivent voir migration + code ensemble
-
Ne JAMAIS modifier un fichier de migration déjà appliqué en prod
- Si erreur, créer une nouvelle migration de correction
- Modifier une migration existante = désynchronisation garantie
Exemple Concret : Migration Manuelle
Quand on n'a pas de modèle SQLAlchemy, on écrit la migration à la main.
Création
alembic revision -m "add_country_column_to_companies"
Écrire le SQL dans le fichier généré
"""add_country_column_to_companies
Revision ID: def456
Revises: abc123
Create Date: 2026-02-21 11:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = 'def456'
down_revision: Union[str, Sequence[str], None] = 'abc123'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column('companies', sa.column('country', sa.String(100), server_default='Benin'))
def downgrade() -> None:
op.drop_column('companies', 'country')
Appliquer
alembic upgrade head
En Cas de Problème
Voir ce qui s'est mal passé
# État actuel vs attendu
alembic current
alembic history
# Voir le détail d'une migration
alembic show <revision_id>
Rollback
# Annuler la dernière migration
alembic downgrade -1
# Annuler plusieurs migrations
alembic downgrade -3
Désynchronisation avec prod
Si alembic current ne correspond pas à la prod :
- Vérifier l'état de prod :
SELECT * FROM alembic_version; - Si la migration est déjà appliquée en prod mais pas en local :
alembic stamp <revision_id> - Si la migration n'est pas appliquée en prod mais local est ahead :
alembic downgrade <revision_id>
Attention : Ces commandes modifient le state Alembic. Utiliser avec précaution et toujours vérifier le résultat.