//! High-performance PostgreSQL database abstraction library //! //! This library provides a clean, type-safe abstraction over PostgreSQL with: //! - Connection pooling with health monitoring //! - Type-safe query building //! - Transaction management with savepoints //! - Comprehensive error handling //! - Performance metrics and monitoring //! //! # Examples //! //! ## Basic Usage //! //! ```rust,no_run //! use database::{Database, DatabaseConfig}; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! let config = DatabaseConfig::default(); //! let db = Database::new(config).await?; //! //! // Execute a simple query //! let count: i64 = db.fetch_one("SELECT COUNT(*) FROM users").await?; //! println!("User count: {}", count); //! //! Ok(()) //! } //! ``` //! //! ## Transaction Usage //! //! ```rust,no_run //! use database::{Database, DatabaseConfig}; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! let config = DatabaseConfig::default(); //! let db = Database::new(config).await?; //! //! // Use transaction with retry logic //! let result = db.with_transaction(|mut tx| async move { //! tx.execute("INSERT INTO users (name) VALUES ('Alice')").await?; //! tx.execute("UPDATE settings SET user_count = user_count + 1").await?; //! Ok((42, tx)) // Return value and transaction //! }).await?; //! //! println!("Transaction result: {}", result); //! Ok(()) //! } //! ``` pub mod error; pub mod pool; pub mod query; pub mod transaction; use crate::error::{DatabaseError, DatabaseResult, ErrorContext}; use crate::pool::{DatabasePool, PoolStats}; use crate::transaction::{DatabaseTransaction, TransactionManager, TransactionStats}; use crate::query::QueryBuilder; use config::database::DatabaseConfig; // serde imports removed - not needed use sqlx::postgres::PgRow; use sqlx::FromRow; use sqlx::migrate::Migrator; use std::future::Future; use std::time::Duration; use std::path::Path; use tracing::{debug, info}; /// Main database interface providing high-level operations #[derive(Debug, Clone)] pub struct Database { pool: DatabasePool, transaction_manager: TransactionManager, config: DatabaseConfig, } impl Database { /// Create a new database instance pub async fn new(config: DatabaseConfig) -> DatabaseResult { // Validate configuration config.validate().map_err(|e| DatabaseError::Configuration { message: e })?; info!( "Initializing database with application name: {}", config.application_name.as_deref().unwrap_or("foxhunt") ); // Create connection pool let pool = DatabasePool::new(config.pool.clone()).await?; // Create transaction manager let transaction_manager = TransactionManager::new(pool.clone(), config.transaction.clone()); let database = Self { pool, transaction_manager, config, }; // Perform initial health check database.health_check().await?; info!("Database initialization completed successfully"); Ok(database) } /// Create a database instance from environment variables pub async fn from_env() -> DatabaseResult { let database_url = std::env::var("DATABASE_URL").map_err(|_| DatabaseError::Configuration { message: "DATABASE_URL environment variable not set".to_string(), })?; let mut config = DatabaseConfig::new(); config.url = database_url; Self::new(config).await } /// Get a connection from the pool pub async fn acquire(&self) -> DatabaseResult> { self.pool.acquire().await } /// Execute a query and return affected rows pub async fn execute(&self, query: &str) -> DatabaseResult { if self.config.enable_query_logging { debug!("Executing query: {}", query); } let result = sqlx::query(query) .execute(self.pool.inner()) .await .with_query_context(query)?; Ok(result.rows_affected()) } /// Execute a parameterized query with a single parameter pub async fn execute_with_param(&self, query: &str, param: T) -> DatabaseResult where T: for<'q> sqlx::Encode<'q, sqlx::Postgres> + sqlx::Type + Send, { if self.config.enable_query_logging { debug!("Executing parameterized query: {}", query); } let result = sqlx::query(query) .bind(param) .execute(self.pool.inner()) .await .with_query_context(query)?; Ok(result.rows_affected()) } /// Fetch all rows from a query pub async fn fetch_all(&self, query: &str) -> DatabaseResult> where T: for<'r> FromRow<'r, PgRow> + Send + Unpin, { if self.config.enable_query_logging { debug!("Fetching all rows: {}", query); } let rows = sqlx::query_as(query) .fetch_all(self.pool.inner()) .await .with_query_context(query)?; Ok(rows) } /// Fetch one row from a query pub async fn fetch_one(&self, query: &str) -> DatabaseResult where T: for<'r> FromRow<'r, PgRow> + Send + Unpin, { if self.config.enable_query_logging { debug!("Fetching one row: {}", query); } let row = sqlx::query_as(query) .fetch_one(self.pool.inner()) .await .with_query_context(query)?; Ok(row) } /// Fetch optional row from a query pub async fn fetch_optional(&self, query: &str) -> DatabaseResult> where T: for<'r> FromRow<'r, PgRow> + Send + Unpin, { if self.config.enable_query_logging { debug!("Fetching optional row: {}", query); } let row = sqlx::query_as(query) .fetch_optional(self.pool.inner()) .await .with_query_context(query)?; Ok(row) } /// Begin a new transaction pub async fn begin_transaction(&self) -> DatabaseResult { self.transaction_manager.begin().await } /// Begin a transaction with custom timeout pub async fn begin_transaction_with_timeout( &self, timeout: Duration, ) -> DatabaseResult { self.transaction_manager.begin_with_timeout(timeout).await } /// Execute a closure within a transaction pub async fn with_transaction(&self, f: F) -> DatabaseResult where F: Fn(DatabaseTransaction) -> Fut + Send + Sync, Fut: Future> + Send, R: Send, { self.transaction_manager.with_transaction(f).await } /// Execute a closure within a transaction with custom timeout pub async fn with_transaction_timeout( &self, f: F, timeout: Duration, ) -> DatabaseResult where F: Fn(DatabaseTransaction) -> Fut + Send + Sync, Fut: Future> + Send, R: Send, { self.transaction_manager .with_transaction_timeout(f, timeout) .await } /// Create a new query builder pub fn query_builder() -> QueryBuilder { QueryBuilder::new() } /// Create a SELECT query builder pub fn select>(columns: &[T]) -> crate::query::SelectBuilder { QueryBuilder::select(columns) } /// Create an INSERT query builder pub fn insert(table: &str) -> crate::query::InsertBuilder { QueryBuilder::insert(table) } /// Create an UPDATE query builder pub fn update(table: &str) -> crate::query::UpdateBuilder { QueryBuilder::update(table) } /// Create a DELETE query builder pub fn delete(table: &str) -> crate::query::DeleteBuilder { QueryBuilder::delete(table) } /// Perform a health check on the database pub async fn health_check(&self) -> DatabaseResult { self.pool.health_check().await } /// Get connection pool statistics pub async fn pool_stats(&self) -> PoolStats { self.pool.stats().await } /// Get transaction statistics pub fn transaction_stats(&self) -> TransactionStats { self.transaction_manager.stats() } /// Get database configuration pub fn config(&self) -> &DatabaseConfig { &self.config } /// Test database connectivity pub async fn ping(&self) -> DatabaseResult<()> { self.pool.ping().await } /// Reset all statistics pub async fn reset_stats(&self) { self.pool.reset_stats().await; self.transaction_manager.reset_stats(); info!("All database statistics reset"); } /// Close the database connection pool pub async fn close(&self) { info!("Closing database connection pool"); self.pool.close().await; } /// Check if the database connection pool is closed pub fn is_closed(&self) -> bool { self.pool.is_closed() } /// Get current pool size pub fn pool_size(&self) -> u32 { self.pool.size() } /// Get number of idle connections pub fn idle_connections(&self) -> usize { self.pool.num_idle() } /// Execute a database migration pub async fn migrate(&self) -> DatabaseResult<()> { info!("Running database migrations"); let migrator = Migrator::new(Path::new("./migrations")) .await .map_err(|e| DatabaseError::Migration { message: format!("Migration setup failed: {}", e), })?; migrator .run(self.pool.inner()) .await .map_err(|e| DatabaseError::Migration { message: format!("Migration failed: {}", e), })?; info!("Database migrations completed successfully"); Ok(()) } /// Execute raw SQL with a single parameter pub async fn execute_raw(&self, sql: &str, arg: T) -> DatabaseResult where T: for<'q> sqlx::Encode<'q, sqlx::Postgres> + sqlx::Type + Send, { if self.config.enable_query_logging { debug!("Executing raw SQL: {}", sql); } let result = sqlx::query(sql) .bind(arg) .execute(self.pool.inner()) .await .with_query_context(sql)?; Ok(result.rows_affected()) } /// Check if a table exists pub async fn table_exists(&self, table_name: &str) -> DatabaseResult { let exists = sqlx::query_scalar( "SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 )", ) .bind(table_name) .fetch_one(self.pool.inner()) .await .with_context(&format!("Failed to check if table '{}' exists", table_name))?; Ok(exists) } /// Get database version pub async fn version(&self) -> DatabaseResult { let version: String = sqlx::query_scalar("SELECT version()") .fetch_one(self.pool.inner()) .await .with_context("Failed to get database version")?; Ok(version) } /// Get database size in bytes pub async fn database_size(&self) -> DatabaseResult { let size: i64 = sqlx::query_scalar("SELECT pg_database_size(current_database())") .fetch_one(self.pool.inner()) .await .with_context("Failed to get database size")?; Ok(size) } } /// Database health information #[derive(Debug, Clone)] pub struct HealthInfo { /// Database is healthy pub healthy: bool, /// Database version pub version: String, /// Connection pool status pub pool_stats: PoolStats, /// Transaction statistics pub transaction_stats: TransactionStats, /// Database size in bytes pub database_size: i64, /// Response time in milliseconds pub response_time_ms: u64, } impl Database { /// Get comprehensive health information pub async fn health_info(&self) -> DatabaseResult { let start = std::time::Instant::now(); // Perform health check let healthy = self.health_check().await.unwrap_or(false); // Get version (may fail if unhealthy) let version = self .version() .await .unwrap_or_else(|_| "unknown".to_string()); // Get statistics let pool_stats = self.pool_stats().await; let transaction_stats = self.transaction_stats(); // Get database size (may fail if unhealthy) let database_size = self.database_size().await.unwrap_or(0); let response_time_ms = start.elapsed().as_millis() as u64; Ok(HealthInfo { healthy, version, pool_stats, transaction_stats, database_size, response_time_ms, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_database_config_default() { let config = DatabaseConfig::default(); assert_eq!(config.application_name, "foxhunt-config"); assert!(!config.enable_query_logging); assert!(config.enable_metrics); } #[test] fn test_database_config_new() { let url = "postgresql://localhost:5432/test".to_string(); let config = DatabaseConfig::new(url.clone()); assert_eq!(config.pool.database_url, url); } #[test] fn test_database_config_builder() { let config = DatabaseConfig::default() .with_application_name("test-app".to_string()) .with_query_logging(true) .with_metrics(false); assert_eq!(config.application_name, "test-app"); assert!(config.enable_query_logging); assert!(!config.enable_metrics); } #[test] fn test_config_validation() { let mut config = DatabaseConfig::default(); assert!(config.validate().is_ok()); config.application_name = String::new(); assert!(config.validate().is_err()); } }