use crate::error::PostgresError;
use crate::{TransactionManager, TransactionTrait};
use async_trait::async_trait;
use sqlx::postgres::{PgPool, PgPoolOptions, PgTransaction};
use sqlx::query;
use std::fmt::Debug;
use std::process::Command;
use std::sync::Arc;
use testkit_core::{
DatabaseBackend, DatabaseConfig, DatabaseName, DatabasePool, TestDatabaseConnection,
TestDatabaseInstance,
};
use url;
#[derive(Debug)]
pub struct SqlxConnection {
pool: Arc<PgPool>,
connection_string: String,
}
impl Clone for SqlxConnection {
fn clone(&self) -> Self {
SqlxConnection {
pool: self.pool.clone(),
connection_string: self.connection_string.clone(),
}
}
}
impl SqlxConnection {
pub async fn connect(connection_string: impl Into<String>) -> Result<Self, PostgresError> {
let connection_string = connection_string.into();
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(5) .connect(&connection_string)
.await
.map_err(|e| PostgresError::ConnectionError(e.to_string()))?;
Ok(Self {
pool: Arc::new(pool),
connection_string,
})
}
pub fn pool_connection(&self) -> &PgPool {
&self.pool
}
pub fn client(&self) -> &SqlxConnection {
self
}
pub async fn execute(
&self,
query: &str,
_params: &[&(dyn std::fmt::Debug + Sync)],
) -> Result<u64, PostgresError> {
let result = sqlx::query(query)
.execute(&*self.pool)
.await
.map_err(|e| PostgresError::QueryError(e.to_string()))?;
Ok(result.rows_affected())
}
pub async fn query(
&self,
query: &str,
_params: &[&(dyn std::fmt::Debug + Sync)],
) -> Result<Vec<sqlx::postgres::PgRow>, PostgresError> {
let result = sqlx::query(query)
.fetch_all(&*self.pool)
.await
.map_err(|e| PostgresError::QueryError(e.to_string()))?;
Ok(result)
}
pub async fn with_connection<F, R, E>(
connection_string: impl Into<String>,
operation: F,
) -> Result<R, PostgresError>
where
F: FnOnce(&mut SqlxConnection) -> futures::future::BoxFuture<'_, Result<R, E>>,
E: std::error::Error + Send + Sync + 'static,
{
let mut conn = Self::connect(connection_string).await?;
let result = operation(&mut conn)
.await
.map_err(|e| PostgresError::QueryError(e.to_string()))?;
Ok(result)
}
}
impl TestDatabaseConnection for SqlxConnection {
fn connection_string(&self) -> String {
self.connection_string.clone()
}
}
#[derive(Clone)]
pub struct SqlxPool {
pool: PgPool,
connection_string: String,
}
#[async_trait]
impl DatabasePool for SqlxPool {
type Connection = SqlxConnection;
type Error = PostgresError;
async fn acquire(&self) -> Result<Self::Connection, Self::Error> {
let _conn = self
.pool
.acquire()
.await
.map_err(|e| PostgresError::ConnectionError(e.to_string()))?;
Ok(SqlxConnection {
pool: Arc::new(self.pool.clone()),
connection_string: self.connection_string.clone(),
})
}
async fn release(&self, _conn: Self::Connection) -> Result<(), Self::Error> {
Ok(())
}
fn connection_string(&self) -> String {
self.connection_string.clone()
}
}
#[derive(Clone, Debug)]
pub struct SqlxPostgresBackend {
config: DatabaseConfig,
}
#[async_trait]
impl DatabaseBackend for SqlxPostgresBackend {
type Connection = SqlxConnection;
type Pool = SqlxPool;
type Error = PostgresError;
async fn new(config: DatabaseConfig) -> Result<Self, Self::Error> {
Ok(Self { config })
}
async fn create_pool(
&self,
name: &DatabaseName,
config: &DatabaseConfig,
) -> Result<Self::Pool, Self::Error> {
let connection_string = self.connection_string(name);
let max_connections = config.max_connections.unwrap_or(5);
let pool_options = PgPoolOptions::new().max_connections(max_connections as u32);
let pool = pool_options
.connect(&connection_string)
.await
.map_err(|e| PostgresError::ConnectionError(e.to_string()))?;
Ok(SqlxPool {
pool,
connection_string,
})
}
async fn connect(&self, name: &DatabaseName) -> Result<Self::Connection, Self::Error> {
let connection_string = self.connection_string(name);
SqlxConnection::connect(connection_string).await
}
async fn connect_with_string(
&self,
connection_string: &str,
) -> Result<Self::Connection, Self::Error> {
SqlxConnection::connect(connection_string).await
}
async fn create_database(
&self,
_pool: &Self::Pool,
name: &DatabaseName,
) -> Result<(), Self::Error> {
let _url = url::Url::parse(&self.config.admin_url)
.map_err(|e| PostgresError::ConfigError(e.to_string()))?;
let admin_pool = PgPoolOptions::new()
.max_connections(1)
.connect(&self.config.admin_url)
.await
.map_err(|e| PostgresError::ConnectionError(e.to_string()))?;
let db_name = name.as_str();
let create_query = format!("CREATE DATABASE \"{}\"", db_name);
query(&create_query)
.execute(&admin_pool)
.await
.map_err(|e| PostgresError::DatabaseCreationError(e.to_string()))?;
Ok(())
}
fn drop_database(&self, name: &DatabaseName) -> Result<(), Self::Error> {
let url = match url::Url::parse(&self.config.admin_url) {
Ok(url) => url,
Err(e) => {
tracing::error!("Failed to parse admin URL: {}", e);
return Err(PostgresError::ConfigError(e.to_string()));
}
};
let database_name = name.as_str();
let test_user = url.username();
let database_host = format!(
"{}://{}:{}@{}:{}",
url.scheme(),
test_user,
url.password().unwrap_or(""),
url.host_str().unwrap_or("localhost"),
url.port().unwrap_or(5432)
);
let output = Command::new("psql")
.arg(&database_host)
.arg("-c")
.arg(format!("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> pg_backend_pid();", database_name))
.output();
if let Err(e) = output {
tracing::warn!(
"Failed to terminate connections to database {}: {}",
database_name,
e
);
}
let output = Command::new("psql")
.arg(&database_host)
.arg("-c")
.arg(format!("DROP DATABASE IF EXISTS \"{}\";", database_name))
.output();
match output {
Ok(output) => {
if output.status.success() {
tracing::info!("Successfully dropped database {}", name);
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::error!("Failed to drop database {}: {}", name, stderr);
Err(PostgresError::DatabaseDropError(stderr.to_string()))
}
}
Err(e) => {
tracing::error!("Failed to execute psql command to drop {}: {}", name, e);
Err(PostgresError::DatabaseDropError(e.to_string()))
}
}
}
fn connection_string(&self, name: &DatabaseName) -> String {
let url = url::Url::parse(&self.config.admin_url).expect("Failed to parse admin URL");
let scheme = url.scheme();
let username = url.username();
let password = url.password().unwrap_or("");
let host = url.host_str().unwrap_or("localhost");
let port = url.port().unwrap_or(5432);
format!(
"{}://{}:{}@{}:{}/{}",
scheme,
username,
password,
host,
port,
name.as_str()
)
}
}
pub struct SqlxTransaction {
transaction: Option<sqlx::Transaction<'static, sqlx::Postgres>>,
}
#[async_trait]
impl TransactionTrait for SqlxTransaction {
type Error = PostgresError;
async fn commit(&mut self) -> Result<(), Self::Error> {
if let Some(tx) = self.transaction.take() {
tx.commit()
.await
.map_err(|e| PostgresError::TransactionError(e.to_string()))
} else {
Err(PostgresError::TransactionError(
"No transaction to commit".to_string(),
))
}
}
async fn rollback(&mut self) -> Result<(), Self::Error> {
if let Some(tx) = self.transaction.take() {
tx.rollback()
.await
.map_err(|e| PostgresError::TransactionError(e.to_string()))
} else {
Err(PostgresError::TransactionError(
"No transaction to rollback".to_string(),
))
}
}
}
#[async_trait]
impl TransactionManager for TestDatabaseInstance<SqlxPostgresBackend> {
type Error = PostgresError;
type Tx = SqlxTransaction;
type Connection = SqlxConnection;
async fn begin_transaction(&mut self) -> Result<Self::Tx, Self::Error> {
let pool = &self.pool.pool;
let tx: PgTransaction = pool
.begin()
.await
.map_err(|e| PostgresError::TransactionError(e.to_string()))?;
Ok(SqlxTransaction {
transaction: Some(tx),
})
}
async fn commit_transaction(tx: &mut Self::Tx) -> Result<(), Self::Error> {
tx.commit().await
}
async fn rollback_transaction(tx: &mut Self::Tx) -> Result<(), Self::Error> {
tx.rollback().await
}
}
pub async fn sqlx_postgres_backend() -> Result<SqlxPostgresBackend, PostgresError> {
let config = DatabaseConfig::default();
SqlxPostgresBackend::new(config).await
}
pub async fn sqlx_postgres_backend_with_config(
config: DatabaseConfig,
) -> Result<SqlxPostgresBackend, PostgresError> {
SqlxPostgresBackend::new(config).await
}