use std::collections::HashMap;
use chrono::{DateTime, Utc};
use serde::Serialize;
#[cfg(feature = "postgres")]
use tokio_postgres::Client;
use crate::config::WaypointConfig;
use crate::db::DbClient;
use crate::error::Result;
use crate::history::{self, AppliedMigration};
use crate::migration::{scan_migrations, MigrationKind, MigrationVersion, ResolvedMigration};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum MigrationState {
Pending,
Applied,
Failed,
Missing,
Outdated,
OutOfOrder,
BelowBaseline,
Ignored,
Baseline,
Undone,
}
impl std::fmt::Display for MigrationState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MigrationState::Pending => write!(f, "Pending"),
MigrationState::Applied => write!(f, "Applied"),
MigrationState::Failed => write!(f, "Failed"),
MigrationState::Missing => write!(f, "Missing"),
MigrationState::Outdated => write!(f, "Outdated"),
MigrationState::OutOfOrder => write!(f, "Out of Order"),
MigrationState::BelowBaseline => write!(f, "Below Baseline"),
MigrationState::Ignored => write!(f, "Ignored"),
MigrationState::Baseline => write!(f, "Baseline"),
MigrationState::Undone => write!(f, "Undone"),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct MigrationInfo {
pub version: Option<String>,
pub description: String,
pub migration_type: String,
pub script: String,
pub state: MigrationState,
pub installed_on: Option<DateTime<Utc>>,
pub execution_time: Option<i32>,
pub checksum: Option<i32>,
}
#[cfg(feature = "postgres")]
pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<Vec<MigrationInfo>> {
let schema = &config.migrations.schema;
let table = &config.migrations.table;
if !history::history_table_exists(client, schema, table).await? {
let resolved = scan_migrations(&config.migrations.locations)?;
return Ok(pending_only(resolved));
}
let applied = history::get_applied_migrations(client, schema, table).await?;
let resolved = scan_migrations(&config.migrations.locations)?;
Ok(merge(applied, resolved))
}
pub async fn execute_db(client: &DbClient, config: &WaypointConfig) -> Result<Vec<MigrationInfo>> {
let schema = client.resolve_schema(&config.migrations.schema).await?;
let schema = schema.as_str();
let table = &config.migrations.table;
if !history::history_table_exists_db(client, schema, table).await? {
let resolved = scan_migrations(&config.migrations.locations)?;
return Ok(pending_only(resolved));
}
let applied = history::get_applied_migrations_db(client, schema, table).await?;
let resolved = scan_migrations(&config.migrations.locations)?;
Ok(merge(applied, resolved))
}
fn pending_only(resolved: Vec<ResolvedMigration>) -> Vec<MigrationInfo> {
resolved
.into_iter()
.filter(|m| !m.is_undo())
.map(|m| {
let version = m.version().map(|v| v.raw.clone());
let migration_type = m.migration_type().to_string();
MigrationInfo {
version,
description: m.description,
migration_type,
script: m.script,
state: MigrationState::Pending,
installed_on: None,
execution_time: None,
checksum: Some(m.checksum),
}
})
.collect()
}
fn merge(applied: Vec<AppliedMigration>, resolved: Vec<ResolvedMigration>) -> Vec<MigrationInfo> {
let effective = history::effective_applied_versions(&applied);
let resolved_by_version: HashMap<String, &ResolvedMigration> = resolved
.iter()
.filter(|m| m.is_versioned())
.filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
.collect();
let resolved_by_script: HashMap<String, &ResolvedMigration> = resolved
.iter()
.filter(|m| !m.is_versioned() && !m.is_undo())
.map(|m| (m.script.clone(), m))
.collect();
let baseline_version = applied
.iter()
.find(|a| a.migration_type == "BASELINE")
.and_then(|a| a.version.as_ref())
.and_then(|v| MigrationVersion::parse(v).ok());
let highest_applied = effective
.iter()
.filter_map(|v| MigrationVersion::parse(v).ok())
.max();
let mut infos: Vec<MigrationInfo> = Vec::new();
let mut seen_versions: HashMap<String, bool> = HashMap::new();
let mut seen_scripts: HashMap<String, bool> = HashMap::new();
for am in &applied {
let is_versioned = am.version.is_some();
let is_repeatable = am.version.is_none() && am.migration_type != "BASELINE";
let state = if am.migration_type == "BASELINE" {
MigrationState::Baseline
} else if am.migration_type == "UNDO_SQL" {
MigrationState::Undone
} else if !am.success {
MigrationState::Failed
} else if is_versioned {
if let Some(ref version) = am.version {
if !effective.contains(version) {
MigrationState::Undone
} else if resolved_by_version.contains_key(version) {
MigrationState::Applied
} else {
MigrationState::Missing
}
} else {
MigrationState::Applied
}
} else if is_repeatable {
if let Some(resolved) = resolved_by_script.get(&am.script) {
if Some(resolved.checksum) != am.checksum {
MigrationState::Outdated
} else {
MigrationState::Applied
}
} else {
MigrationState::Missing
}
} else {
MigrationState::Applied
};
if let Some(ref v) = am.version {
seen_versions.insert(v.clone(), true);
}
if am.version.is_none() {
seen_scripts.insert(am.script.clone(), true);
}
infos.push(MigrationInfo {
version: am.version.clone(),
description: am.description.clone(),
migration_type: am.migration_type.clone(),
script: am.script.clone(),
state,
installed_on: Some(am.installed_on),
execution_time: Some(am.execution_time),
checksum: am.checksum,
});
}
for m in &resolved {
if m.is_undo() {
continue;
}
match &m.kind {
MigrationKind::Versioned(version) => {
if seen_versions.contains_key(&version.raw) {
continue;
}
let state = if let Some(ref bv) = baseline_version {
if version <= bv {
MigrationState::BelowBaseline
} else if let Some(ref highest) = highest_applied {
if version < highest {
MigrationState::OutOfOrder
} else {
MigrationState::Pending
}
} else {
MigrationState::Pending
}
} else if let Some(ref highest) = highest_applied {
if version < highest {
MigrationState::OutOfOrder
} else {
MigrationState::Pending
}
} else {
MigrationState::Pending
};
infos.push(MigrationInfo {
version: Some(version.raw.clone()),
description: m.description.clone(),
migration_type: m.migration_type().to_string(),
script: m.script.clone(),
state,
installed_on: None,
execution_time: None,
checksum: Some(m.checksum),
});
}
MigrationKind::Repeatable => {
if seen_scripts.contains_key(&m.script) {
continue;
}
infos.push(MigrationInfo {
version: None,
description: m.description.clone(),
migration_type: m.migration_type().to_string(),
script: m.script.clone(),
state: MigrationState::Pending,
installed_on: None,
execution_time: None,
checksum: Some(m.checksum),
});
}
MigrationKind::Undo(_) => unreachable!("undo files are skipped above"),
}
}
infos.sort_by(|a, b| match (&a.version, &b.version) {
(Some(av), Some(bv)) => {
let pa = MigrationVersion::parse(av);
let pb = MigrationVersion::parse(bv);
match (pa, pb) {
(Ok(pa), Ok(pb)) => pa.cmp(&pb),
_ => av.cmp(bv),
}
}
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => a.description.cmp(&b.description),
});
infos
}