pub use self::logger::*;
use thiserror::Error;
use tokio_postgres::Client;
use tokio_postgres::error::SqlState;
mod logger;
pub async fn migrate(
client: &Client,
mut logger: impl Logger,
history_table: &str,
migrations: &[Migration],
) -> Result<(), Error> {
let mut sql = format!("SELECT version FROM {history_table} ORDER BY version DESC LIMIT 1");
let current: Option<i32> = match client.query_opt_scalar(&sql, &[]).await {
Ok(v) => v,
Err(e) if e.code() == Some(&SqlState::UNDEFINED_TABLE) => {
logger.create_history_table(history_table);
sql = format!(
"CREATE TABLE {history_table} (version integer NOT NULL, name text, applied_time timestamp with time zone NOT NULL, PRIMARY KEY (version))"
);
client
.batch_execute(&sql)
.await
.map_err(Error::CreateHistoryTable)?;
None
}
Err(e) => return Err(Error::QueryVersion(e)),
};
let current = current
.map(usize::try_from)
.transpose()
.map_err(|_| Error::InvalidVersion)?;
let next = current.map(|v| v + 1).unwrap_or(0);
let sql =
format!("INSERT INTO {history_table} (version, name, applied_time) VALUES ($1, $2, now())");
logger.start(current);
for next in next.. {
let m = match migrations.get(next) {
Some(v) => v,
None => break,
};
logger.run(m.name);
client
.batch_execute(m.script)
.await
.map_err(|e| Error::ExecuteMigration(m.name, e))?;
let version = i32::try_from(next).unwrap();
client
.execute(&sql, &[&version, &m.name])
.await
.map_err(|e| Error::UpdateVersion(m.name, e))?;
}
Ok(())
}
pub struct Migration {
pub name: &'static str,
pub script: &'static str,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("couldn't create table for migrations history")]
CreateHistoryTable(#[source] tokio_postgres::Error),
#[error("couldn't query database version")]
QueryVersion(#[source] tokio_postgres::Error),
#[error("current database version is invalid")]
InvalidVersion,
#[error("couldn't execute migration '{0}'")]
ExecuteMigration(&'static str, #[source] tokio_postgres::Error),
#[error("couldn't update database version to {0}")]
UpdateVersion(&'static str, #[source] tokio_postgres::Error),
}