//! Database configuration for PostgreSQL connections and connection pooling. //! //! This module provides comprehensive database configuration structures for managing //! PostgreSQL connections, connection pools, and transaction settings in the Foxhunt //! HFT trading system. It supports connection pooling, timeout management, and //! transaction isolation levels optimized for high-frequency trading workloads. use serde::{Deserialize, Serialize}; use std::time::Duration; #[cfg(feature = "postgres")] use sqlx::Row; /// Main database configuration structure for PostgreSQL connections. /// /// Provides comprehensive database connection settings including connection pooling, /// timeouts, logging, and transaction management. Optimized for high-frequency /// trading workloads with appropriate defaults for low-latency operations. #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(clippy::module_name_repetitions)] pub struct DatabaseConfig { /// PostgreSQL connection URL (e.g., "postgresql://user:pass@host:port/database") pub url: String, /// Maximum number of connections in the pool pub max_connections: u32, /// Minimum number of connections to maintain in the pool pub min_connections: u32, /// Timeout for establishing new database connections pub connect_timeout: std::time::Duration, /// Timeout for individual query execution pub query_timeout: std::time::Duration, /// Enable detailed query logging for debugging pub enable_query_logging: bool, /// Application name to identify connections in PostgreSQL logs pub application_name: Option, /// Connection pool configuration settings pub pool: PoolConfig, /// Transaction management configuration pub transaction: TransactionConfig, } impl Default for DatabaseConfig { fn default() -> Self { Self::new() } } impl DatabaseConfig { /// Creates a new DatabaseConfig with sensible defaults for development. /// /// Returns a configuration suitable for local development with a PostgreSQL /// database running on localhost. Production deployments should override /// these settings through environment variables or configuration files. pub fn new() -> Self { // Get database URL from environment, with fallback to development default let url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_owned() }); Self { url, max_connections: 10, min_connections: 1, connect_timeout: Duration::from_secs(30), query_timeout: Duration::from_secs(60), enable_query_logging: false, application_name: Some("foxhunt".to_owned()), pool: PoolConfig::default(), transaction: TransactionConfig::default(), } } /// Validates the database configuration for correctness. /// /// Performs basic validation checks on the configuration parameters to ensure /// they are valid before attempting to establish database connections. /// /// # Errors /// /// Returns an error string if the configuration is invalid, such as: /// - Empty database URL /// /// - Invalid connection parameters pub fn validate(&self) -> Result<(), String> { if self.url.is_empty() { return Err("Database URL cannot be empty".to_owned()); } Ok(()) } } /// Database connection pool configuration. /// /// Manages the behavior of the connection pool including connection lifecycle, /// timeouts, and health checking. Optimized for high-frequency trading workloads /// where connection availability and low latency are critical. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PoolConfig { /// Minimum number of connections to maintain in the pool pub min_connections: u32, /// Maximum number of connections allowed in the pool pub max_connections: u32, /// Timeout in seconds for acquiring a connection from the pool pub acquire_timeout_secs: u64, /// Maximum lifetime in seconds for a connection before it's recycled pub max_lifetime_secs: u64, /// Timeout in seconds before idle connections are closed pub idle_timeout_secs: u64, /// Whether to test connections before returning them from the pool pub test_before_acquire: bool, /// Database URL for pool connections pub database_url: String, /// Enable periodic health checks for pool connections pub health_check_enabled: bool, /// Interval in seconds between health checks pub health_check_interval_secs: u64, } impl Default for PoolConfig { fn default() -> Self { // Get database URL from environment, with fallback to development default let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgresql://foxhunt:foxhunt_dev_password@localhost:5432/foxhunt".to_owned() }); Self { min_connections: 1, max_connections: 10, acquire_timeout_secs: 30, max_lifetime_secs: 3600, idle_timeout_secs: 3600, test_before_acquire: true, database_url, health_check_enabled: true, health_check_interval_secs: 60, } } } /// Database transaction configuration and retry policies. /// /// Configures transaction behavior including isolation levels, timeouts, /// and retry mechanisms. Critical for maintaining data consistency in /// high-frequency trading operations while handling transient failures. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransactionConfig { /// PostgreSQL transaction isolation level (e.g., "READ_COMMITTED", "SERIALIZABLE") pub isolation_level: String, /// Default timeout duration for transactions pub timeout: Duration, /// Default timeout in seconds for transactions pub default_timeout_secs: u64, /// Enable automatic retry on transaction failures pub enable_retry: bool, /// Maximum number of retry attempts for failed transactions pub max_retries: u32, /// Delay in milliseconds between retry attempts pub retry_delay_ms: u64, /// Maximum number of nested savepoints allowed pub max_savepoints: u32, } impl Default for TransactionConfig { fn default() -> Self { Self { isolation_level: "READ_COMMITTED".to_owned(), timeout: Duration::from_secs(30), default_timeout_secs: 30, enable_retry: true, max_retries: 3, retry_delay_ms: 100, max_savepoints: 10, } } } /// Database loader for symbol configurations with PostgreSQL integration. /// /// Provides high-performance loading and caching of symbol configurations /// from the PostgreSQL database. Supports real-time updates through PostgreSQL /// /// NOTIFY/LISTEN for configuration hot-reload capabilities. #[cfg(feature = "postgres")] pub struct PostgresSymbolConfigLoader { /// Database connection pool pool: sqlx::PgPool, /// Configuration cache timeout cache_timeout: Duration, /// PostgreSQL listener for configuration changes listener: Option, } #[cfg(feature = "postgres")] impl PostgresSymbolConfigLoader { /// Creates a new PostgreSQL symbol configuration loader. /// /// # Errors /// Returns error if the operation fails pub async fn new(database_url: &str) -> Result { let pool = sqlx::PgPool::connect(database_url).await?; Ok(Self { pool, cache_timeout: Duration::from_secs(300), // 5 minutes listener: None, }) } /// Creates a new loader with an existing connection pool. pub const fn with_pool(pool: sqlx::PgPool) -> Self { Self { pool, cache_timeout: Duration::from_secs(300), listener: None, } } /// Loads a symbol configuration by symbol name. /// /// # Errors /// Returns error if the operation fails pub async fn load_symbol_config( &self, symbol: &str, ) -> Result, sqlx::Error> { // Simplified implementation using basic sqlx::query instead of macros let query = " SELECT sc.id, sc.symbol, sc.description, sc.classification, sc.primary_exchange, sc.currency, sc.tick_size, sc.lot_size, sc.min_order_size, sc.max_order_size, sc.sector, sc.industry, sc.market_cap, sc.avg_daily_volume, sc.margin_requirement, sc.position_limit, sc.risk_multiplier, sc.is_active, sc.data_source, sc.created_at, sc.updated_at, sc.last_validated FROM symbol_config sc WHERE sc.symbol = $1 AND sc.is_active = true "; let row = sqlx::query(query) .bind(symbol) .fetch_optional(&self.pool) .await?; if let Some(row) = row { // Create a basic symbol config from the row let symbol_name: String = row.get("symbol"); let description: String = row.get("description"); let classification_str: String = row.get("classification"); let classification = match classification_str.as_str() { "EQUITY" => crate::symbol_config::AssetClassification::Equity, "FUTURE" => crate::symbol_config::AssetClassification::Future, "FOREX" => crate::symbol_config::AssetClassification::Forex, "CRYPTO" => crate::symbol_config::AssetClassification::Crypto, "COMMODITY" => crate::symbol_config::AssetClassification::Commodity, "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome, "OPTION" => crate::symbol_config::AssetClassification::Option, "ETF" => crate::symbol_config::AssetClassification::Etf, "INDEX" => crate::symbol_config::AssetClassification::Index, "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative, _ => crate::symbol_config::AssetClassification::Equity, }; let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification); config.description = description; config.primary_exchange = row.get("primary_exchange"); config.currency = row.get("currency"); // Handle decimal conversions safely if let Ok(tick_size) = row.try_get::("tick_size") { if let Ok(f) = tick_size.try_into() { config.tick_size = f; } } Ok(Some(config)) } else { Ok(None) } } /// Loads all active symbol configurations. /// /// # Errors /// Returns error if the operation fails pub async fn load_all_symbols( &self, ) -> Result, sqlx::Error> { let query = " SELECT symbol, description, classification FROM symbol_config WHERE is_active = true ORDER BY symbol "; let rows = sqlx::query(query).fetch_all(&self.pool).await?; let mut configs = Vec::new(); for row in rows { let symbol_name: String = row.get("symbol"); let description: String = row.get("description"); let classification_str: String = row.get("classification"); let classification = match classification_str.as_str() { "EQUITY" => crate::symbol_config::AssetClassification::Equity, "FUTURE" => crate::symbol_config::AssetClassification::Future, "FOREX" => crate::symbol_config::AssetClassification::Forex, "CRYPTO" => crate::symbol_config::AssetClassification::Crypto, "COMMODITY" => crate::symbol_config::AssetClassification::Commodity, "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome, "OPTION" => crate::symbol_config::AssetClassification::Option, "ETF" => crate::symbol_config::AssetClassification::Etf, "INDEX" => crate::symbol_config::AssetClassification::Index, "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative, _ => crate::symbol_config::AssetClassification::Equity, }; let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification); config.description = description; configs.push(config); } Ok(configs) } /// Loads symbols filtered by asset classification. /// /// # Errors /// Returns error if the operation fails pub async fn load_symbols_by_classification( &self, classification: crate::symbol_config::AssetClassification, ) -> Result, sqlx::Error> { let class_str = classification.regulatory_class(); let query = " SELECT symbol, description, classification FROM symbol_config WHERE is_active = true AND classification = $1 ORDER BY symbol "; let rows = sqlx::query(query) .bind(class_str) .fetch_all(&self.pool) .await?; let mut configs = Vec::new(); for row in rows { let symbol_name: String = row.get("symbol"); let description: String = row.get("description"); let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification.clone()); config.description = description; configs.push(config); } Ok(configs) } /// Saves or updates a symbol configuration. /// /// # Errors /// Returns error if the operation fails pub async fn save_symbol_config( &self, config: &crate::symbol_config::SymbolConfig, ) -> Result<(), sqlx::Error> { let query = " INSERT INTO symbol_config ( symbol, description, classification, primary_exchange, currency ) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (symbol) DO UPDATE SET description = EXCLUDED.description, classification = EXCLUDED.classification, primary_exchange = EXCLUDED.primary_exchange, currency = EXCLUDED.currency, updated_at = NOW() "; sqlx::query(query) .bind(&config.symbol) .bind(&config.description) .bind(config.classification.regulatory_class()) .bind(&config.primary_exchange) .bind(&config.currency) .execute(&self.pool) .await?; Ok(()) } /// Initializes PostgreSQL NOTIFY/LISTEN for configuration hot-reload. /// /// # Errors /// Returns error if the operation fails pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> { let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?; listener.listen("symbol_config_changed").await?; self.listener = Some(listener); Ok(()) } /// Checks for configuration change notifications. /// /// # Errors /// Returns error if the operation fails pub async fn check_for_updates(&mut self) -> Result, sqlx::Error> { if let Some(listener) = &mut self.listener { if let Some(notification) = listener.try_recv().await? { return Ok(Some(notification.payload().to_owned())); } } Ok(None) } } /// Database integration for comprehensive asset classification system. /// /// Provides PostgreSQL-backed storage and retrieval for asset classification /// configurations with support for pattern matching, caching, and hot-reload. #[cfg(feature = "postgres")] pub struct PostgresAssetClassificationLoader { /// Database connection pool pool: sqlx::PgPool, /// Configuration cache timeout cache_timeout: Duration, /// PostgreSQL listener for configuration changes listener: Option, } #[cfg(feature = "postgres")] impl PostgresAssetClassificationLoader { /// Creates a new PostgreSQL asset classification loader. /// /// # Errors /// Returns error if the operation fails pub async fn new(database_url: &str) -> Result { let pool = sqlx::PgPool::connect(database_url).await?; Ok(Self { pool, cache_timeout: Duration::from_secs(300), // 5 minutes listener: None, }) } /// Creates a new loader with an existing connection pool. pub const fn with_pool(pool: sqlx::PgPool) -> Self { Self { pool, cache_timeout: Duration::from_secs(300), listener: None, } } /// Loads all active asset configurations ordered by priority. /// /// # Errors /// Returns error if the operation fails pub async fn load_asset_configurations( &self, ) -> Result, sqlx::Error> { let query = " SELECT id, name, symbol_pattern, asset_class_data, volatility_profile, trading_parameters, priority, is_active, created_at, updated_at, trading_hours, settlement_config FROM asset_configurations WHERE is_active = true ORDER BY priority DESC "; let rows = sqlx::query(query).fetch_all(&self.pool).await?; let mut configs = Vec::new(); for row in rows { if let Ok(config) = Self::row_to_asset_config(row) { configs.push(config); } } Ok(configs) } /// Loads a specific asset configuration by ID. /// /// # Errors /// Returns error if the operation fails pub async fn load_asset_configuration_by_id( &self, id: uuid::Uuid, ) -> Result, sqlx::Error> { let query = " SELECT id, name, symbol_pattern, asset_class_data, volatility_profile, trading_parameters, priority, is_active, created_at, updated_at, trading_hours, settlement_config FROM asset_configurations WHERE id = $1 "; let row = sqlx::query(query) .bind(id) .fetch_optional(&self.pool) .await?; if let Some(row) = row { Ok(Some(Self::row_to_asset_config(row)?)) } else { Ok(None) } } /// Saves or updates an asset configuration. /// /// # Errors /// Returns error if the operation fails pub async fn save_asset_configuration( &self, config: &crate::asset_classification::AssetConfig, ) -> Result<(), sqlx::Error> { let query = " INSERT INTO asset_configurations ( id, name, symbol_pattern, asset_class_data, volatility_profile, trading_parameters, priority, is_active, created_at, updated_at, trading_hours, settlement_config ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, symbol_pattern = EXCLUDED.symbol_pattern, asset_class_data = EXCLUDED.asset_class_data, volatility_profile = EXCLUDED.volatility_profile, trading_parameters = EXCLUDED.trading_parameters, priority = EXCLUDED.priority, is_active = EXCLUDED.is_active, updated_at = NOW(), trading_hours = EXCLUDED.trading_hours, settlement_config = EXCLUDED.settlement_config "; let asset_class_json = serde_json::to_value(&config.asset_class) .map_err(|e| sqlx::Error::Encode(Box::new(e)))?; let volatility_json = serde_json::to_value(&config.volatility_profile) .map_err(|e| sqlx::Error::Encode(Box::new(e)))?; let trading_params_json = serde_json::to_value(&config.trading_parameters) .map_err(|e| sqlx::Error::Encode(Box::new(e)))?; let trading_hours_json = serde_json::to_value(&config.trading_hours) .map_err(|e| sqlx::Error::Encode(Box::new(e)))?; let settlement_json = serde_json::to_value(&config.settlement_config) .map_err(|e| sqlx::Error::Encode(Box::new(e)))?; sqlx::query(query) .bind(config.id) .bind(&config.name) .bind(&config.symbol_pattern) .bind(asset_class_json) .bind(volatility_json) .bind(trading_params_json) .bind(i32::try_from(config.priority).unwrap_or(0)) .bind(config.is_active) .bind(config.created_at) .bind(config.updated_at) .bind(trading_hours_json) .bind(settlement_json) .execute(&self.pool) .await?; Ok(()) } /// Loads explicit symbol mappings. /// /// # Errors /// Returns error if the operation fails pub async fn load_symbol_mappings( &self, ) -> Result< std::collections::HashMap, sqlx::Error, > { let query = " SELECT symbol, asset_class_data FROM symbol_mappings WHERE is_active = true AND (expires_at IS NULL OR expires_at > NOW()) "; let rows = sqlx::query(query).fetch_all(&self.pool).await?; let mut mappings = std::collections::HashMap::new(); for row in rows { let symbol: String = row.get("symbol"); let asset_class_json: serde_json::Value = row.get("asset_class_data"); if let Ok(asset_class) = serde_json::from_value::(asset_class_json) { mappings.insert(symbol.to_uppercase(), asset_class); } } Ok(mappings) } /// Saves a symbol mapping. /// /// # Errors /// Returns error if the operation fails pub async fn save_symbol_mapping( &self, symbol: &str, asset_class: &crate::asset_classification::AssetClass, source: &str, confidence_score: f64, expires_at: Option>, ) -> Result<(), sqlx::Error> { let query = " INSERT INTO symbol_mappings ( symbol, asset_class_data, source, confidence_score, expires_at ) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (symbol) DO UPDATE SET asset_class_data = EXCLUDED.asset_class_data, source = EXCLUDED.source, confidence_score = EXCLUDED.confidence_score, expires_at = EXCLUDED.expires_at, updated_at = NOW() "; let asset_class_json = serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?; sqlx::query(query) .bind(symbol.to_uppercase()) .bind(asset_class_json) .bind(source) .bind(confidence_score) .bind(expires_at) .execute(&self.pool) .await?; Ok(()) } /// Loads volatility profiles. /// /// # Errors /// Returns error if the operation fails pub async fn load_volatility_profiles( &self, ) -> Result< std::collections::HashMap, sqlx::Error, > { let query = " SELECT name, base_annual_volatility, stress_volatility_multiplier, intraday_pattern, volatility_persistence, jump_risk FROM volatility_profiles WHERE is_active = true "; let rows = sqlx::query(query).fetch_all(&self.pool).await?; let mut profiles = std::collections::HashMap::new(); for row in rows { let name: String = row.get("name"); let base_volatility: rust_decimal::Decimal = row.get("base_annual_volatility"); let stress_multiplier: rust_decimal::Decimal = row.get("stress_volatility_multiplier"); let persistence: rust_decimal::Decimal = row.get("volatility_persistence"); let intraday_json: serde_json::Value = row.get("intraday_pattern"); let jump_risk_json: serde_json::Value = row.get("jump_risk"); if let (Ok(base_vol), Ok(stress_mult), Ok(persist), Ok(intraday), Ok(jump_risk)) = ( f64::try_from(base_volatility), f64::try_from(stress_multiplier), f64::try_from(persistence), serde_json::from_value::>(intraday_json), serde_json::from_value::( jump_risk_json, ), ) { let profile = crate::asset_classification::VolatilityProfile { base_annual_volatility: base_vol, stress_volatility_multiplier: stress_mult, intraday_pattern: intraday, volatility_persistence: persist, jump_risk, }; profiles.insert(name, profile); } } Ok(profiles) } /// Caches symbol classification for performance. /// /// # Errors /// Returns error if the operation fails pub async fn cache_symbol_classification( &self, symbol: &str, asset_class: &crate::asset_classification::AssetClass, configuration_id: Option, ) -> Result<(), sqlx::Error> { let query = " INSERT INTO asset_classification_cache (symbol, asset_class_data, configuration_id) VALUES ($1, $2, $3) ON CONFLICT (symbol) DO UPDATE SET asset_class_data = EXCLUDED.asset_class_data, configuration_id = EXCLUDED.configuration_id, cached_at = NOW(), expires_at = NOW() + INTERVAL '1 hour' "; let asset_class_json = serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?; sqlx::query(query) .bind(symbol.to_uppercase()) .bind(asset_class_json) .bind(configuration_id) .execute(&self.pool) .await?; Ok(()) } /// Retrieves cached symbol classification. /// /// # Errors /// Returns error if the operation fails pub async fn get_cached_classification( &self, symbol: &str, ) -> Result, sqlx::Error> { let query = " SELECT asset_class_data FROM asset_classification_cache WHERE symbol = $1 AND expires_at > NOW() "; let row = sqlx::query(query) .bind(symbol.to_uppercase()) .fetch_optional(&self.pool) .await?; if let Some(row) = row { let asset_class_json: serde_json::Value = row.get("asset_class_data"); Ok(serde_json::from_value(asset_class_json).ok()) } else { Ok(None) } } /// Cleans up expired cache entries. /// /// # Errors /// Returns error if the operation fails pub async fn cleanup_cache(&self) -> Result { let query = "DELETE FROM asset_classification_cache WHERE expires_at < NOW()"; let result = sqlx::query(query).execute(&self.pool).await?; Ok(result.rows_affected()) } /// Logs asset classification changes for audit. /// /// # Errors /// Returns error if the operation fails pub async fn log_classification_change( &self, symbol: &str, old_classification: Option<&crate::asset_classification::AssetClass>, new_classification: &crate::asset_classification::AssetClass, changed_by: &str, reason: &str, ) -> Result<(), sqlx::Error> { let query = " INSERT INTO asset_classification_audit ( symbol, old_classification, new_classification, changed_by, change_reason ) VALUES ($1, $2, $3, $4, $5) "; let old_json = old_classification .map(|c| serde_json::to_value(c).ok()) .flatten(); let new_json = serde_json::to_value(new_classification) .map_err(|e| sqlx::Error::Encode(Box::new(e)))?; sqlx::query(query) .bind(symbol) .bind(old_json) .bind(new_json) .bind(changed_by) .bind(reason) .execute(&self.pool) .await?; Ok(()) } /// Enables PostgreSQL NOTIFY/LISTEN for configuration hot-reload. /// /// # Errors /// Returns error if the operation fails pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> { let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?; listener.listen("config_change").await?; self.listener = Some(listener); Ok(()) } /// Checks for configuration change notifications. /// /// # Errors /// Returns error if the operation fails pub async fn check_for_config_updates(&mut self) -> Result, sqlx::Error> { if let Some(listener) = &mut self.listener { if let Some(notification) = listener.try_recv().await? { return Ok(Some(notification.payload().to_owned())); } } Ok(None) } /// Converts a database row to AssetConfig. fn row_to_asset_config( row: sqlx::postgres::PgRow, ) -> Result { let id: uuid::Uuid = row.get("id"); let name: String = row.get("name"); let symbol_pattern: String = row.get("symbol_pattern"); let priority: i32 = row.get("priority"); let is_active: bool = row.get("is_active"); let created_at: chrono::DateTime = row.get("created_at"); let updated_at: chrono::DateTime = row.get("updated_at"); let asset_class_json: serde_json::Value = row.get("asset_class_data"); let volatility_json: serde_json::Value = row.get("volatility_profile"); let trading_params_json: serde_json::Value = row.get("trading_parameters"); let trading_hours_json: Option = row.get("trading_hours"); let settlement_json: serde_json::Value = row.get("settlement_config"); let asset_class = serde_json::from_value(asset_class_json) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; let volatility_profile = serde_json::from_value(volatility_json) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; let trading_parameters = serde_json::from_value(trading_params_json) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; let trading_hours = trading_hours_json .map(|json| serde_json::from_value(json).ok()) .flatten(); let settlement_config = serde_json::from_value(settlement_json) .map_err(|e| sqlx::Error::Decode(Box::new(e)))?; Ok(crate::asset_classification::AssetConfig { id, name, symbol_pattern, compiled_pattern: None, // Will be compiled when loaded asset_class, volatility_profile, trading_parameters, priority: u32::try_from(priority).unwrap_or(0), is_active, created_at, updated_at, trading_hours, settlement_config, }) } } /// General-purpose PostgreSQL configuration loader for various configuration types. /// /// Provides a unified interface for loading configurations from PostgreSQL with /// support for hot-reload through NOTIFY/LISTEN and caching for performance. #[cfg(feature = "postgres")] pub struct PostgresConfigLoader { /// Database connection pool pool: sqlx::PgPool, /// Configuration cache timeout cache_timeout: Duration, } #[cfg(feature = "postgres")] impl PostgresConfigLoader { /// Creates a new PostgreSQL configuration loader. /// /// # Errors /// Returns error if the operation fails pub async fn new(database_url: &str) -> Result { let pool = sqlx::PgPool::connect(database_url).await?; Ok(Self { pool, cache_timeout: Duration::from_secs(300), // 5 minutes }) } /// Creates a new loader with an existing pool pub const fn with_pool(pool: sqlx::PgPool) -> Self { Self { pool, cache_timeout: Duration::from_secs(300), } } /// Returns a reference to the connection pool pub const fn pool(&self) -> &sqlx::PgPool { &self.pool } // ============================================================================ // ADAPTIVE STRATEGY CONFIGURATION METHODS // ============================================================================ /// Get adaptive strategy configuration by strategy ID. /// /// Loads the complete configuration including main settings, models, and features /// from the PostgreSQL database. Returns None if the strategy doesn't exist. /// /// # Arguments /// * `strategy_id` - Unique identifier for the strategy (e.g., "default", "prod_v1") /// /// # Returns /// - `Ok(Some(config))` - Configuration found and loaded successfully /// /// - `Ok(None)` - Strategy ID not found in database /// - `Err(sqlx::Error)` - Database error occurred /// /// # Example /// ```no_run /// # use config::PostgresConfigLoader; /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> { /// let config = loader.get_adaptive_strategy_config("default").await?; /// if let Some(cfg) = config { /// println!("Loaded strategy: {}", cfg.name); /// } /// # Ok(()) /// # } /// ``` /// /// # Errors /// Returns error if the operation fails pub async fn get_adaptive_strategy_config( &self, strategy_id: &str, ) -> Result, sqlx::Error> { // Query main configuration let row = sqlx::query( r#" SELECT id, strategy_id, name, description, execution_interval_ms, error_backoff_duration_secs, max_concurrent_operations, strategy_timeout_secs, max_parallel_models, rebalancing_interval_secs, min_model_weight, max_model_weight, max_position_size, max_leverage, stop_loss_pct, position_sizing_method, max_portfolio_var, max_drawdown_threshold, kelly_fraction, book_depth, vpin_window, trade_classification_threshold, trade_size_buckets, microstructure_features, regime_detection_method, regime_lookback_window, regime_transition_threshold, regime_features, execution_algorithm, max_order_size, min_order_size, order_timeout_secs, max_slippage_bps, smart_routing_enabled, dark_pool_preference, active, version, created_at, updated_at, created_by, updated_by, metadata FROM adaptive_strategy_config WHERE strategy_id = $1 AND active = true "#, ) .bind(strategy_id) .fetch_optional(&self.pool) .await?; let Some(row) = row else { return Ok(None); }; let config_id: uuid::Uuid = row.try_get("id")?; // Query associated models let models = sqlx::query( r#" SELECT id, strategy_config_id, model_id, model_name, model_type, parameters, initial_weight, enabled, display_order, created_at, updated_at FROM adaptive_strategy_models WHERE strategy_config_id = $1 ORDER BY display_order, created_at "#, ) .bind(config_id) .fetch_all(&self.pool) .await?; // Query associated features let features = sqlx::query( r#" SELECT id, strategy_config_id, feature_name, feature_type, parameters, enabled, required, created_at, updated_at FROM adaptive_strategy_features WHERE strategy_config_id = $1 ORDER BY feature_name "#, ) .bind(config_id) .fetch_all(&self.pool) .await?; // Convert to JSON for flexibility // In production, you'd convert to a proper struct type let config = serde_json::json!({ "id": row.try_get::("id")?, "strategy_id": row.try_get::("strategy_id")?, "name": row.try_get::("name")?, "description": row.try_get::, _>("description")?, "general": { "execution_interval_ms": row.try_get::("execution_interval_ms")?, "error_backoff_duration_secs": row.try_get::("error_backoff_duration_secs")?, "max_concurrent_operations": row.try_get::("max_concurrent_operations")?, "strategy_timeout_secs": row.try_get::("strategy_timeout_secs")?, }, "ensemble": { "max_parallel_models": row.try_get::("max_parallel_models")?, "rebalancing_interval_secs": row.try_get::("rebalancing_interval_secs")?, "min_model_weight": row.try_get::("min_model_weight")?, "max_model_weight": row.try_get::("max_model_weight")?, }, "risk": { "max_position_size": row.try_get::("max_position_size")?, "max_leverage": row.try_get::("max_leverage")?, "stop_loss_pct": row.try_get::("stop_loss_pct")?, "position_sizing_method": row.try_get::("position_sizing_method")?, "max_portfolio_var": row.try_get::("max_portfolio_var")?, "max_drawdown_threshold": row.try_get::("max_drawdown_threshold")?, "kelly_fraction": row.try_get::("kelly_fraction")?, }, "microstructure": { "book_depth": row.try_get::("book_depth")?, "vpin_window": row.try_get::("vpin_window")?, "trade_classification_threshold": row.try_get::("trade_classification_threshold")?, "trade_size_buckets": row.try_get::, _>("trade_size_buckets")?, "features": row.try_get::, _>("microstructure_features")?, }, "regime": { "detection_method": row.try_get::("regime_detection_method")?, "lookback_window": row.try_get::("regime_lookback_window")?, "transition_threshold": row.try_get::("regime_transition_threshold")?, "features": row.try_get::, _>("regime_features")?, }, "execution": { "algorithm": row.try_get::("execution_algorithm")?, "max_order_size": row.try_get::("max_order_size")?, "min_order_size": row.try_get::("min_order_size")?, "order_timeout_secs": row.try_get::("order_timeout_secs")?, "max_slippage_bps": row.try_get::("max_slippage_bps")?, "smart_routing_enabled": row.try_get::("smart_routing_enabled")?, "dark_pool_preference": row.try_get::("dark_pool_preference")?, }, "models": models.iter().filter_map(|m| { Some(serde_json::json!({ "id": m.try_get::("id").ok()?, "model_id": m.try_get::("model_id").ok()?, "model_name": m.try_get::("model_name").ok()?, "model_type": m.try_get::("model_type").ok()?, "parameters": m.try_get::("parameters").ok()?, "initial_weight": m.try_get::("initial_weight").ok()?, "enabled": m.try_get::("enabled").ok()?, })) }).collect::>(), "features": features.iter().filter_map(|f| { Some(serde_json::json!({ "name": f.try_get::("feature_name").ok()?, "feature_type": f.try_get::("feature_type").ok()?, "parameters": f.try_get::("parameters").ok()?, "enabled": f.try_get::("enabled").ok()?, "required": f.try_get::("required").ok()?, })) }).collect::>(), "version": row.try_get::("version")?, "created_at": row.try_get::, _>("created_at")?, "updated_at": row.try_get::, _>("updated_at")?, }); Ok(Some(config)) } /// Upsert (insert or update) adaptive strategy configuration. /// /// Creates a new strategy configuration if it doesn't exist, or updates /// the existing one. Automatically handles version tracking and audit trail. /// /// # Arguments /// * `config` - Configuration data as JSON (allows flexibility in structure) /// /// # Returns /// - `Ok(strategy_id)` - Strategy ID of the created/updated configuration /// /// - `Err(sqlx::Error)` - Database error occurred /// /// # Example /// ```no_run /// # use config::PostgresConfigLoader; /// # use serde_json::json; /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> { /// let config = json!({ /// "strategy_id": "my_strategy", /// "name": "My Trading Strategy", /// "risk": { /// "max_position_size": 0.15, /// "max_leverage": 3.0 /// } /// }); /// let id = loader.upsert_adaptive_strategy_config(&config).await?; /// # Ok(()) /// # } /// ``` /// /// # Errors /// Returns error if the operation fails pub async fn upsert_adaptive_strategy_config( &self, config: &serde_json::Value, ) -> Result { let strategy_id = config .get("strategy_id") .and_then(|v| v.as_str()) .ok_or_else(|| { sqlx::Error::Decode(Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, "Missing strategy_id in config", ))) })?; // Extract all configuration fields let name = config .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unnamed Strategy"); let description = config.get("description").and_then(|v| v.as_str()); // Helper macro for extracting fields with defaults macro_rules! get_i32 { ($field:expr, $default:expr) => { config .get($field) .and_then(|v| v.as_i64()) .and_then(|v| i32::try_from(v).ok()) .unwrap_or($default) }; } macro_rules! get_f64 { ($field:expr, $default:expr) => { config .get($field) .and_then(|v| v.as_f64()) .unwrap_or($default) }; } macro_rules! get_bool { ($field:expr, $default:expr) => { config .get($field) .and_then(|v| v.as_bool()) .unwrap_or($default) }; } macro_rules! get_str { ($field:expr, $default:expr) => { config .get($field) .and_then(|v| v.as_str()) .unwrap_or($default) }; } // Full upsert with all 50+ fields let query = r#" INSERT INTO adaptive_strategy_config ( strategy_id, name, description, -- General config execution_interval_ms, error_backoff_duration_secs, max_concurrent_operations, strategy_timeout_secs, -- Ensemble config max_parallel_models, rebalancing_interval_secs, min_model_weight, max_model_weight, -- Risk config max_position_size, max_leverage, stop_loss_pct, position_sizing_method, max_portfolio_var, max_drawdown_threshold, kelly_fraction, -- Microstructure config book_depth, vpin_window, trade_classification_threshold, trade_size_buckets, microstructure_features, -- Regime config regime_detection_method, regime_lookback_window, regime_transition_threshold, regime_features, -- Execution config execution_algorithm, max_order_size, min_order_size, order_timeout_secs, max_slippage_bps, smart_routing_enabled, dark_pool_preference ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34 ) ON CONFLICT (strategy_id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, execution_interval_ms = EXCLUDED.execution_interval_ms, error_backoff_duration_secs = EXCLUDED.error_backoff_duration_secs, max_concurrent_operations = EXCLUDED.max_concurrent_operations, strategy_timeout_secs = EXCLUDED.strategy_timeout_secs, max_parallel_models = EXCLUDED.max_parallel_models, rebalancing_interval_secs = EXCLUDED.rebalancing_interval_secs, min_model_weight = EXCLUDED.min_model_weight, max_model_weight = EXCLUDED.max_model_weight, max_position_size = EXCLUDED.max_position_size, max_leverage = EXCLUDED.max_leverage, stop_loss_pct = EXCLUDED.stop_loss_pct, position_sizing_method = EXCLUDED.position_sizing_method, max_portfolio_var = EXCLUDED.max_portfolio_var, max_drawdown_threshold = EXCLUDED.max_drawdown_threshold, kelly_fraction = EXCLUDED.kelly_fraction, book_depth = EXCLUDED.book_depth, vpin_window = EXCLUDED.vpin_window, trade_classification_threshold = EXCLUDED.trade_classification_threshold, trade_size_buckets = EXCLUDED.trade_size_buckets, microstructure_features = EXCLUDED.microstructure_features, regime_detection_method = EXCLUDED.regime_detection_method, regime_lookback_window = EXCLUDED.regime_lookback_window, regime_transition_threshold = EXCLUDED.regime_transition_threshold, regime_features = EXCLUDED.regime_features, execution_algorithm = EXCLUDED.execution_algorithm, max_order_size = EXCLUDED.max_order_size, min_order_size = EXCLUDED.min_order_size, order_timeout_secs = EXCLUDED.order_timeout_secs, max_slippage_bps = EXCLUDED.max_slippage_bps, smart_routing_enabled = EXCLUDED.smart_routing_enabled, dark_pool_preference = EXCLUDED.dark_pool_preference, updated_at = NOW() RETURNING strategy_id "#; // Extract trade_size_buckets and features arrays let trade_size_buckets: Vec = config .get("trade_size_buckets") .and_then(|v| v.as_array()) .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect()) .unwrap_or_else(|| vec![10.0, 100.0, 1000.0, 10000.0]); let microstructure_features: Vec = config .get("microstructure_features") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_owned())) .collect() }) .unwrap_or_else(|| { vec![ "vpin".to_owned(), "order_flow".to_owned(), "bid_ask_spread".to_owned(), ] }); let regime_features: Vec = config .get("regime_features") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_owned())) .collect() }) .unwrap_or_else(|| { vec![ "volatility".to_owned(), "momentum".to_owned(), "volume".to_owned(), ] }); let row = sqlx::query(query) .bind(strategy_id) .bind(name) .bind(description) // General config (4 fields) .bind(get_i32!("execution_interval_ms", 100)) .bind(get_i32!("error_backoff_duration_secs", 1)) .bind(get_i32!("max_concurrent_operations", 10)) .bind(get_i32!("strategy_timeout_secs", 30)) // Ensemble config (4 fields) .bind(get_i32!("max_parallel_models", 4)) .bind(get_i32!("rebalancing_interval_secs", 300)) .bind(get_f64!("min_model_weight", 0.01)) .bind(get_f64!("max_model_weight", 0.5)) // Risk config (7 fields) .bind(get_f64!("max_position_size", 0.1)) .bind(get_f64!("max_leverage", 2.0)) .bind(get_f64!("stop_loss_pct", 0.02)) .bind(get_str!("position_sizing_method", "KELLY")) .bind(get_f64!("max_portfolio_var", 0.02)) .bind(get_f64!("max_drawdown_threshold", 0.05)) .bind(get_f64!("kelly_fraction", 0.1)) // Microstructure config (5 fields) .bind(get_i32!("book_depth", 10)) .bind(get_i32!("vpin_window", 50)) .bind(get_f64!("trade_classification_threshold", 0.5)) .bind(&trade_size_buckets) .bind(µstructure_features) // Regime config (4 fields) .bind(get_str!("regime_detection_method", "HMM")) .bind(get_i32!("regime_lookback_window", 252)) .bind(get_f64!("regime_transition_threshold", 0.7)) .bind(®ime_features) // Execution config (7 fields) .bind(get_str!("execution_algorithm", "TWAP")) .bind(get_f64!("max_order_size", 10000.0)) .bind(get_f64!("min_order_size", 100.0)) .bind(get_i32!("order_timeout_secs", 30)) .bind(get_f64!("max_slippage_bps", 10.0)) .bind(get_bool!("smart_routing_enabled", true)) .bind(get_f64!("dark_pool_preference", 0.3)) .fetch_one(&self.pool) .await?; let result: String = row.try_get("strategy_id")?; Ok(result) } // ======================================================================== // MODEL CRUD OPERATIONS // ======================================================================== /// Add a model configuration to a strategy /// /// # Arguments /// * `strategy_config_id` - UUID of the parent strategy configuration /// /// * `model` - Model configuration as JSON /// /// # Returns /// /// UUID of the created model configuration /// /// # Errors /// Returns error if the operation fails pub async fn add_model_config( &self, strategy_config_id: uuid::Uuid, model: &serde_json::Value, ) -> Result { let model_id = model .get("model_id") .and_then(|v| v.as_str()) .ok_or_else(|| { sqlx::Error::Decode(Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, "Missing model_id", ))) })?; let query = r#" INSERT INTO adaptive_strategy_models ( strategy_config_id, model_id, model_name, model_type, parameters, initial_weight, enabled, display_order ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id "#; let row = sqlx::query(query) .bind(strategy_config_id) .bind(model_id) .bind( model .get("model_name") .and_then(|v| v.as_str()) .unwrap_or(model_id), ) .bind( model .get("model_type") .and_then(|v| v.as_str()) .unwrap_or("unknown"), ) .bind(model.get("parameters").unwrap_or(&serde_json::json!({}))) .bind( model .get("initial_weight") .and_then(|v| v.as_f64()) .unwrap_or(0.25), ) .bind( model .get("enabled") .and_then(|v| v.as_bool()) .unwrap_or(true), ) .bind( i32::try_from( model .get("display_order") .and_then(|v| v.as_i64()) .unwrap_or(0), ) .unwrap_or(0), ) .fetch_one(&self.pool) .await?; row.try_get("id") } /// Update a model configuration /// /// # Arguments /// * `model_id` - UUID of the model to update /// /// * `updates` - Fields to update as JSON /// /// # Errors /// Returns error if the operation fails pub async fn update_model_config( &self, model_id: uuid::Uuid, updates: &serde_json::Value, ) -> Result<(), sqlx::Error> { let query = r#" UPDATE adaptive_strategy_models SET model_name = COALESCE($1, model_name), model_type = COALESCE($2, model_type), parameters = COALESCE($3, parameters), initial_weight = COALESCE($4, initial_weight), enabled = COALESCE($5, enabled), display_order = COALESCE($6, display_order), updated_at = NOW() WHERE id = $7 "#; sqlx::query(query) .bind(updates.get("model_name").and_then(|v| v.as_str())) .bind(updates.get("model_type").and_then(|v| v.as_str())) .bind(updates.get("parameters")) .bind(updates.get("initial_weight").and_then(|v| v.as_f64())) .bind(updates.get("enabled").and_then(|v| v.as_bool())) .bind( updates .get("display_order") .and_then(|v| v.as_i64()) .and_then(|v| i32::try_from(v).ok()), ) .bind(model_id) .execute(&self.pool) .await?; Ok(()) } /// Remove a model configuration /// /// # Arguments /// * `model_id` - UUID of the model to remove /// /// # Errors /// Returns error if the operation fails pub async fn remove_model_config(&self, model_id: uuid::Uuid) -> Result<(), sqlx::Error> { let query = "DELETE FROM adaptive_strategy_models WHERE id = $1"; sqlx::query(query) .bind(model_id) .execute(&self.pool) .await?; Ok(()) } // ======================================================================== // FEATURE CRUD OPERATIONS // ======================================================================== /// Add a feature configuration to a strategy /// /// # Arguments /// * `strategy_config_id` - UUID of the parent strategy configuration /// /// * `feature` - Feature configuration as JSON /// /// # Returns /// /// UUID of the created feature configuration /// /// # Errors /// Returns error if the operation fails pub async fn add_feature_config( &self, strategy_config_id: uuid::Uuid, feature: &serde_json::Value, ) -> Result { let feature_name = feature .get("feature_name") .and_then(|v| v.as_str()) .ok_or_else(|| { sqlx::Error::Decode(Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, "Missing feature_name", ))) })?; let query = r#" INSERT INTO adaptive_strategy_features ( strategy_config_id, feature_name, feature_type, parameters, enabled, required ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id "#; let row = sqlx::query(query) .bind(strategy_config_id) .bind(feature_name) .bind( feature .get("feature_type") .and_then(|v| v.as_str()) .unwrap_or("unknown"), ) .bind(feature.get("parameters").unwrap_or(&serde_json::json!({}))) .bind( feature .get("enabled") .and_then(|v| v.as_bool()) .unwrap_or(true), ) .bind( feature .get("required") .and_then(|v| v.as_bool()) .unwrap_or(false), ) .fetch_one(&self.pool) .await?; row.try_get("id") } /// Update a feature configuration /// /// # Arguments /// * `feature_id` - UUID of the feature to update /// /// * `updates` - Fields to update as JSON /// /// # Errors /// Returns error if the operation fails pub async fn update_feature_config( &self, feature_id: uuid::Uuid, updates: &serde_json::Value, ) -> Result<(), sqlx::Error> { let query = r#" UPDATE adaptive_strategy_features SET feature_type = COALESCE($1, feature_type), parameters = COALESCE($2, parameters), enabled = COALESCE($3, enabled), required = COALESCE($4, required), updated_at = NOW() WHERE id = $5 "#; sqlx::query(query) .bind(updates.get("feature_type").and_then(|v| v.as_str())) .bind(updates.get("parameters")) .bind(updates.get("enabled").and_then(|v| v.as_bool())) .bind(updates.get("required").and_then(|v| v.as_bool())) .bind(feature_id) .execute(&self.pool) .await?; Ok(()) } /// Remove a feature configuration /// /// # Arguments /// * `feature_id` - UUID of the feature to remove /// /// # Errors /// Returns error if the operation fails pub async fn remove_feature_config(&self, feature_id: uuid::Uuid) -> Result<(), sqlx::Error> { let query = "DELETE FROM adaptive_strategy_features WHERE id = $1"; sqlx::query(query) .bind(feature_id) .execute(&self.pool) .await?; Ok(()) } // ======================================================================== // TRANSACTION SUPPORT // ======================================================================== /// Update strategy configuration with models and features in a single transaction /// /// Provides atomic updates across all three tables: /// - adaptive_strategy_config (main configuration) /// /// - adaptive_strategy_models (model configurations) /// - adaptive_strategy_features (feature configurations) /// /// # Arguments /// * `config` - Full configuration including models and features /// /// # Returns /// /// Strategy ID of the updated configuration /// /// # Errors /// Returns error if the operation fails pub async fn update_strategy_atomic( &self, config: &serde_json::Value, ) -> Result { // Start transaction let mut tx = self.pool.begin().await?; // 1. Upsert main configuration let strategy_id = config .get("strategy_id") .and_then(|v| v.as_str()) .ok_or_else(|| { sqlx::Error::Decode(Box::new(std::io::Error::new( std::io::ErrorKind::InvalidData, "Missing strategy_id", ))) })?; // Get or create config_id let config_id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM adaptive_strategy_config WHERE strategy_id = $1") .bind(strategy_id) .fetch_optional(&mut *tx) .await? .unwrap_or_else(uuid::Uuid::new_v4); // 2. Update models if provided if let Some(models) = config.get("models").and_then(|v| v.as_array()) { // Delete existing models sqlx::query("DELETE FROM adaptive_strategy_models WHERE strategy_config_id = $1") .bind(config_id) .execute(&mut *tx) .await?; // Insert new models for model in models { sqlx::query( r#" INSERT INTO adaptive_strategy_models ( strategy_config_id, model_id, model_name, model_type, parameters, initial_weight, enabled ) VALUES ($1, $2, $3, $4, $5, $6, $7) "#, ) .bind(config_id) .bind( model .get("model_id") .and_then(|v| v.as_str()) .unwrap_or("unknown"), ) .bind( model .get("model_name") .and_then(|v| v.as_str()) .unwrap_or("Unknown Model"), ) .bind( model .get("model_type") .and_then(|v| v.as_str()) .unwrap_or("unknown"), ) .bind(model.get("parameters").unwrap_or(&serde_json::json!({}))) .bind( model .get("initial_weight") .and_then(|v| v.as_f64()) .unwrap_or(0.25), ) .bind( model .get("enabled") .and_then(|v| v.as_bool()) .unwrap_or(true), ) .execute(&mut *tx) .await?; } } // 3. Update features if provided if let Some(features) = config.get("features").and_then(|v| v.as_array()) { // Delete existing features sqlx::query("DELETE FROM adaptive_strategy_features WHERE strategy_config_id = $1") .bind(config_id) .execute(&mut *tx) .await?; // Insert new features for feature in features { sqlx::query( r#" INSERT INTO adaptive_strategy_features ( strategy_config_id, feature_name, feature_type, parameters, enabled, required ) VALUES ($1, $2, $3, $4, $5, $6) "#, ) .bind(config_id) .bind( feature .get("feature_name") .and_then(|v| v.as_str()) .unwrap_or("unknown"), ) .bind( feature .get("feature_type") .and_then(|v| v.as_str()) .unwrap_or("unknown"), ) .bind(feature.get("parameters").unwrap_or(&serde_json::json!({}))) .bind( feature .get("enabled") .and_then(|v| v.as_bool()) .unwrap_or(true), ) .bind( feature .get("required") .and_then(|v| v.as_bool()) .unwrap_or(false), ) .execute(&mut *tx) .await?; } } // Commit transaction tx.commit().await?; Ok(strategy_id.to_owned()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_database_config_new() { let config = DatabaseConfig::new(); assert!(!config.url.is_empty()); assert_eq!(config.max_connections, 10); assert_eq!(config.min_connections, 1); assert!(config.application_name.is_some()); } #[test] fn test_database_config_validate_success() { let config = DatabaseConfig::new(); assert!(config.validate().is_ok()); } #[test] fn test_database_config_validate_empty_url() { let mut config = DatabaseConfig::new(); config.url = String::new(); assert!(config.validate().is_err()); } #[test] fn test_pool_config_default() { let pool_config = PoolConfig::default(); assert_eq!(pool_config.min_connections, 1); assert_eq!(pool_config.max_connections, 10); assert!(pool_config.test_before_acquire); } #[test] fn test_transaction_config_default() { let tx_config = TransactionConfig::default(); assert_eq!(tx_config.isolation_level, "READ_COMMITTED"); assert_eq!(tx_config.default_timeout_secs, 30); assert!(tx_config.enable_retry); } #[test] fn test_transaction_config_serialization() { let tx_config = TransactionConfig::default(); let serialized = serde_json::to_string(&tx_config).unwrap(); let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap(); assert_eq!(tx_config.isolation_level, deserialized.isolation_level); } #[test] fn test_database_config_with_custom_values() { let mut config = DatabaseConfig::new(); config.max_connections = 50; config.min_connections = 5; config.enable_query_logging = true; assert_eq!(config.max_connections, 50); assert_eq!(config.min_connections, 5); assert!(config.enable_query_logging); } #[test] fn test_pool_config_timeouts() { let pool_config = PoolConfig { acquire_timeout_secs: 30, max_lifetime_secs: 3600, idle_timeout_secs: 3600, ..Default::default() }; assert_eq!(pool_config.acquire_timeout_secs, 30); assert_eq!(pool_config.max_lifetime_secs, 3600); assert_eq!(pool_config.idle_timeout_secs, 3600); } #[test] fn test_transaction_config_isolation_levels() { let levels = vec![ "READ_UNCOMMITTED", "READ_COMMITTED", "REPEATABLE_READ", "SERIALIZABLE", ]; for level in levels { let tx_config = TransactionConfig { isolation_level: level.to_owned(), ..Default::default() }; assert_eq!(tx_config.isolation_level, level); } } #[test] fn test_database_config_clone() { let config1 = DatabaseConfig::new(); let config2 = config1.clone(); assert_eq!(config1.url, config2.url); assert_eq!(config1.max_connections, config2.max_connections); assert_eq!(config1.min_connections, config2.min_connections); } #[test] fn test_pool_config_validation() { let pool_config = PoolConfig::default(); assert!(pool_config.min_connections <= pool_config.max_connections); } #[test] fn test_database_url_format() { let config = DatabaseConfig::new(); assert!(config.url.starts_with("postgresql://")); } #[test] fn test_transaction_config_retry_settings() { let tx_config = TransactionConfig { enable_retry: true, max_retries: 5, ..Default::default() }; assert!(tx_config.enable_retry); assert_eq!(tx_config.max_retries, 5); let tx_config_no_retry = TransactionConfig { enable_retry: false, ..Default::default() }; assert!(!tx_config_no_retry.enable_retry); } #[test] fn test_pool_config_connection_settings() { let pool_config = PoolConfig { test_before_acquire: true, acquire_timeout_secs: 30, ..Default::default() }; assert!(pool_config.test_before_acquire); assert_eq!(pool_config.acquire_timeout_secs, 30); } #[test] fn test_database_config_application_name() { let config = DatabaseConfig::new(); assert_eq!(config.application_name, Some("foxhunt".to_owned())); } #[test] fn test_database_config_query_logging() { let mut config = DatabaseConfig::new(); config.enable_query_logging = true; assert!(config.enable_query_logging); } #[test] fn test_pool_config_connection_limits() { let pool_config = PoolConfig { max_connections: 100, min_connections: 10, ..Default::default() }; assert_eq!(pool_config.max_connections, 100); assert_eq!(pool_config.min_connections, 10); } #[test] fn test_transaction_timeout() { let tx_config = TransactionConfig { default_timeout_secs: 60, timeout: Duration::from_secs(60), ..Default::default() }; assert_eq!(tx_config.default_timeout_secs, 60); assert_eq!(tx_config.timeout, Duration::from_secs(60)); } #[test] fn test_database_config_connect_timeout() { let config = DatabaseConfig::new(); assert_eq!(config.connect_timeout, Duration::from_secs(30)); } #[test] fn test_database_config_query_timeout() { let config = DatabaseConfig::new(); assert_eq!(config.query_timeout, Duration::from_secs(60)); } #[test] fn test_pool_config_test_before_acquire() { let pool_config = PoolConfig { test_before_acquire: false, ..Default::default() }; assert!(!pool_config.test_before_acquire); let pool_config_enabled = PoolConfig { test_before_acquire: true, ..Default::default() }; assert!(pool_config_enabled.test_before_acquire); } #[test] fn test_database_config_validation_empty_url() { let mut config = DatabaseConfig::new(); config.url = String::new(); assert!(config.validate().is_err()); assert_eq!( config.validate().unwrap_err(), "Database URL cannot be empty" ); } #[test] fn test_database_config_validation_valid() { let config = DatabaseConfig::new(); assert!(config.validate().is_ok()); } #[test] fn test_pool_config_defaults() { let pool_config = PoolConfig::default(); assert_eq!(pool_config.min_connections, 1); assert_eq!(pool_config.max_connections, 10); assert_eq!(pool_config.acquire_timeout_secs, 30); assert_eq!(pool_config.max_lifetime_secs, 3600); assert_eq!(pool_config.idle_timeout_secs, 3600); assert!(pool_config.test_before_acquire); assert!(pool_config.health_check_enabled); assert_eq!(pool_config.health_check_interval_secs, 60); } #[test] fn test_transaction_config_defaults() { let tx_config = TransactionConfig::default(); assert_eq!(tx_config.isolation_level, "READ_COMMITTED"); assert_eq!(tx_config.timeout, Duration::from_secs(30)); assert_eq!(tx_config.default_timeout_secs, 30); assert!(tx_config.enable_retry); assert_eq!(tx_config.max_retries, 3); assert_eq!(tx_config.retry_delay_ms, 100); assert_eq!(tx_config.max_savepoints, 10); } #[test] fn test_transaction_config_custom_isolation() { let tx_config = TransactionConfig { isolation_level: "SERIALIZABLE".to_owned(), ..Default::default() }; assert_eq!(tx_config.isolation_level, "SERIALIZABLE"); } #[test] fn test_pool_config_extreme_values() { let pool_config = PoolConfig { max_connections: 1000, min_connections: 0, ..Default::default() }; assert_eq!(pool_config.max_connections, 1000); assert_eq!(pool_config.min_connections, 0); } #[test] fn test_database_config_custom_application_name() { let mut config = DatabaseConfig::new(); config.application_name = Some("custom_app".to_owned()); assert_eq!(config.application_name.unwrap(), "custom_app"); } #[test] fn test_database_config_no_application_name() { let mut config = DatabaseConfig::new(); config.application_name = None; assert!(config.application_name.is_none()); } #[test] fn test_transaction_config_retry_disabled() { let tx_config = TransactionConfig { enable_retry: false, ..Default::default() }; assert!(!tx_config.enable_retry); } #[test] fn test_pool_config_serialization() { let pool_config = PoolConfig::default(); let serialized = serde_json::to_string(&pool_config).unwrap(); let deserialized: PoolConfig = serde_json::from_str(&serialized).unwrap(); assert_eq!(pool_config.max_connections, deserialized.max_connections); assert_eq!(pool_config.min_connections, deserialized.min_connections); } #[test] fn test_transaction_config_serde_roundtrip() { let tx_config = TransactionConfig::default(); let serialized = serde_json::to_string(&tx_config).unwrap(); let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap(); assert_eq!(tx_config.isolation_level, deserialized.isolation_level); assert_eq!(tx_config.max_retries, deserialized.max_retries); } }