#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use std::{future::Future, pin::Pin, sync::Arc};
use async_trait::async_trait;
use switchy_database::{Database, DatabaseError};
use switchy_schema::{
MigrationError,
migration::{Migration, MigrationSource},
runner::{MigrationRunner, RollbackStrategy},
};
pub use switchy_database;
pub use switchy_schema;
#[cfg(feature = "sqlite")]
pub use builder::MigrationTestBuilder;
pub mod mutations;
#[cfg(feature = "sqlite")]
pub mod assertions;
#[cfg(feature = "sqlite")]
pub mod builder;
pub mod integration_tests;
#[cfg(feature = "snapshots")]
pub mod snapshots;
#[derive(Debug, thiserror::Error)]
pub enum TestError {
#[error(transparent)]
Migration(#[from] MigrationError),
#[error(transparent)]
Database(#[from] DatabaseError),
#[cfg(feature = "sqlite")]
#[error(transparent)]
DatabaseInit(#[from] switchy_database_connection::InitSqliteSqlxDatabaseError),
}
#[cfg(feature = "snapshots")]
pub use snapshots::{
MigrationSnapshotTest, Result as SnapshotResult, SnapshotError, SnapshotTester,
};
#[cfg(feature = "sqlite")]
pub async fn create_empty_in_memory()
-> Result<Box<dyn Database>, switchy_database_connection::InitSqliteSqlxDatabaseError> {
switchy_database_connection::init_sqlite_sqlx(None).await
}
struct VecMigrationSource<'a> {
migrations: Vec<Arc<dyn Migration<'a> + 'a>>,
}
impl<'a> VecMigrationSource<'a> {
#[must_use]
fn new(migrations: Vec<Arc<dyn Migration<'a> + 'a>>) -> Self {
Self { migrations }
}
}
#[async_trait]
impl<'a> MigrationSource<'a> for VecMigrationSource<'a> {
async fn migrations(&self) -> switchy_schema::Result<Vec<Arc<dyn Migration<'a> + 'a>>> {
Ok(self.migrations.clone()) }
}
pub async fn verify_migrations_full_cycle<'a>(
db: &dyn Database,
migrations: Vec<Arc<dyn Migration<'a> + 'a>>,
) -> Result<(), TestError> {
let source = VecMigrationSource::new(migrations);
let runner = MigrationRunner::new(Box::new(source));
runner.run(db).await?;
runner.rollback(db, RollbackStrategy::All).await?;
Ok(())
}
pub async fn verify_migrations_with_state<'a, F>(
db: &dyn Database,
migrations: Vec<Arc<dyn Migration<'a> + 'a>>,
setup: F,
) -> Result<(), TestError>
where
F: for<'db> FnOnce(
&'db dyn Database,
)
-> Pin<Box<dyn Future<Output = Result<(), DatabaseError>> + Send + 'db>>,
{
setup(db).await?;
let source = VecMigrationSource::new(migrations);
let runner = MigrationRunner::new(Box::new(source));
runner.run(db).await?;
runner.rollback(db, RollbackStrategy::All).await?;
Ok(())
}
pub async fn verify_migrations_with_mutations<'a, M>(
db: &dyn Database,
migrations: Vec<Arc<dyn Migration<'a> + 'a>>,
mutations: M,
) -> Result<(), TestError>
where
M: mutations::MutationProvider,
{
let source = VecMigrationSource::new(migrations.clone());
let runner = MigrationRunner::new(Box::new(source));
let mut migration_map = std::collections::BTreeMap::new();
for migration in &migrations {
migration_map.insert(migration.id().to_string(), Arc::clone(migration));
}
for (migration_id, migration) in &migration_map {
let single_migration_source = VecMigrationSource::new(vec![Arc::clone(migration)]);
let single_runner = MigrationRunner::new(Box::new(single_migration_source));
single_runner.run(db).await?;
if let Some(mutation) = mutations.get_mutation(migration_id).await {
mutation.execute(db).await?;
}
}
runner.rollback(db, RollbackStrategy::All).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use switchy_schema::migration::Migration;
struct TestMigration {
id: String,
up_sql: String,
down_sql: Option<String>,
}
impl TestMigration {
fn new(id: &str, up_sql: &str, down_sql: Option<&str>) -> Self {
Self {
id: id.to_string(),
up_sql: up_sql.to_string(),
down_sql: down_sql.map(String::from),
}
}
}
#[async_trait]
impl Migration<'static> for TestMigration {
fn id(&self) -> &str {
&self.id
}
async fn up(&self, db: &dyn Database) -> switchy_schema::Result<()> {
db.exec_raw(&self.up_sql).await?;
Ok(())
}
async fn down(&self, db: &dyn Database) -> switchy_schema::Result<()> {
if let Some(down_sql) = &self.down_sql {
db.exec_raw(down_sql).await?;
}
Ok(())
}
}
#[switchy_async::test]
async fn test_vec_migration_source() {
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let migration2 = Arc::new(TestMigration::new(
"002_create_posts",
"CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER)",
Some("DROP TABLE posts"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1, migration2];
let source = VecMigrationSource::new(test_migrations.clone());
let result = source.migrations().await.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].id(), "001_create_users");
assert_eq!(result[1].id(), "002_create_posts");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_full_cycle() {
let db = create_empty_in_memory().await.unwrap();
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let migration2 = Arc::new(TestMigration::new(
"002_create_posts",
"CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT)",
Some("DROP TABLE posts"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1, migration2];
let result = verify_migrations_full_cycle(db.as_ref(), test_migrations).await;
assert!(result.is_ok(), "Full cycle verification failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_with_state() {
let db = create_empty_in_memory().await.unwrap();
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let migration2 = Arc::new(TestMigration::new(
"002_add_email_column",
"ALTER TABLE existing_data ADD COLUMN email TEXT",
Some("ALTER TABLE existing_data DROP COLUMN email"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1, migration2];
let result = verify_migrations_with_state(db.as_ref(), test_migrations, |db| {
Box::pin(async move {
db.exec_raw("CREATE TABLE existing_data (id INTEGER PRIMARY KEY, name TEXT)")
.await?;
db.exec_raw("INSERT INTO existing_data (name) VALUES ('test')")
.await?;
Ok(())
})
})
.await;
assert!(result.is_ok(), "With state verification failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_empty_list() {
let db = create_empty_in_memory().await.unwrap();
let migrations: Vec<Arc<dyn Migration<'static> + 'static>> = vec![];
let result = verify_migrations_full_cycle(db.as_ref(), migrations).await;
assert!(result.is_ok(), "Empty migration list failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_single_migration() {
let db = create_empty_in_memory().await.unwrap();
let migration = Arc::new(TestMigration::new(
"001_single_table",
"CREATE TABLE single_table (id INTEGER PRIMARY KEY)",
Some("DROP TABLE single_table"),
)) as Arc<dyn Migration<'static> + 'static>;
let single_migration = vec![migration];
let result = verify_migrations_full_cycle(db.as_ref(), single_migration).await;
assert!(result.is_ok(), "Single migration failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_with_mutations_btreemap() {
let db = create_empty_in_memory().await.unwrap();
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let migration2 = Arc::new(TestMigration::new(
"002_create_posts",
"CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, title TEXT)",
Some("DROP TABLE posts"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1, migration2];
let mut mutation_map = std::collections::BTreeMap::new();
mutation_map.insert(
"001_create_users".to_string(),
Arc::new("INSERT INTO users (name) VALUES ('test_user')".to_string())
as Arc<dyn switchy_database::Executable>,
);
let result =
verify_migrations_with_mutations(db.as_ref(), test_migrations, mutation_map).await;
assert!(result.is_ok(), "Mutations with BTreeMap failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_with_mutations_vec() {
let db = create_empty_in_memory().await.unwrap();
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1];
let mutations = vec![(
"001_create_users".to_string(),
Arc::new("INSERT INTO users (name) VALUES ('test_user')".to_string())
as Arc<dyn switchy_database::Executable>,
)];
let result =
verify_migrations_with_mutations(db.as_ref(), test_migrations, mutations).await;
assert!(result.is_ok(), "Mutations with Vec failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_with_mutations_builder() {
let db = create_empty_in_memory().await.unwrap();
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1];
let mutations = crate::mutations::MutationBuilder::new()
.add_mutation(
"001_create_users",
"INSERT INTO users (name) VALUES ('builder_user')",
)
.build();
let result =
verify_migrations_with_mutations(db.as_ref(), test_migrations, mutations).await;
assert!(result.is_ok(), "Mutations with builder failed: {result:?}");
}
#[cfg(feature = "sqlite")]
#[switchy_async::test]
async fn test_verify_migrations_with_no_mutations() {
let db = create_empty_in_memory().await.unwrap();
let migration1 = Arc::new(TestMigration::new(
"001_create_users",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
Some("DROP TABLE users"),
)) as Arc<dyn Migration<'static> + 'static>;
let test_migrations = vec![migration1];
let mutations =
std::collections::BTreeMap::<String, Arc<dyn switchy_database::Executable>>::new();
let result =
verify_migrations_with_mutations(db.as_ref(), test_migrations, mutations).await;
assert!(
result.is_ok(),
"Migrations with no mutations failed: {result:?}"
);
}
}