# 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)
| 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.