use crate::sql_generator::PostgresSqlGenerator;
use crate::tls::PgTlsMode;
use async_trait::async_trait;
use deadpool_postgres::{Config, Pool, Runtime};
use rust_ef::error::{EFError, EFResult};
#[cfg(feature = "tracing")]
use rust_ef::provider::IAsyncConnection;
use rust_ef::provider::{IDatabaseProvider, ISqlGenerator};
use tokio_postgres::NoTls;
pub struct PostgresProvider {
pool: Pool,
#[cfg(feature = "tracing")]
slow_query_threshold_ms: std::sync::atomic::AtomicU64,
}
impl PostgresProvider {
pub fn new(connection_string: &str, pool_size: usize) -> EFResult<Self> {
let connector = native_tls::TlsConnector::builder()
.build()
.map_err(|e| EFError::connection(format!("TLS connector init failed: {}", e)))?;
Self::new_with_tls(connection_string, pool_size, PgTlsMode::Require(connector))
}
pub fn new_insecure(connection_string: &str, pool_size: usize) -> EFResult<Self> {
Self::new_with_tls(connection_string, pool_size, PgTlsMode::Disable)
}
pub fn new_with_tls(
connection_string: &str,
pool_size: usize,
tls: PgTlsMode,
) -> EFResult<Self> {
let config: tokio_postgres::Config = connection_string
.parse()
.map_err(|e| EFError::connection(format!("Invalid connection string: {}", e)))?;
let mut cfg = Config::new();
if let Some(tokio_postgres::config::Host::Tcp(h)) = config.get_hosts().first() {
cfg.host = Some(h.clone());
}
cfg.port = Some(config.get_ports().first().copied().unwrap_or(5432));
cfg.dbname = Some(config.get_dbname().unwrap_or("postgres").to_string());
cfg.user = Some(config.get_user().unwrap_or("postgres").to_string());
cfg.password = config
.get_password()
.map(|p| String::from_utf8_lossy(p).to_string());
if pool_size > 0 {
cfg.pool.get_or_insert_default().max_size = pool_size;
}
let pool = match tls {
PgTlsMode::Disable => cfg
.create_pool(Some(Runtime::Tokio1), NoTls)
.map_err(|e| EFError::connection(format!("Failed to create pool: {}", e)))?,
PgTlsMode::Require(connector) => {
let tls = postgres_native_tls::MakeTlsConnector::new(connector);
cfg.create_pool(Some(Runtime::Tokio1), tls)
.map_err(|e| EFError::connection(format!("Failed to create pool: {}", e)))?
}
};
Ok(Self {
pool,
#[cfg(feature = "tracing")]
slow_query_threshold_ms: std::sync::atomic::AtomicU64::new(0),
})
}
}
#[async_trait]
impl IDatabaseProvider for PostgresProvider {
fn sql_generator(&self) -> &'static dyn ISqlGenerator {
&PostgresSqlGenerator
}
async fn get_connection(&self) -> EFResult<Box<dyn rust_ef::provider::IAsyncConnection>> {
let _guard = rust_ef::observability::PoolAcquireGuard::new("PostgreSQL");
let client = self
.pool
.get()
.await
.map_err(|e| EFError::connection(format!("Pool error: {}", e)))?;
#[cfg_attr(not(feature = "tracing"), allow(unused_mut))]
let mut conn = Box::new(crate::connection::PostgresConnection::new(client));
#[cfg(feature = "tracing")]
{
let ms = self
.slow_query_threshold_ms
.load(std::sync::atomic::Ordering::Relaxed);
if ms > 0 {
conn.set_slow_query_threshold(std::time::Duration::from_millis(ms));
}
}
Ok(conn)
}
async fn execute_migration_command(&self, sql: &str) -> EFResult<()> {
let client = self
.pool
.get()
.await
.map_err(|e| EFError::connection(format!("Pool error: {}", e)))?;
client
.batch_execute(sql)
.await
.map_err(|e| EFError::migration(format!("Migration execution failed: {}", e)))?;
Ok(())
}
fn name(&self) -> &str {
"PostgreSQL"
}
fn migration_dialect(&self) -> rust_ef::migration::MigrationDialect {
rust_ef::migration::MigrationDialect::Postgres
}
#[cfg(feature = "tracing")]
fn set_slow_query_threshold(&self, threshold: std::time::Duration) {
self.slow_query_threshold_ms.store(
threshold.as_millis() as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
}
impl std::fmt::Debug for PostgresProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PostgresProvider")
.field("name", &self.name())
.finish()
}
}