Skip to main content

Crate rhei_tokio_rusqlite

Crate rhei_tokio_rusqlite 

Source
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 ──  result

The 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 Connection wrapper.