use anyhow::{bail, Context, Result};
use std::fs;
use crate::state::Baseline;
use crate::{AppliedMigration, Migration};
pub fn version_lte(v1: &str, v2: &str) -> bool {
v1 <= v2
}
#[derive(Debug, Clone)]
pub struct DeletedItem {
pub path: String,
pub is_directory: bool,
}
pub fn delete_baselined_migrations(
baseline_version: &str,
available: &[Migration],
) -> Result<Vec<DeletedItem>> {
let mut deleted = Vec::new();
for migration in available {
if version_lte(&migration.version, baseline_version) {
if migration.file_path.exists() {
fs::remove_file(&migration.file_path).with_context(|| {
format!(
"Failed to delete migration file: {}",
migration.file_path.display()
)
})?;
deleted.push(DeletedItem {
path: migration.file_path.display().to_string(),
is_directory: false,
});
}
if let Some(parent) = migration.file_path.parent() {
let asset_dir = parent.join(&migration.id);
if asset_dir.exists() && asset_dir.is_dir() {
fs::remove_dir_all(&asset_dir).with_context(|| {
format!(
"Failed to delete migration asset directory: {}",
asset_dir.display()
)
})?;
deleted.push(DeletedItem {
path: asset_dir.display().to_string(),
is_directory: true,
});
}
}
}
}
Ok(deleted)
}
pub fn validate_baseline(
version: &str,
available: &[Migration],
applied: &[AppliedMigration],
existing_baseline: Option<&Baseline>,
) -> Result<()> {
let matching_migration = available.iter().find(|m| m.version == version);
if matching_migration.is_none() {
bail!("No migration found with version '{}'", version);
}
if let Some(existing) = existing_baseline {
if version < existing.version.as_str() {
bail!(
"Cannot move baseline backward from '{}' to '{}'",
existing.version,
version
);
}
}
let applied_ids: std::collections::HashSet<&str> =
applied.iter().map(|a| a.id.as_str()).collect();
for migration in available {
if version_lte(&migration.version, version) && !applied_ids.contains(migration.id.as_str())
{
bail!(
"Cannot baseline: migration '{}' has not been applied",
migration.id
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use std::path::PathBuf;
#[test]
fn test_version_lte() {
assert!(version_lte("1f700", "1f700"));
assert!(version_lte("1f700", "1f710"));
assert!(!version_lte("1f710", "1f700"));
assert!(version_lte("00000", "zzzzz"));
}
#[test]
fn test_validate_baseline_no_matching_migration() {
let available = vec![Migration {
id: "1f700-first".to_string(),
version: "1f700".to_string(),
file_path: PathBuf::from("1f700-first.sh"),
}];
let applied = vec![];
let result = validate_baseline("1f800", &available, &applied, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No migration found"));
}
#[test]
fn test_validate_baseline_unapplied_migration() {
let available = vec![
Migration {
id: "1f700-first".to_string(),
version: "1f700".to_string(),
file_path: PathBuf::from("1f700-first.sh"),
},
Migration {
id: "1f710-second".to_string(),
version: "1f710".to_string(),
file_path: PathBuf::from("1f710-second.sh"),
},
];
let applied = vec![AppliedMigration {
id: "1f710-second".to_string(),
applied_at: Utc::now(),
}];
let result = validate_baseline("1f710", &available, &applied, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("has not been applied"));
}
#[test]
fn test_validate_baseline_backward_movement() {
let available = vec![
Migration {
id: "1f700-first".to_string(),
version: "1f700".to_string(),
file_path: PathBuf::from("1f700-first.sh"),
},
Migration {
id: "1f710-second".to_string(),
version: "1f710".to_string(),
file_path: PathBuf::from("1f710-second.sh"),
},
];
let applied = vec![
AppliedMigration {
id: "1f700-first".to_string(),
applied_at: Utc::now(),
},
AppliedMigration {
id: "1f710-second".to_string(),
applied_at: Utc::now(),
},
];
let existing = Baseline {
version: "1f710".to_string(),
created: Utc::now(),
summary: None,
};
let result = validate_baseline("1f700", &available, &applied, Some(&existing));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("backward"));
}
#[test]
fn test_validate_baseline_success() {
let available = vec![
Migration {
id: "1f700-first".to_string(),
version: "1f700".to_string(),
file_path: PathBuf::from("1f700-first.sh"),
},
Migration {
id: "1f710-second".to_string(),
version: "1f710".to_string(),
file_path: PathBuf::from("1f710-second.sh"),
},
];
let applied = vec![
AppliedMigration {
id: "1f700-first".to_string(),
applied_at: Utc::now(),
},
AppliedMigration {
id: "1f710-second".to_string(),
applied_at: Utc::now(),
},
];
let result = validate_baseline("1f710", &available, &applied, None);
assert!(result.is_ok());
}
#[test]
fn test_delete_baselined_migrations_with_asset_dirs() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let migrations_dir = temp_dir.path();
let migration_file = migrations_dir.join("1f700-first.sh");
fs::write(&migration_file, "#!/bin/bash\necho hello").unwrap();
let asset_dir = migrations_dir.join("1f700-first");
fs::create_dir(&asset_dir).unwrap();
fs::write(asset_dir.join("config.json"), "{}").unwrap();
fs::write(asset_dir.join("template.txt"), "template").unwrap();
let migration_file2 = migrations_dir.join("1f710-second.sh");
fs::write(&migration_file2, "#!/bin/bash\necho world").unwrap();
let available = vec![
Migration {
id: "1f700-first".to_string(),
version: "1f700".to_string(),
file_path: migration_file.clone(),
},
Migration {
id: "1f710-second".to_string(),
version: "1f710".to_string(),
file_path: migration_file2.clone(),
},
];
let deleted = delete_baselined_migrations("1f710", &available).unwrap();
assert_eq!(deleted.len(), 3);
let files: Vec<_> = deleted.iter().filter(|d| !d.is_directory).collect();
let dirs: Vec<_> = deleted.iter().filter(|d| d.is_directory).collect();
assert_eq!(files.len(), 2);
assert_eq!(dirs.len(), 1);
assert!(!migration_file.exists());
assert!(!migration_file2.exists());
assert!(!asset_dir.exists());
}
#[test]
fn test_delete_baselined_migrations_no_asset_dir() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let migrations_dir = temp_dir.path();
let migration_file = migrations_dir.join("1f700-first.sh");
fs::write(&migration_file, "#!/bin/bash\necho hello").unwrap();
let available = vec![Migration {
id: "1f700-first".to_string(),
version: "1f700".to_string(),
file_path: migration_file.clone(),
}];
let deleted = delete_baselined_migrations("1f700", &available).unwrap();
assert_eq!(deleted.len(), 1);
assert!(!deleted[0].is_directory);
assert!(!migration_file.exists());
}
}