forge_core/config/
database.rs

1use serde::{Deserialize, Serialize};
2
3/// Database configuration.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct DatabaseConfig {
6    /// Primary database connection URL.
7    /// Can be empty if `embedded = true`.
8    #[serde(default)]
9    pub url: String,
10
11    /// Use embedded PostgreSQL (zero external dependencies).
12    /// When true, starts a bundled PostgreSQL instance automatically.
13    /// Great for development and small production deployments.
14    /// Requires the `embedded-db` feature.
15    #[serde(default)]
16    pub embedded: bool,
17
18    /// Data directory for embedded PostgreSQL.
19    /// Only used when `embedded = true`.
20    /// Defaults to `.forge/postgres` in the current directory.
21    #[serde(default)]
22    pub data_dir: Option<String>,
23
24    /// Connection pool size.
25    #[serde(default = "default_pool_size")]
26    pub pool_size: u32,
27
28    /// Pool checkout timeout in seconds.
29    #[serde(default = "default_pool_timeout")]
30    pub pool_timeout_secs: u64,
31
32    /// Statement timeout in seconds.
33    #[serde(default = "default_statement_timeout")]
34    pub statement_timeout_secs: u64,
35
36    /// Read replica URLs for scaling reads.
37    #[serde(default)]
38    pub replica_urls: Vec<String>,
39
40    /// Whether to route read queries to replicas.
41    #[serde(default)]
42    pub read_from_replica: bool,
43
44    /// Connection pool isolation configuration.
45    #[serde(default)]
46    pub pools: PoolsConfig,
47}
48
49impl Default for DatabaseConfig {
50    fn default() -> Self {
51        Self {
52            url: String::new(),
53            embedded: false,
54            data_dir: None,
55            pool_size: default_pool_size(),
56            pool_timeout_secs: default_pool_timeout(),
57            statement_timeout_secs: default_statement_timeout(),
58            replica_urls: Vec::new(),
59            read_from_replica: false,
60            pools: PoolsConfig::default(),
61        }
62    }
63}
64
65fn default_pool_size() -> u32 {
66    50
67}
68
69fn default_pool_timeout() -> u64 {
70    30
71}
72
73fn default_statement_timeout() -> u64 {
74    30
75}
76
77/// Pool isolation configuration for different workloads.
78#[derive(Debug, Clone, Serialize, Deserialize, Default)]
79pub struct PoolsConfig {
80    /// Default pool for queries/mutations.
81    #[serde(default)]
82    pub default: Option<PoolConfig>,
83
84    /// Pool for background jobs.
85    #[serde(default)]
86    pub jobs: Option<PoolConfig>,
87
88    /// Pool for observability writes.
89    #[serde(default)]
90    pub observability: Option<PoolConfig>,
91
92    /// Pool for long-running analytics.
93    #[serde(default)]
94    pub analytics: Option<PoolConfig>,
95}
96
97/// Individual pool configuration.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct PoolConfig {
100    /// Pool size.
101    pub size: u32,
102
103    /// Checkout timeout in seconds.
104    #[serde(default = "default_pool_timeout")]
105    pub timeout_secs: u64,
106
107    /// Statement timeout in seconds (optional override).
108    pub statement_timeout_secs: Option<u64>,
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_default_database_config() {
117        let config = DatabaseConfig::default();
118        assert_eq!(config.pool_size, 50);
119        assert_eq!(config.pool_timeout_secs, 30);
120    }
121
122    #[test]
123    fn test_parse_database_config() {
124        let toml = r#"
125            url = "postgres://localhost/test"
126            pool_size = 100
127            replica_urls = ["postgres://replica1/test", "postgres://replica2/test"]
128            read_from_replica = true
129        "#;
130
131        let config: DatabaseConfig = toml::from_str(toml).unwrap();
132        assert_eq!(config.pool_size, 100);
133        assert_eq!(config.replica_urls.len(), 2);
134        assert!(config.read_from_replica);
135    }
136}