use sqlx::PgPool;
use sqlx::postgres::PgConnectOptions;
use sqlx::{Connection, Executor};
use testcontainers::ContainerAsync;
use testcontainers::ImageExt;
use testcontainers::core::Mount;
use testcontainers::runners::AsyncRunner;
use testcontainers_modules::postgres::Postgres;
use tokio::sync::OnceCell;
static ADMIN_URL: OnceCell<String> = OnceCell::const_new();
pub(crate) async fn start_shared_container() -> Option<ContainerAsync<Postgres>> {
if let Ok(url) = std::env::var("DATABASE_URL") {
ADMIN_URL
.set(url)
.expect("start_shared_container must run exactly once");
return None;
}
let container = Postgres::default()
.with_tag("18-alpine")
.with_container_name(format!("reliar-pg-{}", uuid::Uuid::now_v7().simple()))
.with_label("reliar.test", "true")
.with_mount(
Mount::tmpfs_mount("/var/lib/postgresql").with_size_bytes(6 * 1024 * 1024 * 1024),
)
.start()
.await
.expect("start postgres container");
let port = container
.get_host_port_ipv4(5432)
.await
.expect("mapped port");
let url = format!("postgres://postgres:postgres@127.0.0.1:{port}/postgres");
ADMIN_URL
.set(url)
.expect("start_shared_container must run exactly once");
Some(container)
}
fn admin_url() -> &'static str {
ADMIN_URL
.get()
.expect("start_shared_container must run before any scenario touches Postgres")
}
async fn create_fresh_database() -> PgConnectOptions {
let admin = PgPool::connect(admin_url())
.await
.expect("connect to admin database");
let name = format!("t_{}", uuid::Uuid::now_v7().simple());
sqlx::query(sqlx::AssertSqlSafe(format!(r#"CREATE DATABASE "{name}""#)))
.execute(&admin)
.await
.expect("create test database");
let options: PgConnectOptions = admin_url()
.parse()
.expect("admin url parses as PgConnectOptions");
options.database(&name)
}
pub(crate) async fn fresh_unmigrated_db() -> PgPool {
PgPool::connect_with(create_fresh_database().await)
.await
.expect("connect to fresh database")
}
static TEMPLATE_NAME: OnceCell<String> = OnceCell::const_new();
async fn template_name() -> &'static str {
TEMPLATE_NAME
.get_or_init(|| async {
let options = create_fresh_database().await;
let name = options.get_database().unwrap().to_owned();
let pool = PgPool::connect_with(options)
.await
.expect("connect to template database");
reliar_store_postgres::migrate(&pool, reliar_store_postgres::MigrateOptions::default())
.await
.expect("migrate the template database");
pool.close().await;
name
})
.await
}
pub(crate) async fn fresh_db() -> PgPool {
let admin = PgPool::connect(admin_url())
.await
.expect("connect to admin database");
let name = format!("t_{}", uuid::Uuid::now_v7().simple());
let template = template_name().await;
sqlx::query(sqlx::AssertSqlSafe(format!(
r#"CREATE DATABASE "{name}" TEMPLATE "{template}""#
)))
.execute(&admin)
.await
.expect("clone the migrated template database");
let options: PgConnectOptions = admin_url()
.parse()
.expect("admin url parses as PgConnectOptions");
PgPool::connect_with(
options
.database(&name)
.options([("search_path", "reliar,public")]),
)
.await
.expect("connect with search_path set")
}
static ALL_MIGRATIONS: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");
pub(crate) async fn apply_migration_prefix(pool: &PgPool, count: usize) {
let mut conn = sqlx::postgres::PgConnection::connect_with(&pool.connect_options())
.await
.expect("dedicated connection for the partial migration");
conn.execute("SET search_path = reliar, public")
.await
.expect("set search_path for the unqualified migrations");
let mut prefix = sqlx::migrate::Migrator {
migrations: std::borrow::Cow::Owned(ALL_MIGRATIONS.migrations[..count].to_vec()),
ignore_missing: ALL_MIGRATIONS.ignore_missing,
locking: ALL_MIGRATIONS.locking,
no_tx: ALL_MIGRATIONS.no_tx,
table_name: ALL_MIGRATIONS.table_name.clone(),
create_schemas: ALL_MIGRATIONS.create_schemas.clone(),
};
prefix.create_schema("reliar".to_owned());
prefix.dangerous_set_table_name("reliar._migrations");
prefix.set_locking(false);
prefix
.run(&mut conn)
.await
.unwrap_or_else(|err| panic!("apply the first {count} migrations: {err}"));
conn.close().await.expect("close the dedicated connection");
}