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 :
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¶
Sortie attendue :
Vérifiez la table dans PostgreSQL :
Tester le rollback¶
Sortie attendue :
Vérifiez que la table a bien disparu :
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() |