Skip to main content

prax_sqlx/
config.rs

1//! SQLx configuration for database connections.
2
3use crate::error::{SqlxError, SqlxResult};
4use std::time::Duration;
5
6/// Database backend type.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DatabaseBackend {
9    /// PostgreSQL database
10    Postgres,
11    /// MySQL database
12    MySql,
13    /// SQLite database
14    Sqlite,
15}
16
17impl DatabaseBackend {
18    /// Parse backend from URL scheme.
19    pub fn from_url(url: &str) -> SqlxResult<Self> {
20        if url.starts_with("postgres://") || url.starts_with("postgresql://") {
21            Ok(Self::Postgres)
22        } else if url.starts_with("mysql://") || url.starts_with("mariadb://") {
23            Ok(Self::MySql)
24        } else if url.starts_with("sqlite://") || url.starts_with("file:") {
25            Ok(Self::Sqlite)
26        } else {
27            Err(SqlxError::config(
28                "Unknown database URL scheme. Expected postgres://, mysql://, or sqlite://",
29            ))
30        }
31    }
32}
33
34/// SQLx pool configuration.
35#[derive(Debug, Clone)]
36pub struct SqlxConfig {
37    /// Database connection URL
38    pub url: String,
39    /// Database backend type (auto-detected from URL)
40    pub backend: DatabaseBackend,
41    /// Maximum number of connections in the pool
42    pub max_connections: u32,
43    /// Minimum number of connections to keep idle
44    pub min_connections: u32,
45    /// Connection timeout
46    pub connect_timeout: Duration,
47    /// Idle connection timeout
48    pub idle_timeout: Option<Duration>,
49    /// Maximum lifetime of a connection
50    pub max_lifetime: Option<Duration>,
51    /// Statement cache capacity per connection (applied at connect time on all
52    /// backends; set to 0 to disable caching)
53    pub statement_cache_capacity: usize,
54    /// SSL/TLS mode (honored on PostgreSQL and MySQL; ignored with a warning
55    /// on SQLite)
56    pub ssl_mode: SslMode,
57    /// Application name (honored on PostgreSQL only; ignored with a warning on
58    /// MySQL and SQLite)
59    pub application_name: Option<String>,
60}
61
62/// SSL mode for connections.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum SslMode {
65    /// Disable SSL
66    Disable,
67    /// Prefer SSL but allow non-SSL
68    #[default]
69    Prefer,
70    /// Require SSL
71    Require,
72    /// Require SSL and verify server certificate
73    VerifyCa,
74    /// Require SSL and verify server certificate and hostname
75    VerifyFull,
76}
77
78impl Default for SqlxConfig {
79    fn default() -> Self {
80        Self {
81            url: String::new(),
82            backend: DatabaseBackend::Postgres,
83            max_connections: 10,
84            min_connections: 1,
85            connect_timeout: Duration::from_secs(30),
86            idle_timeout: Some(Duration::from_secs(600)),
87            max_lifetime: Some(Duration::from_secs(1800)),
88            statement_cache_capacity: 100,
89            ssl_mode: SslMode::default(),
90            application_name: None,
91        }
92    }
93}
94
95impl SqlxConfig {
96    /// Create a new configuration from a database URL.
97    ///
98    /// # Example
99    ///
100    /// ```rust
101    /// use prax_sqlx::SqlxConfig;
102    ///
103    /// let config = SqlxConfig::from_url("postgres://user:pass@localhost/mydb").unwrap();
104    /// assert_eq!(config.max_connections, 10);
105    /// ```
106    pub fn from_url(url: impl Into<String>) -> SqlxResult<Self> {
107        let url = url.into();
108        let backend = DatabaseBackend::from_url(&url)?;
109
110        Ok(Self {
111            url,
112            backend,
113            ..Default::default()
114        })
115    }
116
117    /// Create a builder for more detailed configuration.
118    pub fn builder(url: impl Into<String>) -> SqlxConfigBuilder {
119        SqlxConfigBuilder::new(url)
120    }
121
122    /// Set max connections.
123    pub fn with_max_connections(mut self, max: u32) -> Self {
124        self.max_connections = max;
125        self
126    }
127
128    /// Set min connections.
129    pub fn with_min_connections(mut self, min: u32) -> Self {
130        self.min_connections = min;
131        self
132    }
133
134    /// Set connection timeout.
135    pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
136        self.connect_timeout = timeout;
137        self
138    }
139
140    /// Set idle timeout.
141    pub fn with_idle_timeout(mut self, timeout: Duration) -> Self {
142        self.idle_timeout = Some(timeout);
143        self
144    }
145
146    /// Set max lifetime.
147    pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
148        self.max_lifetime = Some(lifetime);
149        self
150    }
151
152    /// Set statement cache capacity.
153    pub fn with_statement_cache(mut self, capacity: usize) -> Self {
154        self.statement_cache_capacity = capacity;
155        self
156    }
157
158    /// Set SSL mode.
159    pub fn with_ssl_mode(mut self, mode: SslMode) -> Self {
160        self.ssl_mode = mode;
161        self
162    }
163
164    /// Set application name.
165    pub fn with_application_name(mut self, name: impl Into<String>) -> Self {
166        self.application_name = Some(name.into());
167        self
168    }
169}
170
171/// Builder for SQLx configuration.
172pub struct SqlxConfigBuilder {
173    config: SqlxConfig,
174}
175
176impl SqlxConfigBuilder {
177    /// Create a new builder with a database URL.
178    pub fn new(url: impl Into<String>) -> Self {
179        let url = url.into();
180        let backend = DatabaseBackend::from_url(&url).unwrap_or(DatabaseBackend::Postgres);
181
182        Self {
183            config: SqlxConfig {
184                url,
185                backend,
186                ..Default::default()
187            },
188        }
189    }
190
191    /// Set max connections.
192    pub fn max_connections(mut self, max: u32) -> Self {
193        self.config.max_connections = max;
194        self
195    }
196
197    /// Set min connections.
198    pub fn min_connections(mut self, min: u32) -> Self {
199        self.config.min_connections = min;
200        self
201    }
202
203    /// Set connection timeout.
204    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
205        self.config.connect_timeout = timeout;
206        self
207    }
208
209    /// Set idle timeout.
210    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
211        self.config.idle_timeout = Some(timeout);
212        self
213    }
214
215    /// Disable idle timeout.
216    pub fn no_idle_timeout(mut self) -> Self {
217        self.config.idle_timeout = None;
218        self
219    }
220
221    /// Set max lifetime.
222    pub fn max_lifetime(mut self, lifetime: Duration) -> Self {
223        self.config.max_lifetime = Some(lifetime);
224        self
225    }
226
227    /// Disable max lifetime.
228    pub fn no_max_lifetime(mut self) -> Self {
229        self.config.max_lifetime = None;
230        self
231    }
232
233    /// Set statement cache capacity.
234    pub fn statement_cache(mut self, capacity: usize) -> Self {
235        self.config.statement_cache_capacity = capacity;
236        self
237    }
238
239    /// Set SSL mode.
240    pub fn ssl_mode(mut self, mode: SslMode) -> Self {
241        self.config.ssl_mode = mode;
242        self
243    }
244
245    /// Require SSL.
246    pub fn require_ssl(mut self) -> Self {
247        self.config.ssl_mode = SslMode::Require;
248        self
249    }
250
251    /// Set application name.
252    pub fn application_name(mut self, name: impl Into<String>) -> Self {
253        self.config.application_name = Some(name.into());
254        self
255    }
256
257    /// Build the configuration.
258    pub fn build(self) -> SqlxConfig {
259        self.config
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_backend_detection() {
269        assert_eq!(
270            DatabaseBackend::from_url("postgres://localhost/db").unwrap(),
271            DatabaseBackend::Postgres
272        );
273        assert_eq!(
274            DatabaseBackend::from_url("postgresql://localhost/db").unwrap(),
275            DatabaseBackend::Postgres
276        );
277        assert_eq!(
278            DatabaseBackend::from_url("mysql://localhost/db").unwrap(),
279            DatabaseBackend::MySql
280        );
281        assert_eq!(
282            DatabaseBackend::from_url("sqlite://./test.db").unwrap(),
283            DatabaseBackend::Sqlite
284        );
285        assert_eq!(
286            DatabaseBackend::from_url("file:./test.db").unwrap(),
287            DatabaseBackend::Sqlite
288        );
289
290        assert!(DatabaseBackend::from_url("unknown://localhost").is_err());
291    }
292
293    #[test]
294    fn test_config_from_url() {
295        let config = SqlxConfig::from_url("postgres://user:pass@localhost:5432/mydb").unwrap();
296        assert_eq!(config.backend, DatabaseBackend::Postgres);
297        assert_eq!(config.max_connections, 10);
298    }
299
300    #[test]
301    fn test_config_builder() {
302        let config = SqlxConfig::builder("postgres://localhost/db")
303            .max_connections(20)
304            .min_connections(5)
305            .connect_timeout(Duration::from_secs(10))
306            .require_ssl()
307            .application_name("prax-app")
308            .build();
309
310        assert_eq!(config.max_connections, 20);
311        assert_eq!(config.min_connections, 5);
312        assert_eq!(config.connect_timeout, Duration::from_secs(10));
313        assert_eq!(config.ssl_mode, SslMode::Require);
314        assert_eq!(config.application_name, Some("prax-app".to_string()));
315    }
316
317    #[test]
318    fn test_config_with_methods() {
319        let config = SqlxConfig::from_url("mysql://localhost/db")
320            .unwrap()
321            .with_max_connections(50)
322            .with_statement_cache(200);
323
324        assert_eq!(config.max_connections, 50);
325        assert_eq!(config.statement_cache_capacity, 200);
326    }
327}