//! PostgreSQL connection pool wrapper. //! //! Provides [`Database`] — a thin, cloneable wrapper around `sqlx::PgPool` //! with health-check and graceful shutdown. use std::time::Duration; use sqlx::Row; use sqlx::postgres::{PgPool, PgPoolOptions}; use crate::ImksResult; use super::config::DatabaseConfig; /// Cloneable handle to the PostgreSQL connection pool. /// /// All query execution goes through `pool()` which returns a `&PgPool`. #[derive(Clone)] pub struct Database { pool: PgPool, } impl Database { /// Create a new pool from config and verify connectivity with a ping. pub async fn connect(config: &DatabaseConfig) -> ImksResult { let pool = PgPoolOptions::new() .max_connections(config.max_connections) .min_connections(config.min_connections) .acquire_timeout(Duration::from_secs(config.connect_timeout_secs)) .idle_timeout(Duration::from_secs(config.idle_timeout_secs)) .connect(&config.url) .await?; tracing::info!( max_connections = config.max_connections, min_connections = config.min_connections, "PostgreSQL pool created" ); let db = Self { pool }; db.health_check().await?; Ok(db) } /// Wrap an existing `PgPool` (useful for tests with shared pools). pub fn from_pool(pool: PgPool) -> Self { Self { pool } } /// Access the inner `PgPool` for query execution. pub fn pool(&self) -> &PgPool { &self.pool } /// Verify the database is reachable by executing `SELECT 1`. pub async fn health_check(&self) -> ImksResult<()> { let row = sqlx::query("SELECT 1 AS alive") .fetch_one(&self.pool) .await?; let alive: i32 = row.get("alive"); if alive != 1 { return Err(crate::ImksError::Internal( "Database health check returned unexpected value".into(), )); } tracing::debug!("Database health check passed"); Ok(()) } /// Gracefully close all connections in the pool. pub async fn close(&self) { self.pool.close().await; tracing::info!("Database pool closed"); } }