libsql
Use libSQL from Gleam!
This is a Gleam library for libSQL with the same ergonomics as
sqlight, built on a Rust NIF that wraps
the official libsql crate.
Features
- In-memory (
:memory:) and local file databases
- Remote
libsql:// connections (Turso, etc.)
- Embedded replica sync (local read replicas with remote Turso primary)
- Synced database — write offline locally, sync to remote later
- Type-safe parameter binding — positional
? and named :name
- Batch execution (bulk inserts/updates in a single NIF roundtrip)
- Prepared statement caching (
prepare / exec_prepared / query_prepared)
- Decoder-based query results (like sqlight)
- Convenience helpers:
query_one, query_first, last_insert_rowid, changes
- Transactions (
BEGIN / COMMIT / ROLLBACK + combinator)
- Connection control:
interrupt, total_changes
- Full SQLite error code mapping
Requirements
No Rust toolchain is required for end-users — precompiled NIF binaries are
downloaded automatically for supported platforms (Linux x86_64, macOS x86_64,
macOS aarch64). For other platforms, Rust & Cargo are needed to build from
source.
Building from source
If a precompiled binary is not available for your platform, compile the NIF
locally:
make build
Or manually:
cd native/libsql_nif && cargo build --release
cp target/release/liblibsql_nif.so ../../priv/libsql_nif.so # Linux
# cp target/release/liblibsql_nif.dylib ../../priv/libsql_nif.dylib # macOS
Running tests
make test
Usage
import gleam/dynamic/decode
import libsql
pub fn main() {
use conn <- libsql.with_connection(":memory:")
let sql = "
create table cats (name text, age int);
insert into cats (name, age) values
('Nubi', 4),
('Biffy', 10),
('Ginny', 6);
"
let assert Ok(Nil) = libsql.exec(sql, conn)
let cat_decoder = {
use name <- decode.field(0, decode.string)
use age <- decode.field(1, decode.int)
decode.success(#(name, age))
}
let sql = "
select name, age from cats
where age < ?
"
let assert Ok([#("Nubi", 4), #("Ginny", 6)]) =
libsql.query(sql, on: conn, with: [libsql.int(7)], expecting: cat_decoder)
}
Remote connection
import gleam/dynamic/decode
import libsql
pub fn main() {
let url = "libsql://my-db.turso.io"
let token = "my-auth-token"
use conn <- libsql.with_remote_connection(url, token)
let assert Ok([#("hello", 42)]) =
libsql.query(
"select 'hello', 42",
on: conn,
with: [],
expecting: {
use a <- decode.field(0, decode.string)
use b <- decode.field(1, decode.int)
decode.success(#(a, b))
},
)
}
Transactions
pub fn insert_user(conn, name, age) {
libsql.transaction(conn, fn() {
use _ <- result.try(libsql.exec("insert into users (name) values (?)", conn))
use _ <- result.try(libsql.exec("insert into logs (action) values ('created')", conn))
Ok(Nil)
})
}
If the inner function returns Error(...), the transaction is automatically
rolled back.
Named parameters
libsql.query_named(
"select * from cats where name = :name and age > :min_age",
conn,
[
#$(":name", libsql.text("Nubi")),
#$(":min_age", libsql.int(2)),
],
expecting: cat_decoder,
)
Batch execution
libsql.exec_batch(
"insert into users (name, age) values (?, ?)",
conn,
[
[libsql.text("Alice"), libsql.int(30)],
[libsql.text("Bob"), libsql.int(25)],
[libsql.text("Carol"), libsql.int(35)],
],
)
Batch operations are typically wrapped in a transaction for atomicity:
libsql.transaction(conn, fn() {
libsql.exec_batch(
"insert into logs (action) values (?)",
conn,
actions |> list.map(fn(a) { [libsql.text(a)] }),
)
})
Convenience helpers
Single-row queries:
// Expect exactly one row — errors on 0 or multiple
let assert Ok(user) =
libsql.query_one(
"select * from users where id = ?",
conn,
[libsql.int(42)],
user_decoder,
)
// Get the first row (or None)
let assert Ok(option.Some(user)) =
libsql.query_first(
"select * from users where email = ?",
conn,
[libsql.text("alice@example.com")],
user_decoder,
)
Insert metadata:
let assert Ok(Nil) = libsql.exec("insert into users (name) values ('Alice')", conn)
let assert Ok(id) = libsql.last_insert_rowid(conn)
let assert Ok(Nil) = libsql.exec("update users set active = 1", conn)
let assert Ok(3) = libsql.changes(conn)
Prepared statements
libsql.with_statement("insert into users (name, age) values (?, ?)", conn, fn(stmt) {
let assert Ok(Nil) = libsql.exec_prepared(stmt, [libsql.text("Alice"), libsql.int(30)])
let assert Ok(Nil) = libsql.exec_prepared(stmt, [libsql.text("Bob"), libsql.int(25)])
})
Prepared statements avoid re-parsing SQL on each execution, giving a
significant performance boost for repeated queries.
Embedded replica
let db_path = "/tmp/my_replica.db"
let url = "libsql://my-db.turso.io"
let token = "my-auth-token"
let assert Ok(conn) = libsql.open_replica(db_path, url, token)
// Sync with remote primary
let assert Ok(replicated) = libsql.sync(conn)
// replicated.frame_no -> Option(Int)
// replicated.frames_synced -> Int
// Queries are served from the local replica
let assert Ok([#("hello", 42)]) =
libsql.query(
"select 'hello', 42",
conn,
[],
expecting: {
use a <- decode.field(0, decode.string)
use b <- decode.field(1, decode.int)
decode.success(#(a, b))
},
)
Synced database (offline writes)
Unlike a replica (which delegates writes to the remote), a synced database
keeps writes local and pushes them when you call sync():
let db_path = "/tmp/my_synced.db"
let url = "libsql://my-db.turso.io"
let token = "my-auth-token"
let assert Ok(conn) = libsql.open_synced_database(db_path, url, token)
// Pull remote state first (recommended)
let assert Ok(_) = libsql.sync(conn)
// Writes are local and work offline
let assert Ok(Nil) = libsql.exec("create table todos (id integer primary key, task text)", conn)
let assert Ok(Nil) = libsql.exec("insert into todos (task) values ('Buy milk')", conn)
// Read locally at full SQLite speed
let assert Ok(["Buy milk"]) =
libsql.query(
"select task from todos",
conn,
[],
decode.field(0, decode.string, decode.success),
)
// Push local changes to remote
let assert Ok(replicated) = libsql.sync(conn)
API
The API mirrors sqlight closely:
libsql.open(path) – open a local or in-memory database
libsql.open_remote(url, token) – open a remote libSQL database
libsql.open_replica(path, url, token) – open an embedded replica
libsql.open_synced_database(path, url, token) – open a synced database (offline writes)
libsql.close(connection) – close a connection
libsql.with_connection(path, fn) – open local, run function, auto-close
libsql.with_remote_connection(url, token, fn) – open remote, run function, auto-close
libsql.with_replica_connection(path, url, token, fn) – open replica, run function, auto-close
libsql.with_synced_database(path, url, token, fn) – open synced, run function, auto-close
libsql.sync(connection) – sync replica or synced db with remote
libsql.replication_index(connection) – current replication index
libsql.begin(on: connection) – start a transaction
libsql.commit(on: connection) – commit a transaction
libsql.rollback(on: connection) – rollback a transaction
libsql.transaction(on: connection, run: fn) – run a function inside a transaction
libsql.exec(sql, on: connection) – execute SQL without returning rows
libsql.exec_batch(sql, on:, with:) – execute a statement multiple times with different params
libsql.prepare(sql, on: connection) – compile a prepared statement
libsql.exec_prepared(on: statement, with:) – execute a prepared statement
libsql.query_prepared(on: statement, with:, expecting:) – query via prepared statement
libsql.finalize(statement) – release a prepared statement
libsql.with_statement(sql, on:, run:) – prepare, run function, auto-finalize
libsql.query(sql, on:, with:, expecting:) – execute SQL with positional params
libsql.query_named(sql, on:, with:, expecting:) – execute SQL with named params
libsql.query_first(sql, on:, with:, expecting:) – return first row or None
libsql.query_one(sql, on:, with:, expecting:) – expect exactly one row
libsql.last_insert_rowid(connection) – last auto-generated rowid
libsql.changes(connection) – rows affected by last statement
libsql.total_changes(connection) – total changes since db opened
libsql.interrupt(connection) – cancel a long-running query
libsql.int/1, libsql.float/1, libsql.text/1, libsql.blob/1,
libsql.bool/1, libsql.null/0, libsql.nullable/2 – value constructors
Architecture
┌─────────────────┐
│ Gleam API │ src/libsql.gleam
├─────────────────┤
│ Erlang FFI shim │ src/libsql_ffi.erl
├─────────────────┤
│ Rust NIF │ native/libsql_nif/src/lib.rs
├─────────────────┤
│ libsql crate │ (Rust) – wraps SQLite C + remote protocol
└─────────────────┘
Future work
License
Apache-2.0