Skip to content

Rust — rusqlite

rusqlite loads extensions when built with the load_extension feature.

Cargo.toml

toml
[dependencies]
rusqlite = { version = "0.32", features = ["load_extension"] }
# add "bundled" to have rusqlite compile its own SQLite

Load and set up

rust
use rusqlite::{Connection, LoadExtensionGuard};

fn main() -> rusqlite::Result<()> {
    let conn = Connection::open("app.db")?;

    // LoadExtensionGuard enables the loader and re-disables it on drop.
    unsafe {
        let _guard = LoadExtensionGuard::new(&conn)?;
        conn.load_extension("dist/rls.so", None)?; // None => entry sqlite3_rls_init
    }

    conn.execute_batch(
        "CREATE TABLE documents(id INTEGER PRIMARY KEY, owner_id INT NOT NULL, title TEXT);
         INSERT INTO documents(owner_id, title) VALUES (1,'Alice'),(2,'Bob');",
    )?;

    conn.query_row(
        "SELECT rls_protect(?1)",
        [r#"{"table":"documents","select":"owner_id = rls_ctx('user_id')"}"#],
        |r| r.get::<_, String>(0),
    )?;
    conn.query_row("SELECT rls_seal('CHANGE_ME')", [], |r| r.get::<_, i64>(0))?;
    conn.query_row("SELECT rls_guard()", [], |r| r.get::<_, i64>(0))?;
    Ok(())
}

Serve queries

rust
conn.query_row("SELECT rls_set_context(?1)", [r#"{"user_id":1}"#], |r| r.get::<_, i64>(0))?;

let mut stmt = conn.prepare("SELECT id, title FROM documents")?;
let rows: Vec<(i64, String)> = stmt
    .query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?
    .collect::<rusqlite::Result<_>>()?;   // [(1, "Alice")]

Important: don't set a second authorizer

One authorizer per connection

SQLite allows only one authorizer per connection, and this extension installs its own to make rls_guard() work. Calling conn.authorizer(...) on the same connection displaces RLS enforcement. If you need custom authorization, express it in the policy expressions, or run it on a separate connection.

Connection pools (r2d2 / deadpool)

Do the load + rls_protect + rls_config + rls_seal + rls_guard in the pool's connection customizer / on_acquire, so every pooled connection is guarded before first use. Bind the context per checkout, per query:

rust
// inside your pool's customizer:
unsafe {
    let _g = LoadExtensionGuard::new(conn)?;
    conn.load_extension("dist/rls.so", None)?;
}
conn.query_row("SELECT rls_protect(?1)", [POLICY_JSON], |r| r.get::<_, String>(0))?;
conn.query_row("SELECT rls_seal(?1)", [seal_token], |r| r.get::<_, i64>(0))?;
conn.query_row("SELECT rls_guard()", [], |r| r.get::<_, i64>(0))?;

Notes

  • load_extension is unsafe because loading native code is inherently so; LoadExtensionGuard scopes the enable so application queries can't reload.
  • Pass claims as bound parameters (?1) as shown — never format them into the SQL string.
  • A denied direct base-table read surfaces as a rusqlite::Error wrapping SQLITE_AUTH; a write policy violation as a SQLITE_CONSTRAINT error from the trigger's RAISE(ABORT).

A loadable SQLite extension. No warranty; test against your own schema.