use crate::{PgBackend, classify};
use persistant_relational::RelError;
use sqlx::SqlSafeStr;
use sqlx::migrate::{Migration as SqlxMigration, MigrationType, Migrator};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Migration {
pub version: i64,
pub description: String,
pub sql: String,
}
impl Migration {
pub fn new(version: i64, description: impl Into<String>, sql: impl Into<String>) -> Self {
Migration {
version,
description: description.into(),
sql: sql.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MigrateError {
ChecksumMismatch {
version: i64,
},
NewerThanProgram {
version: i64,
},
Dirty {
version: i64,
},
Failed {
version: i64,
error: RelError,
},
Backend(String),
}
impl fmt::Display for MigrateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MigrateError::ChecksumMismatch { version } => {
write!(f, "migration {version} was applied and has been modified")
}
MigrateError::NewerThanProgram { version } => write!(
f,
"the database has applied migration {version}, which this program does not know"
),
MigrateError::Dirty { version } => {
write!(
f,
"migration {version} is partially applied; fix it by hand"
)
}
MigrateError::Failed { version, error } => {
write!(f, "migration {version} failed: {error}")
}
MigrateError::Backend(m) => write!(f, "migration: {m}"),
}
}
}
impl std::error::Error for MigrateError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MigrateReport {
pub applied: Vec<i64>,
pub already_applied: Vec<i64>,
}
impl PgBackend {
pub async fn migrate(&self, migrations: &[Migration]) -> Result<MigrateReport, MigrateError> {
let sqlx_migrations: Vec<SqlxMigration> = migrations
.iter()
.map(|m| {
SqlxMigration::new(
m.version,
m.description.clone().into(),
MigrationType::Simple,
sqlx::AssertSqlSafe(m.sql.clone()).into_sql_str(),
false,
)
})
.collect();
let before = self.applied_versions().await?;
let migrator = Migrator::with_migrations(sqlx_migrations);
migrator.run(&self.pool).await.map_err(|e| match e {
sqlx::migrate::MigrateError::VersionMismatch(version) => {
MigrateError::ChecksumMismatch { version }
}
sqlx::migrate::MigrateError::VersionMissing(version) => {
MigrateError::NewerThanProgram { version }
}
sqlx::migrate::MigrateError::Dirty(version) => MigrateError::Dirty { version },
sqlx::migrate::MigrateError::ExecuteMigration(e, version) => MigrateError::Failed {
version,
error: classify(e),
},
other => MigrateError::Backend(other.to_string()),
})?;
let after = self.applied_versions().await?;
let applied = after
.iter()
.copied()
.filter(|v| !before.contains(v))
.collect();
Ok(MigrateReport {
applied,
already_applied: before,
})
}
pub async fn applied_versions(&self) -> Result<Vec<i64>, MigrateError> {
let exists: bool = sqlx::query_scalar("SELECT to_regclass('_sqlx_migrations') IS NOT NULL")
.fetch_one(&self.pool)
.await
.map_err(|e| MigrateError::Backend(e.to_string()))?;
if !exists {
return Ok(Vec::new());
}
sqlx::query_scalar("SELECT version FROM _sqlx_migrations WHERE success ORDER BY version")
.fetch_all(&self.pool)
.await
.map_err(|e| MigrateError::Backend(e.to_string()))
}
}