sqlite-hardened
Correct-by-default sqlx::SqlitePool for SQLite, with the footgun fixes built in.
use sqlite_hardened::{HardenedSqlite, SqlitePoolExt};
let pool = HardenedSqlite::new("sqlite://app.db").connect().await?;
let mut tx = pool.begin_immediate().await?; // BEGIN IMMEDIATE — safe read-then-write
// ...
tx.commit().await?;
Defaults (all overridable via the builder)
| Setting | Default | Why |
|---|---|---|
| journal_mode | WAL | concurrent readers |
| foreign_keys | ON | SQLite defaults OFF per connection |
| busy_timeout | 5s | wait-and-retry instead of instant SQLITE_BUSY |
| synchronous | NORMAL | safe under WAL, skips an fsync per commit |
| create_if_missing | true | first boot on a fresh volume |
| max_connections | 8 |
URL params vs. the builder
The connection URL is parsed first, so pass-through params (filename, mode,
vfs, immutable, cache, …) are honored. The hardened settings above are then
always enforced from the builder — a URL query param cannot weaken them (that
is the point of "hardened"). Override a hardened setting with its builder method:
HardenedSqlite::new("sqlite://app.db").foreign_keys(false).connect().await?;
In-memory pools
HardenedSqlite::new(":memory:") gives you a uniquely-named cache=shared
database with one pinned connection — so parallel test pools don't collide and
the schema isn't destroyed when the pool idles to zero connections. (In-memory
pools keep a minimum of one connection regardless of min_connections.)
Why begin_immediate
sqlx's begin() uses a deferred BEGIN; a read-then-write transaction can hit a
busy-snapshot error that a busy_timeout can't fix. begin_immediate() takes the
write lock up front.
Non-sqlx callers
HardenedSqlite::pragma_statements() returns the raw PRAGMA statements for use
with rusqlite or another driver.
Pairs with
tenant-scope— scope every query by tenant.sqlx::migrate!/migrate-guard— schema + migration-collision safety.
License
Licensed under either of MIT or Apache-2.0 at your option.