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 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 let mut opts = ConnectOptions::new(url.to_owned());
21 // Statements log at the default `INFO` level under target `sqlx::query`.
22 opts.sqlx_logging(sql_logging);
23 opts
24}
25
26/// Connects to the database named by the current environment's
27/// `config/<env>.yml` `database.url`, honouring its `logger.sql` toggle.
28pub async fn connect() -> Result<DatabaseConnection, DbErr> {
29 let config = config::load();
30 Database::connect(options(config.database().url.as_str(), config.logger().sql)).await
31}
32
33/// Connects using an explicit database URL, bypassing the config file. SQL
34/// statement logging stays enabled (the default).
35pub async fn connect_with_url(url: &str) -> Result<DatabaseConnection, DbErr> {
36 Database::connect(options(url, true)).await
37}
38
39/// Connects (via [`connect`]) and installs the process-global pool, returning a
40/// reference to it. Idempotent: if already initialised, the existing connection
41/// is returned and the new one discarded. Call once at server boot.
42pub async fn init() -> Result<&'static DatabaseConnection, DbErr> {
43 if let Some(existing) = POOL.get() {
44 return Ok(existing);
45 }
46 let conn = connect().await?;
47 let _ = POOL.set(conn);
48 Ok(POOL.get().expect("pool was just set"))
49}
50
51/// Installs an already-open connection as the global pool (e.g. in tests).
52/// Returns `Err` with the connection back if one was already installed.
53pub fn set_pool(conn: DatabaseConnection) -> Result<(), DatabaseConnection> {
54 POOL.set(conn)
55}
56
57/// Returns the global connection, panicking if [`init`]/[`set_pool`] was never
58/// called. Use from request handlers where boot is guaranteed to have run.
59pub fn pool() -> &'static DatabaseConnection {
60 POOL.get().expect(
61 "database pool not initialised; call doido_model::pool::init() at boot before handling requests",
62 )
63}
64
65/// Returns the global connection if installed, else `None`.
66pub fn try_pool() -> Option<&'static DatabaseConnection> {
67 POOL.get()
68}
69
70/// A process-global lock for serializing tests that share the global pool.
71///
72/// The pool is process-global, so tests that install it and run requests must
73/// not execute concurrently. Generated scaffold controller tests hold this lock
74/// for their duration; hold it in any hand-written test that touches the global
75/// pool. Poisoning is ignored so one failing test doesn't cascade.
76pub fn test_lock() -> std::sync::MutexGuard<'static, ()> {
77 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
78 LOCK.lock().unwrap_or_else(|e| e.into_inner())
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[tokio::test]
86 async fn set_pool_then_pool_returns_it() {
87 // No pool before install.
88 assert!(try_pool().is_none());
89
90 let conn = connect_with_url("sqlite::memory:").await.unwrap();
91 set_pool(conn).expect("first install succeeds");
92
93 // Now both accessors see it; a second install is rejected.
94 assert!(try_pool().is_some());
95 let _ = pool();
96 let second = connect_with_url("sqlite::memory:").await.unwrap();
97 assert!(set_pool(second).is_err());
98 }
99}