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}