# mapepire
[](https://github.com/MeridianGroupInt/mapepire-rs/actions/workflows/ci.yml?query=branch%3Amain)
[](https://github.com/MeridianGroupInt/mapepire-rs/actions/workflows/audit-cron.yml)
[](https://deps.rs/repo/github/MeridianGroupInt/mapepire-rs)
[](Cargo.toml)
[](#license)
Async Rust client SDK for [Mapepire](https://mapepire-ibmi.github.io/) —
a cloud-friendly access layer for **Db2 for IBM i** that exposes the
database over TLS-secured `WebSockets`.
> **Status:** v0.4 ready (observability + cleanup). Not yet on
> [crates.io](https://crates.io). The full v1.0 surface (real-IBM-i CI,
> examples) lands in v1.0.
Sibling SDKs exist for [Node.js](https://github.com/Mapepire-IBMi/mapepire-js),
[Python](https://github.com/Mapepire-IBMi/mapepire-python),
[Java](https://github.com/Mapepire-IBMi/mapepire-java),
[Go](https://github.com/Mapepire-IBMi/mapepire-go),
[PHP](https://github.com/Mapepire-IBMi/mapepire-php), and
[C#/.NET](https://github.com/Mapepire-IBMi/mapepire-csharp). This crate
fills the Rust gap with a parity-first design.
## Quick look
```rust,no_run
use mapepire::{DaemonServer, Pool, TlsConfig};
# async fn example() -> mapepire::Result<()> {
let server = DaemonServer::builder()
.host("ibmi.example.com")
.user("DCURTIS")
.password(std::env::var("MAPEPIRE_PASSWORD").unwrap())
.tls(TlsConfig::Verified)
.build()
.expect("missing required field");
let pool = Pool::builder(server).max_size(8).build().await?;
// One-shot SQL via the routed pool.
let _rows = pool.execute("SELECT 1 FROM SYSIBM.SYSDUMMY1").await?;
// Transactional work via Reserved (BEGIN / DML / COMMIT all on one socket).
let conn = pool.acquire().await?.rollback_on_drop();
conn.execute("BEGIN").await?;
conn.execute_with(
"UPDATE ORDERS SET STATUS = ? WHERE ID = ?",
&[serde_json::json!("paid"), serde_json::json!(42)],
).await?;
conn.execute("COMMIT").await?;
# Ok(()) }
```
`Pool::builder(server).max_size(8).build().await?` warms a `deadpool`-backed
connection pool of `Job` handles. `pool.execute(...)` runs one-shot SQL via
the v0.3 §7.3 three-tier routing scan (idle → least-busy → fair-queue).
`pool.acquire()` returns a [`Reserved`] handle that pins one socket for
`BEGIN` / DML / `COMMIT` so the entire transaction lands on the same
connection. The opt-in `.rollback_on_drop()` fires a best-effort `ROLLBACK`
if the handle drops without an explicit `COMMIT` / `ROLLBACK`.
The `password` setter takes ownership of a `String` and immediately moves
it into a zeroizing buffer (`Password`). `DaemonServer` is not `Clone` —
the `Pool::builder` constructor takes `impl Into<Arc<DaemonServer>>` so the
single config is shared across every pooled connection.
### Runnable examples
The [`examples/`](examples/) directory ships six runnable demos covering
the common patterns:
| [`examples/one_shot.rs`](examples/one_shot.rs) | `Pool::builder` → `pool.execute(sql)` → iterate rows |
| [`examples/prepared.rs`](examples/prepared.rs) | `Job::prepare` + `Query::execute_with` reused across calls |
| [`examples/transaction.rs`](examples/transaction.rs) | `pool.acquire().rollback_on_drop()` + v0.4 typed `begin/commit` |
| [`examples/streaming.rs`](examples/streaming.rs) | `Rows::stream_typed::<T>` with a `serde::Deserialize` row struct |
| [`examples/with_tracing.rs`](examples/with_tracing.rs) | `tracing-subscriber` registration + per-execute span output |
| [`examples/cl_command.rs`](examples/cl_command.rs) | `Job::cl(...)` + `ClMessage` walkthrough |
Each example reads `MAPEPIRE_HOST`, `MAPEPIRE_USER`, and `MAPEPIRE_PASSWORD`
from the environment. Run with `cargo run --example <name>` (or
`cargo run --example with_tracing --features tracing` for the tracing demo).
### Single-connection alternative
If you only need one connection (e.g. a CLI tool or a one-shot script),
`Job::connect` skips the pool entirely:
```rust,no_run
use mapepire::{DaemonServer, Job, TlsConfig};
# async fn example() -> mapepire::Result<()> {
let server = DaemonServer::builder()
.host("daemon.example.com")
.port(8076)
.user("USER")
.password(std::env::var("MAPEPIRE_PASSWORD").unwrap())
.tls(TlsConfig::Verified)
.build()
.expect("missing required field");
let job = Job::connect(&server).await?;
let rows = job.execute("SELECT NAME, COUNT FROM SCHEMA.STATS").await?;
let dynamic = rows.into_dynamic().await?;
for row in dynamic {
let name: String = row.get("NAME")?;
let count: i64 = row.get("COUNT")?;
println!("{name}: {count}");
}
# Ok(()) }
```
`Job::connect` performs the full TCP → TLS → WebSocket Upgrade →
`Connect` handshake and resolves once the daemon confirms the session.
## Cargo features
| `rustls-tls` | **on** | Pure-Rust TLS via `rustls` (target for v0.2 transport) |
| `native-tls` | off | OS-platform TLS via `native-tls` (alternate v0.2 backend) |
| `insecure-tls` | off | Compile-time gate for `TlsConfig::Insecure` (skip server-cert validation; **never use in production**) |
| `serde-config` | off | `DaemonServerSpec` DTO for loading from config files (TOML/YAML/JSON via consumer's choice of parser) |
| `tracing` | off | [`tracing`](https://docs.rs/tracing) span instrumentation on every public dispatch entry point (`Job::execute`, `Pool::execute`, `Reserved::*`). Per-pool [`ParameterLogging`](https://docs.rs/mapepire/latest/mapepire/enum.ParameterLogging.html) governs whether parameter values appear on spans. |
| `metrics` | off | [`metrics`](https://docs.rs/metrics) facade integration. Counters / gauges / histograms documented at [`mapepire::observability`](https://docs.rs/mapepire/latest/mapepire/observability/index.html). |
A `compile_error!` guard fires when neither `rustls-tls` nor `native-tls`
is enabled — disabling default features requires an explicit alternate
TLS backend selection.
## Observability (optional)
Enable `tracing` and/or `metrics` features for production observability:
```toml
[dependencies]
mapepire = { version = "0.4", features = ["rustls-tls", "tracing", "metrics"] }
tracing-subscriber = "0.3"
metrics-exporter-prometheus = "0.15"
```
```rust,ignore
# fn install() -> Result<(), Box<dyn std::error::Error>> {
// Tracing — fmt subscriber to stderr.
tracing_subscriber::fmt::init();
// Metrics — Prometheus exporter on :9000/metrics.
metrics_exporter_prometheus::PrometheusBuilder::new().install()?;
# Ok(()) }
```
Once installed, every `Job::execute` / `Pool::execute` / `Pool::acquire` /
`Reserved::*` call emits the relevant spans and metrics. Both features are
zero-cost when disabled.
The metric-name contract is documented at
[`mapepire::observability`](https://docs.rs/mapepire/latest/mapepire/observability/index.html)
and is **SemVer-stable** — names won't be renamed without a major bump.
## Roadmap
- **v0.1** — protocol foundation (done).
- **v0.2** — transport, `Job::connect`, integration tests (done).
- **v0.3** — `Pool` with `deadpool`, `Reserved` for transactions, public
`Executor` / `FromRow` traits, diagnostic methods carried over from
v0.2 (done).
- **v0.4** — `tracing` and `metrics` feature flags; `idle_timeout`
enforcement; `rollback_on_drop` tightened to only-if-in-tx;
registry-backed routing fast path (done).
- **v1.0** — examples, real-IBM-i CI, donation proposal to the
[Mapepire-IBMi](https://github.com/Mapepire-IBMi) GitHub org.
## Documentation
- [`AGENTS.md`](AGENTS.md) — contributor and AI-assistant guide
(architecture, coding standards, security invariants, MSRV policy)
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to open a PR
- [`SECURITY.md`](SECURITY.md) — vulnerability reporting
- `Makefile` — `make help` lists all dev tasks
## License
Dual-licensed under either of:
- [MIT license](LICENSE-MIT) ([https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT))
- [Apache License, Version 2.0](LICENSE-APACHE) ([https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0))
at your option. By contributing, you agree your contribution will be
dual-licensed as above.