use sea_orm::DatabaseConnection;
use sea_orm_migration::MigratorTrait;
use super::config::DatabaseConfig;
use super::connection::DbConnection;
use crate::container::testing::{TestContainer, TestContainerGuard};
use crate::error::FrameworkError;
static TEST_DB_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub struct TestDatabase {
conn: DbConnection,
_guard: TestContainerGuard,
}
impl TestDatabase {
pub async fn fresh<M: MigratorTrait>() -> Result<Self, FrameworkError> {
let guard = TestContainer::fake();
let seq = TEST_DB_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let url = format!("sqlite:file:ferro_testdb_{seq}?mode=memory&cache=shared");
let config = DatabaseConfig::builder()
.url(url)
.max_connections(8)
.min_connections(1)
.logging(false)
.build();
let conn = DbConnection::connect(&config).await?;
M::up(conn.inner(), None)
.await
.map_err(|e| FrameworkError::database(format!("Migration failed: {e}")))?;
TestContainer::singleton(conn.clone());
Ok(Self {
conn,
_guard: guard,
})
}
pub fn conn(&self) -> &DatabaseConnection {
self.conn.inner()
}
pub fn db(&self) -> &DbConnection {
&self.conn
}
}
#[macro_export]
#[allow(clippy::crate_in_macro_def)]
macro_rules! test_database {
() => {
$crate::testing::TestDatabase::fresh::<crate::migrations::Migrator>()
.await
.expect("Failed to set up test database")
};
($migrator:ty) => {
$crate::testing::TestDatabase::fresh::<$migrator>()
.await
.expect("Failed to set up test database")
};
}
#[cfg(test)]
mod fresh_pool_tests {
use super::TestDatabase;
use crate::database::DB;
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement, TransactionTrait};
use sea_orm_migration::{MigrationTrait, MigratorTrait};
struct NoopMigrator;
impl MigratorTrait for NoopMigrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![]
}
}
#[tokio::test]
async fn nested_connection_during_open_txn_does_not_deadlock() {
let _db = TestDatabase::fresh::<NoopMigrator>().await.unwrap();
let backend = DatabaseBackend::Sqlite;
let c = DB::connection().unwrap();
c.inner()
.execute(Statement::from_string(
backend,
"CREATE TABLE outer_t (id INTEGER PRIMARY KEY)",
))
.await
.unwrap();
c.inner()
.execute(Statement::from_string(
backend,
"CREATE TABLE other_t (id INTEGER PRIMARY KEY, v TEXT)",
))
.await
.unwrap();
c.inner()
.execute(Statement::from_string(
backend,
"INSERT INTO other_t (id, v) VALUES (1, 'committed')",
))
.await
.unwrap();
let txn = DB::connection()
.unwrap()
.inner()
.clone()
.begin()
.await
.unwrap();
txn.execute(Statement::from_string(
backend,
"INSERT INTO outer_t (id) VALUES (99)",
))
.await
.unwrap();
let nested = DB::connection().unwrap();
let rows = nested
.inner()
.query_all(Statement::from_string(
backend,
"SELECT id FROM other_t WHERE id = 1",
))
.await
.unwrap();
assert_eq!(
rows.len(),
1,
"nested connection must read committed data without deadlocking"
);
txn.commit().await.unwrap();
}
#[tokio::test]
async fn fresh_databases_are_isolated() {
let backend = DatabaseBackend::Sqlite;
{
let _db1 = TestDatabase::fresh::<NoopMigrator>().await.unwrap();
DB::connection()
.unwrap()
.inner()
.execute(Statement::from_string(
backend,
"CREATE TABLE iso_t (id INTEGER PRIMARY KEY)",
))
.await
.unwrap();
}
let _db2 = TestDatabase::fresh::<NoopMigrator>().await.unwrap();
let res = DB::connection()
.unwrap()
.inner()
.query_all(Statement::from_string(
backend,
"SELECT name FROM sqlite_master WHERE type='table' AND name='iso_t'",
))
.await
.unwrap();
assert!(
res.is_empty(),
"a fresh test DB must not see a prior DB's tables (isolation)"
);
}
}