use cratefield_adapter_postgres::testing::TempDb;
use cratefield_adapter_postgres::{Postgres, select_set};
use cratefield_core::{Database, DbError, Module, Statement};
use std::sync::Arc;
pub(crate) struct PgFixture {
runtime: tokio::runtime::Runtime,
pool: Arc<Postgres>,
temp: TempDb,
}
impl PgFixture {
pub(crate) fn create(base: &str, modules: &[Arc<dyn Module>]) -> Result<Self, String> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.map_err(|err| format!("cannot start the parity runtime: {err}"))?;
let setup = runtime.block_on(async {
let temp = TempDb::create(base, "parity")
.await
.ok_or_else(|| "cannot create a throwaway database".to_owned())?;
temp.assert_postgres_16().await;
let result = async {
let db = Postgres::connect(&temp.url)
.await
.map_err(|err| err.to_string())?;
for module in modules {
let set = select_set(&module.migrations())
.map_err(|reason| format!("module {}: {reason}", module.name()))?;
db.apply_migrations(module.name(), set)
.await
.map_err(|err| format!("migration for {}: {err}", module.name()))?;
}
Ok(db)
}
.await;
match result {
Ok(db) => Ok((temp, db)),
Err(message) => {
temp.finish().await;
Err(message)
}
}
});
match setup {
Ok((temp, pool)) => Ok(Self {
runtime,
pool: Arc::new(pool),
temp,
}),
Err(message) => Err(message),
}
}
pub(crate) fn database(&self) -> Arc<dyn Database> {
Arc::new(MarshalledDatabase {
handle: self.runtime.handle().clone(),
inner: self.pool.clone(),
})
}
pub(crate) fn shutdown(self) {
let Self {
runtime,
pool,
temp,
} = self;
runtime.block_on(async move {
let _ = pool.close().await;
temp.finish().await;
});
}
}
struct MarshalledDatabase {
handle: tokio::runtime::Handle,
inner: Arc<Postgres>,
}
impl MarshalledDatabase {
fn marshal<T: Send + 'static>(
&self,
call: impl Future<Output = Result<T, DbError>> + Send + 'static,
) -> Result<T, DbError> {
let (tx, rx) = std::sync::mpsc::channel();
self.handle.spawn(async move {
let _ = tx.send(call.await);
});
match rx.recv() {
Ok(result) => result,
Err(_) => Err(DbError::Execute(
"the parity runtime dropped a database call".to_owned(),
)),
}
}
}
#[async_trait::async_trait]
impl Database for MarshalledDatabase {
async fn execute(&self, stmt: &Statement) -> Result<u64, DbError> {
let inner = self.inner.clone();
let stmt = stmt.clone();
self.marshal(async move { inner.execute(&stmt).await })
}
async fn query(&self, stmt: &Statement) -> Result<cratefield_core::Rows, DbError> {
let inner = self.inner.clone();
let stmt = stmt.clone();
self.marshal(async move { inner.query(&stmt).await })
}
async fn batch(&self, stmts: &[Statement]) -> Result<(), DbError> {
let inner = self.inner.clone();
let stmts = stmts.to_vec();
self.marshal(async move { inner.batch(&stmts).await })
}
}
pub(crate) fn migrations_apply_twice(modules: &[Arc<dyn Module>]) -> Result<(), String> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.map_err(|err| format!("cannot start the parity runtime: {err}"))?;
runtime.block_on(async {
for round in 1..=2 {
let Some(temp) = TempDb::create(
&cratefield_adapter_postgres::testing::base_url()
.expect("the caller checked FZ_TEST_POSTGRES_URL"),
"conformance",
)
.await
else {
return Err("cannot create a throwaway database".to_owned());
};
let result: Result<(), String> = async {
let db = Postgres::connect(&temp.url)
.await
.map_err(|err| err.to_string())?;
for module in modules {
let set = select_set(&module.migrations())
.map_err(|reason| format!("module {}: {reason}", module.name()))?;
db.apply_migrations(module.name(), set)
.await
.map_err(|err| format!("round {round}, {}: {err}", module.name()))?;
}
Ok(())
}
.await;
temp.finish().await;
result?;
}
Ok(())
})
}