pub mod error;
pub mod file;
pub mod lock;
pub mod tracker;
pub use error::{Error, Result};
pub use file::{Migration, MigrationFile};
pub use lock::LockFile;
pub use tracker::MigrationTracker;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct MigrationConfig {
pub migrations_dir: PathBuf,
pub state_dir: PathBuf,
pub auto_timestamp: bool,
}
impl Default for MigrationConfig {
fn default() -> Self {
Self {
migrations_dir: PathBuf::from("migrations"),
state_dir: PathBuf::from(".kimberlite/migrations"),
auto_timestamp: true,
}
}
}
impl MigrationConfig {
pub fn with_migrations_dir(dir: impl Into<PathBuf>) -> Self {
Self {
migrations_dir: dir.into(),
..Default::default()
}
}
pub fn lock_file_path(&self) -> PathBuf {
self.state_dir.join(".lock")
}
}
pub struct MigrationManager {
config: MigrationConfig,
tracker: MigrationTracker,
}
impl MigrationManager {
pub fn new(config: MigrationConfig) -> Result<Self> {
let tracker = MigrationTracker::new(config.state_dir.clone())?;
Ok(Self { config, tracker })
}
pub fn list_files(&self) -> Result<Vec<MigrationFile>> {
MigrationFile::discover(&self.config.migrations_dir)
}
pub fn list_pending(&self) -> Result<Vec<MigrationFile>> {
let all_files = self.list_files()?;
let applied = self.tracker.list_applied()?;
let applied_ids: std::collections::HashSet<_> = applied.iter().map(|m| m.id).collect();
Ok(all_files
.into_iter()
.filter(|f| !applied_ids.contains(&f.migration.id))
.collect())
}
pub fn create(&self, name: &str) -> Result<MigrationFile> {
MigrationFile::create(
&self.config.migrations_dir,
name,
self.config.auto_timestamp,
)
}
pub fn record_applied(&self, file: &MigrationFile) -> Result<tracker::AppliedMigration> {
self.tracker.record_applied(
file.migration.id,
file.migration.name.clone(),
file.checksum.clone(),
)
}
pub fn remove_applied(&self, id: u32) -> Result<()> {
self.tracker.remove_applied(id)
}
pub fn up_sql(file: &MigrationFile) -> &str {
if let Some(idx) = file.migration.sql.find("-- Down Migration") {
file.migration.sql[..idx].trim_end()
} else {
file.migration.sql.trim()
}
}
pub fn down_sql(file: &MigrationFile) -> Option<&str> {
file.migration
.sql
.find("-- Down Migration")
.map(|idx| {
let after = &file.migration.sql[idx..];
after.find('\n').map_or("", |nl| after[nl + 1..].trim())
})
.filter(|s| !s.is_empty())
}
pub fn validate(&self) -> Result<()> {
let lock = LockFile::load(&self.config.lock_file_path())?;
let files = self.list_files()?;
lock.validate(&files)?;
for (i, file) in files.iter().enumerate() {
let expected_id = (i + 1) as u32;
if file.migration.id != expected_id {
return Err(Error::InvalidSequence {
expected: expected_id,
found: file.migration.id,
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_migration_config_default() {
let config = MigrationConfig::default();
assert_eq!(config.migrations_dir, PathBuf::from("migrations"));
assert_eq!(config.state_dir, PathBuf::from(".kimberlite/migrations"));
assert!(config.auto_timestamp);
}
#[test]
fn test_migration_config_lock_path() {
let config = MigrationConfig::default();
assert_eq!(
config.lock_file_path(),
PathBuf::from(".kimberlite/migrations/.lock")
);
}
#[test]
fn test_migration_manager_creation() {
let temp = TempDir::new().unwrap();
let config = MigrationConfig {
migrations_dir: temp.path().join("migrations"),
state_dir: temp.path().join("state"),
auto_timestamp: false,
};
std::fs::create_dir_all(&config.migrations_dir).unwrap();
std::fs::create_dir_all(&config.state_dir).unwrap();
let manager = MigrationManager::new(config);
assert!(manager.is_ok());
}
}