zirv-db-sqlx 0.3.0

A convenient wrapper around sqlx: global connection-pool management, tunable pool settings, graceful shutdown, and transaction helpers for MySQL, PostgreSQL, and SQLite.
Documentation
# 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

| Feature | Default | Description |
|---------|:-------:|-------------|
| `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.

| Config key | Type | Default | Meaning |
|------------|------|---------|---------|
| `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.