pub mod prelude {
pub use async_trait::async_trait;
pub use sea_orm_migration::prelude::*;
}
pub use sea_orm::DbErr;
pub use sea_orm_migration::MigrationTrait;
pub use sea_orm_migration::MigratorTrait;
pub use sea_orm_migration::SchemaManager;
pub use sea_orm_migration::prelude::{DeriveIden, DeriveMigrationName};
#[derive(Debug, PartialEq)]
pub enum MigrateCommand {
Up,
Down { steps: u32 },
Status,
Fresh,
Reset,
}
pub fn parse_args(args: &[String]) -> Result<MigrateCommand, String> {
match args.first().map(|s| s.as_str()) {
Some("up") => Ok(MigrateCommand::Up),
Some("down") => {
let steps = parse_steps(&args[1..])?;
Ok(MigrateCommand::Down { steps })
}
Some("status") => Ok(MigrateCommand::Status),
Some("fresh") => Ok(MigrateCommand::Fresh),
Some("reset") => Ok(MigrateCommand::Reset),
Some(other) => Err(format!(
"Unknown subcommand: '{}'. Valid: up | down [--steps N] | status | fresh | reset",
other
)),
None => Err(
"No subcommand given. Usage: rapina_migrate <up|down|status|fresh|reset>".to_string(),
),
}
}
fn parse_steps(args: &[String]) -> Result<u32, String> {
if args.is_empty() {
return Ok(1);
}
match args[0].as_str() {
"--steps" => {
if args.len() < 2 {
return Err("--steps requires a value".to_string());
}
args[1]
.parse::<u32>()
.map_err(|_| format!("Invalid steps value: '{}'", args[1]))
}
other => Err(format!("Unexpected argument: '{}'", other)),
}
}
#[macro_export]
macro_rules! migrations {
($($module:ident ),* $(,)?) => {
pub struct Migrator;
#[$crate::async_trait::async_trait]
impl $crate::sea_orm_migration::MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn $crate::sea_orm_migration::MigrationTrait>> {
vec![
$(Box::new($module::Migration), )*
]
}
}
}
}
pub async fn run_pending<M: MigratorTrait>(
conn: &sea_orm::DatabaseConnection,
) -> Result<(), DbErr> {
tracing::info!("Running pending database migrations...");
M::up(conn, None).await?;
tracing::info!("All migrations applied successfully");
Ok(())
}
pub fn format_pending_migrations_warning(names: &[&str]) -> String {
let n = names.len();
let list = names.join(", ");
format!(
"You have {n} unapplied migration(s). Your project may not work correctly until you apply them.\n\
Pending migration(s): {list}.\n\
Run `rapina migrate up` to apply them. If the migrate binary is not set up yet, run `rapina migrate init` first."
)
}
pub async fn run_startup_migrations<M: MigratorTrait>(
conn: &sea_orm::DatabaseConnection,
auto_apply: bool,
) -> Result<(), DbErr> {
if auto_apply {
run_pending::<M>(conn).await
} else {
let pending = M::get_pending_migrations(conn).await?;
if !pending.is_empty() {
let names: Vec<&str> = pending.iter().map(|m| m.name()).collect();
tracing::warn!("{}", format_pending_migrations_warning(&names));
}
Ok(())
}
}
pub async fn rollback<M: MigratorTrait>(
conn: &sea_orm::DatabaseConnection,
steps: Option<u32>,
) -> Result<(), DbErr> {
let steps = steps.unwrap_or(1);
tracing::info!(steps, "Rolling back migrations...");
M::down(conn, Some(steps)).await?;
tracing::info!("Rollback complete");
Ok(())
}
pub async fn status<M: MigratorTrait>(conn: &sea_orm::DatabaseConnection) -> Result<(), DbErr> {
M::status(conn).await
}
pub async fn run_cli<M: MigratorTrait>() {
dotenvy::dotenv().ok();
let raw_args: Vec<String> = std::env::args().skip(1).collect();
let db_url = match std::env::var("DATABASE_URL") {
Ok(url) => url,
Err(_) => {
eprintln!("Error: DATABASE_URL environment variable is not set.");
eprintln!(" Set it in your .env file or export it before running.");
std::process::exit(1);
}
};
let cmd = match parse_args(&raw_args) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let conn = match sea_orm::Database::connect(&db_url).await {
Ok(c) => c,
Err(e) => {
eprintln!("Error: Could not connect to database: {e}");
std::process::exit(1);
}
};
let result: Result<(), sea_orm::DbErr> = match cmd {
MigrateCommand::Up => {
let r = M::up(&conn, None).await;
if r.is_ok() {
println!("Migrations applied successfully.");
}
r
}
MigrateCommand::Down { steps } => {
let r = M::down(&conn, Some(steps)).await;
if r.is_ok() {
println!("Rolled back {steps} migration(s).");
}
r
}
MigrateCommand::Status => M::status(&conn).await,
MigrateCommand::Fresh => {
let r = M::fresh(&conn).await;
if r.is_ok() {
println!("Database cleared and migrations re-applied (fresh).");
}
r
}
MigrateCommand::Reset => {
let r = M::refresh(&conn).await;
if r.is_ok() {
println!("Migrations reset (down all + up all).");
}
r
}
};
if let Err(e) = result {
eprintln!("Error: {e}");
std::process::exit(1);
}
}