Skip to main content

prax_sqlx/
pool.rs

1//! Connection pool management for SQLx.
2
3use crate::config::{DatabaseBackend, SqlxConfig, SslMode};
4use crate::error::{SqlxError, SqlxResult};
5
6/// A wrapper around SQLx connection pools supporting multiple databases.
7#[derive(Clone)]
8pub enum SqlxPool {
9    /// PostgreSQL connection pool
10    #[cfg(feature = "postgres")]
11    Postgres(sqlx::PgPool),
12    /// MySQL connection pool
13    #[cfg(feature = "mysql")]
14    MySql(sqlx::MySqlPool),
15    /// SQLite connection pool
16    #[cfg(feature = "sqlite")]
17    Sqlite(sqlx::SqlitePool),
18}
19
20/// Translate the crate's [`SslMode`] onto sqlx's `PgSslMode`.
21///
22/// Pure mapping (no I/O) so every mode can be asserted in unit tests.
23#[cfg(feature = "postgres")]
24fn pg_ssl_mode(mode: SslMode) -> sqlx::postgres::PgSslMode {
25    use sqlx::postgres::PgSslMode;
26
27    match mode {
28        SslMode::Disable => PgSslMode::Disable,
29        SslMode::Prefer => PgSslMode::Prefer,
30        SslMode::Require => PgSslMode::Require,
31        SslMode::VerifyCa => PgSslMode::VerifyCa,
32        SslMode::VerifyFull => PgSslMode::VerifyFull,
33    }
34}
35
36/// Translate the crate's [`SslMode`] onto sqlx's `MySqlSslMode`.
37///
38/// Note the `VerifyFull` → `VerifyIdentity` rename (MySQL's name for
39/// hostname verification).
40#[cfg(feature = "mysql")]
41fn mysql_ssl_mode(mode: SslMode) -> sqlx::mysql::MySqlSslMode {
42    use sqlx::mysql::MySqlSslMode;
43
44    match mode {
45        SslMode::Disable => MySqlSslMode::Disabled,
46        SslMode::Prefer => MySqlSslMode::Preferred,
47        SslMode::Require => MySqlSslMode::Required,
48        SslMode::VerifyCa => MySqlSslMode::VerifyCa,
49        SslMode::VerifyFull => MySqlSslMode::VerifyIdentity,
50    }
51}
52
53impl SqlxPool {
54    /// Create a new pool from configuration.
55    pub async fn connect(config: &SqlxConfig) -> SqlxResult<Self> {
56        match config.backend {
57            #[cfg(feature = "postgres")]
58            DatabaseBackend::Postgres => {
59                use std::str::FromStr;
60
61                use sqlx::postgres::PgConnectOptions;
62
63                let mut options = PgConnectOptions::from_str(&config.url)?
64                    .statement_cache_capacity(config.statement_cache_capacity)
65                    .ssl_mode(pg_ssl_mode(config.ssl_mode));
66                if let Some(name) = &config.application_name {
67                    options = options.application_name(name);
68                }
69
70                let pool = sqlx::postgres::PgPoolOptions::new()
71                    .max_connections(config.max_connections)
72                    .min_connections(config.min_connections)
73                    .acquire_timeout(config.connect_timeout)
74                    .idle_timeout(config.idle_timeout)
75                    .max_lifetime(config.max_lifetime)
76                    .connect_with(options)
77                    .await?;
78                Ok(Self::Postgres(pool))
79            }
80            #[cfg(feature = "mysql")]
81            DatabaseBackend::MySql => {
82                use std::str::FromStr;
83
84                use sqlx::mysql::MySqlConnectOptions;
85
86                let options = MySqlConnectOptions::from_str(&config.url)?
87                    .statement_cache_capacity(config.statement_cache_capacity)
88                    .ssl_mode(mysql_ssl_mode(config.ssl_mode));
89                if config.application_name.is_some() {
90                    tracing::warn!(
91                        "application_name is not supported by the sqlx MySQL backend; ignoring"
92                    );
93                }
94
95                let pool = sqlx::mysql::MySqlPoolOptions::new()
96                    .max_connections(config.max_connections)
97                    .min_connections(config.min_connections)
98                    .acquire_timeout(config.connect_timeout)
99                    .idle_timeout(config.idle_timeout)
100                    .max_lifetime(config.max_lifetime)
101                    .connect_with(options)
102                    .await?;
103                Ok(Self::MySql(pool))
104            }
105            #[cfg(feature = "sqlite")]
106            DatabaseBackend::Sqlite => {
107                use std::str::FromStr;
108
109                use sqlx::sqlite::SqliteConnectOptions;
110
111                let options = SqliteConnectOptions::from_str(&config.url)?
112                    .statement_cache_capacity(config.statement_cache_capacity);
113                if config.ssl_mode != SslMode::default() {
114                    tracing::warn!("ssl_mode is not applicable to SQLite connections; ignoring");
115                }
116                if config.application_name.is_some() {
117                    tracing::warn!(
118                        "application_name is not supported by the sqlx SQLite backend; ignoring"
119                    );
120                }
121
122                let pool = sqlx::sqlite::SqlitePoolOptions::new()
123                    .max_connections(config.max_connections)
124                    .min_connections(config.min_connections)
125                    .acquire_timeout(config.connect_timeout)
126                    .idle_timeout(config.idle_timeout)
127                    .max_lifetime(config.max_lifetime)
128                    .connect_with(options)
129                    .await?;
130                Ok(Self::Sqlite(pool))
131            }
132            #[allow(unreachable_patterns)]
133            _ => Err(SqlxError::config(format!(
134                "Database backend {:?} not enabled. Enable the corresponding feature.",
135                config.backend
136            ))),
137        }
138    }
139
140    /// Get the database backend type.
141    pub fn backend(&self) -> DatabaseBackend {
142        match self {
143            #[cfg(feature = "postgres")]
144            Self::Postgres(_) => DatabaseBackend::Postgres,
145            #[cfg(feature = "mysql")]
146            Self::MySql(_) => DatabaseBackend::MySql,
147            #[cfg(feature = "sqlite")]
148            Self::Sqlite(_) => DatabaseBackend::Sqlite,
149        }
150    }
151
152    /// Close the pool.
153    pub async fn close(&self) {
154        match self {
155            #[cfg(feature = "postgres")]
156            Self::Postgres(pool) => pool.close().await,
157            #[cfg(feature = "mysql")]
158            Self::MySql(pool) => pool.close().await,
159            #[cfg(feature = "sqlite")]
160            Self::Sqlite(pool) => pool.close().await,
161        }
162    }
163
164    /// Check if the pool is closed.
165    pub fn is_closed(&self) -> bool {
166        match self {
167            #[cfg(feature = "postgres")]
168            Self::Postgres(pool) => pool.is_closed(),
169            #[cfg(feature = "mysql")]
170            Self::MySql(pool) => pool.is_closed(),
171            #[cfg(feature = "sqlite")]
172            Self::Sqlite(pool) => pool.is_closed(),
173        }
174    }
175
176    /// Get pool statistics.
177    pub fn size(&self) -> u32 {
178        match self {
179            #[cfg(feature = "postgres")]
180            Self::Postgres(pool) => pool.size(),
181            #[cfg(feature = "mysql")]
182            Self::MySql(pool) => pool.size(),
183            #[cfg(feature = "sqlite")]
184            Self::Sqlite(pool) => pool.size(),
185        }
186    }
187
188    /// Get number of idle connections.
189    pub fn num_idle(&self) -> usize {
190        match self {
191            #[cfg(feature = "postgres")]
192            Self::Postgres(pool) => pool.num_idle(),
193            #[cfg(feature = "mysql")]
194            Self::MySql(pool) => pool.num_idle(),
195            #[cfg(feature = "sqlite")]
196            Self::Sqlite(pool) => pool.num_idle(),
197        }
198    }
199
200    /// Get the underlying PostgreSQL pool.
201    #[cfg(feature = "postgres")]
202    pub fn as_postgres(&self) -> Option<&sqlx::PgPool> {
203        match self {
204            Self::Postgres(pool) => Some(pool),
205            #[allow(unreachable_patterns)]
206            _ => None,
207        }
208    }
209
210    /// Get the underlying MySQL pool.
211    #[cfg(feature = "mysql")]
212    pub fn as_mysql(&self) -> Option<&sqlx::MySqlPool> {
213        match self {
214            Self::MySql(pool) => Some(pool),
215            #[allow(unreachable_patterns)]
216            _ => None,
217        }
218    }
219
220    /// Get the underlying SQLite pool.
221    #[cfg(feature = "sqlite")]
222    pub fn as_sqlite(&self) -> Option<&sqlx::SqlitePool> {
223        match self {
224            Self::Sqlite(pool) => Some(pool),
225            #[allow(unreachable_patterns)]
226            _ => None,
227        }
228    }
229}
230
231/// Pool builder for SQLx.
232pub struct SqlxPoolBuilder {
233    config: SqlxConfig,
234}
235
236impl SqlxPoolBuilder {
237    /// Create a new pool builder from a configuration.
238    pub fn new(config: SqlxConfig) -> Self {
239        Self { config }
240    }
241
242    /// Create a new pool builder from a URL.
243    pub fn from_url(url: impl Into<String>) -> SqlxResult<Self> {
244        let config = SqlxConfig::from_url(url)?;
245        Ok(Self { config })
246    }
247
248    /// Set max connections.
249    pub fn max_connections(mut self, max: u32) -> Self {
250        self.config.max_connections = max;
251        self
252    }
253
254    /// Set min connections.
255    pub fn min_connections(mut self, min: u32) -> Self {
256        self.config.min_connections = min;
257        self
258    }
259
260    /// Build and connect the pool.
261    pub async fn build(self) -> SqlxResult<SqlxPool> {
262        SqlxPool::connect(&self.config).await
263    }
264}
265
266/// Pool status information.
267#[derive(Debug, Clone)]
268pub struct PoolStatus {
269    /// Total pool size
270    pub size: u32,
271    /// Number of idle connections
272    pub idle: usize,
273    /// Whether the pool is closed
274    pub is_closed: bool,
275    /// Database backend type
276    pub backend: DatabaseBackend,
277}
278
279impl SqlxPool {
280    /// Get the pool status.
281    pub fn status(&self) -> PoolStatus {
282        PoolStatus {
283            size: self.size(),
284            idle: self.num_idle(),
285            is_closed: self.is_closed(),
286            backend: self.backend(),
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_pool_builder() {
297        let builder = SqlxPoolBuilder::from_url("postgres://localhost/test").unwrap();
298        let builder = builder.max_connections(20).min_connections(5);
299        assert_eq!(builder.config.max_connections, 20);
300        assert_eq!(builder.config.min_connections, 5);
301    }
302
303    #[cfg(feature = "postgres")]
304    #[test]
305    fn test_pg_ssl_mode_mapping() {
306        use sqlx::postgres::PgSslMode;
307
308        assert!(matches!(pg_ssl_mode(SslMode::Disable), PgSslMode::Disable));
309        assert!(matches!(pg_ssl_mode(SslMode::Prefer), PgSslMode::Prefer));
310        assert!(matches!(pg_ssl_mode(SslMode::Require), PgSslMode::Require));
311        assert!(matches!(
312            pg_ssl_mode(SslMode::VerifyCa),
313            PgSslMode::VerifyCa
314        ));
315        assert!(matches!(
316            pg_ssl_mode(SslMode::VerifyFull),
317            PgSslMode::VerifyFull
318        ));
319    }
320
321    #[cfg(feature = "mysql")]
322    #[test]
323    fn test_mysql_ssl_mode_mapping() {
324        use sqlx::mysql::MySqlSslMode;
325
326        assert!(matches!(
327            mysql_ssl_mode(SslMode::Disable),
328            MySqlSslMode::Disabled
329        ));
330        assert!(matches!(
331            mysql_ssl_mode(SslMode::Prefer),
332            MySqlSslMode::Preferred
333        ));
334        assert!(matches!(
335            mysql_ssl_mode(SslMode::Require),
336            MySqlSslMode::Required
337        ));
338        assert!(matches!(
339            mysql_ssl_mode(SslMode::VerifyCa),
340            MySqlSslMode::VerifyCa
341        ));
342        assert!(matches!(
343            mysql_ssl_mode(SslMode::VerifyFull),
344            MySqlSslMode::VerifyIdentity
345        ));
346    }
347}