# zirv-db-sqlx
A small, convenient wrapper around [SQLx](https://github.com/launchbadge/sqlx) that
standardises connection-pool management, pool tuning, graceful shutdown, and
transaction handling across a service. Works with **MySQL**, **PostgreSQL**, and
**SQLite**.
Built on top of [`zirv-config`](https://crates.io/crates/zirv-config): the pool reads
its settings from the `database.*` configuration namespace.
## Features
- **Global connection pool** stored in a process-wide `OnceLock`. Initialize once
with `init_db_pool!()`, then read it anywhere with `get_db_pool!()` (it returns a
shared reference, so there is no per-call cost).
- **Tunable pool settings** read from configuration, each falling back to sqlx's own
default when unset (see [Configuration](#configuration)).
- **Graceful shutdown** via `close_db_pool()`, which drains and closes the pool.
- **Transaction helpers** (`start_transaction!`, `commit_transaction!`,
`rollback_transaction!`) that remove the usual boilerplate and log failures with
[`tracing`](https://crates.io/crates/tracing).
- **Multi-database** support selected at compile time through Cargo features.
## Installation
`zirv-db-sqlx` requires you to choose **one database backend** and **one runtime + TLS
backend**. The default is MySQL on Tokio with native-tls:
```toml
[dependencies]
zirv-db-sqlx = "0.3"
```
For PostgreSQL or SQLite, disable the defaults and pick your backend:
```toml
# PostgreSQL, Tokio, rustls
zirv-db-sqlx = { version = "0.3", default-features = false, features = ["postgres", "runtime-tokio", "tls-rustls"] }
# SQLite, Tokio, native-tls
zirv-db-sqlx = { version = "0.3", default-features = false, features = ["sqlite", "runtime-tokio", "tls-native-tls"] }
```
> Enabling zero or more than one database backend is a compile error with a clear
> message telling you what to fix.
### Feature flags
| `mysql` | ✅ | MySQL / MariaDB backend. |
| `postgres` | | PostgreSQL backend. |
| `sqlite` | | SQLite backend. |
| `runtime-tokio` | ✅ | Tokio async runtime. |
| `tls-native-tls` | ✅ | TLS via the system's native TLS stack. |
| `tls-rustls` | | TLS via rustls. |
## Configuration
Register a `database` configuration block with `zirv-config` before calling
`init_db_pool!()`. Only `url` is required; everything else falls back to the same
default sqlx uses, so you keep sane behaviour out of the box.
| `database.url` | string | (required) | Connection string passed to sqlx. |
| `database.max_connections` | u32 | `10` | Hard cap on open connections. |
| `database.min_connections` | u32 | `0` | Connections kept warm while idle. |
| `database.acquire_timeout_seconds` | u64 | `30` | How long `acquire` waits before erroring. |
| `database.idle_timeout_seconds` | u64 | `600` | Reap idle connections after this long. `0` disables. |
| `database.max_lifetime_seconds` | u64 | `1800` | Recycle connections older than this. `0` disables. |
| `database.test_before_acquire` | bool | `true` | Ping a connection before handing it out. |
## Usage
### Setting up and using the pool
```rust
use zirv_config::register_config;
use zirv_db_sqlx::{init_db_pool, get_db_pool, close_db_pool};
#[derive(serde::Serialize)]
struct DatabaseConfig {
url: String,
max_connections: u32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
register_config!("database", DatabaseConfig {
url: "mysql://user:password@localhost/db_name".to_string(),
max_connections: 10,
});
// Initialize the pool once at startup. Fails fast if the database is unreachable.
init_db_pool!();
// Read the pool anywhere.
let pool = get_db_pool!();
let row: (i64,) = sqlx::query_as("SELECT 1").fetch_one(pool).await?;
assert_eq!(row.0, 1);
// Drain and close the pool on shutdown.
close_db_pool().await;
Ok(())
}
```
### Transactions
The transaction macros log failures and early-return the error, so the enclosing
function must return `Result<_, sqlx::Error>`. Do not add a `?` to them.
```rust
use zirv_db_sqlx::{start_transaction, commit_transaction, rollback_transaction};
use sqlx::types::Uuid;
async fn rename_user(id: Uuid, name: &str) -> Result<(), sqlx::Error> {
let mut tx = start_transaction!();
if let Err(e) = sqlx::query("UPDATE users SET name = ? WHERE id = ?")
.bind(name)
.bind(id)
.execute(&mut *tx)
.await
{
rollback_transaction!(tx);
return Err(e);
}
commit_transaction!(tx);
Ok(())
}
```
## API beyond the macros
- `zirv_db_sqlx::db::get_db_pool() -> &'static DbPool` — the function the macro wraps.
- `zirv_db_sqlx::close_db_pool().await` — graceful shutdown.
- `zirv_db_sqlx::PoolSettings` — the resolved pool configuration, including
`PoolSettings::from_config()` and `to_pool_options()` if you want to build the pool
yourself.
- `zirv_db_sqlx::{Db, DbPool}` — type aliases for the selected backend and its pool.
## Minimum Supported Rust Version
1.94, matching sqlx 0.9.
## Contributing
Contributions are welcome. Please open an issue or pull request.
## License
Licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at your option.