Skip to main content

doido_model/
pool.rs

1//! Database connections backed by the model [`config`](crate::config).
2//!
3//! Besides the one-shot [`connect`]/[`connect_with_url`] helpers, this module
4//! holds a process-global connection installed once at application boot
5//! ([`init`]) and read from request handlers via [`pool`]. Controllers reach it
6//! through `Context::db()`.
7
8use crate::config;
9use crate::sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr};
10use std::sync::OnceLock;
11
12/// Process-global connection, installed by [`init`]/[`set_pool`].
13static POOL: OnceLock<DatabaseConnection> = OnceLock::new();
14
15/// Builds [`ConnectOptions`] toggling sea-orm's SQL statement logging, so
16/// queries surface through the global tracing subscriber (target `sqlx::query`,
17/// level `INFO`) when `sql_logging` is on. The flag comes from the `logger.sql`
18/// config key; tune visibility further with `RUST_LOG` (see `doido_core::logger`).
19fn options(url: &str, sql_logging: bool) -> ConnectOptions {
20    pool_options(url, sql_logging, None, None)
21}
22
23/// Builds [`ConnectOptions`] with SQL logging plus optional pool tuning:
24/// `max_connections` (config `database.pool`) and `connect_timeout` in seconds
25/// (config `database.connect_timeout`).
26pub fn pool_options(
27    url: &str,
28    sql_logging: bool,
29    max_connections: Option<u32>,
30    connect_timeout_secs: Option<u64>,
31) -> ConnectOptions {
32    let mut opts = ConnectOptions::new(url.to_owned());
33    // Statements log at the default `INFO` level under target `sqlx::query`.
34    opts.sqlx_logging(sql_logging);
35    if let Some(max) = max_connections {
36        opts.max_connections(max);
37    }
38    if let Some(secs) = connect_timeout_secs {
39        opts.connect_timeout(std::time::Duration::from_secs(secs));
40    }
41    opts
42}
43
44/// Connects to the database named by the current environment's
45/// `config/<env>.yml` `database.url`, honouring its `logger.sql` toggle and any
46/// `database.pool` / `database.connect_timeout` knobs.
47pub async fn connect() -> Result<DatabaseConnection, DbErr> {
48    let config = config::load();
49    let db = config.database();
50    Database::connect(pool_options(
51        db.url.as_str(),
52        config.logger().sql,
53        db.pool,
54        db.connect_timeout,
55    ))
56    .await
57}
58
59/// Connects using an explicit database URL, bypassing the config file. SQL
60/// statement logging stays enabled (the default).
61pub async fn connect_with_url(url: &str) -> Result<DatabaseConnection, DbErr> {
62    Database::connect(options(url, true)).await
63}
64
65/// Connects (via [`connect`]) and installs the process-global pool, returning a
66/// reference to it. Idempotent: if already initialised, the existing connection
67/// is returned and the new one discarded. Call once at server boot.
68pub async fn init() -> Result<&'static DatabaseConnection, DbErr> {
69    if let Some(existing) = POOL.get() {
70        return Ok(existing);
71    }
72    let conn = connect().await?;
73    let _ = POOL.set(conn);
74    Ok(POOL.get().expect("pool was just set"))
75}
76
77/// Installs an already-open connection as the global pool (e.g. in tests).
78/// Returns `Err` with the connection back if one was already installed.
79pub fn set_pool(conn: DatabaseConnection) -> Result<(), DatabaseConnection> {
80    POOL.set(conn)
81}
82
83/// Returns the global connection, panicking if [`init`]/[`set_pool`] was never
84/// called. Use from request handlers where boot is guaranteed to have run.
85pub fn pool() -> &'static DatabaseConnection {
86    POOL.get().expect(
87        "database pool not initialised; call doido_model::pool::init() at boot before handling requests",
88    )
89}
90
91/// Returns the global connection if installed, else `None`.
92pub fn try_pool() -> Option<&'static DatabaseConnection> {
93    POOL.get()
94}
95
96/// A process-global lock for serializing tests that share the global pool.
97///
98/// The pool is process-global, so tests that install it and run requests must
99/// not execute concurrently. Generated scaffold controller tests hold this lock
100/// for their duration; hold it in any hand-written test that touches the global
101/// pool. Poisoning is ignored so one failing test doesn't cascade.
102pub fn test_lock() -> std::sync::MutexGuard<'static, ()> {
103    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
104    LOCK.lock().unwrap_or_else(|e| e.into_inner())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[tokio::test]
112    async fn set_pool_then_pool_returns_it() {
113        // No pool before install.
114        assert!(try_pool().is_none());
115
116        let conn = connect_with_url("sqlite::memory:").await.unwrap();
117        set_pool(conn).expect("first install succeeds");
118
119        // Now both accessors see it; a second install is rejected.
120        assert!(try_pool().is_some());
121        let _ = pool();
122        let second = connect_with_url("sqlite::memory:").await.unwrap();
123        assert!(set_pool(second).is_err());
124    }
125}