use crate::PgBackend;
use persistant_relational::{MigrateError, MigrateReport, Migration, RelError};
use sqlx::ConnectOptions;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)]
pub struct TestDb {
admin: PgConnectOptions,
name: String,
backend: Option<PgBackend>,
}
fn unique_name() -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
format!(
"persistant_test_{}_{}_{}",
std::process::id(),
nanos,
COUNTER.fetch_add(1, Ordering::Relaxed)
)
}
fn failed(what: &str, e: sqlx::Error) -> RelError {
RelError::Backend(format!("{what}: {e}"))
}
impl TestDb {
pub async fn create(admin: &PgBackend) -> Result<TestDb, RelError> {
let name = unique_name();
sqlx::query(sqlx::AssertSqlSafe(format!("CREATE DATABASE {name}")))
.execute(admin.pool())
.await
.map_err(|e| failed("create the test database (does the role have CREATEDB?)", e))?;
let admin_options = (*admin.pool().connect_options()).clone();
let options = admin_options
.clone()
.database(&name)
.disable_statement_logging();
let connected = PgPoolOptions::new()
.max_connections(8)
.connect_with(options)
.await;
let backend = match connected {
Ok(pool) => PgBackend::from_pool(pool).await,
Err(e) => Err(failed("connect to the test database", e)),
};
match backend {
Ok(backend) => Ok(TestDb {
admin: admin_options.clone(),
name,
backend: Some(backend),
}),
Err(e) => {
let _ = drop_named(&admin_options, &name).await;
Err(e)
}
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn backend(&self) -> &PgBackend {
self.backend
.as_ref()
.expect("the test database is dropped only by consuming it")
}
pub async fn migrate(&self, migrations: &[Migration]) -> Result<MigrateReport, MigrateError> {
self.backend().migrate(migrations).await
}
pub async fn drop_database(mut self) -> Result<(), RelError> {
let dropped = drop_named(&self.admin, &self.name).await;
self.backend.take();
dropped
}
}
async fn drop_named(admin: &PgConnectOptions, name: &str) -> Result<(), RelError> {
let mut conn = admin
.connect()
.await
.map_err(|e| failed("connect to drop the test database", e))?;
sqlx::query(sqlx::AssertSqlSafe(format!(
"DROP DATABASE IF EXISTS {name} WITH (FORCE)"
)))
.execute(&mut conn)
.await
.map(|_| ())
.map_err(|e| failed("drop the test database", e))
}
impl Drop for TestDb {
fn drop(&mut self) {
let Some(backend) = self.backend.take() else {
return;
};
drop(backend);
let (admin, name) = (self.admin.clone(), self.name.clone());
let cleanup = std::thread::spawn(move || {
let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
return;
};
rt.block_on(async {
let _ = drop_named(&admin, &name).await;
});
});
let _ = cleanup.join();
}
}