use turso_orm_migration::prelude::*;
mod m20240101_000001_create_user {
use turso_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub(crate) struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table("user")
.col(ColumnDef::integer("id").primary_key().auto_increment())
.col(ColumnDef::text("email").not_null().unique_key()),
)
.await?;
manager
.create_index(
CreateIndex::new()
.name("idx-user-email")
.table("user")
.col("email"),
)
.await
}
async fn down(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table("user")).await
}
}
}
mod m20240102_000001_add_name {
use turso_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub(crate) struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table("user")
.add_column(ColumnDef::text("name")),
)
.await?;
manager
.get_connection()
.execute_unprepared("INSERT INTO user (email, name) VALUES ('seed@x.io', 'Seed')")
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager
.alter_table(Table::alter().table("user").drop_column("name"))
.await
}
}
}
mod m20240103_000001_failing {
use turso_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub(crate) struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table("orphan")
.col(ColumnDef::integer("id").primary_key()),
)
.await?;
Err(DbErr::Migration("boom".into()))
}
}
}
mod m20240104_000001_create_audit {
use turso_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub(crate) struct Migration;
#[async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table("audit")
.col(ColumnDef::integer("id").primary_key()),
)
.await
}
async fn down(&self, manager: &SchemaManager<'_>) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table("audit")).await
}
}
}
struct Migrator;
#[async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240101_000001_create_user::Migration),
Box::new(m20240102_000001_add_name::Migration),
]
}
fn migration_table_name() -> &'static str {
"schema_history"
}
}
struct FailingMigrator;
#[async_trait]
impl MigratorTrait for FailingMigrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240101_000001_create_user::Migration),
Box::new(m20240103_000001_failing::Migration),
]
}
}
#[tokio::test]
async fn up_down_status_refresh() {
let db = Database::connect(ConnectOptions::in_memory())
.await
.expect("open");
Migrator::up(&db, None).await.expect("up");
let versions = db
.query_all(Statement::from_string("SELECT version FROM schema_history"))
.await
.expect("bookkeeping");
assert_eq!(versions.len(), 2);
let status = Migrator::status(&db).await.expect("status");
assert_eq!(
status
.iter()
.map(|s| (s.name.as_str(), s.applied))
.collect::<Vec<_>>(),
[
("m20240101_000001_create_user", true),
("m20240102_000001_add_name", true)
]
);
let row = db
.query_one(Statement::from_string(
"SELECT name FROM user WHERE email = 'seed@x.io'",
))
.await
.expect("query")
.expect("seeded");
assert_eq!(row.get::<String>("name").unwrap(), "Seed");
Migrator::down(&db, Some(1)).await.expect("down one");
let status = Migrator::status(&db).await.expect("status");
assert!(!status[1].applied);
Migrator::refresh(&db).await.expect("refresh");
assert_eq!(
Migrator::get_applied_migrations(&db)
.await
.expect("applied")
.len(),
2
);
Migrator::fresh(&db).await.expect("fresh");
assert_eq!(
Migrator::get_applied_migrations(&db)
.await
.expect("applied")
.len(),
2
);
Migrator::reset(&db).await.expect("reset");
assert!(
Migrator::get_applied_migrations(&db)
.await
.expect("applied")
.is_empty()
);
}
#[tokio::test]
async fn failed_migration_is_rolled_back() {
let db = Database::connect(ConnectOptions::in_memory())
.await
.expect("open");
let err = FailingMigrator::up(&db, None).await.expect_err("fails");
assert!(matches!(err, DbErr::Migration(_)));
let applied = FailingMigrator::get_applied_migrations(&db)
.await
.expect("applied");
assert_eq!(applied, ["m20240101_000001_create_user"]);
let txn = db.begin().await.expect("begin");
let manager = SchemaManager::new(&txn);
assert!(manager.has_table("user").await.expect("has"));
assert!(!manager.has_table("orphan").await.expect("has"));
assert!(manager.has_index("idx-user-email").await.expect("has"));
assert!(manager.has_column("user", "email").await.expect("has"));
txn.rollback().await.expect("rollback");
}
struct StaleBeforeUp;
#[async_trait]
impl MigratorTrait for StaleBeforeUp {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20240101_000001_create_user::Migration)]
}
fn migration_table_name() -> &'static str {
Migrator::migration_table_name()
}
async fn get_applied_migrations(_db: &Database) -> Result<Vec<String>, DbErr> {
Ok(Vec::new())
}
}
struct StaleBeforeDown;
#[async_trait]
impl MigratorTrait for StaleBeforeDown {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20240101_000001_create_user::Migration)]
}
fn migration_table_name() -> &'static str {
Migrator::migration_table_name()
}
async fn get_applied_migrations(_db: &Database) -> Result<Vec<String>, DbErr> {
Ok(vec!["m20240101_000001_create_user".to_owned()])
}
}
#[tokio::test]
async fn stale_migrator_skips_settled_migrations() {
let db = Database::connect(ConnectOptions::in_memory())
.await
.expect("open");
Migrator::up(&db, Some(1)).await.expect("up");
StaleBeforeUp::up(&db, None).await.expect("stale up");
assert_eq!(
Migrator::get_applied_migrations(&db)
.await
.expect("applied"),
["m20240101_000001_create_user"]
);
Migrator::down(&db, None).await.expect("down");
StaleBeforeDown::down(&db, None).await.expect("stale down");
assert!(
Migrator::get_applied_migrations(&db)
.await
.expect("applied")
.is_empty()
);
}
struct DuplicateMigrator;
#[async_trait]
impl MigratorTrait for DuplicateMigrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240101_000001_create_user::Migration),
Box::new(m20240101_000001_create_user::Migration),
]
}
}
struct FirstOnly;
#[async_trait]
impl MigratorTrait for FirstOnly {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20240101_000001_create_user::Migration)]
}
fn migration_table_name() -> &'static str {
Migrator::migration_table_name()
}
}
struct StrictFirstOnly;
#[async_trait]
impl MigratorTrait for StrictFirstOnly {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
FirstOnly::migrations()
}
fn migration_table_name() -> &'static str {
Migrator::migration_table_name()
}
fn strict() -> bool {
true
}
}
struct AuditOnly;
#[async_trait]
impl MigratorTrait for AuditOnly {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20240104_000001_create_audit::Migration)]
}
}
struct AuditLast;
#[async_trait]
impl MigratorTrait for AuditLast {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20240101_000001_create_user::Migration),
Box::new(m20240104_000001_create_audit::Migration),
]
}
}
struct StrictAuditLast;
#[async_trait]
impl MigratorTrait for StrictAuditLast {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
AuditLast::migrations()
}
fn strict() -> bool {
true
}
}
fn is_duplicate(err: &DbErr) -> bool {
matches!(err, DbErr::Migration(msg) if msg.contains("duplicate"))
}
#[tokio::test]
async fn duplicate_names_are_rejected() {
let db = Database::connect(ConnectOptions::in_memory())
.await
.expect("open");
assert!(is_duplicate(
&DuplicateMigrator::up(&db, None).await.expect_err("up")
));
assert!(is_duplicate(
&DuplicateMigrator::down(&db, None).await.expect_err("down")
));
assert!(is_duplicate(
&DuplicateMigrator::fresh(&db).await.expect_err("fresh")
));
assert!(is_duplicate(
&DuplicateMigrator::status(&db).await.expect_err("status")
));
assert!(is_duplicate(
&DuplicateMigrator::check(&db).await.expect_err("check")
));
let txn = db.begin().await.expect("begin");
let manager = SchemaManager::new(&txn);
assert!(!manager.has_table("turso_migrations").await.expect("has"));
assert!(!manager.has_table("user").await.expect("has"));
txn.rollback().await.expect("rollback");
}
#[tokio::test]
async fn unknown_applied_migration_warns_or_fails() {
let db = Database::connect(ConnectOptions::in_memory())
.await
.expect("open");
Migrator::up(&db, None).await.expect("up");
let unknown = [MigrationIssue::Unknown(
"m20240102_000001_add_name".to_owned(),
)];
assert_eq!(FirstOnly::check(&db).await.expect("check"), unknown);
FirstOnly::up(&db, None).await.expect("lenient up");
let err = StrictFirstOnly::up(&db, None).await.expect_err("strict up");
assert!(matches!(err, DbErr::Migration(ref msg) if msg.contains("m20240102_000001_add_name")));
StrictFirstOnly::refresh(&db)
.await
.expect_err("strict refresh");
assert_eq!(
Migrator::get_applied_migrations(&db)
.await
.expect("applied")
.len(),
2
);
}
#[tokio::test]
async fn out_of_order_migration_warns_or_fails() {
let db = Database::connect(ConnectOptions::in_memory())
.await
.expect("open");
AuditOnly::up(&db, None).await.expect("audit up");
let out_of_order = [MigrationIssue::OutOfOrder(
"m20240101_000001_create_user".to_owned(),
)];
assert_eq!(AuditLast::check(&db).await.expect("check"), out_of_order);
StrictAuditLast::up(&db, None).await.expect_err("strict up");
assert_eq!(
AuditLast::get_applied_migrations(&db)
.await
.expect("applied"),
["m20240104_000001_create_audit"]
);
AuditLast::up(&db, None).await.expect("lenient up");
assert!(AuditLast::check(&db).await.expect("check").is_empty());
assert!(
AuditLast::status(&db)
.await
.expect("status")
.iter()
.all(|s| s.applied)
);
}