use std::path::Path;
use tork_orm_core::migration::{FileMigrator, OnMismatch};
use tork_orm_core::{Database, Value};
fn write_migration(dir: &Path, revision: &str, down: &str, name: &str, up: &str, down_sql: &str) {
let down_line = if down.is_empty() {
"-- down_revision:".to_string()
} else {
format!("-- down_revision: {down}")
};
let content = format!(
"-- revision: {revision}\n{down_line}\n-- migrate:up\n{up}\n-- migrate:down\n{down_sql}\n"
);
std::fs::write(dir.join(format!("{revision}_{name}.sql")), content).unwrap();
}
async fn table_exists(db: &Database, name: &str) -> bool {
let rows = db
.fetch_all(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?".into(),
vec![Value::Text(name.into())],
)
.await
.unwrap();
!rows.is_empty()
}
fn seed_chain(dir: &Path) {
write_migration(
dir,
"aaaa11112222",
"",
"create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);",
"DROP TABLE users;",
);
write_migration(
dir,
"bbbb33334444",
"aaaa11112222",
"create_posts",
"CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL);",
"DROP TABLE posts;",
);
}
#[tokio::test]
async fn up_applies_the_chain_and_down_reverts_it() {
let dir = tempfile::tempdir().unwrap();
seed_chain(dir.path());
let db = Database::connect(":memory:", 1).await.unwrap();
let migrator = FileMigrator::new(db.clone(), dir.path());
let applied = migrator.up().await.unwrap();
assert_eq!(applied.len(), 2);
assert_eq!(applied[0].name, "create_users");
assert_eq!(applied[1].name, "create_posts");
assert!(table_exists(&db, "users").await);
assert!(table_exists(&db, "posts").await);
assert_eq!(migrator.up().await.unwrap().len(), 0);
let reverted = migrator.down(1).await.unwrap();
assert_eq!(reverted.len(), 1);
assert_eq!(reverted[0].name, "create_posts");
assert!(table_exists(&db, "users").await);
assert!(!table_exists(&db, "posts").await);
assert_eq!(migrator.down_all().await.unwrap().len(), 1);
assert!(!table_exists(&db, "users").await);
}
#[tokio::test]
async fn status_reports_applied_and_pending() {
let dir = tempfile::tempdir().unwrap();
seed_chain(dir.path());
let db = Database::connect(":memory:", 1).await.unwrap();
let migrator = FileMigrator::new(db, dir.path());
let before = migrator.status().await.unwrap();
assert_eq!(before.len(), 2);
assert!(!before[0].applied);
assert!(!before[1].applied);
migrator.up_to("aaaa").await.unwrap(); let after = migrator.status().await.unwrap();
assert!(after[0].applied);
assert_eq!(after[0].checksum_matches, Some(true));
assert!(!after[1].applied);
}
#[tokio::test]
async fn a_failed_migration_rolls_back_and_keeps_earlier_ones() {
let dir = tempfile::tempdir().unwrap();
write_migration(
dir.path(),
"aaaa11112222",
"",
"create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY);",
"DROP TABLE users;",
);
write_migration(
dir.path(),
"bbbb33334444",
"aaaa11112222",
"broken",
"CREATE TABLE widgets (id INTEGER PRIMARY KEY); THIS IS NOT SQL;",
"DROP TABLE widgets;",
);
let db = Database::connect(":memory:", 1).await.unwrap();
let migrator = FileMigrator::new(db.clone(), dir.path());
let error = migrator.up().await.unwrap_err();
assert_eq!(error.kind(), tork_orm_core::ErrorKind::Query);
assert!(table_exists(&db, "users").await);
assert!(!table_exists(&db, "widgets").await);
let rows = db
.fetch_all("SELECT revision FROM _tork_migrations".into(), vec![])
.await
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get::<String>("revision").unwrap(), "aaaa11112222");
}
#[tokio::test]
async fn a_changed_file_is_reported_and_can_error() {
let dir = tempfile::tempdir().unwrap();
write_migration(
dir.path(),
"aaaa11112222",
"",
"create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY);",
"DROP TABLE users;",
);
let db = Database::connect(":memory:", 1).await.unwrap();
FileMigrator::new(db.clone(), dir.path()).up().await.unwrap();
write_migration(
dir.path(),
"aaaa11112222",
"",
"create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, extra TEXT);",
"DROP TABLE users;",
);
let status = FileMigrator::new(db.clone(), dir.path())
.status()
.await
.unwrap();
assert_eq!(status[0].checksum_matches, Some(false));
let error = FileMigrator::new(db, dir.path())
.on_checksum_mismatch(OnMismatch::Error)
.up()
.await
.unwrap_err();
assert_eq!(error.kind(), tork_orm_core::ErrorKind::Configuration);
}
#[tokio::test]
async fn editing_an_applied_migration_aborts_up_by_default() {
let dir = tempfile::tempdir().unwrap();
seed_chain(dir.path());
let db = Database::connect(":memory:", 1).await.unwrap();
FileMigrator::new(db.clone(), dir.path())
.up()
.await
.unwrap();
write_migration(
dir.path(),
"aaaa11112222",
"",
"create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, extra TEXT);",
"DROP TABLE users;",
);
let error = FileMigrator::new(db.clone(), dir.path())
.up()
.await
.unwrap_err();
assert_eq!(error.kind(), tork_orm_core::ErrorKind::Configuration);
let applied = FileMigrator::new(db, dir.path())
.on_checksum_mismatch(OnMismatch::Warn)
.up()
.await
.unwrap();
assert_eq!(applied.len(), 0);
}
#[tokio::test]
async fn down_to_reverts_only_applied_migrations_after_target() {
let dir = tempfile::tempdir().unwrap();
seed_chain(dir.path()); write_migration(
dir.path(),
"cccc55556666",
"bbbb33334444",
"create_comments",
"CREATE TABLE comments (id INTEGER PRIMARY KEY);",
"DROP TABLE comments;",
);
let db = Database::connect(":memory:", 1).await.unwrap();
let migrator = FileMigrator::new(db.clone(), dir.path());
migrator.up_to("bbbb33334444").await.unwrap();
assert!(table_exists(&db, "users").await);
assert!(table_exists(&db, "posts").await);
assert!(!table_exists(&db, "comments").await);
let reverted = migrator.down_to("aaaa11112222").await.unwrap();
assert_eq!(reverted.len(), 1);
assert_eq!(reverted[0].name, "create_posts");
assert!(
table_exists(&db, "users").await,
"the target migration and earlier ones must survive down_to"
);
assert!(!table_exists(&db, "posts").await);
}