use diesel::Connection;
use diesel::migration::Migration;
use diesel_migrations::{HarnessWithOutput, MigrationHarness};
pub use diesel_migrations::EmbeddedMigrations;
pub use diesel_migrations::embed_migrations;
#[derive(Debug)]
pub struct MigrationResult {
pub applied: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum MigrationError {
#[error("failed to connect to database: {0}")]
Connection(String),
#[error("migration failed: {0}")]
Migration(String),
}
pub fn run_pending(
database_url: &str,
migrations: EmbeddedMigrations,
) -> Result<MigrationResult, MigrationError> {
let mut conn = diesel::PgConnection::establish(database_url)
.map_err(|e| MigrationError::Connection(e.to_string()))?;
let mut harness = HarnessWithOutput::write_to_stdout(&mut conn);
let applied = harness
.run_pending_migrations(migrations)
.map_err(|e| MigrationError::Migration(e.to_string()))?;
Ok(MigrationResult {
applied: applied.iter().map(|m| format!("{m}")).collect(),
})
}
pub fn pending_migrations(
database_url: &str,
migrations: EmbeddedMigrations,
) -> Result<Vec<String>, MigrationError> {
let mut conn = diesel::PgConnection::establish(database_url)
.map_err(|e| MigrationError::Connection(e.to_string()))?;
let pending = conn
.pending_migrations(migrations)
.map_err(|e| MigrationError::Migration(e.to_string()))?;
Ok(pending
.iter()
.map(|m| m.name().version().to_string())
.collect())
}
#[allow(clippy::cognitive_complexity)]
pub(crate) fn auto_migrate(
database_url: &str,
profile: Option<&str>,
migrations: EmbeddedMigrations,
) {
let is_dev = profile == Some("dev");
if is_dev {
tracing::info!("Dev mode: running pending database migrations...");
match run_pending(database_url, migrations) {
Ok(result) if result.applied.is_empty() => {
tracing::info!("No pending migrations");
}
Ok(result) => {
for name in &result.applied {
tracing::info!(migration = %name, "Applied migration");
}
tracing::info!(
count = result.applied.len(),
"All pending migrations applied"
);
}
Err(e) => {
tracing::error!(error = %e, "Failed to run migrations");
std::process::exit(1);
}
}
} else {
match pending_migrations(database_url, migrations) {
Ok(pending) if pending.is_empty() => {
tracing::info!("Database migrations are up to date");
}
Ok(pending) => {
tracing::warn!(
count = pending.len(),
"Pending migrations detected. Run `autumn migrate` to apply them."
);
for name in &pending {
tracing::warn!(migration = %name, "Pending migration");
}
}
Err(e) => {
tracing::warn!(error = %e, "Could not check migration status");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn migration_result_debug() {
let result = MigrationResult {
applied: vec!["00000000000001".to_string()],
};
let debug = format!("{result:?}");
assert!(debug.contains("00000000000001"));
}
#[test]
fn migration_error_display_connection() {
let err = MigrationError::Connection("refused".to_string());
let msg = err.to_string();
assert!(msg.contains("connect"));
assert!(msg.contains("refused"));
}
#[test]
fn migration_error_display_migration() {
let err = MigrationError::Migration("syntax error".to_string());
let msg = err.to_string();
assert!(msg.contains("migration failed"));
assert!(msg.contains("syntax error"));
}
}