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

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 que up() a créé. Toute migration sans down() fonctionnel sera refusée en review.
  • Les colonnes created_at et updated_at sont TIMESTAMPTZ NOT NULL (pas TIMESTAMP — 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()

Voir aussi