Skip to main content

mempill_postgres/
connection.rs

1//! PostgresPersistenceStore: r2d2 pool construction + refinery migration bootstrap.
2//!
3//! ## Connection string format
4//!
5//! Accepts libpq-style key-value strings, for example:
6//! ```text
7//! host=localhost port=5432 user=mempill dbname=mempill password=secret
8//! ```
9//! Or a URI: `postgresql://mempill:secret@localhost:5432/mempill`.
10//!
11//! ## TLS
12//!
13//! v0.3 uses `NoTls` (suitable for local Docker / CI environments).
14//! // v0.3.1: add TlsMode param to accept a `native_tls::TlsConnector` for cloud Postgres (RDS, Neon, Supabase).
15//!
16//! ## Pool settings
17//!
18//! - `max_size = 20` connections
19//! - `connection_timeout = 5s`
20
21use r2d2::Pool;
22use r2d2_postgres::PostgresConnectionManager;
23use postgres::NoTls;
24
25/// Error type for PostgresPersistenceStore operations.
26#[derive(Debug, thiserror::Error)]
27pub enum PostgresStoreError {
28    #[error("postgres driver error: {0}")]
29    Postgres(#[from] postgres::Error),
30
31    #[error("r2d2 pool error: {0}")]
32    Pool(#[from] r2d2::Error),
33
34    #[error("refinery migration error: {0}")]
35    Migration(#[from] refinery::Error),
36
37    #[error("domain mapping error: {0}")]
38    Mapping(String),
39}
40
41/// The PostgreSQL-backed `PersistencePort` implementation.
42///
43/// Construct via [`PostgresPersistenceStore::new`].
44/// Clone-friendly: the inner `r2d2::Pool` is `Arc`-wrapped by r2d2.
45pub struct PostgresPersistenceStore {
46    pub(crate) pool: Pool<PostgresConnectionManager<NoTls>>,
47}
48
49impl PostgresPersistenceStore {
50    /// Bootstrap entry point: run refinery migrations on a dedicated connection,
51    /// then build the r2d2 connection pool.
52    ///
53    /// # Errors
54    ///
55    /// Returns `PostgresStoreError` if the connection string is invalid, the DB
56    /// is unreachable, migrations fail, or the pool cannot be built.
57    pub fn new(connection_string: &str) -> Result<Self, PostgresStoreError> {
58        // 1. Dedicated migration connection (not from pool — avoids pool startup contention).
59        let mut mig_client = postgres::Client::connect(connection_string, NoTls)?;
60        crate::migrations::runner().run(&mut mig_client)?;
61        drop(mig_client);
62
63        // 2. Build the r2d2 pool.
64        let manager = PostgresConnectionManager::new(
65            connection_string.parse()?,
66            NoTls,
67        );
68        let pool = r2d2::Pool::builder()
69            .max_size(20)
70            .connection_timeout(std::time::Duration::from_secs(5))
71            .build(manager)?;
72
73        Ok(Self { pool })
74    }
75}