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.

Prerequisites

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

Expected output:

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

Expected output:

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