dbkit-rs
Multi-backend database infrastructure for Rust applications.
Transactional writes go through [sqlx]'s Any driver — the backend (Postgres,
MySQL, or SQLite) is chosen by the connection URL scheme. Analytical reads go
through a pluggable, Arrow-based engine (DuckDB or DataFusion) behind feature
flags.
Upgrading from 0.2? See the migration guide.
Features
- Multi-backend writes —
ConnectionManager+BaseHandlerover sqlx;postgres://,mysql://, orsqlite://selects the backend - Connection pooling — auto-create database, configurable pool size and timeouts
- Configurable —
DbkitConfigbuilder with URL construction, SSL modes, and pool tuning - Unified writes —
execute_writewithWriteOp(single / batch DDL / batch params) and neutralDbValueparameters - Bulk Postgres writes —
PgHandler::copy_in(COPY) andcopy_upsert(COPY → staging → set-basedON CONFLICT) for high-volume inserts/upserts - Analytical reads —
execute_readreturnsVec<RecordBatch>(Arrow);execute_read_as::<T>deserializes rows into typed structs viaserde_arrow - Pluggable read engines — DuckDB or DataFusion (both may be enabled together; they share an Arrow version)
- Transactional → analytical sync —
sync_tables()/sync_query()copy data into the read engine (any backend × any engine); DuckDB can also attach Postgres live - Backend-aware migrations —
InitializationHandlerwith named migrations tracked by content hash, portable across Postgres/MySQL/SQLite - Concurrent cache — generic DashMap-based key-value cache with named buckets
- Unicode normalization —
BaseHandler::normalize_name()for consistent name matching via NFD decomposition
Usage
use ;
// Connect — the URL scheme picks the backend (postgres:// / mysql:// / sqlite://)
let conn = new.await?;
// Or use the config builder
let config = builder
.host
.database
.user
.password
.pool_size
.build;
let conn = connect.await?;
// `pool()` yields a sqlx `AnyPool` (cheaply cloneable)
let handler = new;
// Write. Placeholders are backend-native: `$1` for Postgres, `?` for MySQL/SQLite.
handler.execute_write.await?;
Migrations
InitializationHandler tracks named migrations by content hash in a
_dbkit_migrations table whose DDL adapts to the backend.
use InitializationHandler;
let init = new;
init.run_named_migration.await?;
The tracking table is portable, but the migration SQL you supply is backend-native — write it for the database you connected to.
Pool health
let status = conn.pool_status;
println!;
Analytical reads (optional)
Enable a read engine. DuckDB and DataFusion share the Arrow RecordBatch
contract, so either can be used the same way:
= { = "0.4", = ["duckdb"] } # or "datafusion"
Attach a read engine to the handler:
// DuckDB (in-memory, bundled)
let handler = with_duckdb?;
// or DataFusion (pure-Rust, no FFI)
let handler = with_datafusion?;
Sync, then read
Copy transactional tables into the analytical engine, then query them:
use DbValue;
// Whole tables
handler.sync_tables.await?;
// A filtered / arbitrary query, loaded as a named table
handler.sync_query.await?;
// Arrow read
let batches = handler.execute_read.await?;
// ...or deserialize straight into typed rows
let orders: =
handler.execute_read_as.await?;
Live Postgres attach (DuckDB)
DuckDB can query Postgres tables directly — no copy — via the pg catalog:
let handler = with_duckdb_attached_postgres?;
let batches = handler.execute_read.await?;
Rich Postgres types (uuid / timestamps / json)
The multi-backend Any pool only represents basic scalars (bool / int / float /
text / bytes) — the price of Postgres/MySQL/SQLite interchangeability. For
queries involving native Postgres types (uuid, timestamptz, jsonb, arrays,
decimal), enable postgres-native and drop to a real PgPool with full sqlx
type support:
= { = "0.4", = ["postgres-native"] }
let pg = conn.pg_native_pool.await?; // native PgPool, rich types
let row = query
.bind
.fetch_one
.await?;
let id: Uuid = row.get;
let ts: DateTime = row.get;
Use the Any pool (handler.execute_write) for portable work, and the native
pool only where you need Postgres-specific types. (Other type features such as
rust_decimal can be enabled by adding sqlx to your own Cargo.toml with the
relevant feature — cargo unifies it with dbkit's.)
Bulk writes (Postgres)
On a native PgPool (postgres-native), PgHandler offers three write paths.
Pick by what the operation needs:
| Use… | When |
|---|---|
copy_in |
Plain bulk insert into one table. Fastest (~30–50× row-by-row). All-or-nothing; no ON CONFLICT. |
copy_upsert |
Bulk upsert: COPY → temp staging table → one set-based INSERT…SELECT…ON CONFLICT (~10× faster than row-by-row ON CONFLICT). |
WriteOp::BatchParams |
Anything COPY can't do — see the four cases below. |
use ;
let pg = new;
let rows = vec!;
// Fastest plain bulk insert.
pg.copy_in.await?;
// Bulk upsert: overwrite `name` on id conflict. An empty update list ⇒ DO NOTHING.
pg.copy_upsert.await?;
// Row-by-row batch. `isolate_rows: false` = fast all-or-nothing;
// `true` = wrap each row in a SAVEPOINT and skip bad rows.
pg.execute_write.await?;
copy_upsert is the default for bulk inserts/upserts into a Postgres table, but
BatchParams stays essential for four things COPY/staging genuinely can't do:
- Non-INSERT statements —
BatchParamsruns any query per param set (UPDATE … WHERE,DELETE, function calls, multi-CTE). Staging only does insert-shaped loads. - Per-row error isolation —
ON CONFLICTonly resolves unique/exclusion conflicts; a CHECK/FK/NOT-NULL violation or bad value aborts the whole stagedINSERT…SELECT.BatchParams { isolate_rows: true }skips any bad row and commits the rest. - Non-Postgres backends —
copy_*are Postgres-only;BatchParamsruns on theAnypool (MySQL/SQLite). - Small batches — the temp-table + COPY + insert-select setup has fixed
overhead; for tens of rows, plain
BatchParams(or a single multi-row INSERT) wins.
copy_in / copy_upsert are all-or-nothing and Postgres-only. Within one
copy_upsert call the conflict key must be unique across rows (duplicate keys
make ON CONFLICT DO UPDATE error — de-duplicate first).
Name normalization
let normalized = normalize_name;
assert_eq!;
Cache
Both key and value types are generic, defaulting to String:
use Cache;
let cache: Cache = with_buckets;
cache.set;
let val = cache.get;
// Typed keys and values
let counts: = with_buckets;
counts.set;
assert_eq!;
Feature flags
| Feature | Default | Description |
|---|---|---|
postgres |
on | Postgres backend (sqlx) |
postgres-native |
off | Native PgPool with full Postgres types (uuid/chrono/json) — see below |
mysql |
off | MySQL backend (sqlx) |
sqlite |
off | SQLite backend (sqlx) |
duckdb |
off | DuckDB analytical read engine (bundled) + typed reads |
datafusion |
off | DataFusion analytical read engine (pure-Rust) + typed reads |
duckdb and datafusion currently share an Arrow version and may be enabled
together.
License
MIT