Expand description
Async rusqlite wrapper using a dedicated OS thread and
a crossbeam_channel dispatch loop.
§Why this crate?
rusqlite is inherently synchronous — every call blocks the calling thread. Running
SQLite operations directly inside a Tokio task would block the async executor. The
standard solution is spawn_blocking, but it creates a new thread-pool thread per
call and incurs overhead on every dispatch.
This crate takes a different approach: a single OS thread is dedicated to the
database connection for its entire lifetime. Callers send closures over a
Sender<Message> (crossbeam unbounded channel) and receive results via
oneshot::Receiver<T>. Because the connection thread is long-lived, there is no
per-call thread-spawn cost, and rusqlite::Connection never crosses thread
boundaries.
§Differences from tokio-rusqlite
The tokio-rusqlite crate uses spawn_blocking
(one thread per call from the pool). This crate permanently binds one std::thread
to one rusqlite::Connection, which is cheaper for workloads with many short
sequential calls (no thread-pool churn) and guarantees that SQLite’s
PRAGMA/BEGIN/COMMIT session state is never observed on a different thread.
§Threading model
async caller
│ conn.call(|c| { … })
│
▼
Sender<Message> ──crossbeam channel──► background OS thread
│ rusqlite::Connection
│ executes closure
▼
oneshot::Receiver<R> ◄── tokio oneshot ── resultThe Connection handle is Clone: all clones share the same channel and the
same underlying SQLite connection, so they are all serialized through the single
background thread. SQLite’s WAL mode is recommended when multiple handles are used
concurrently from different clones.
§Error handling
All fallible operations return Error. Rusqlite errors are wrapped in
Error::Rusqlite; callers can also return arbitrary errors via Error::Other.
§Example
// tokio::runtime::Runtime is available because the workspace tokio dep includes `rt`.
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let conn = rhei_tokio_rusqlite::Connection::open_in_memory().await
.expect("open in-memory db");
// DDL + DML in one closure — the connection is not re-entrant, so keep
// transactions inside a single call() to avoid deadlocks.
conn.call(|c| {
c.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)", [])?;
c.execute("INSERT INTO users (name) VALUES (?1)", ["Alice"])?;
c.execute("INSERT INTO users (name) VALUES (?1)", ["Bob"])?;
Ok(())
}).await.expect("setup");
let count: i64 = conn.call(|c| {
c.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))
.map_err(Into::into)
}).await.expect("count");
assert_eq!(count, 2);
conn.close().await.expect("close");
});Structs§
- Connection
- An async handle to a SQLite connection that runs on a dedicated background thread.
Enums§
- Error
- Errors returned by the async
Connectionwrapper.