Skip to main content

minco_sqlx_postgres/
lib.rs

1//! Bounded `SQLx` `PostgreSQL` pools for local servers and serverless runtimes.
2#![forbid(unsafe_code)]
3
4use serde::{Deserialize, Serialize};
5pub use sqlx::PgPool;
6use sqlx::postgres::PgPoolOptions;
7use std::{path::Path, time::Duration};
8use thiserror::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct PostgresPoolConfig {
12    pub url: String,
13    pub max_connections: u32,
14    pub acquire_timeout_seconds: u64,
15    pub idle_timeout_seconds: u64,
16}
17
18impl PostgresPoolConfig {
19    pub fn serverless(url: impl Into<String>) -> Self {
20        Self {
21            url: url.into(),
22            max_connections: 2,
23            acquire_timeout_seconds: 5,
24            idle_timeout_seconds: 60,
25        }
26    }
27    pub fn validate(&self) -> Result<(), PostgresError> {
28        if self.url.trim().is_empty() {
29            return Err(PostgresError::InvalidConfig("database URL is empty".into()));
30        }
31        if self.max_connections == 0 {
32            return Err(PostgresError::InvalidConfig(
33                "max_connections must be at least 1".into(),
34            ));
35        }
36        Ok(())
37    }
38}
39
40pub async fn connect(config: &PostgresPoolConfig) -> Result<PgPool, PostgresError> {
41    config.validate()?;
42    Ok(PgPoolOptions::new()
43        .min_connections(0)
44        .max_connections(config.max_connections)
45        .acquire_timeout(Duration::from_secs(config.acquire_timeout_seconds))
46        .idle_timeout(Some(Duration::from_secs(config.idle_timeout_seconds)))
47        .connect(&config.url)
48        .await?)
49}
50
51pub fn connect_lazy(config: &PostgresPoolConfig) -> Result<PgPool, PostgresError> {
52    config.validate()?;
53    Ok(PgPoolOptions::new()
54        .min_connections(0)
55        .max_connections(config.max_connections)
56        .acquire_timeout(Duration::from_secs(config.acquire_timeout_seconds))
57        .idle_timeout(Some(Duration::from_secs(config.idle_timeout_seconds)))
58        .connect_lazy(&config.url)?)
59}
60
61pub async fn migrate(pool: &PgPool, path: impl AsRef<Path>) -> Result<(), PostgresError> {
62    let migrator = sqlx::migrate::Migrator::new(path.as_ref()).await?;
63    migrator.run(pool).await?;
64    Ok(())
65}
66
67pub async fn ready(pool: &PgPool) -> bool {
68    matches!(
69        sqlx::query_scalar::<_, i32>("SELECT 1")
70            .fetch_one(pool)
71            .await,
72        Ok(1)
73    )
74}
75
76#[derive(Debug, Error)]
77pub enum PostgresError {
78    #[error("invalid PostgreSQL configuration: {0}")]
79    InvalidConfig(String),
80    #[error("PostgreSQL error: {0}")]
81    Sqlx(#[from] sqlx::Error),
82    #[error("PostgreSQL migration error: {0}")]
83    Migration(#[from] sqlx::migrate::MigrateError),
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    #[test]
90    fn serverless_defaults_bound_connection_pressure() {
91        let config = PostgresPoolConfig::serverless("postgres://example.invalid/db");
92        assert_eq!(config.max_connections, 2);
93        assert_eq!(config.acquire_timeout_seconds, 5);
94    }
95}