use chrono::{DateTime, Utc};
use crate::db::DbClient;
use crate::error::{Result, WaypointError};
#[derive(Debug, Clone)]
pub struct AppliedMigration {
pub installed_rank: i32,
pub version: Option<String>,
pub description: String,
pub migration_type: String,
pub script: String,
pub checksum: Option<i32>,
pub installed_by: String,
pub installed_on: DateTime<Utc>,
pub execution_time: i32,
pub success: bool,
pub reversal_sql: Option<String>,
}
pub const NON_REALIGNABLE_TYPES: [&str; 2] = ["UNDO_SQL", "BASELINE"];
#[cfg(feature = "postgres")]
pub use crate::engines::postgres::history::{
create_history_table, delete_failed_migrations, get_applied_migrations, has_entries,
history_table_exists, insert_applied_migration, next_installed_rank, update_checksum,
update_repeatable_checksum,
};
pub async fn create_history_table_db(client: &DbClient, schema: &str, table: &str) -> Result<()> {
let dialect = client.dialect();
let ddl = dialect.history_table_ddl(schema, table);
if let Err(e) = client.execute_raw(&ddl).await {
if !is_benign_index_dup(&e) {
return Err(e);
}
}
upgrade_history_table_db(client, schema, table).await?;
Ok(())
}
async fn upgrade_history_table_db(client: &DbClient, schema: &str, table: &str) -> Result<()> {
if history_column_exists_db(client, schema, table, "reversal_sql").await? {
return Ok(());
}
let fq = client.dialect().qualified_table(schema, table);
let sql = match client.dialect_kind() {
crate::dialect::DialectKind::Postgres => {
format!("ALTER TABLE {fq} ADD COLUMN IF NOT EXISTS reversal_sql TEXT")
}
crate::dialect::DialectKind::Mysql => {
format!("ALTER TABLE {fq} ADD COLUMN reversal_sql LONGTEXT")
}
};
client.execute_raw(&sql).await.map(|_| ()).map_err(|e| {
WaypointError::ConfigError(format!(
"Could not add the `reversal_sql` column to {}.{}: {}. Waypoint reads \
this column on every history query, so the connecting role needs \
ALTER on the history table at least once to complete the upgrade.",
schema, table, e
))
})
}
async fn history_column_exists_db(
client: &DbClient,
schema: &str,
table: &str,
column: &str,
) -> Result<bool> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
let row = c
.query_one(
"SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
)",
&[&schema, &table, &column],
)
.await?;
Ok(row.get::<_, bool>(0))
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
use mysql_async::prelude::*;
let mut conn = pool.get_conn().await?;
let found: Option<i64> = conn
.exec_first(
"SELECT 1 FROM information_schema.columns \
WHERE table_schema = ? AND table_name = ? AND column_name = ? LIMIT 1",
(schema, table, column),
)
.await?;
Ok(found.is_some())
}
}
}
fn is_benign_index_dup(e: &WaypointError) -> bool {
let msg = e.to_string().to_lowercase();
msg.contains("er_dup_keyname") || msg.contains("duplicate key name")
}
pub async fn history_table_exists_db(client: &DbClient, schema: &str, table: &str) -> Result<bool> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::history_table_exists(c, schema, table).await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::history_table_exists(pool, schema, table).await
}
}
}
pub async fn get_applied_migrations_db(
client: &DbClient,
schema: &str,
table: &str,
) -> Result<Vec<AppliedMigration>> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::get_applied_migrations(c, schema, table).await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::get_applied_migrations(pool, schema, table).await
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn insert_applied_migration_db(
client: &DbClient,
schema: &str,
table: &str,
version: Option<&str>,
description: &str,
migration_type: &str,
script: &str,
checksum: Option<i32>,
installed_by: &str,
execution_time: i32,
success: bool,
) -> Result<()> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::insert_applied_migration(
c,
schema,
table,
version,
description,
migration_type,
script,
checksum,
installed_by,
execution_time,
success,
)
.await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::insert_applied_migration(
pool,
schema,
table,
version,
description,
migration_type,
script,
checksum,
installed_by,
execution_time,
success,
)
.await
}
}
}
pub async fn has_entries_db(client: &DbClient, schema: &str, table: &str) -> Result<bool> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::has_entries(c, schema, table).await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::has_entries(pool, schema, table).await
}
}
}
pub async fn delete_failed_migrations_db(
client: &DbClient,
schema: &str,
table: &str,
) -> Result<u64> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::delete_failed_migrations(c, schema, table).await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::delete_failed_migrations(pool, schema, table).await
}
}
}
pub async fn update_checksum_db(
client: &DbClient,
schema: &str,
table: &str,
version: &str,
new_checksum: i32,
) -> Result<()> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::update_checksum(
c,
schema,
table,
version,
new_checksum,
)
.await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::update_checksum(
pool,
schema,
table,
version,
new_checksum,
)
.await
}
}
}
pub async fn update_repeatable_checksum_db(
client: &DbClient,
schema: &str,
table: &str,
script: &str,
new_checksum: i32,
) -> Result<()> {
match client {
#[cfg(feature = "postgres")]
DbClient::Postgres(c) => {
crate::engines::postgres::history::update_repeatable_checksum(
c,
schema,
table,
script,
new_checksum,
)
.await
}
#[cfg(feature = "mysql")]
DbClient::Mysql(pool) => {
crate::engines::mysql::history::update_repeatable_checksum(
pool,
schema,
table,
script,
new_checksum,
)
.await
}
}
}
pub fn effective_applied_versions(
applied: &[AppliedMigration],
) -> std::collections::HashSet<String> {
let mut effective = std::collections::HashSet::new();
for am in applied {
if !am.success {
continue;
}
if let Some(ref version) = am.version {
if am.migration_type == "UNDO_SQL" {
effective.remove(version);
} else {
effective.insert(version.clone());
}
}
}
effective
}