#[cfg(feature = "test-pg")]
pub(crate) mod pg {
use backon::{ConstantBuilder, Retryable as _};
use martin_core::tiles::postgres::PostgresPool;
use testcontainers_modules::postgres::Postgres;
use testcontainers_modules::testcontainers::runners::AsyncRunner as _;
use testcontainers_modules::testcontainers::{ContainerAsync, ImageExt as _};
use crate::config::file::CachePolicy;
use crate::config::file::postgres::{PostgresAutoDiscoveryBuilder, PostgresConfig};
use crate::config::primitives::IdResolver;
pub(crate) async fn start_postgres_11_with_posgis_3_container() -> ContainerAsync<Postgres> {
const MAX_START_ATTEMPTS: usize = 3;
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2);
(|| async {
Postgres::default()
.with_name("postgis/postgis")
.with_tag("11-3.0") .start()
.await
})
.retry(
ConstantBuilder::default()
.with_delay(RETRY_DELAY)
.with_max_times(MAX_START_ATTEMPTS),
)
.sleep(tokio::time::sleep)
.await
.expect("failed to launch container after retry attempts")
}
pub(crate) async fn connection_string(container: &ContainerAsync<Postgres>) -> String {
let host = container.get_host().await.expect("resolve container host");
let port = container
.get_host_port_ipv4(5432)
.await
.expect("resolve container port");
format!("postgres://postgres:postgres@{host}:{port}/postgres?sslmode=disable")
}
pub(crate) async fn builder_for(
config_yaml: &str,
) -> (
PostgresAutoDiscoveryBuilder,
ContainerAsync<Postgres>,
String,
) {
let container = start_postgres_11_with_posgis_3_container().await;
let connection_string = connection_string(&container).await;
let mut config: PostgresConfig =
serde_saphyr::from_str(config_yaml).expect("parse PostgresConfig YAML");
config.connection_string = Some(connection_string.clone());
let builder = PostgresAutoDiscoveryBuilder::new(
&config,
IdResolver::default(),
CachePolicy::default(),
)
.await
.expect("create PostgresAutoDiscoveryBuilder");
(builder, container, connection_string)
}
pub(crate) async fn seed(connection_string: &str, sql: &str) {
let pool = PostgresPool::new(connection_string, None, None, None, 2)
.await
.expect("open seed pool");
pool.get()
.await
.expect("acquire seed connection")
.batch_execute(sql)
.await
.expect("execute seed SQL");
}
}
#[cfg(feature = "unstable-duckdb")]
pub(crate) mod duckdb {
use std::path::PathBuf;
use duckdb::Connection;
use martin_core::tiles::duckdb::DuckDBPool;
use tempfile::TempDir;
pub(crate) struct TestDatabase {
_dir: TempDir,
path: PathBuf,
}
impl TestDatabase {
pub(crate) fn from_sql(filename: &str, sql: &str) -> Self {
let dir = TempDir::new().expect("temporary DuckDB directory");
let path = dir.path().join(filename);
let conn = Connection::open(&path).expect("writable DuckDB database");
conn.execute_batch("INSTALL spatial;")
.expect("spatial extension installed");
conn.execute_batch("LOAD spatial;")
.expect("spatial extension loaded");
conn.execute_batch(sql).expect("fixture SQL executed");
Self { _dir: dir, path }
}
pub(crate) fn read_only_pool(&self, id: &str, pool_size: usize) -> DuckDBPool {
DuckDBPool::new_database_file(id.to_string(), self.path.clone(), pool_size, None, None)
.expect("read-only DuckDB pool")
}
}
pub(crate) struct TestGeoParquet {
_dir: TempDir,
path: PathBuf,
}
impl TestGeoParquet {
pub(crate) fn from_sql(
parquet_filename: &str,
setup_sql: &str,
source_table: &str,
) -> Self {
let dir = TempDir::new().expect("temporary GeoParquet directory");
let path = dir.path().join(parquet_filename);
let conn = Connection::open_in_memory().expect("in-memory DuckDB database");
conn.execute_batch("INSTALL spatial;")
.expect("spatial extension installed");
conn.execute_batch("LOAD spatial;")
.expect("spatial extension loaded");
conn.execute_batch(setup_sql)
.expect("GeoParquet fixture SQL executed");
let export_path = path.to_str().expect("parquet path must be UTF-8");
let escaped_export_path = export_path.replace('\'', "''");
conn.execute_batch(&format!(
"COPY {source_table} TO '{escaped_export_path}' (FORMAT PARQUET);"
))
.expect("GeoParquet fixture exported");
Self { _dir: dir, path }
}
#[must_use]
pub(crate) fn path(&self) -> &std::path::Path {
&self.path
}
pub(crate) fn query_pool(&self, id: &str, pool_size: usize) -> DuckDBPool {
let db_path = self._dir.path().join("_query.duckdb");
let conn = Connection::open(&db_path).expect("writable scratch DuckDB database");
conn.execute_batch("INSTALL spatial;")
.expect("spatial extension installed");
conn.execute_batch("LOAD spatial;")
.expect("spatial extension loaded");
drop(conn);
DuckDBPool::new_database_file(id.to_string(), db_path, pool_size, None, None)
.expect("read-only DuckDB query pool")
}
}
}