Ajouter une migration de base de données (SeaORM)
Ce guide couvre la création, l'application et le rollback d'une migration SeaORM dans gitrust.
Pré-requis
- Environnement local fonctionnel (voir
tutorials/01-getting-started.md). - PostgreSQL local démarré.
Convention de nommage
Chaque fichier de migration suit le format :
m{AAAAMMJJ}_{NNNNNN}_{description_snake_case}.rs
AAAAMMJJ: date du jour en UTC.NNNNNN: numéro séquentiel sur 6 chiffres, incrémenté par rapport au dernier fichier existant.description_snake_case: description courte en minuscules.
Exemple : m20260501_000023_create_notify_jobs.rs
Consultez le tableau des migrations existantes dans reference/schema-base-donnees.md avant d'attribuer un numéro pour éviter les collisions.
Structure d'un fichier de migration
Créez crates/gitrust-core/src/migrations/m20260501_000023_create_notify_jobs.rs :
use sea_orm_migration::prelude::*; /// Nom de migration dérivé automatiquement du nom du module. #[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .create_table( Table::create() .table(NotifyJobs::Table) .if_not_exists() .col(ColumnDef::new(NotifyJobs::Id).uuid().not_null().primary_key()) .col(ColumnDef::new(NotifyJobs::OwnerId).uuid().not_null()) .col( ColumnDef::new(NotifyJobs::Status) .string() .not_null() .default("pending"), ) .col(ColumnDef::new(NotifyJobs::ErrorMessage).text().null()) .col( ColumnDef::new(NotifyJobs::CreatedAt) .timestamp_with_time_zone() .not_null(), ) .col( ColumnDef::new(NotifyJobs::UpdatedAt) .timestamp_with_time_zone() .not_null(), ) .to_owned(), ) .await?; // Index composite pour les requêtes fréquentes manager .create_index( Index::create() .table(NotifyJobs::Table) .name("notify_jobs_owner_status_idx") .col(NotifyJobs::OwnerId) .col(NotifyJobs::Status) .to_owned(), ) .await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(NotifyJobs::Table).to_owned()) .await } } /// Identifiant Iden pour chaque colonne et la table. #[derive(DeriveIden)] enum NotifyJobs { Table, Id, OwnerId, Status, ErrorMessage, CreatedAt, UpdatedAt, }
Règles obligatoires :
up()doit toujours utiliser.if_not_exists()pour être idempotent.down()doit annuler exactement ce queup()a créé. Toute migration sansdown()fonctionnel sera refusée en review.- Les colonnes
created_atetupdated_atsontTIMESTAMPTZ NOT NULL(pasTIMESTAMP— gitrust est UTC strict). - Les clés primaires sont des
UUID(jamais des entiers auto-incrémentés).
Enregistrer la migration dans le Migrator
Ouvrez crates/gitrust-core/src/migrations/mod.rs et ajoutez la migration dans la liste du Migrator en respectant l'ordre chronologique :
pub mod m20260501_000023_create_notify_jobs; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec<Box<dyn MigrationTrait>> { vec![ // ... migrations existantes dans l'ordre ... Box::new(m20260501_000023_create_notify_jobs::Migration), ] } }
L'ordre est strict : SeaORM exécute les migrations dans l'ordre de la liste. Ne réorganisez jamais les migrations existantes.
Appliquer la migration en local
cargo run --bin gitrust -- migrate
Sortie attendue :
Applying migration 'm20260501_000023_create_notify_jobs' Migration applied successfully
Vérifiez la table dans PostgreSQL :
psql $DATABASE_URL -c "\d notify_jobs"
Tester le rollback
cargo run --bin gitrust -- migrate down
Sortie attendue :
Rolling back migration 'm20260501_000023_create_notify_jobs' Rollback applied successfully
Vérifiez que la table a bien disparu :
psql $DATABASE_URL -c "\dt notify_jobs" # → doit retourner "no relations found"
Cas d'usage courants
Ajouter une colonne à une table existante
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .alter_table( Table::alter() .table(Repositories::Table) .add_column( ColumnDef::new(Repositories::IsArchived) .boolean() .not_null() .default(false), ) .to_owned(), ) .await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .alter_table( Table::alter() .table(Repositories::Table) .drop_column(Repositories::IsArchived) .to_owned(), ) .await }
Créer un index unique composite
manager .create_index( Index::create() .table(Labels::Table) .name("labels_owner_repo_name_type_idx") .col(Labels::OwnerId) .col(Labels::RepositoryId) .col(Labels::Name) .col(Labels::LabelType) .unique() .to_owned(), ) .await
Gotchas PostgreSQL
| Situation | Comportement | Solution |
|---|---|---|
Ajouter une colonne NOT NULL sans DEFAULT sur une table peuplée | Erreur PG : column cannot be added without a default value | Toujours fournir un DEFAULT lors de l'ajout d'une colonne NOT NULL |
ALTER TABLE ... DROP COLUMN avec des FK dépendantes | Erreur PG : contrainte FK bloquante | Supprimer d'abord les FK avec ForeignKey::drop() dans down() |
TIMESTAMPTZ vs TIMESTAMP | TIMESTAMP ignore le fuseau horaire, crée des bugs sur les serveurs non-UTC | Utiliser toujours timestamp_with_time_zone() |
| Migration dans une transaction | Toutes les migrations gitrust s'exécutent dans une transaction implicite | Ne pas appeler BEGIN/COMMIT manuellement dans up() |
GitRust