zirv_db_sqlx/lib.rs
1//! # zirv-db-sqlx
2//!
3//! A small, convenient wrapper around [sqlx](https://github.com/launchbadge/sqlx)
4//! that standardises three things across a service:
5//!
6//! - **Global connection pool** managed in a process-wide [`OnceLock`](std::sync::OnceLock),
7//! initialized once with [`init_db_pool!`] and read anywhere with [`get_db_pool!`].
8//! - **Tunable pool settings** read from the `database.*` configuration namespace via
9//! [`zirv-config`](https://docs.rs/zirv-config) (see [`db::PoolSettings`]).
10//! - **Transaction helpers** ([`start_transaction!`], [`commit_transaction!`],
11//! [`rollback_transaction!`]) that remove the usual boilerplate.
12//!
13//! The crate also exposes [`close_db_pool`] for graceful shutdown.
14//!
15//! ## Choosing a database backend
16//!
17//! Enable **exactly one** of the `mysql` (default), `postgres`, or `sqlite` features,
18//! together with **exactly one** runtime/TLS feature (`runtime-tokio-native-tls`,
19//! the default, or `runtime-tokio-rustls`). For a non-default backend, disable
20//! default features:
21//!
22//! ```toml
23//! # PostgreSQL with rustls
24//! zirv-db-sqlx = { version = "0.3", default-features = false, features = ["postgres", "runtime-tokio-rustls"] }
25//! ```
26//!
27//! ## Example
28//!
29//! ```no_run
30//! use zirv_config::register_config;
31//! use zirv_db_sqlx::{init_db_pool, get_db_pool, close_db_pool};
32//!
33//! #[derive(serde::Serialize)]
34//! struct DatabaseConfig {
35//! url: String,
36//! max_connections: u32,
37//! }
38//!
39//! # async fn run() -> Result<(), sqlx::Error> {
40//! register_config!("database", DatabaseConfig {
41//! url: "mysql://user:password@localhost/db".to_string(),
42//! max_connections: 10,
43//! });
44//!
45//! init_db_pool!();
46//!
47//! let pool = get_db_pool!();
48//! // ... use `pool` with sqlx queries ...
49//!
50//! close_db_pool().await; // graceful shutdown
51//! # Ok(())
52//! # }
53//! ```
54
55pub mod db;
56
57pub use db::{Db, DbPool, PoolSettings, close_db_pool};
58
59// --- Compile-time feature validation -------------------------------------------------
60//
61// Exactly one database backend must be selected. These guards turn an otherwise
62// cryptic type or trait error into a clear, actionable message.
63
64#[cfg(not(any(feature = "mysql", feature = "postgres", feature = "sqlite")))]
65compile_error!(
66 "zirv-db-sqlx: no database backend selected. Enable exactly one of the \
67 `mysql`, `postgres`, or `sqlite` features."
68);
69
70#[cfg(any(
71 all(feature = "mysql", feature = "postgres"),
72 all(feature = "mysql", feature = "sqlite"),
73 all(feature = "postgres", feature = "sqlite"),
74))]
75compile_error!(
76 "zirv-db-sqlx: multiple database backends selected. Enable exactly ONE of the \
77 `mysql`, `postgres`, or `sqlite` features (use `default-features = false` for \
78 a non-default backend)."
79);
80
81/// Initializes the global database pool.
82///
83/// Thin wrapper over [`db::init_db_pool`] that awaits it. Call once, early in your
84/// application (typically in `main`), after registering the `database` config block.
85///
86/// # Example
87/// ```no_run
88/// use zirv_db_sqlx::init_db_pool;
89/// # async fn run() {
90/// init_db_pool!();
91/// # }
92/// ```
93#[macro_export]
94macro_rules! init_db_pool {
95 () => {
96 $crate::db::init_db_pool().await
97 };
98}
99
100/// Retrieves a reference to the global database pool.
101///
102/// Thin wrapper over [`db::get_db_pool`]. Panics if [`init_db_pool!`] has not run.
103///
104/// # Example
105/// ```no_run
106/// use zirv_db_sqlx::get_db_pool;
107/// # fn run() {
108/// let pool = get_db_pool!();
109/// // Use `pool` for database operations...
110/// # }
111/// ```
112#[macro_export]
113macro_rules! get_db_pool {
114 () => {
115 $crate::db::get_db_pool()
116 };
117}
118
119/// Begins a new transaction on the global pool.
120///
121/// On failure the error is logged with [`tracing`] and returned, so the calling
122/// function must return a `Result<_, sqlx::Error>`.
123///
124/// # Example
125/// ```no_run
126/// use zirv_db_sqlx::{start_transaction, commit_transaction};
127///
128/// async fn perform_db_operations() -> Result<(), sqlx::Error> {
129/// let mut tx = start_transaction!();
130/// // Execute operations within the transaction...
131/// commit_transaction!(tx);
132/// Ok(())
133/// }
134/// ```
135#[macro_export]
136macro_rules! start_transaction {
137 () => {
138 match $crate::db::get_db_pool().begin().await {
139 Ok(tx) => tx,
140 Err(e) => {
141 ::tracing::error!(error = ?e, "Failed to start transaction");
142 return Err(e);
143 }
144 }
145 };
146}
147
148/// Commits an active transaction, logging and returning the error on failure.
149///
150/// # Example
151/// ```no_run
152/// use zirv_db_sqlx::{start_transaction, commit_transaction};
153///
154/// async fn perform_db_operations() -> Result<(), sqlx::Error> {
155/// let mut tx = start_transaction!();
156/// // Execute operations within the transaction...
157/// commit_transaction!(tx);
158/// Ok(())
159/// }
160/// ```
161#[macro_export]
162macro_rules! commit_transaction {
163 ($tx:expr) => {
164 match $tx.commit().await {
165 Ok(_) => (),
166 Err(e) => {
167 ::tracing::error!(error = ?e, "Failed to commit transaction");
168 return Err(e);
169 }
170 }
171 };
172}
173
174/// Rolls back an active transaction, logging and returning the error on failure.
175///
176/// # Example
177/// ```no_run
178/// use zirv_db_sqlx::{start_transaction, rollback_transaction};
179///
180/// async fn perform_db_operations() -> Result<(), sqlx::Error> {
181/// let mut tx = start_transaction!();
182/// // Execute operations within the transaction...
183/// rollback_transaction!(tx);
184/// Ok(())
185/// }
186/// ```
187#[macro_export]
188macro_rules! rollback_transaction {
189 ($tx:expr) => {
190 match $tx.rollback().await {
191 Ok(_) => (),
192 Err(e) => {
193 ::tracing::error!(error = ?e, "Failed to rollback transaction");
194 return Err(e);
195 }
196 }
197 };
198}