Coverage Report

Created: 2025-10-06 12:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/jgrusewski/Work/foxhunt/config/src/database.rs
Line
Count
Source
1
//! Database configuration for PostgreSQL connections and connection pooling.
2
//!
3
//! This module provides comprehensive database configuration structures for managing
4
//! PostgreSQL connections, connection pools, and transaction settings in the Foxhunt
5
//! HFT trading system. It supports connection pooling, timeout management, and
6
//! transaction isolation levels optimized for high-frequency trading workloads.
7
8
use serde::{Deserialize, Serialize};
9
use std::time::Duration;
10
11
#[cfg(feature = "postgres")]
12
use sqlx::Row;
13
14
/// Main database configuration structure for PostgreSQL connections.
15
///
16
/// Provides comprehensive database connection settings including connection pooling,
17
/// timeouts, logging, and transaction management. Optimized for high-frequency
18
/// trading workloads with appropriate defaults for low-latency operations.
19
#[derive(Debug, Clone, Serialize, Deserialize)]
20
pub struct DatabaseConfig {
21
    /// PostgreSQL connection URL (e.g., "postgresql://user:pass@host:port/database")
22
    pub url: String,
23
    /// Maximum number of connections in the pool
24
    pub max_connections: u32,
25
    /// Minimum number of connections to maintain in the pool
26
    pub min_connections: u32,
27
    /// Timeout for establishing new database connections
28
    pub connect_timeout: std::time::Duration,
29
    /// Timeout for individual query execution
30
    pub query_timeout: std::time::Duration,
31
    /// Enable detailed query logging for debugging
32
    pub enable_query_logging: bool,
33
    /// Application name to identify connections in PostgreSQL logs
34
    pub application_name: Option<String>,
35
    /// Connection pool configuration settings
36
    pub pool: PoolConfig,
37
    /// Transaction management configuration
38
    pub transaction: TransactionConfig,
39
}
40
41
impl Default for DatabaseConfig {
42
0
    fn default() -> Self {
43
0
        Self::new()
44
0
    }
45
}
46
47
impl DatabaseConfig {
48
    /// Creates a new DatabaseConfig with sensible defaults for development.
49
    ///
50
    /// Returns a configuration suitable for local development with a PostgreSQL
51
    /// database running on localhost. Production deployments should override
52
    /// these settings through environment variables or configuration files.
53
14
    pub fn new() -> Self {
54
14
        Self {
55
14
            url: "postgresql://localhost/foxhunt".to_string(),
56
14
            max_connections: 10,
57
14
            min_connections: 1,
58
14
            connect_timeout: Duration::from_secs(30),
59
14
            query_timeout: Duration::from_secs(60),
60
14
            enable_query_logging: false,
61
14
            application_name: Some("foxhunt".to_string()),
62
14
            pool: PoolConfig::default(),
63
14
            transaction: TransactionConfig::default(),
64
14
        }
65
14
    }
66
67
    /// Validates the database configuration for correctness.
68
    ///
69
    /// Performs basic validation checks on the configuration parameters to ensure
70
    /// they are valid before attempting to establish database connections.
71
    ///
72
    /// # Errors
73
    ///
74
    /// Returns an error string if the configuration is invalid, such as:
75
    /// - Empty database URL
76
    /// - Invalid connection parameters
77
5
    pub fn validate(&self) -> Result<(), String> {
78
5
        if self.url.is_empty() {
79
3
            return Err("Database URL cannot be empty".to_string());
80
2
        }
81
2
        Ok(())
82
5
    }
83
}
84
85
/// Database connection pool configuration.
86
///
87
/// Manages the behavior of the connection pool including connection lifecycle,
88
/// timeouts, and health checking. Optimized for high-frequency trading workloads
89
/// where connection availability and low latency are critical.
90
#[derive(Debug, Clone, Serialize, Deserialize)]
91
pub struct PoolConfig {
92
    /// Minimum number of connections to maintain in the pool
93
    pub min_connections: u32,
94
    /// Maximum number of connections allowed in the pool
95
    pub max_connections: u32,
96
    /// Timeout in seconds for acquiring a connection from the pool
97
    pub acquire_timeout_secs: u64,
98
    /// Maximum lifetime in seconds for a connection before it's recycled
99
    pub max_lifetime_secs: u64,
100
    /// Timeout in seconds before idle connections are closed
101
    pub idle_timeout_secs: u64,
102
    /// Whether to test connections before returning them from the pool
103
    pub test_before_acquire: bool,
104
    /// Database URL for pool connections
105
    pub database_url: String,
106
    /// Enable periodic health checks for pool connections
107
    pub health_check_enabled: bool,
108
    /// Interval in seconds between health checks
109
    pub health_check_interval_secs: u64,
110
}
111
112
impl Default for PoolConfig {
113
24
    fn default() -> Self {
114
24
        Self {
115
24
            min_connections: 1,
116
24
            max_connections: 10,
117
24
            acquire_timeout_secs: 30,
118
24
            max_lifetime_secs: 1800,
119
24
            idle_timeout_secs: 600,
120
24
            test_before_acquire: true,
121
24
            database_url: "postgresql://localhost/foxhunt".to_string(),
122
24
            health_check_enabled: true,
123
24
            health_check_interval_secs: 60,
124
24
        }
125
24
    }
126
}
127
128
/// Database transaction configuration and retry policies.
129
///
130
/// Configures transaction behavior including isolation levels, timeouts,
131
/// and retry mechanisms. Critical for maintaining data consistency in
132
/// high-frequency trading operations while handling transient failures.
133
#[derive(Debug, Clone, Serialize, Deserialize)]
134
pub struct TransactionConfig {
135
    /// PostgreSQL transaction isolation level (e.g., "READ_COMMITTED", "SERIALIZABLE")
136
    pub isolation_level: String,
137
    /// Default timeout duration for transactions
138
    pub timeout: Duration,
139
    /// Default timeout in seconds for transactions
140
    pub default_timeout_secs: u64,
141
    /// Enable automatic retry on transaction failures
142
    pub enable_retry: bool,
143
    /// Maximum number of retry attempts for failed transactions
144
    pub max_retries: u32,
145
    /// Delay in milliseconds between retry attempts
146
    pub retry_delay_ms: u64,
147
    /// Maximum number of nested savepoints allowed
148
    pub max_savepoints: u32,
149
}
150
151
impl Default for TransactionConfig {
152
27
    fn default() -> Self {
153
27
        Self {
154
27
            isolation_level: "READ_COMMITTED".to_string(),
155
27
            timeout: Duration::from_secs(30),
156
27
            default_timeout_secs: 30,
157
27
            enable_retry: true,
158
27
            max_retries: 3,
159
27
            retry_delay_ms: 100,
160
27
            max_savepoints: 10,
161
27
        }
162
27
    }
163
}
164
165
/// Database loader for symbol configurations with PostgreSQL integration.
166
///
167
/// Provides high-performance loading and caching of symbol configurations
168
/// from the PostgreSQL database. Supports real-time updates through PostgreSQL
169
/// NOTIFY/LISTEN for configuration hot-reload capabilities.
170
#[cfg(feature = "postgres")]
171
pub struct PostgresSymbolConfigLoader {
172
    /// Database connection pool
173
    pool: sqlx::PgPool,
174
    /// Configuration cache timeout
175
    cache_timeout: Duration,
176
    /// PostgreSQL listener for configuration changes
177
    listener: Option<sqlx::postgres::PgListener>,
178
}
179
180
#[cfg(feature = "postgres")]
181
impl PostgresSymbolConfigLoader {
182
    /// Creates a new PostgreSQL symbol configuration loader.
183
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
184
        let pool = sqlx::PgPool::connect(database_url).await?;
185
186
        Ok(Self {
187
            pool,
188
            cache_timeout: Duration::from_secs(300), // 5 minutes
189
            listener: None,
190
        })
191
    }
192
193
    /// Creates a new loader with an existing connection pool.
194
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
195
        Self {
196
            pool,
197
            cache_timeout: Duration::from_secs(300),
198
            listener: None,
199
        }
200
    }
201
202
    /// Loads a symbol configuration by symbol name.
203
    pub async fn load_symbol_config(
204
        &self,
205
        symbol: &str,
206
    ) -> Result<Option<crate::symbol_config::SymbolConfig>, sqlx::Error> {
207
        // Simplified implementation using basic sqlx::query instead of macros
208
        let query = "
209
            SELECT 
210
                sc.id,
211
                sc.symbol,
212
                sc.description,
213
                sc.classification,
214
                sc.primary_exchange,
215
                sc.currency,
216
                sc.tick_size,
217
                sc.lot_size,
218
                sc.min_order_size,
219
                sc.max_order_size,
220
                sc.sector,
221
                sc.industry,
222
                sc.market_cap,
223
                sc.avg_daily_volume,
224
                sc.margin_requirement,
225
                sc.position_limit,
226
                sc.risk_multiplier,
227
                sc.is_active,
228
                sc.data_source,
229
                sc.created_at,
230
                sc.updated_at,
231
                sc.last_validated
232
            FROM symbol_config sc
233
            WHERE sc.symbol = $1 AND sc.is_active = true
234
        ";
235
236
        let row = sqlx::query(query)
237
            .bind(symbol)
238
            .fetch_optional(&self.pool)
239
            .await?;
240
241
        if let Some(row) = row {
242
            // Create a basic symbol config from the row
243
            let symbol_name: String = row.get("symbol");
244
            let description: String = row.get("description");
245
            let classification_str: String = row.get("classification");
246
247
            let classification = match classification_str.as_str() {
248
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
249
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
250
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
251
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
252
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
253
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
254
                "OPTION" => crate::symbol_config::AssetClassification::Option,
255
                "ETF" => crate::symbol_config::AssetClassification::Etf,
256
                "INDEX" => crate::symbol_config::AssetClassification::Index,
257
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
258
                _ => crate::symbol_config::AssetClassification::Equity,
259
            };
260
261
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
262
            config.description = description;
263
            config.primary_exchange = row.get("primary_exchange");
264
            config.currency = row.get("currency");
265
266
            // Handle decimal conversions safely
267
            if let Ok(tick_size) = row.try_get::<rust_decimal::Decimal, _>("tick_size") {
268
                if let Ok(f) = tick_size.try_into() {
269
                    config.tick_size = f;
270
                }
271
            }
272
273
            Ok(Some(config))
274
        } else {
275
            Ok(None)
276
        }
277
    }
278
279
    /// Loads all active symbol configurations.
280
    pub async fn load_all_symbols(
281
        &self,
282
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
283
        let query = "
284
            SELECT symbol, description, classification
285
            FROM symbol_config 
286
            WHERE is_active = true
287
            ORDER BY symbol
288
        ";
289
290
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
291
292
        let mut configs = Vec::new();
293
        for row in rows {
294
            let symbol_name: String = row.get("symbol");
295
            let description: String = row.get("description");
296
            let classification_str: String = row.get("classification");
297
298
            let classification = match classification_str.as_str() {
299
                "EQUITY" => crate::symbol_config::AssetClassification::Equity,
300
                "FUTURE" => crate::symbol_config::AssetClassification::Future,
301
                "FOREX" => crate::symbol_config::AssetClassification::Forex,
302
                "CRYPTO" => crate::symbol_config::AssetClassification::Crypto,
303
                "COMMODITY" => crate::symbol_config::AssetClassification::Commodity,
304
                "FIXED_INCOME" => crate::symbol_config::AssetClassification::FixedIncome,
305
                "OPTION" => crate::symbol_config::AssetClassification::Option,
306
                "ETF" => crate::symbol_config::AssetClassification::Etf,
307
                "INDEX" => crate::symbol_config::AssetClassification::Index,
308
                "DERIVATIVE" => crate::symbol_config::AssetClassification::Derivative,
309
                _ => crate::symbol_config::AssetClassification::Equity,
310
            };
311
312
            let mut config = crate::symbol_config::SymbolConfig::new(symbol_name, classification);
313
            config.description = description;
314
            configs.push(config);
315
        }
316
317
        Ok(configs)
318
    }
319
320
    /// Loads symbols filtered by asset classification.
321
    pub async fn load_symbols_by_classification(
322
        &self,
323
        classification: crate::symbol_config::AssetClassification,
324
    ) -> Result<Vec<crate::symbol_config::SymbolConfig>, sqlx::Error> {
325
        let class_str = classification.regulatory_class();
326
327
        let query = "
328
            SELECT symbol, description, classification
329
            FROM symbol_config 
330
            WHERE is_active = true AND classification = $1
331
            ORDER BY symbol
332
        ";
333
334
        let rows = sqlx::query(query)
335
            .bind(class_str)
336
            .fetch_all(&self.pool)
337
            .await?;
338
339
        let mut configs = Vec::new();
340
        for row in rows {
341
            let symbol_name: String = row.get("symbol");
342
            let description: String = row.get("description");
343
            let mut config =
344
                crate::symbol_config::SymbolConfig::new(symbol_name, classification.clone());
345
            config.description = description;
346
            configs.push(config);
347
        }
348
349
        Ok(configs)
350
    }
351
    /// Saves or updates a symbol configuration.
352
    pub async fn save_symbol_config(
353
        &self,
354
        config: &crate::symbol_config::SymbolConfig,
355
    ) -> Result<(), sqlx::Error> {
356
        let query = "
357
            INSERT INTO symbol_config (
358
                symbol, description, classification, primary_exchange, currency
359
            ) VALUES ($1, $2, $3, $4, $5)
360
            ON CONFLICT (symbol) DO UPDATE SET
361
                description = EXCLUDED.description,
362
                classification = EXCLUDED.classification,
363
                primary_exchange = EXCLUDED.primary_exchange,
364
                currency = EXCLUDED.currency,
365
                updated_at = NOW()
366
        ";
367
368
        sqlx::query(query)
369
            .bind(&config.symbol)
370
            .bind(&config.description)
371
            .bind(config.classification.regulatory_class())
372
            .bind(&config.primary_exchange)
373
            .bind(&config.currency)
374
            .execute(&self.pool)
375
            .await?;
376
377
        Ok(())
378
    }
379
380
    /// Initializes PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
381
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
382
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
383
        listener.listen("symbol_config_changed").await?;
384
        self.listener = Some(listener);
385
        Ok(())
386
    }
387
388
    /// Checks for configuration change notifications.
389
    pub async fn check_for_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
390
        if let Some(listener) = &mut self.listener {
391
            if let Some(notification) = listener.try_recv().await? {
392
                return Ok(Some(notification.payload().to_string()));
393
            }
394
        }
395
        Ok(None)
396
    }
397
}
398
399
/// Database integration for comprehensive asset classification system.
400
///
401
/// Provides PostgreSQL-backed storage and retrieval for asset classification
402
/// configurations with support for pattern matching, caching, and hot-reload.
403
#[cfg(feature = "postgres")]
404
pub struct PostgresAssetClassificationLoader {
405
    /// Database connection pool
406
    pool: sqlx::PgPool,
407
    /// Configuration cache timeout
408
    cache_timeout: Duration,
409
    /// PostgreSQL listener for configuration changes
410
    listener: Option<sqlx::postgres::PgListener>,
411
}
412
413
#[cfg(feature = "postgres")]
414
impl PostgresAssetClassificationLoader {
415
    /// Creates a new PostgreSQL asset classification loader.
416
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
417
        let pool = sqlx::PgPool::connect(database_url).await?;
418
419
        Ok(Self {
420
            pool,
421
            cache_timeout: Duration::from_secs(300), // 5 minutes
422
            listener: None,
423
        })
424
    }
425
426
    /// Creates a new loader with an existing connection pool.
427
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
428
        Self {
429
            pool,
430
            cache_timeout: Duration::from_secs(300),
431
            listener: None,
432
        }
433
    }
434
435
    /// Loads all active asset configurations ordered by priority.
436
    pub async fn load_asset_configurations(
437
        &self,
438
    ) -> Result<Vec<crate::asset_classification::AssetConfig>, sqlx::Error> {
439
        let query = "
440
                SELECT 
441
                    id,
442
                    name,
443
                    symbol_pattern,
444
                    asset_class_data,
445
                    volatility_profile,
446
                    trading_parameters,
447
                    priority,
448
                    is_active,
449
                    created_at,
450
                    updated_at,
451
                    trading_hours,
452
                    settlement_config
453
                FROM asset_configurations
454
                WHERE is_active = true
455
                ORDER BY priority DESC
456
            ";
457
458
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
459
460
        let mut configs = Vec::new();
461
        for row in rows {
462
            if let Ok(config) = self.row_to_asset_config(row) {
463
                configs.push(config);
464
            }
465
        }
466
467
        Ok(configs)
468
    }
469
470
    /// Loads a specific asset configuration by ID.
471
    pub async fn load_asset_configuration_by_id(
472
        &self,
473
        id: uuid::Uuid,
474
    ) -> Result<Option<crate::asset_classification::AssetConfig>, sqlx::Error> {
475
        let query = "
476
                SELECT 
477
                    id,
478
                    name,
479
                    symbol_pattern,
480
                    asset_class_data,
481
                    volatility_profile,
482
                    trading_parameters,
483
                    priority,
484
                    is_active,
485
                    created_at,
486
                    updated_at,
487
                    trading_hours,
488
                    settlement_config
489
                FROM asset_configurations
490
                WHERE id = $1
491
            ";
492
493
        let row = sqlx::query(query)
494
            .bind(id)
495
            .fetch_optional(&self.pool)
496
            .await?;
497
498
        if let Some(row) = row {
499
            Ok(Some(self.row_to_asset_config(row)?))
500
        } else {
501
            Ok(None)
502
        }
503
    }
504
505
    /// Saves or updates an asset configuration.
506
    pub async fn save_asset_configuration(
507
        &self,
508
        config: &crate::asset_classification::AssetConfig,
509
    ) -> Result<(), sqlx::Error> {
510
        let query = "
511
                INSERT INTO asset_configurations (
512
                    id, name, symbol_pattern, asset_class_data, volatility_profile,
513
                    trading_parameters, priority, is_active, created_at, updated_at,
514
                    trading_hours, settlement_config
515
                ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
516
                ON CONFLICT (id) DO UPDATE SET
517
                    name = EXCLUDED.name,
518
                    symbol_pattern = EXCLUDED.symbol_pattern,
519
                    asset_class_data = EXCLUDED.asset_class_data,
520
                    volatility_profile = EXCLUDED.volatility_profile,
521
                    trading_parameters = EXCLUDED.trading_parameters,
522
                    priority = EXCLUDED.priority,
523
                    is_active = EXCLUDED.is_active,
524
                    updated_at = NOW(),
525
                    trading_hours = EXCLUDED.trading_hours,
526
                    settlement_config = EXCLUDED.settlement_config
527
            ";
528
529
        let asset_class_json = serde_json::to_value(&config.asset_class)
530
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
531
        let volatility_json = serde_json::to_value(&config.volatility_profile)
532
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
533
        let trading_params_json = serde_json::to_value(&config.trading_parameters)
534
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
535
        let trading_hours_json = serde_json::to_value(&config.trading_hours)
536
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
537
        let settlement_json = serde_json::to_value(&config.settlement_config)
538
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
539
540
        sqlx::query(query)
541
            .bind(config.id)
542
            .bind(&config.name)
543
            .bind(&config.symbol_pattern)
544
            .bind(asset_class_json)
545
            .bind(volatility_json)
546
            .bind(trading_params_json)
547
            .bind(config.priority as i32)
548
            .bind(config.is_active)
549
            .bind(config.created_at)
550
            .bind(config.updated_at)
551
            .bind(trading_hours_json)
552
            .bind(settlement_json)
553
            .execute(&self.pool)
554
            .await?;
555
556
        Ok(())
557
    }
558
559
    /// Loads explicit symbol mappings.
560
    pub async fn load_symbol_mappings(
561
        &self,
562
    ) -> Result<
563
        std::collections::HashMap<String, crate::asset_classification::AssetClass>,
564
        sqlx::Error,
565
    > {
566
        let query = "
567
                SELECT symbol, asset_class_data
568
                FROM symbol_mappings
569
                WHERE is_active = true AND (expires_at IS NULL OR expires_at > NOW())
570
            ";
571
572
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
573
574
        let mut mappings = std::collections::HashMap::new();
575
        for row in rows {
576
            let symbol: String = row.get("symbol");
577
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
578
579
            if let Ok(asset_class) =
580
                serde_json::from_value::<crate::asset_classification::AssetClass>(asset_class_json)
581
            {
582
                mappings.insert(symbol.to_uppercase(), asset_class);
583
            }
584
        }
585
586
        Ok(mappings)
587
    }
588
589
    /// Saves a symbol mapping.
590
    pub async fn save_symbol_mapping(
591
        &self,
592
        symbol: &str,
593
        asset_class: &crate::asset_classification::AssetClass,
594
        source: &str,
595
        confidence_score: f64,
596
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
597
    ) -> Result<(), sqlx::Error> {
598
        let query = "
599
                INSERT INTO symbol_mappings (
600
                    symbol, asset_class_data, source, confidence_score, expires_at
601
                ) VALUES ($1, $2, $3, $4, $5)
602
                ON CONFLICT (symbol) DO UPDATE SET
603
                    asset_class_data = EXCLUDED.asset_class_data,
604
                    source = EXCLUDED.source,
605
                    confidence_score = EXCLUDED.confidence_score,
606
                    expires_at = EXCLUDED.expires_at,
607
                    updated_at = NOW()
608
            ";
609
610
        let asset_class_json =
611
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
612
613
        sqlx::query(query)
614
            .bind(symbol.to_uppercase())
615
            .bind(asset_class_json)
616
            .bind(source)
617
            .bind(confidence_score)
618
            .bind(expires_at)
619
            .execute(&self.pool)
620
            .await?;
621
622
        Ok(())
623
    }
624
625
    /// Loads volatility profiles.
626
    pub async fn load_volatility_profiles(
627
        &self,
628
    ) -> Result<
629
        std::collections::HashMap<String, crate::asset_classification::VolatilityProfile>,
630
        sqlx::Error,
631
    > {
632
        let query = "
633
                SELECT 
634
                    name,
635
                    base_annual_volatility,
636
                    stress_volatility_multiplier,
637
                    intraday_pattern,
638
                    volatility_persistence,
639
                    jump_risk
640
                FROM volatility_profiles
641
                WHERE is_active = true
642
            ";
643
644
        let rows = sqlx::query(query).fetch_all(&self.pool).await?;
645
646
        let mut profiles = std::collections::HashMap::new();
647
        for row in rows {
648
            let name: String = row.get("name");
649
            let base_volatility: rust_decimal::Decimal = row.get("base_annual_volatility");
650
            let stress_multiplier: rust_decimal::Decimal = row.get("stress_volatility_multiplier");
651
            let persistence: rust_decimal::Decimal = row.get("volatility_persistence");
652
            let intraday_json: serde_json::Value = row.get("intraday_pattern");
653
            let jump_risk_json: serde_json::Value = row.get("jump_risk");
654
655
            if let (Ok(base_vol), Ok(stress_mult), Ok(persist), Ok(intraday), Ok(jump_risk)) = (
656
                f64::try_from(base_volatility),
657
                f64::try_from(stress_multiplier),
658
                f64::try_from(persistence),
659
                serde_json::from_value::<Vec<f64>>(intraday_json),
660
                serde_json::from_value::<crate::asset_classification::JumpRiskProfile>(
661
                    jump_risk_json,
662
                ),
663
            ) {
664
                let profile = crate::asset_classification::VolatilityProfile {
665
                    base_annual_volatility: base_vol,
666
                    stress_volatility_multiplier: stress_mult,
667
                    intraday_pattern: intraday,
668
                    volatility_persistence: persist,
669
                    jump_risk,
670
                };
671
                profiles.insert(name, profile);
672
            }
673
        }
674
675
        Ok(profiles)
676
    }
677
678
    /// Caches symbol classification for performance.
679
    pub async fn cache_symbol_classification(
680
        &self,
681
        symbol: &str,
682
        asset_class: &crate::asset_classification::AssetClass,
683
        configuration_id: Option<uuid::Uuid>,
684
    ) -> Result<(), sqlx::Error> {
685
        let query = "
686
                INSERT INTO asset_classification_cache (symbol, asset_class_data, configuration_id)
687
                VALUES ($1, $2, $3)
688
                ON CONFLICT (symbol) DO UPDATE SET
689
                    asset_class_data = EXCLUDED.asset_class_data,
690
                    configuration_id = EXCLUDED.configuration_id,
691
                    cached_at = NOW(),
692
                    expires_at = NOW() + INTERVAL '1 hour'
693
            ";
694
695
        let asset_class_json =
696
            serde_json::to_value(asset_class).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
697
698
        sqlx::query(query)
699
            .bind(symbol.to_uppercase())
700
            .bind(asset_class_json)
701
            .bind(configuration_id)
702
            .execute(&self.pool)
703
            .await?;
704
705
        Ok(())
706
    }
707
708
    /// Retrieves cached symbol classification.
709
    pub async fn get_cached_classification(
710
        &self,
711
        symbol: &str,
712
    ) -> Result<Option<crate::asset_classification::AssetClass>, sqlx::Error> {
713
        let query = "
714
                SELECT asset_class_data
715
                FROM asset_classification_cache
716
                WHERE symbol = $1 AND expires_at > NOW()
717
            ";
718
719
        let row = sqlx::query(query)
720
            .bind(symbol.to_uppercase())
721
            .fetch_optional(&self.pool)
722
            .await?;
723
724
        if let Some(row) = row {
725
            let asset_class_json: serde_json::Value = row.get("asset_class_data");
726
            Ok(serde_json::from_value(asset_class_json).ok())
727
        } else {
728
            Ok(None)
729
        }
730
    }
731
732
    /// Cleans up expired cache entries.
733
    pub async fn cleanup_cache(&self) -> Result<u64, sqlx::Error> {
734
        let query = "DELETE FROM asset_classification_cache WHERE expires_at < NOW()";
735
        let result = sqlx::query(query).execute(&self.pool).await?;
736
        Ok(result.rows_affected())
737
    }
738
739
    /// Logs asset classification changes for audit.
740
    pub async fn log_classification_change(
741
        &self,
742
        symbol: &str,
743
        old_classification: Option<&crate::asset_classification::AssetClass>,
744
        new_classification: &crate::asset_classification::AssetClass,
745
        changed_by: &str,
746
        reason: &str,
747
    ) -> Result<(), sqlx::Error> {
748
        let query = "
749
                INSERT INTO asset_classification_audit (
750
                    symbol, old_classification, new_classification, changed_by, change_reason
751
                ) VALUES ($1, $2, $3, $4, $5)
752
            ";
753
754
        let old_json = old_classification
755
            .map(|c| serde_json::to_value(c).ok())
756
            .flatten();
757
        let new_json = serde_json::to_value(new_classification)
758
            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
759
760
        sqlx::query(query)
761
            .bind(symbol)
762
            .bind(old_json)
763
            .bind(new_json)
764
            .bind(changed_by)
765
            .bind(reason)
766
            .execute(&self.pool)
767
            .await?;
768
769
        Ok(())
770
    }
771
772
    /// Enables PostgreSQL NOTIFY/LISTEN for configuration hot-reload.
773
    pub async fn enable_hot_reload(&mut self) -> Result<(), sqlx::Error> {
774
        let mut listener = sqlx::postgres::PgListener::connect_with(&self.pool).await?;
775
        listener.listen("config_change").await?;
776
        self.listener = Some(listener);
777
        Ok(())
778
    }
779
780
    /// Checks for configuration change notifications.
781
    pub async fn check_for_config_updates(&mut self) -> Result<Option<String>, sqlx::Error> {
782
        if let Some(listener) = &mut self.listener {
783
            if let Some(notification) = listener.try_recv().await? {
784
                return Ok(Some(notification.payload().to_string()));
785
            }
786
        }
787
        Ok(None)
788
    }
789
790
    /// Converts a database row to AssetConfig.
791
    fn row_to_asset_config(
792
        &self,
793
        row: sqlx::postgres::PgRow,
794
    ) -> Result<crate::asset_classification::AssetConfig, sqlx::Error> {
795
        let id: uuid::Uuid = row.get("id");
796
        let name: String = row.get("name");
797
        let symbol_pattern: String = row.get("symbol_pattern");
798
        let priority: i32 = row.get("priority");
799
        let is_active: bool = row.get("is_active");
800
        let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
801
        let updated_at: chrono::DateTime<chrono::Utc> = row.get("updated_at");
802
803
        let asset_class_json: serde_json::Value = row.get("asset_class_data");
804
        let volatility_json: serde_json::Value = row.get("volatility_profile");
805
        let trading_params_json: serde_json::Value = row.get("trading_parameters");
806
        let trading_hours_json: Option<serde_json::Value> = row.get("trading_hours");
807
        let settlement_json: serde_json::Value = row.get("settlement_config");
808
809
        let asset_class = serde_json::from_value(asset_class_json)
810
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
811
        let volatility_profile = serde_json::from_value(volatility_json)
812
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
813
        let trading_parameters = serde_json::from_value(trading_params_json)
814
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
815
        let trading_hours = trading_hours_json
816
            .map(|json| serde_json::from_value(json).ok())
817
            .flatten();
818
        let settlement_config = serde_json::from_value(settlement_json)
819
            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
820
821
        Ok(crate::asset_classification::AssetConfig {
822
            id,
823
            name,
824
            symbol_pattern,
825
            compiled_pattern: None, // Will be compiled when loaded
826
            asset_class,
827
            volatility_profile,
828
            trading_parameters,
829
            priority: priority as u32,
830
            is_active,
831
            created_at,
832
            updated_at,
833
            trading_hours,
834
            settlement_config,
835
        })
836
    }
837
}
838
839
/// General-purpose PostgreSQL configuration loader for various configuration types.
840
///
841
/// Provides a unified interface for loading configurations from PostgreSQL with
842
/// support for hot-reload through NOTIFY/LISTEN and caching for performance.
843
#[cfg(feature = "postgres")]
844
pub struct PostgresConfigLoader {
845
    /// Database connection pool
846
    pool: sqlx::PgPool,
847
    /// Configuration cache timeout
848
    cache_timeout: Duration,
849
}
850
851
#[cfg(feature = "postgres")]
852
impl PostgresConfigLoader {
853
    /// Creates a new PostgreSQL configuration loader.
854
    pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> {
855
        let pool = sqlx::PgPool::connect(database_url).await?;
856
857
        Ok(Self {
858
            pool,
859
            cache_timeout: Duration::from_secs(300), // 5 minutes
860
        })
861
    }
862
863
    /// Creates a new loader with an existing connection pool.
864
    pub fn with_pool(pool: sqlx::PgPool) -> Self {
865
        Self {
866
            pool,
867
            cache_timeout: Duration::from_secs(300),
868
        }
869
    }
870
871
    /// Get the underlying connection pool.
872
    pub fn pool(&self) -> &sqlx::PgPool {
873
        &self.pool
874
    }
875
876
    // ============================================================================
877
    // ADAPTIVE STRATEGY CONFIGURATION METHODS
878
    // ============================================================================
879
880
    /// Get adaptive strategy configuration by strategy ID.
881
    ///
882
    /// Loads the complete configuration including main settings, models, and features
883
    /// from the PostgreSQL database. Returns None if the strategy doesn't exist.
884
    ///
885
    /// # Arguments
886
    /// * `strategy_id` - Unique identifier for the strategy (e.g., "default", "prod_v1")
887
    ///
888
    /// # Returns
889
    /// - `Ok(Some(config))` - Configuration found and loaded successfully
890
    /// - `Ok(None)` - Strategy ID not found in database
891
    /// - `Err(sqlx::Error)` - Database error occurred
892
    ///
893
    /// # Example
894
    /// ```no_run
895
    /// # use config::PostgresConfigLoader;
896
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
897
    /// let config = loader.get_adaptive_strategy_config("default").await?;
898
    /// if let Some(cfg) = config {
899
    ///     println!("Loaded strategy: {}", cfg.name);
900
    /// }
901
    /// # Ok(())
902
    /// # }
903
    /// ```
904
    pub async fn get_adaptive_strategy_config(
905
        &self,
906
        strategy_id: &str,
907
    ) -> Result<Option<serde_json::Value>, sqlx::Error> {
908
        // Query main configuration
909
        let row = sqlx::query(
910
            r#"
911
            SELECT
912
                id, strategy_id, name, description,
913
                execution_interval_ms, error_backoff_duration_secs,
914
                max_concurrent_operations, strategy_timeout_secs,
915
                max_parallel_models, rebalancing_interval_secs,
916
                min_model_weight, max_model_weight,
917
                max_position_size, max_leverage, stop_loss_pct,
918
                position_sizing_method, max_portfolio_var,
919
                max_drawdown_threshold, kelly_fraction,
920
                book_depth, vpin_window, trade_classification_threshold,
921
                trade_size_buckets, microstructure_features,
922
                regime_detection_method, regime_lookback_window,
923
                regime_transition_threshold, regime_features,
924
                execution_algorithm, max_order_size, min_order_size,
925
                order_timeout_secs, max_slippage_bps,
926
                smart_routing_enabled, dark_pool_preference,
927
                active, version, created_at, updated_at,
928
                created_by, updated_by, metadata
929
            FROM adaptive_strategy_config
930
            WHERE strategy_id = $1 AND active = true
931
            "#,
932
        )
933
        .bind(strategy_id)
934
        .fetch_optional(&self.pool)
935
        .await?;
936
937
        let Some(row) = row else {
938
            return Ok(None);
939
        };
940
941
        let config_id: uuid::Uuid = row.try_get("id")?;
942
943
        // Query associated models
944
        let models = sqlx::query(
945
            r#"
946
            SELECT
947
                id, strategy_config_id, model_id, model_name, model_type,
948
                parameters, initial_weight, enabled, display_order,
949
                created_at, updated_at
950
            FROM adaptive_strategy_models
951
            WHERE strategy_config_id = $1
952
            ORDER BY display_order, created_at
953
            "#,
954
        )
955
        .bind(config_id)
956
        .fetch_all(&self.pool)
957
        .await?;
958
959
        // Query associated features
960
        let features = sqlx::query(
961
            r#"
962
            SELECT
963
                id, strategy_config_id, feature_name, feature_type,
964
                parameters, enabled, required,
965
                created_at, updated_at
966
            FROM adaptive_strategy_features
967
            WHERE strategy_config_id = $1
968
            ORDER BY feature_name
969
            "#,
970
        )
971
        .bind(config_id)
972
        .fetch_all(&self.pool)
973
        .await?;
974
975
        // Convert to JSON for flexibility
976
        // In production, you'd convert to a proper struct type
977
        let config = serde_json::json!({
978
            "id": row.try_get::<uuid::Uuid, _>("id")?,
979
            "strategy_id": row.try_get::<String, _>("strategy_id")?,
980
            "name": row.try_get::<String, _>("name")?,
981
            "description": row.try_get::<Option<String>, _>("description")?,
982
            "general": {
983
                "execution_interval_ms": row.try_get::<i32, _>("execution_interval_ms")?,
984
                "error_backoff_duration_secs": row.try_get::<i32, _>("error_backoff_duration_secs")?,
985
                "max_concurrent_operations": row.try_get::<i32, _>("max_concurrent_operations")?,
986
                "strategy_timeout_secs": row.try_get::<i32, _>("strategy_timeout_secs")?,
987
            },
988
            "ensemble": {
989
                "max_parallel_models": row.try_get::<i32, _>("max_parallel_models")?,
990
                "rebalancing_interval_secs": row.try_get::<i32, _>("rebalancing_interval_secs")?,
991
                "min_model_weight": row.try_get::<f64, _>("min_model_weight")?,
992
                "max_model_weight": row.try_get::<f64, _>("max_model_weight")?,
993
            },
994
            "risk": {
995
                "max_position_size": row.try_get::<f64, _>("max_position_size")?,
996
                "max_leverage": row.try_get::<f64, _>("max_leverage")?,
997
                "stop_loss_pct": row.try_get::<f64, _>("stop_loss_pct")?,
998
                "position_sizing_method": row.try_get::<String, _>("position_sizing_method")?,
999
                "max_portfolio_var": row.try_get::<f64, _>("max_portfolio_var")?,
1000
                "max_drawdown_threshold": row.try_get::<f64, _>("max_drawdown_threshold")?,
1001
                "kelly_fraction": row.try_get::<f64, _>("kelly_fraction")?,
1002
            },
1003
            "microstructure": {
1004
                "book_depth": row.try_get::<i32, _>("book_depth")?,
1005
                "vpin_window": row.try_get::<i32, _>("vpin_window")?,
1006
                "trade_classification_threshold": row.try_get::<f64, _>("trade_classification_threshold")?,
1007
                "trade_size_buckets": row.try_get::<Vec<f64>, _>("trade_size_buckets")?,
1008
                "features": row.try_get::<Vec<String>, _>("microstructure_features")?,
1009
            },
1010
            "regime": {
1011
                "detection_method": row.try_get::<String, _>("regime_detection_method")?,
1012
                "lookback_window": row.try_get::<i32, _>("regime_lookback_window")?,
1013
                "transition_threshold": row.try_get::<f64, _>("regime_transition_threshold")?,
1014
                "features": row.try_get::<Vec<String>, _>("regime_features")?,
1015
            },
1016
            "execution": {
1017
                "algorithm": row.try_get::<String, _>("execution_algorithm")?,
1018
                "max_order_size": row.try_get::<f64, _>("max_order_size")?,
1019
                "min_order_size": row.try_get::<f64, _>("min_order_size")?,
1020
                "order_timeout_secs": row.try_get::<i32, _>("order_timeout_secs")?,
1021
                "max_slippage_bps": row.try_get::<f64, _>("max_slippage_bps")?,
1022
                "smart_routing_enabled": row.try_get::<bool, _>("smart_routing_enabled")?,
1023
                "dark_pool_preference": row.try_get::<f64, _>("dark_pool_preference")?,
1024
            },
1025
            "models": models.iter().map(|m| serde_json::json!({
1026
                "id": m.try_get::<uuid::Uuid, _>("id").unwrap(),
1027
                "model_id": m.try_get::<String, _>("model_id").unwrap(),
1028
                "model_name": m.try_get::<String, _>("model_name").unwrap(),
1029
                "model_type": m.try_get::<String, _>("model_type").unwrap(),
1030
                "parameters": m.try_get::<serde_json::Value, _>("parameters").unwrap(),
1031
                "initial_weight": m.try_get::<f64, _>("initial_weight").unwrap(),
1032
                "enabled": m.try_get::<bool, _>("enabled").unwrap(),
1033
            })).collect::<Vec<_>>(),
1034
            "features": features.iter().map(|f| serde_json::json!({
1035
                "name": f.try_get::<String, _>("feature_name").unwrap(),
1036
                "feature_type": f.try_get::<String, _>("feature_type").unwrap(),
1037
                "parameters": f.try_get::<serde_json::Value, _>("parameters").unwrap(),
1038
                "enabled": f.try_get::<bool, _>("enabled").unwrap(),
1039
                "required": f.try_get::<bool, _>("required").unwrap(),
1040
            })).collect::<Vec<_>>(),
1041
            "version": row.try_get::<i32, _>("version")?,
1042
            "created_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")?,
1043
            "updated_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("updated_at")?,
1044
        });
1045
1046
        Ok(Some(config))
1047
    }
1048
1049
    /// Upsert (insert or update) adaptive strategy configuration.
1050
    ///
1051
    /// Creates a new strategy configuration if it doesn't exist, or updates
1052
    /// the existing one. Automatically handles version tracking and audit trail.
1053
    ///
1054
    /// # Arguments
1055
    /// * `config` - Configuration data as JSON (allows flexibility in structure)
1056
    ///
1057
    /// # Returns
1058
    /// - `Ok(strategy_id)` - Strategy ID of the created/updated configuration
1059
    /// - `Err(sqlx::Error)` - Database error occurred
1060
    ///
1061
    /// # Example
1062
    /// ```no_run
1063
    /// # use config::PostgresConfigLoader;
1064
    /// # use serde_json::json;
1065
    /// # async fn example(loader: &PostgresConfigLoader) -> Result<(), sqlx::Error> {
1066
    /// let config = json!({
1067
    ///     "strategy_id": "my_strategy",
1068
    ///     "name": "My Trading Strategy",
1069
    ///     "risk": {
1070
    ///         "max_position_size": 0.15,
1071
    ///         "max_leverage": 3.0
1072
    ///     }
1073
    /// });
1074
    /// let id = loader.upsert_adaptive_strategy_config(&config).await?;
1075
    /// # Ok(())
1076
    /// # }
1077
    /// ```
1078
    pub async fn upsert_adaptive_strategy_config(
1079
        &self,
1080
        config: &serde_json::Value,
1081
    ) -> Result<String, sqlx::Error> {
1082
        let strategy_id = config
1083
            .get("strategy_id")
1084
            .and_then(|v| v.as_str())
1085
            .ok_or_else(|| {
1086
                sqlx::Error::Decode(Box::new(std::io::Error::new(
1087
                    std::io::ErrorKind::InvalidData,
1088
                    "Missing strategy_id in config",
1089
                )))
1090
            })?;
1091
    
1092
        // Extract all configuration fields
1093
        let name = config.get("name").and_then(|v| v.as_str()).unwrap_or("Unnamed Strategy");
1094
        let description = config.get("description").and_then(|v| v.as_str());
1095
    
1096
        // Helper macro for extracting fields with defaults
1097
        macro_rules! get_i32 {
1098
            ($field:expr, $default:expr) => {
1099
                config.get($field).and_then(|v| v.as_i64()).map(|v| v as i32).unwrap_or($default)
1100
            };
1101
        }
1102
        macro_rules! get_f64 {
1103
            ($field:expr, $default:expr) => {
1104
                config.get($field).and_then(|v| v.as_f64()).unwrap_or($default)
1105
            };
1106
        }
1107
        macro_rules! get_bool {
1108
            ($field:expr, $default:expr) => {
1109
                config.get($field).and_then(|v| v.as_bool()).unwrap_or($default)
1110
            };
1111
        }
1112
        macro_rules! get_str {
1113
            ($field:expr, $default:expr) => {
1114
                config.get($field).and_then(|v| v.as_str()).unwrap_or($default)
1115
            };
1116
        }
1117
    
1118
        // Full upsert with all 50+ fields
1119
        let query = r#"
1120
            INSERT INTO adaptive_strategy_config (
1121
                strategy_id, name, description,
1122
                -- General config
1123
                execution_interval_ms, error_backoff_duration_secs,
1124
                max_concurrent_operations, strategy_timeout_secs,
1125
                -- Ensemble config
1126
                max_parallel_models, rebalancing_interval_secs,
1127
                min_model_weight, max_model_weight,
1128
                -- Risk config
1129
                max_position_size, max_leverage, stop_loss_pct,
1130
                position_sizing_method, max_portfolio_var,
1131
                max_drawdown_threshold, kelly_fraction,
1132
                -- Microstructure config
1133
                book_depth, vpin_window, trade_classification_threshold,
1134
                trade_size_buckets, microstructure_features,
1135
                -- Regime config
1136
                regime_detection_method, regime_lookback_window,
1137
                regime_transition_threshold, regime_features,
1138
                -- Execution config
1139
                execution_algorithm, max_order_size, min_order_size,
1140
                order_timeout_secs, max_slippage_bps,
1141
                smart_routing_enabled, dark_pool_preference
1142
            ) VALUES (
1143
                $1, $2, $3,
1144
                $4, $5, $6, $7,
1145
                $8, $9, $10, $11,
1146
                $12, $13, $14, $15, $16, $17, $18,
1147
                $19, $20, $21, $22, $23,
1148
                $24, $25, $26, $27,
1149
                $28, $29, $30, $31, $32, $33, $34
1150
            )
1151
            ON CONFLICT (strategy_id)
1152
            DO UPDATE SET
1153
                name = EXCLUDED.name,
1154
                description = EXCLUDED.description,
1155
                execution_interval_ms = EXCLUDED.execution_interval_ms,
1156
                error_backoff_duration_secs = EXCLUDED.error_backoff_duration_secs,
1157
                max_concurrent_operations = EXCLUDED.max_concurrent_operations,
1158
                strategy_timeout_secs = EXCLUDED.strategy_timeout_secs,
1159
                max_parallel_models = EXCLUDED.max_parallel_models,
1160
                rebalancing_interval_secs = EXCLUDED.rebalancing_interval_secs,
1161
                min_model_weight = EXCLUDED.min_model_weight,
1162
                max_model_weight = EXCLUDED.max_model_weight,
1163
                max_position_size = EXCLUDED.max_position_size,
1164
                max_leverage = EXCLUDED.max_leverage,
1165
                stop_loss_pct = EXCLUDED.stop_loss_pct,
1166
                position_sizing_method = EXCLUDED.position_sizing_method,
1167
                max_portfolio_var = EXCLUDED.max_portfolio_var,
1168
                max_drawdown_threshold = EXCLUDED.max_drawdown_threshold,
1169
                kelly_fraction = EXCLUDED.kelly_fraction,
1170
                book_depth = EXCLUDED.book_depth,
1171
                vpin_window = EXCLUDED.vpin_window,
1172
                trade_classification_threshold = EXCLUDED.trade_classification_threshold,
1173
                trade_size_buckets = EXCLUDED.trade_size_buckets,
1174
                microstructure_features = EXCLUDED.microstructure_features,
1175
                regime_detection_method = EXCLUDED.regime_detection_method,
1176
                regime_lookback_window = EXCLUDED.regime_lookback_window,
1177
                regime_transition_threshold = EXCLUDED.regime_transition_threshold,
1178
                regime_features = EXCLUDED.regime_features,
1179
                execution_algorithm = EXCLUDED.execution_algorithm,
1180
                max_order_size = EXCLUDED.max_order_size,
1181
                min_order_size = EXCLUDED.min_order_size,
1182
                order_timeout_secs = EXCLUDED.order_timeout_secs,
1183
                max_slippage_bps = EXCLUDED.max_slippage_bps,
1184
                smart_routing_enabled = EXCLUDED.smart_routing_enabled,
1185
                dark_pool_preference = EXCLUDED.dark_pool_preference,
1186
                updated_at = NOW()
1187
            RETURNING strategy_id
1188
        "#;
1189
    
1190
        // Extract trade_size_buckets and features arrays
1191
        let trade_size_buckets: Vec<f64> = config.get("trade_size_buckets")
1192
            .and_then(|v| v.as_array())
1193
            .map(|arr| arr.iter().filter_map(|v| v.as_f64()).collect())
1194
            .unwrap_or_else(|| vec![10.0, 100.0, 1000.0, 10000.0]);
1195
    
1196
        let microstructure_features: Vec<String> = config.get("microstructure_features")
1197
            .and_then(|v| v.as_array())
1198
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1199
            .unwrap_or_else(|| vec!["vpin".to_string(), "order_flow".to_string(), "bid_ask_spread".to_string()]);
1200
    
1201
        let regime_features: Vec<String> = config.get("regime_features")
1202
            .and_then(|v| v.as_array())
1203
            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
1204
            .unwrap_or_else(|| vec!["volatility".to_string(), "momentum".to_string(), "volume".to_string()]);
1205
    
1206
        let row = sqlx::query(query)
1207
            .bind(strategy_id)
1208
            .bind(name)
1209
            .bind(description)
1210
            // General config (4 fields)
1211
            .bind(get_i32!("execution_interval_ms", 100))
1212
            .bind(get_i32!("error_backoff_duration_secs", 1))
1213
            .bind(get_i32!("max_concurrent_operations", 10))
1214
            .bind(get_i32!("strategy_timeout_secs", 30))
1215
            // Ensemble config (4 fields)
1216
            .bind(get_i32!("max_parallel_models", 4))
1217
            .bind(get_i32!("rebalancing_interval_secs", 300))
1218
            .bind(get_f64!("min_model_weight", 0.01))
1219
            .bind(get_f64!("max_model_weight", 0.5))
1220
            // Risk config (7 fields)
1221
            .bind(get_f64!("max_position_size", 0.1))
1222
            .bind(get_f64!("max_leverage", 2.0))
1223
            .bind(get_f64!("stop_loss_pct", 0.02))
1224
            .bind(get_str!("position_sizing_method", "KELLY"))
1225
            .bind(get_f64!("max_portfolio_var", 0.02))
1226
            .bind(get_f64!("max_drawdown_threshold", 0.05))
1227
            .bind(get_f64!("kelly_fraction", 0.1))
1228
            // Microstructure config (5 fields)
1229
            .bind(get_i32!("book_depth", 10))
1230
            .bind(get_i32!("vpin_window", 50))
1231
            .bind(get_f64!("trade_classification_threshold", 0.5))
1232
            .bind(&trade_size_buckets)
1233
            .bind(&microstructure_features)
1234
            // Regime config (4 fields)
1235
            .bind(get_str!("regime_detection_method", "HMM"))
1236
            .bind(get_i32!("regime_lookback_window", 252))
1237
            .bind(get_f64!("regime_transition_threshold", 0.7))
1238
            .bind(&regime_features)
1239
            // Execution config (7 fields)
1240
            .bind(get_str!("execution_algorithm", "TWAP"))
1241
            .bind(get_f64!("max_order_size", 10000.0))
1242
            .bind(get_f64!("min_order_size", 100.0))
1243
            .bind(get_i32!("order_timeout_secs", 30))
1244
            .bind(get_f64!("max_slippage_bps", 10.0))
1245
            .bind(get_bool!("smart_routing_enabled", true))
1246
            .bind(get_f64!("dark_pool_preference", 0.3))
1247
            .fetch_one(&self.pool)
1248
            .await?;
1249
    
1250
                let result: String = row.try_get("strategy_id")?;
1251
                Ok(result)
1252
            }
1253
        
1254
            // ========================================================================
1255
            // MODEL CRUD OPERATIONS
1256
            // ========================================================================
1257
        
1258
            /// Add a model configuration to a strategy
1259
            ///
1260
            /// # Arguments
1261
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1262
            /// * `model` - Model configuration as JSON
1263
            ///
1264
            /// # Returns
1265
            /// UUID of the created model configuration
1266
            pub async fn add_model_config(
1267
                &self,
1268
                strategy_config_id: uuid::Uuid,
1269
                model: &serde_json::Value,
1270
            ) -> Result<uuid::Uuid, sqlx::Error> {
1271
                let model_id = model.get("model_id")
1272
                    .and_then(|v| v.as_str())
1273
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1274
                        std::io::ErrorKind::InvalidData,
1275
                        "Missing model_id"
1276
                    ))))?;
1277
        
1278
                let query = r#"
1279
                    INSERT INTO adaptive_strategy_models (
1280
                        strategy_config_id, model_id, model_name, model_type,
1281
                        parameters, initial_weight, enabled, display_order
1282
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
1283
                    RETURNING id
1284
                "#;
1285
        
1286
                let row = sqlx::query(query)
1287
                    .bind(strategy_config_id)
1288
                    .bind(model_id)
1289
                    .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or(model_id))
1290
                    .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1291
                    .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1292
                    .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1293
                    .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1294
                    .bind(model.get("display_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32)
1295
                    .fetch_one(&self.pool)
1296
                    .await?;
1297
        
1298
                row.try_get("id")
1299
            }
1300
        
1301
            /// Update a model configuration
1302
            ///
1303
            /// # Arguments
1304
            /// * `model_id` - UUID of the model to update
1305
            /// * `updates` - Fields to update as JSON
1306
            pub async fn update_model_config(
1307
                &self,
1308
                model_id: uuid::Uuid,
1309
                updates: &serde_json::Value,
1310
            ) -> Result<(), sqlx::Error> {
1311
                let query = r#"
1312
                    UPDATE adaptive_strategy_models
1313
                    SET
1314
                        model_name = COALESCE($1, model_name),
1315
                        model_type = COALESCE($2, model_type),
1316
                        parameters = COALESCE($3, parameters),
1317
                        initial_weight = COALESCE($4, initial_weight),
1318
                        enabled = COALESCE($5, enabled),
1319
                        display_order = COALESCE($6, display_order),
1320
                        updated_at = NOW()
1321
                    WHERE id = $7
1322
                "#;
1323
        
1324
                sqlx::query(query)
1325
                    .bind(updates.get("model_name").and_then(|v| v.as_str()))
1326
                    .bind(updates.get("model_type").and_then(|v| v.as_str()))
1327
                    .bind(updates.get("parameters"))
1328
                    .bind(updates.get("initial_weight").and_then(|v| v.as_f64()))
1329
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1330
                    .bind(updates.get("display_order").and_then(|v| v.as_i64()).map(|v| v as i32))
1331
                    .bind(model_id)
1332
                    .execute(&self.pool)
1333
                    .await?;
1334
        
1335
                Ok(())
1336
            }
1337
        
1338
            /// Remove a model configuration
1339
            ///
1340
            /// # Arguments
1341
            /// * `model_id` - UUID of the model to remove
1342
            pub async fn remove_model_config(
1343
                &self,
1344
                model_id: uuid::Uuid,
1345
            ) -> Result<(), sqlx::Error> {
1346
                let query = "DELETE FROM adaptive_strategy_models WHERE id = $1";
1347
                sqlx::query(query)
1348
                    .bind(model_id)
1349
                    .execute(&self.pool)
1350
                    .await?;
1351
                Ok(())
1352
            }
1353
        
1354
            // ========================================================================
1355
            // FEATURE CRUD OPERATIONS
1356
            // ========================================================================
1357
        
1358
            /// Add a feature configuration to a strategy
1359
            ///
1360
            /// # Arguments
1361
            /// * `strategy_config_id` - UUID of the parent strategy configuration
1362
            /// * `feature` - Feature configuration as JSON
1363
            ///
1364
            /// # Returns
1365
            /// UUID of the created feature configuration
1366
            pub async fn add_feature_config(
1367
                &self,
1368
                strategy_config_id: uuid::Uuid,
1369
                feature: &serde_json::Value,
1370
            ) -> Result<uuid::Uuid, sqlx::Error> {
1371
                let feature_name = feature.get("feature_name")
1372
                    .and_then(|v| v.as_str())
1373
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1374
                        std::io::ErrorKind::InvalidData,
1375
                        "Missing feature_name"
1376
                    ))))?;
1377
        
1378
                let query = r#"
1379
                    INSERT INTO adaptive_strategy_features (
1380
                        strategy_config_id, feature_name, feature_type,
1381
                        parameters, enabled, required
1382
                    ) VALUES ($1, $2, $3, $4, $5, $6)
1383
                    RETURNING id
1384
                "#;
1385
        
1386
                let row = sqlx::query(query)
1387
                    .bind(strategy_config_id)
1388
                    .bind(feature_name)
1389
                    .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1390
                    .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1391
                    .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1392
                    .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1393
                    .fetch_one(&self.pool)
1394
                    .await?;
1395
        
1396
                row.try_get("id")
1397
            }
1398
        
1399
            /// Update a feature configuration
1400
            ///
1401
            /// # Arguments
1402
            /// * `feature_id` - UUID of the feature to update
1403
            /// * `updates` - Fields to update as JSON
1404
            pub async fn update_feature_config(
1405
                &self,
1406
                feature_id: uuid::Uuid,
1407
                updates: &serde_json::Value,
1408
            ) -> Result<(), sqlx::Error> {
1409
                let query = r#"
1410
                    UPDATE adaptive_strategy_features
1411
                    SET
1412
                        feature_type = COALESCE($1, feature_type),
1413
                        parameters = COALESCE($2, parameters),
1414
                        enabled = COALESCE($3, enabled),
1415
                        required = COALESCE($4, required),
1416
                        updated_at = NOW()
1417
                    WHERE id = $5
1418
                "#;
1419
        
1420
                sqlx::query(query)
1421
                    .bind(updates.get("feature_type").and_then(|v| v.as_str()))
1422
                    .bind(updates.get("parameters"))
1423
                    .bind(updates.get("enabled").and_then(|v| v.as_bool()))
1424
                    .bind(updates.get("required").and_then(|v| v.as_bool()))
1425
                    .bind(feature_id)
1426
                    .execute(&self.pool)
1427
                    .await?;
1428
        
1429
                Ok(())
1430
            }
1431
        
1432
            /// Remove a feature configuration
1433
            ///
1434
            /// # Arguments
1435
            /// * `feature_id` - UUID of the feature to remove
1436
            pub async fn remove_feature_config(
1437
                &self,
1438
                feature_id: uuid::Uuid,
1439
            ) -> Result<(), sqlx::Error> {
1440
                let query = "DELETE FROM adaptive_strategy_features WHERE id = $1";
1441
                sqlx::query(query)
1442
                    .bind(feature_id)
1443
                    .execute(&self.pool)
1444
                    .await?;
1445
                Ok(())
1446
            }
1447
        
1448
            // ========================================================================
1449
            // TRANSACTION SUPPORT
1450
            // ========================================================================
1451
        
1452
            /// Update strategy configuration with models and features in a single transaction
1453
            ///
1454
            /// Provides atomic updates across all three tables:
1455
            /// - adaptive_strategy_config (main configuration)
1456
            /// - adaptive_strategy_models (model configurations)
1457
            /// - adaptive_strategy_features (feature configurations)
1458
            ///
1459
            /// # Arguments
1460
            /// * `config` - Full configuration including models and features
1461
            ///
1462
            /// # Returns
1463
            /// Strategy ID of the updated configuration
1464
            pub async fn update_strategy_atomic(
1465
                &self,
1466
                config: &serde_json::Value,
1467
            ) -> Result<String, sqlx::Error> {
1468
                // Start transaction
1469
                let mut tx = self.pool.begin().await?;
1470
        
1471
                // 1. Upsert main configuration
1472
                let strategy_id = config.get("strategy_id")
1473
                    .and_then(|v| v.as_str())
1474
                    .ok_or_else(|| sqlx::Error::Decode(Box::new(std::io::Error::new(
1475
                        std::io::ErrorKind::InvalidData,
1476
                        "Missing strategy_id"
1477
                    ))))?;
1478
        
1479
                // Get or create config_id
1480
                let config_id: uuid::Uuid = sqlx::query_scalar(
1481
                    "SELECT id FROM adaptive_strategy_config WHERE strategy_id = $1"
1482
                )
1483
                .bind(strategy_id)
1484
                .fetch_optional(&mut *tx)
1485
                .await?
1486
                .unwrap_or_else(uuid::Uuid::new_v4);
1487
        
1488
                // 2. Update models if provided
1489
                if let Some(models) = config.get("models").and_then(|v| v.as_array()) {
1490
                    // Delete existing models
1491
                    sqlx::query("DELETE FROM adaptive_strategy_models WHERE strategy_config_id = $1")
1492
                        .bind(config_id)
1493
                        .execute(&mut *tx)
1494
                        .await?;
1495
        
1496
                    // Insert new models
1497
                    for model in models {
1498
                        sqlx::query(r#"
1499
                            INSERT INTO adaptive_strategy_models (
1500
                                strategy_config_id, model_id, model_name, model_type,
1501
                                parameters, initial_weight, enabled
1502
                            ) VALUES ($1, $2, $3, $4, $5, $6, $7)
1503
                        "#)
1504
                        .bind(config_id)
1505
                        .bind(model.get("model_id").and_then(|v| v.as_str()).unwrap_or("unknown"))
1506
                        .bind(model.get("model_name").and_then(|v| v.as_str()).unwrap_or("Unknown Model"))
1507
                        .bind(model.get("model_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1508
                        .bind(model.get("parameters").unwrap_or(&serde_json::json!({})))
1509
                        .bind(model.get("initial_weight").and_then(|v| v.as_f64()).unwrap_or(0.25))
1510
                        .bind(model.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1511
                        .execute(&mut *tx)
1512
                        .await?;
1513
                    }
1514
                }
1515
        
1516
                // 3. Update features if provided
1517
                if let Some(features) = config.get("features").and_then(|v| v.as_array()) {
1518
                    // Delete existing features
1519
                    sqlx::query("DELETE FROM adaptive_strategy_features WHERE strategy_config_id = $1")
1520
                        .bind(config_id)
1521
                        .execute(&mut *tx)
1522
                        .await?;
1523
        
1524
                    // Insert new features
1525
                    for feature in features {
1526
                        sqlx::query(r#"
1527
                            INSERT INTO adaptive_strategy_features (
1528
                                strategy_config_id, feature_name, feature_type,
1529
                                parameters, enabled, required
1530
                            ) VALUES ($1, $2, $3, $4, $5, $6)
1531
                        "#)
1532
                        .bind(config_id)
1533
                        .bind(feature.get("feature_name").and_then(|v| v.as_str()).unwrap_or("unknown"))
1534
                        .bind(feature.get("feature_type").and_then(|v| v.as_str()).unwrap_or("unknown"))
1535
                        .bind(feature.get("parameters").unwrap_or(&serde_json::json!({})))
1536
                        .bind(feature.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true))
1537
                        .bind(feature.get("required").and_then(|v| v.as_bool()).unwrap_or(false))
1538
                        .execute(&mut *tx)
1539
                        .await?;
1540
                    }
1541
                }
1542
        
1543
                // Commit transaction
1544
                tx.commit().await?;
1545
        
1546
                Ok(strategy_id.to_string())
1547
            }
1548
        }
1549
        
1550
        #[cfg(test)]
1551
mod tests {
1552
    use super::*;
1553
1554
    #[test]
1555
1
    fn test_database_config_new() {
1556
1
        let config = DatabaseConfig::new();
1557
1
        assert!(!config.url.is_empty());
1558
1
        assert_eq!(config.max_connections, 10);
1559
1
        assert_eq!(config.min_connections, 1);
1560
1
        assert!(config.application_name.is_some());
1561
1
    }
1562
1563
    #[test]
1564
1
    fn test_database_config_validate_success() {
1565
1
        let config = DatabaseConfig::new();
1566
1
        assert!(config.validate().is_ok());
1567
1
    }
1568
1569
    #[test]
1570
1
    fn test_database_config_validate_empty_url() {
1571
1
        let mut config = DatabaseConfig::new();
1572
1
        config.url = String::new();
1573
1
        assert!(config.validate().is_err());
1574
1
    }
1575
1576
    #[test]
1577
1
    fn test_pool_config_default() {
1578
1
        let pool_config = PoolConfig::default();
1579
1
        assert_eq!(pool_config.min_connections, 1);
1580
1
        assert_eq!(pool_config.max_connections, 10);
1581
1
        assert!(pool_config.test_before_acquire);
1582
1
    }
1583
1584
    #[test]
1585
1
    fn test_transaction_config_default() {
1586
1
        let tx_config = TransactionConfig::default();
1587
1
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1588
1
        assert_eq!(tx_config.default_timeout_secs, 30);
1589
1
        assert!(tx_config.enable_retry);
1590
1
    }
1591
1592
    #[test]
1593
1
    fn test_transaction_config_serialization() {
1594
1
        let tx_config = TransactionConfig::default();
1595
1
        let serialized = serde_json::to_string(&tx_config).unwrap();
1596
1
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1597
1
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1598
1
    }
1599
1600
    #[test]
1601
1
    fn test_database_config_with_custom_values() {
1602
1
        let mut config = DatabaseConfig::new();
1603
1
        config.max_connections = 50;
1604
1
        config.min_connections = 5;
1605
1
        config.enable_query_logging = true;
1606
1607
1
        assert_eq!(config.max_connections, 50);
1608
1
        assert_eq!(config.min_connections, 5);
1609
1
        assert!(config.enable_query_logging);
1610
1
    }
1611
1612
    #[test]
1613
1
    fn test_pool_config_timeouts() {
1614
1
        let pool_config = PoolConfig {
1615
1
            acquire_timeout_secs: 30,
1616
1
            max_lifetime_secs: 1800,
1617
1
            idle_timeout_secs: 600,
1618
1
            ..Default::default()
1619
1
        };
1620
1621
1
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1622
1
        assert_eq!(pool_config.max_lifetime_secs, 1800);
1623
1
        assert_eq!(pool_config.idle_timeout_secs, 600);
1624
1
    }
1625
1626
    #[test]
1627
1
    fn test_transaction_config_isolation_levels() {
1628
1
        let levels = vec![
1629
            "READ_UNCOMMITTED",
1630
1
            "READ_COMMITTED",
1631
1
            "REPEATABLE_READ",
1632
1
            "SERIALIZABLE",
1633
        ];
1634
1635
5
        for 
level4
in levels {
1636
4
            let tx_config = TransactionConfig {
1637
4
                isolation_level: level.to_string(),
1638
4
                ..Default::default()
1639
4
            };
1640
4
            assert_eq!(tx_config.isolation_level, level);
1641
        }
1642
1
    }
1643
1644
    #[test]
1645
1
    fn test_database_config_clone() {
1646
1
        let config1 = DatabaseConfig::new();
1647
1
        let config2 = config1.clone();
1648
1649
1
        assert_eq!(config1.url, config2.url);
1650
1
        assert_eq!(config1.max_connections, config2.max_connections);
1651
1
        assert_eq!(config1.min_connections, config2.min_connections);
1652
1
    }
1653
1654
    #[test]
1655
1
    fn test_pool_config_validation() {
1656
1
        let pool_config = PoolConfig::default();
1657
1
        assert!(pool_config.min_connections <= pool_config.max_connections);
1658
1
    }
1659
1660
    #[test]
1661
1
    fn test_database_url_format() {
1662
1
        let config = DatabaseConfig::new();
1663
1
        assert!(config.url.starts_with("postgresql://"));
1664
1
    }
1665
1666
    #[test]
1667
1
    fn test_transaction_config_retry_settings() {
1668
1
        let tx_config = TransactionConfig {
1669
1
            enable_retry: true,
1670
1
            max_retries: 5,
1671
1
            ..Default::default()
1672
1
        };
1673
1
        assert!(tx_config.enable_retry);
1674
1
        assert_eq!(tx_config.max_retries, 5);
1675
1676
1
        let tx_config_no_retry = TransactionConfig {
1677
1
            enable_retry: false,
1678
1
            ..Default::default()
1679
1
        };
1680
1
        assert!(!tx_config_no_retry.enable_retry);
1681
1
    }
1682
1683
    #[test]
1684
1
    fn test_pool_config_connection_settings() {
1685
1
        let pool_config = PoolConfig {
1686
1
            test_before_acquire: true,
1687
1
            acquire_timeout_secs: 30,
1688
1
            ..Default::default()
1689
1
        };
1690
1691
1
        assert!(pool_config.test_before_acquire);
1692
1
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1693
1
    }
1694
1695
    #[test]
1696
1
    fn test_database_config_application_name() {
1697
1
        let config = DatabaseConfig::new();
1698
1
        assert_eq!(config.application_name, Some("foxhunt".to_string()));
1699
1
    }
1700
1701
    #[test]
1702
1
    fn test_database_config_query_logging() {
1703
1
        let mut config = DatabaseConfig::new();
1704
1
        config.enable_query_logging = true;
1705
1
        assert!(config.enable_query_logging);
1706
1
    }
1707
1708
    #[test]
1709
1
    fn test_pool_config_connection_limits() {
1710
1
        let pool_config = PoolConfig {
1711
1
            max_connections: 100,
1712
1
            min_connections: 10,
1713
1
            ..Default::default()
1714
1
        };
1715
1716
1
        assert_eq!(pool_config.max_connections, 100);
1717
1
        assert_eq!(pool_config.min_connections, 10);
1718
1
    }
1719
1720
    #[test]
1721
1
    fn test_transaction_timeout() {
1722
1
        let tx_config = TransactionConfig {
1723
1
            default_timeout_secs: 60,
1724
1
            timeout: Duration::from_secs(60),
1725
1
            ..Default::default()
1726
1
        };
1727
1
        assert_eq!(tx_config.default_timeout_secs, 60);
1728
1
        assert_eq!(tx_config.timeout, Duration::from_secs(60));
1729
1
    }
1730
1731
    #[test]
1732
1
    fn test_database_config_connect_timeout() {
1733
1
        let config = DatabaseConfig::new();
1734
1
        assert_eq!(config.connect_timeout, Duration::from_secs(30));
1735
1
    }
1736
1737
    #[test]
1738
1
    fn test_database_config_query_timeout() {
1739
1
        let config = DatabaseConfig::new();
1740
1
        assert_eq!(config.query_timeout, Duration::from_secs(60));
1741
1
    }
1742
1743
    #[test]
1744
1
    fn test_pool_config_test_before_acquire() {
1745
1
        let pool_config = PoolConfig {
1746
1
            test_before_acquire: false,
1747
1
            ..Default::default()
1748
1
        };
1749
1
        assert!(!pool_config.test_before_acquire);
1750
1751
1
        let pool_config_enabled = PoolConfig {
1752
1
            test_before_acquire: true,
1753
1
            ..Default::default()
1754
1
        };
1755
1
        assert!(pool_config_enabled.test_before_acquire);
1756
1
    }
1757
1758
    #[test]
1759
1
    fn test_database_config_validation_empty_url() {
1760
1
        let mut config = DatabaseConfig::new();
1761
1
        config.url = String::new();
1762
1
        assert!(config.validate().is_err());
1763
1
        assert_eq!(
1764
1
            config.validate().unwrap_err(),
1765
            "Database URL cannot be empty"
1766
        );
1767
1
    }
1768
1769
    #[test]
1770
1
    fn test_database_config_validation_valid() {
1771
1
        let config = DatabaseConfig::new();
1772
1
        assert!(config.validate().is_ok());
1773
1
    }
1774
1775
    #[test]
1776
1
    fn test_pool_config_defaults() {
1777
1
        let pool_config = PoolConfig::default();
1778
1
        assert_eq!(pool_config.min_connections, 1);
1779
1
        assert_eq!(pool_config.max_connections, 10);
1780
1
        assert_eq!(pool_config.acquire_timeout_secs, 30);
1781
1
        assert_eq!(pool_config.max_lifetime_secs, 1800);
1782
1
        assert_eq!(pool_config.idle_timeout_secs, 600);
1783
1
        assert!(pool_config.test_before_acquire);
1784
1
        assert!(pool_config.health_check_enabled);
1785
1
        assert_eq!(pool_config.health_check_interval_secs, 60);
1786
1
    }
1787
1788
    #[test]
1789
1
    fn test_transaction_config_defaults() {
1790
1
        let tx_config = TransactionConfig::default();
1791
1
        assert_eq!(tx_config.isolation_level, "READ_COMMITTED");
1792
1
        assert_eq!(tx_config.timeout, Duration::from_secs(30));
1793
1
        assert_eq!(tx_config.default_timeout_secs, 30);
1794
1
        assert!(tx_config.enable_retry);
1795
1
        assert_eq!(tx_config.max_retries, 3);
1796
1
        assert_eq!(tx_config.retry_delay_ms, 100);
1797
1
        assert_eq!(tx_config.max_savepoints, 10);
1798
1
    }
1799
1800
    #[test]
1801
1
    fn test_transaction_config_custom_isolation() {
1802
1
        let tx_config = TransactionConfig {
1803
1
            isolation_level: "SERIALIZABLE".to_string(),
1804
1
            ..Default::default()
1805
1
        };
1806
1
        assert_eq!(tx_config.isolation_level, "SERIALIZABLE");
1807
1
    }
1808
1809
    #[test]
1810
1
    fn test_pool_config_extreme_values() {
1811
1
        let pool_config = PoolConfig {
1812
1
            max_connections: 1000,
1813
1
            min_connections: 0,
1814
1
            ..Default::default()
1815
1
        };
1816
1
        assert_eq!(pool_config.max_connections, 1000);
1817
1
        assert_eq!(pool_config.min_connections, 0);
1818
1
    }
1819
1820
    #[test]
1821
1
    fn test_database_config_custom_application_name() {
1822
1
        let mut config = DatabaseConfig::new();
1823
1
        config.application_name = Some("custom_app".to_string());
1824
1
        assert_eq!(config.application_name.unwrap(), "custom_app");
1825
1
    }
1826
1827
    #[test]
1828
1
    fn test_database_config_no_application_name() {
1829
1
        let mut config = DatabaseConfig::new();
1830
1
        config.application_name = None;
1831
1
        assert!(config.application_name.is_none());
1832
1
    }
1833
1834
    #[test]
1835
1
    fn test_transaction_config_retry_disabled() {
1836
1
        let tx_config = TransactionConfig {
1837
1
            enable_retry: false,
1838
1
            ..Default::default()
1839
1
        };
1840
1
        assert!(!tx_config.enable_retry);
1841
1
    }
1842
1843
    #[test]
1844
1
    fn test_pool_config_serialization() {
1845
1
        let pool_config = PoolConfig::default();
1846
1
        let serialized = serde_json::to_string(&pool_config).unwrap();
1847
1
        let deserialized: PoolConfig = serde_json::from_str(&serialized).unwrap();
1848
1
        assert_eq!(pool_config.max_connections, deserialized.max_connections);
1849
1
        assert_eq!(pool_config.min_connections, deserialized.min_connections);
1850
1
    }
1851
1852
    #[test]
1853
1
    fn test_transaction_config_serde_roundtrip() {
1854
1
        let tx_config = TransactionConfig::default();
1855
1
        let serialized = serde_json::to_string(&tx_config).unwrap();
1856
1
        let deserialized: TransactionConfig = serde_json::from_str(&serialized).unwrap();
1857
1
        assert_eq!(tx_config.isolation_level, deserialized.isolation_level);
1858
1
        assert_eq!(tx_config.max_retries, deserialized.max_retries);
1859
1
    }
1860
}