use std::collections::BTreeMap;
use switchy_database::{
Database, DatabaseError,
query::{self, FilterableQuery},
};
use switchy_schema::version::DEFAULT_MIGRATIONS_TABLE;
pub async fn assert_table_exists(db: &dyn Database, table_name: &str) -> Result<(), DatabaseError> {
match query::select(table_name)
.columns(&["*"])
.limit(0)
.execute(db)
.await
{
Ok(_) => Ok(()),
Err(_) => Err(DatabaseError::NoRow), }
}
pub async fn assert_table_not_exists(
db: &dyn Database,
table_name: &str,
) -> Result<(), DatabaseError> {
match query::select(table_name)
.columns(&["*"])
.limit(0)
.execute(db)
.await
{
Ok(_) => Err(DatabaseError::NoRow), Err(_) => Ok(()), }
}
pub async fn assert_column_exists(
db: &dyn Database,
table_name: &str,
column_name: &str,
_expected_type: &str,
) -> Result<(), DatabaseError> {
match query::select(table_name)
.columns(&[column_name])
.execute(db)
.await
{
Ok(_) => Ok(()), Err(_) => Err(DatabaseError::NoRow), }
}
pub async fn assert_column_not_exists(
db: &dyn Database,
table_name: &str,
column_name: &str,
) -> Result<(), DatabaseError> {
query::select(table_name)
.columns(&[column_name])
.execute(db)
.await
.inspect(|x| log::debug!("Column exists: {column_name}, rows: {x:?}"))
.map_or_else(
|_| Ok(()),
|_| Err(DatabaseError::NoRow), )
}
pub async fn assert_row_count(
db: &dyn Database,
table_name: &str,
expected_count: i64,
) -> Result<(), DatabaseError> {
let results = query::select(table_name)
.columns(&["*"])
.execute(db)
.await?;
let actual_count = i64::try_from(results.len()).expect("table row count should fit in i64");
if actual_count == expected_count {
Ok(())
} else {
Err(DatabaseError::NoRow) }
}
pub async fn assert_row_count_min(
db: &dyn Database,
table_name: &str,
min_count: i64,
) -> Result<(), DatabaseError> {
let results = query::select(table_name)
.columns(&["*"])
.execute(db)
.await?;
let actual_count = i64::try_from(results.len()).expect("table row count should fit in i64");
if actual_count >= min_count {
Ok(())
} else {
Err(DatabaseError::NoRow)
}
}
pub async fn assert_foreign_key_integrity(db: &dyn Database) -> Result<(), DatabaseError> {
db.exec_raw("PRAGMA foreign_key_check").await
}
pub async fn assert_migrations_applied(
db: &dyn Database,
migration_ids: &[&str],
) -> Result<(), DatabaseError> {
for &migration_id in migration_ids {
let results = query::select(DEFAULT_MIGRATIONS_TABLE)
.columns(&["name"])
.where_eq("name", migration_id)
.execute(db)
.await?;
if results.is_empty() {
return Err(DatabaseError::NoRow);
}
}
Ok(())
}
pub async fn assert_migrations_not_applied(
db: &dyn Database,
migration_ids: &[&str],
) -> Result<(), DatabaseError> {
let table_check = query::select(DEFAULT_MIGRATIONS_TABLE)
.columns(&["name"])
.limit(1)
.execute(db)
.await;
if table_check.is_err() {
return Ok(());
}
for &migration_id in migration_ids {
let results = query::select(DEFAULT_MIGRATIONS_TABLE)
.columns(&["name"])
.where_eq("name", migration_id)
.execute(db)
.await?;
if !results.is_empty() {
return Err(DatabaseError::NoRow);
}
}
Ok(())
}
pub async fn assert_schema_matches(
db: &dyn Database,
expected_tables: &BTreeMap<String, BTreeMap<String, String>>,
) -> Result<(), DatabaseError> {
for table_name in expected_tables.keys() {
assert_table_exists(db, table_name).await?;
if let Some(columns) = expected_tables.get(table_name) {
for column_name in columns.keys() {
let _results = query::select(table_name)
.columns(&[column_name])
.limit(0)
.execute(db)
.await?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use switchy_database::schema::{Column, DataType};
#[cfg(feature = "sqlite")]
use crate::create_empty_in_memory;
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_table_existence_assertions() {
let db = create_empty_in_memory().await.unwrap();
assert!(assert_table_not_exists(&*db, "test_table").await.is_ok());
assert!(assert_table_exists(&*db, "test_table").await.is_err());
db.create_table("test_table")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.primary_key("id")
.execute(&*db)
.await
.unwrap();
assert!(assert_table_exists(&*db, "test_table").await.is_ok());
assert!(assert_table_not_exists(&*db, "test_table").await.is_err());
}
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_column_existence_assertions() {
let db = create_empty_in_memory().await.unwrap();
db.create_table("users")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.column(Column {
name: "name".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.column(Column {
name: "age".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.primary_key("id")
.execute(&*db)
.await
.unwrap();
db.insert("users")
.value("name", "Alice")
.value("age", 25)
.execute(&*db)
.await
.unwrap();
assert!(
assert_column_exists(&*db, "users", "id", "INTEGER")
.await
.is_ok()
);
assert!(
assert_column_exists(&*db, "users", "name", "TEXT")
.await
.is_ok()
);
assert!(
assert_column_exists(&*db, "users", "age", "INTEGER")
.await
.is_ok()
);
assert!(
assert_column_not_exists(&*db, "users", "email")
.await
.is_ok()
);
assert!(
assert_column_exists(&*db, "users", "email", "TEXT")
.await
.is_err()
);
assert!(
assert_column_not_exists(&*db, "users", "name")
.await
.is_err()
);
}
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_row_count_assertions() {
let db = create_empty_in_memory().await.unwrap();
db.create_table("items")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.column(Column {
name: "name".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.primary_key("id")
.execute(&*db)
.await
.unwrap();
assert!(assert_row_count(&*db, "items", 0).await.is_ok());
assert!(assert_row_count_min(&*db, "items", 0).await.is_ok());
assert!(assert_row_count(&*db, "items", 1).await.is_err());
assert!(assert_row_count_min(&*db, "items", 1).await.is_err());
db.insert("items")
.value("name", "item1")
.execute(&*db)
.await
.unwrap();
db.insert("items")
.value("name", "item2")
.execute(&*db)
.await
.unwrap();
db.insert("items")
.value("name", "item3")
.execute(&*db)
.await
.unwrap();
assert!(assert_row_count(&*db, "items", 3).await.is_ok());
assert!(assert_row_count_min(&*db, "items", 2).await.is_ok());
assert!(assert_row_count_min(&*db, "items", 3).await.is_ok());
assert!(assert_row_count(&*db, "items", 2).await.is_err());
assert!(assert_row_count_min(&*db, "items", 4).await.is_err());
}
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_foreign_key_integrity() {
let db = create_empty_in_memory().await.unwrap();
db.exec_raw("PRAGMA foreign_keys = ON").await.unwrap();
db.create_table("users")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.column(Column {
name: "name".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.primary_key("id")
.execute(&*db)
.await
.unwrap();
db.create_table("posts")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.column(Column {
name: "user_id".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.primary_key("id")
.foreign_key(("user_id", "users(id)"))
.execute(&*db)
.await
.unwrap();
db.insert("users")
.value("id", 1)
.value("name", "Alice")
.execute(&*db)
.await
.unwrap();
db.insert("posts")
.value("user_id", 1)
.execute(&*db)
.await
.unwrap();
assert!(assert_foreign_key_integrity(&*db).await.is_ok());
}
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_migration_state_assertions() {
let db = create_empty_in_memory().await.unwrap();
db.create_table("__switchy_migrations")
.column(Column {
name: "name".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.column(Column {
name: "run_on".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.primary_key("name")
.execute(&*db)
.await
.unwrap();
assert!(
assert_migrations_not_applied(&*db, &["001_initial", "002_users"])
.await
.is_ok()
);
assert!(
assert_migrations_applied(&*db, &["001_initial"])
.await
.is_err()
);
db.insert("__switchy_migrations")
.value("name", "001_initial")
.value("run_on", "2024-01-01")
.execute(&*db)
.await
.unwrap();
db.insert("__switchy_migrations")
.value("name", "002_users")
.value("run_on", "2024-01-02")
.execute(&*db)
.await
.unwrap();
assert!(
assert_migrations_applied(&*db, &["001_initial"])
.await
.is_ok()
);
assert!(
assert_migrations_applied(&*db, &["001_initial", "002_users"])
.await
.is_ok()
);
assert!(
assert_migrations_not_applied(&*db, &["003_future"])
.await
.is_ok()
);
assert!(
assert_migrations_applied(&*db, &["003_future"])
.await
.is_err()
);
assert!(
assert_migrations_not_applied(&*db, &["001_initial"])
.await
.is_err()
);
}
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_schema_comparison() {
use std::collections::BTreeMap;
let db = create_empty_in_memory().await.unwrap();
db.create_table("users")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.column(Column {
name: "name".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.primary_key("id")
.execute(&*db)
.await
.unwrap();
db.create_table("posts")
.column(Column {
name: "id".to_string(),
nullable: false,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.column(Column {
name: "title".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Text,
default: None,
})
.column(Column {
name: "user_id".to_string(),
nullable: true,
auto_increment: false,
data_type: DataType::Int,
default: None,
})
.primary_key("id")
.execute(&*db)
.await
.unwrap();
let mut expected = BTreeMap::new();
let mut users_cols = BTreeMap::new();
users_cols.insert("id".to_string(), "INTEGER".to_string());
users_cols.insert("name".to_string(), "TEXT".to_string());
expected.insert("users".to_string(), users_cols);
let mut posts_cols = BTreeMap::new();
posts_cols.insert("id".to_string(), "INTEGER".to_string());
posts_cols.insert("title".to_string(), "TEXT".to_string());
posts_cols.insert("user_id".to_string(), "INTEGER".to_string());
expected.insert("posts".to_string(), posts_cols);
assert!(assert_schema_matches(&*db, &expected).await.is_ok());
let mut extra_cols = BTreeMap::new();
extra_cols.insert("id".to_string(), "INTEGER".to_string());
expected.insert("comments".to_string(), extra_cols);
assert!(assert_schema_matches(&*db, &expected).await.is_err());
}
#[test_log::test(switchy_async::test)]
#[cfg(feature = "sqlite")]
async fn test_migration_table_missing() {
let db = create_empty_in_memory().await.unwrap();
assert!(
assert_migrations_not_applied(&*db, &["001_initial"])
.await
.is_ok()
);
assert!(
assert_migrations_applied(&*db, &["001_initial"])
.await
.is_err()
);
}
}