Rust API Reference

Complete reference for the Stoolap Rust API (stoolap crate).

[dependencies]
stoolap = "0.4"

Database

The Database struct is the main entry point. It is thread-safe and can be shared across threads via cloning.

Opening a Database

use stoolap::Database;

// In-memory (unique instance per call)
let db = Database::open_in_memory()?;

// In-memory (shared instance, same DSN returns same engine)
let db = Database::open("memory://")?;

// File-based (persistent)
let db = Database::open("file:///path/to/database")?;

// File-based with configuration
let db = Database::open("file:///path/to/db?sync=full&checkpoint_interval=60")?;

open_in_memory() creates a unique, isolated instance each time. open("memory://") returns the same shared engine for the same DSN.

Connection String Options

Parameter Default Description
sync / sync_mode normal Sync mode: none, normal, full (or 0, 1, 2)
checkpoint_interval 60 Seconds between automatic checkpoint cycles
compact_threshold 4 Sub-target volumes per table before merging
keep_snapshots 3 Backup snapshots to retain per table
wal_flush_trigger 32768 WAL flush trigger size in bytes
wal_buffer_size 65536 WAL buffer size in bytes
wal_max_size 67108864 Max WAL file size before rotation (64 MB)
commit_batch_size 100 Commits to batch before syncing (normal mode)
sync_interval_ms 1000 Minimum ms between syncs (normal mode)
wal_compression on LZ4 compression for WAL entries
compression Alias that sets both wal_compression and volume_compression
compression_threshold 64 Minimum bytes before compressing an entry
volume_compression on LZ4 compression for cold volume files
checkpoint_on_close on Seal all hot rows to volumes on clean shutdown
target_volume_rows 1048576 Target rows per cold volume. Controls compaction split boundary.

execute()

Execute DDL or DML statements. Returns the number of rows affected.

use stoolap::params;

// DDL, no parameters
db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)", ())?;

// DML with tuple parameters
db.execute("INSERT INTO users VALUES ($1, $2, $3)", (1, "Alice", 30))?;

// DML with params! macro
db.execute("INSERT INTO users VALUES ($1, $2, $3)", params![2, "Bob", 25])?;

// Returns rows affected
let affected = db.execute("UPDATE users SET name = $1 WHERE id = $2", ("Charlie", 1))?;

query()

Execute a SELECT query and iterate over results.

// Iterate rows
for row in db.query("SELECT * FROM users", ())? {
    let row = row?;
    let id: i64 = row.get(0)?;
    let name: String = row.get_by_name("name")?;
}

// With parameters
for row in db.query("SELECT * FROM users WHERE age > $1", (18,))? {
    let row = row?;
    // ...
}

// Collect into Vec
let users: Vec<_> = db.query("SELECT * FROM users", ())?
    .collect::<Result<Vec<_>, _>>()?;

query_one()

Return a single value. Errors if no rows are returned.

let count: i64 = db.query_one("SELECT COUNT(*) FROM users", ())?;
let name: String = db.query_one("SELECT name FROM users WHERE id = $1", (1,))?;

query_opt()

Return an optional single value. Returns None if no rows match.

let name: Option<String> = db.query_opt("SELECT name FROM users WHERE id = $1", (999,))?;
match name {
    Some(n) => println!("Found: {}", n),
    None => println!("Not found"),
}

Named Parameters

Use :name syntax with named_params! macro or the NamedParams builder.

use stoolap::named_params;

// Execute with named params
db.execute_named(
    "INSERT INTO users VALUES (:id, :name, :age)",
    named_params!{ id: 1, name: "Alice", age: 30 }
)?;

// Query with named params
for row in db.query_named(
    "SELECT * FROM users WHERE age > :min_age",
    named_params!{ min_age: 25 }
)? {
    // ...
}

// Query single value with named params
let count: i64 = db.query_one_named(
    "SELECT COUNT(*) FROM users WHERE age > :min_age",
    named_params!{ min_age: 18 }
)?;

// Builder API (alternative to macro)
use stoolap::NamedParams;
let params = NamedParams::new()
    .add("id", 1)
    .add("name", "Alice")
    .add("age", 30);
db.execute_named("INSERT INTO users VALUES (:id, :name, :age)", params)?;

Timeout Methods

Cancel queries that exceed a time limit. Timeout is in milliseconds; use 0 for no timeout.

// Execute with 5 second timeout
db.execute_with_timeout("DELETE FROM large_table WHERE old = true", (), 5000)?;

// Query with 10 second timeout
for row in db.query_with_timeout("SELECT * FROM large_table", (), 10000)? {
    let row = row?;
    // ...
}

Struct Mapping with FromRow

Map query results directly to structs.

use stoolap::{Database, FromRow, ResultRow, Result};

struct User {
    id: i64,
    name: String,
    email: Option<String>,
}

impl FromRow for User {
    fn from_row(row: &ResultRow) -> Result<Self> {
        Ok(User {
            id: row.get(0)?,
            name: row.get(1)?,
            email: row.get(2)?,
        })
    }
}

// Query and map to structs
let users: Vec<User> = db.query_as("SELECT id, name, email FROM users", ())?;

// With named params
let users: Vec<User> = db.query_as_named(
    "SELECT id, name, email FROM users WHERE id = :id",
    named_params!{ id: 1 }
)?;

Cached Plans

Parse SQL once and execute many times with zero cache-lookup overhead.

// Create a cached plan (parse once)
let insert_plan = db.cached_plan("INSERT INTO users VALUES ($1, $2, $3)")?;

// Execute many times (no parsing, no cache lookup)
db.execute_plan(&insert_plan, (1, "Alice", 30))?;
db.execute_plan(&insert_plan, (2, "Bob", 25))?;

// Query with cached plan
let query_plan = db.cached_plan("SELECT * FROM users WHERE id = $1")?;
let rows = db.query_plan(&query_plan, (1,))?;

// Named params with cached plan
let plan = db.cached_plan("INSERT INTO users VALUES (:id, :name, :age)")?;
db.execute_named_plan(&plan, named_params!{ id: 3, name: "Charlie", age: 35 })?;
let rows = db.query_named_plan(
    &db.cached_plan("SELECT * FROM users WHERE id = :id")?,
    named_params!{ id: 3 }
)?;

Utility Methods

// Close the database (releases file lock immediately)
db.close()?;

// Check if a table exists
if db.table_exists("users")? {
    // ...
}

// Get the DSN this database was opened with
let dsn = db.dsn();  // "memory://" or "file:///path"

// Set default isolation level for new transactions
use stoolap::IsolationLevel;
db.set_default_isolation_level(IsolationLevel::SnapshotIsolation)?;

// Create a point-in-time snapshot (for file-based databases)
db.create_snapshot()?;

// Semantic cache stats
let stats = db.semantic_cache_stats()?;
println!("Hits: {}, Subsumption: {}", stats.hits, stats.subsumption_hits);

// Clear the semantic cache
db.clear_semantic_cache()?;

Prepared Statements

Prepare a statement for repeated execution with different parameters. The SQL is validated at prepare time.

// Prepare once
let stmt = db.prepare("SELECT * FROM users WHERE id = $1")?;

// Execute multiple times
for id in 1..=10 {
    for row in stmt.query((id,))? {
        let row = row?;
        // ...
    }
}

// Single value
let name: String = stmt.query_one((1,))?;

// Optional value
let name: Option<String> = stmt.query_opt((999,))?;

// DML prepared statement
let insert = db.prepare("INSERT INTO users VALUES ($1, $2, $3)")?;
insert.execute((1, "Alice", 30))?;
insert.execute((2, "Bob", 25))?;

// Get the SQL text
assert_eq!(insert.sql(), "INSERT INTO users VALUES ($1, $2, $3)");

Statement holds a weak reference to the Database. It becomes invalid after the Database is dropped.

Transactions

Programmatic API

// Begin with default isolation (ReadCommitted)
let mut tx = db.begin()?;

// Begin with specific isolation level
use stoolap::IsolationLevel;
let mut tx = db.begin_with_isolation(IsolationLevel::SnapshotIsolation)?;

// Execute within transaction
tx.execute("INSERT INTO users VALUES ($1, $2, $3)", (1, "Alice", 30))?;
tx.execute("UPDATE accounts SET balance = balance - $1 WHERE id = $2", (100, 1))?;

// Query within transaction (sees uncommitted changes)
for row in tx.query("SELECT * FROM users", ())? {
    let row = row?;
    // ...
}

// Single value query
let count: i64 = tx.query_one("SELECT COUNT(*) FROM users", ())?;

// Optional value query
let name: Option<String> = tx.query_opt("SELECT name FROM users WHERE id = $1", (999,))?;

// Named parameters
tx.execute_named(
    "INSERT INTO users VALUES (:id, :name, :age)",
    named_params!{ id: 3, name: "Charlie", age: 35 }
)?;

for row in tx.query_named(
    "SELECT * FROM users WHERE age > :min_age",
    named_params!{ min_age: 25 }
)? {
    let row = row?;
    // ...
}

// Commit or rollback
tx.commit()?;
// Or: tx.rollback()?;

Transactions auto-rollback on drop if not committed.

Pre-parsed Statements in Transactions

For batch operations, parse SQL once and execute many times within a transaction.

// Prepare once (via Database)
let insert = db.prepare("INSERT INTO users VALUES ($1, $2, $3)")?;

let mut tx = db.begin()?;
for (id, name, age) in data {
    tx.execute_prepared(&insert, (id, name, age))?;
}
tx.commit()?;

Query with a prepared statement inside a transaction:

let lookup = db.prepare("SELECT * FROM users WHERE id = $1")?;

let mut tx = db.begin()?;
// Reads within the transaction see uncommitted changes
let rows = tx.query_prepared(&lookup, (42,))?;
tx.commit()?;

SQL-based Transactions

db.execute("BEGIN", ())?;
db.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
db.execute("COMMIT", ())?;
// Or: db.execute("ROLLBACK", ())?;

Savepoints

db.execute("BEGIN", ())?;
db.execute("INSERT INTO users VALUES ($1, $2)", (1, "Alice"))?;
db.execute("SAVEPOINT sp1", ())?;
db.execute("INSERT INTO users VALUES ($1, $2)", (2, "Bob"))?;
db.execute("ROLLBACK TO SAVEPOINT sp1", ())?;  // Undo Bob
db.execute("RELEASE SAVEPOINT sp1", ())?;       // Release savepoint
db.execute("COMMIT", ())?;  // Only Alice is inserted

Isolation Levels

// Programmatic
let mut tx = db.begin_with_isolation(IsolationLevel::SnapshotIsolation)?;

// SQL-based
db.execute("BEGIN TRANSACTION ISOLATION LEVEL SNAPSHOT", ())?;

// Set default for all new transactions
db.set_default_isolation_level(IsolationLevel::SnapshotIsolation)?;

The Rust API has two enum variants: ReadCommitted (default) and SnapshotIsolation. At the SQL level, SNAPSHOT, SERIALIZABLE, REPEATABLE READ all map to SnapshotIsolation, and READ UNCOMMITTED maps to ReadCommitted.

Working with Rows

Rows Iterator

query() returns a Rows struct that implements Iterator<Item = Result<ResultRow>>.

let mut rows = db.query("SELECT * FROM users", ())?;

// Column metadata
let columns: &[String] = rows.columns();
let count: usize = rows.column_count();

// Iterate
for row in rows {
    let row = row?;
    // ...
}

// Or collect
let all: Vec<ResultRow> = db.query("SELECT * FROM users", ())?.collect_vec()?;

Zero-Clone Cursor

For bulk serialization, advance() / current_row() avoids per-row cloning.

let mut rows = db.query("SELECT * FROM users", ())?;
while rows.advance() {
    let row: &Row = rows.current_row();
    // Access row by reference, no clone
    if let Some(value) = row.get(0) {
        // ...
    }
}

ResultRow Accessors

let row = db.query("SELECT id, name, active FROM users", ())?
    .next().unwrap()?;

// By index (0-based)
let id: i64 = row.get(0)?;

// By column name (case-insensitive)
let name: String = row.get_by_name("name")?;

// Optional values (for nullable columns)
let active: Option<bool> = row.get_by_name("active")?;

// Raw Value access
let value: Option<&Value> = row.get_value(0);

// NULL check
if row.is_null(2) {
    println!("active is NULL");
}

// Metadata
let columns: &[String] = row.columns();
let len: usize = row.len();
let empty: bool = row.is_empty();

// Get underlying Row
let inner: &Row = row.as_row();
let owned: Row = row.into_inner();

Type Conversions (FromValue, reading)

Types you can use with row.get::<T>():

SQL Type Rust Type
INTEGER i64, i32, f64, bool, String, Value
FLOAT f64, i64, i32, String, Value
TEXT String, Value
BOOLEAN bool, Value
TIMESTAMP String (ISO format), Value
JSON String, Value
NULL Option<T> for any supported T

Parameter Types (ToParam, writing)

Types you can pass as SQL parameters:

Rust Type SQL Value
i8, i16, i32, i64 INTEGER
u8, u16, u32, usize INTEGER
f32, f64 FLOAT
bool BOOLEAN
&str, String, Arc<str> TEXT
DateTime<Utc> TIMESTAMP
Value as-is
Option<T> T or NULL

Tuple parameters support up to 12 elements. For more, use params![] macro or Vec<Value>.

Error Handling

use stoolap::{Database, Error};

match db.execute("INSERT INTO users VALUES (1, 'Alice')", ()) {
    Ok(affected) => println!("Inserted {} rows", affected),
    Err(e) => {
        let msg = e.to_string();
        if msg.contains("UNIQUE constraint") {
            println!("Duplicate key");
        } else if msg.contains("write-write conflict") {
            println!("Transaction conflict, retry");
        } else {
            return Err(e.into());
        }
    }
}

Thread Safety

Database is Send + Sync. Clone to share across threads. Each clone has its own executor with independent transaction state but shares the same storage engine.

use std::sync::Arc;
use std::thread;

let db = Arc::new(Database::open("memory://")?);
db.execute("CREATE TABLE counter (id INTEGER PRIMARY KEY, value INTEGER)", ())?;
db.execute("INSERT INTO counter VALUES (1, 0)", ())?;

let handles: Vec<_> = (0..4).map(|_| {
    let db = Arc::clone(&db);
    thread::spawn(move || {
        for _ in 0..100 {
            db.execute("UPDATE counter SET value = value + 1 WHERE id = 1", ()).unwrap();
        }
    })
}).collect();

for h in handles {
    h.join().unwrap();
}

let count: i64 = db.query_one("SELECT value FROM counter WHERE id = 1", ())?;

Complete Example

use stoolap::{Database, FromRow, ResultRow, Result, named_params, params};

struct User {
    id: i64,
    name: String,
    email: Option<String>,
}

impl FromRow for User {
    fn from_row(row: &ResultRow) -> Result<Self> {
        Ok(User {
            id: row.get(0)?,
            name: row.get(1)?,
            email: row.get(2)?,
        })
    }
}

fn main() -> Result<()> {
    let db = Database::open("file:///tmp/myapp.db")?;

    // Schema
    db.execute("
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTO_INCREMENT,
            name TEXT NOT NULL,
            email TEXT,
            created_at TIMESTAMP DEFAULT NOW()
        )
    ", ())?;

    db.execute("CREATE INDEX IF NOT EXISTS idx_email ON users(email)", ())?;

    // Batch insert with cached plan
    let plan = db.cached_plan("INSERT INTO users (name, email) VALUES ($1, $2)")?;
    db.execute_plan(&plan, ("Alice", "alice@example.com"))?;
    db.execute_plan(&plan, ("Bob", "bob@example.com"))?;

    // Query with struct mapping
    let users: Vec<User> = db.query_as("SELECT id, name, email FROM users", ())?;
    for user in &users {
        println!("{}: {} ({:?})", user.id, user.name, user.email);
    }

    // Aggregation
    let count: i64 = db.query_one("SELECT COUNT(*) FROM users", ())?;
    println!("Total users: {}", count);

    // Window function
    for row in db.query("
        SELECT name, ROW_NUMBER() OVER (ORDER BY created_at) as row_num
        FROM users
    ", ())? {
        let row = row?;
        let name: String = row.get_by_name("name")?;
        let num: i64 = row.get_by_name("row_num")?;
        println!("{}: {}", num, name);
    }

    db.close()?;
    Ok(())
}

Method Reference

Database

Method Returns Description
open(dsn) Result<Database> Open or reuse a database by DSN
open_in_memory() Result<Database> Open a unique in-memory database
execute(sql, params) Result<i64> Execute DDL/DML, return rows affected
query(sql, params) Result<Rows> Execute SELECT, return row iterator
query_one(sql, params) Result<T> Query single value
query_opt(sql, params) Result<Option<T>> Query optional value
execute_named(sql, params) Result<i64> Execute with named params
query_named(sql, params) Result<Rows> Query with named params
query_one_named(sql, params) Result<T> Single value with named params
query_as(sql, params) Result<Vec<T>> Query and map to structs
query_as_named(sql, params) Result<Vec<T>> Map to structs with named params
execute_with_timeout(sql, params, ms) Result<i64> Execute with timeout
query_with_timeout(sql, params, ms) Result<Rows> Query with timeout
cached_plan(sql) Result<CachedPlanRef> Create a cached execution plan
execute_plan(plan, params) Result<i64> Execute cached plan
query_plan(plan, params) Result<Rows> Query with cached plan
execute_named_plan(plan, params) Result<i64> Execute cached plan with named params
query_named_plan(plan, params) Result<Rows> Query cached plan with named params
prepare(sql) Result<Statement> Create a prepared statement
begin() Result<Transaction> Begin transaction (ReadCommitted)
begin_with_isolation(level) Result<Transaction> Begin with isolation level
close() Result<()> Close database, release file lock
table_exists(name) Result<bool> Check if table exists
dsn() &str Get the DSN
set_default_isolation_level(level) Result<()> Set default isolation
create_snapshot() Result<()> Create point-in-time snapshot
semantic_cache_stats() Result<Stats> Get cache statistics
clear_semantic_cache() Result<()> Clear query cache

Statement

Method Returns Description
execute(params) Result<i64> Execute DML
query(params) Result<Rows> Query rows
query_one(params) Result<T> Single value
query_opt(params) Result<Option<T>> Optional value
sql() &str Get the SQL text

Transaction

Method Returns Description
execute(sql, params) Result<i64> Execute DML
query(sql, params) Result<Rows> Query rows
query_one(sql, params) Result<T> Single value
query_opt(sql, params) Result<Option<T>> Optional value
execute_named(sql, params) Result<i64> Execute with named params
query_named(sql, params) Result<Rows> Query with named params
execute_prepared(stmt, params) Result<i64> Execute pre-parsed statement
query_prepared(stmt, params) Result<Rows> Query with pre-parsed statement
commit() Result<()> Commit
rollback() Result<()> Rollback
id() i64 Transaction ID

Rows

Method Returns Description
next() Option<Result<ResultRow>> Iterator next
advance() bool Zero-clone cursor advance
current_row() &Row Current row reference
columns() &[String] Column names
column_count() usize Number of columns
rows_affected() i64 DML rows affected
collect_vec() Result<Vec<ResultRow>> Collect all rows
close() () Close result set

ResultRow

Method Returns Description
get(index) Result<T> Value by column index
get_by_name(name) Result<T> Value by column name
get_value(index) Option<&Value> Raw Value reference
is_null(index) bool Check if NULL
columns() &[String] Column names
len() usize Number of columns
is_empty() bool Empty check
as_row() &Row Reference to inner Row
into_inner() Row Consume into Row