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
- Gleam >= 1.3.0
- Erlang/OTP >= 25
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 buildOr 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 # macOSRunning tests
make testUsage
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 databaselibsql.open_remote(url, token)– open a remote libSQL databaselibsql.open_replica(path, url, token)– open an embedded replicalibsql.open_synced_database(path, url, token)– open a synced database (offline writes)libsql.close(connection)– close a connectionlibsql.with_connection(path, fn)– open local, run function, auto-closelibsql.with_remote_connection(url, token, fn)– open remote, run function, auto-closelibsql.with_replica_connection(path, url, token, fn)– open replica, run function, auto-closelibsql.with_synced_database(path, url, token, fn)– open synced, run function, auto-closelibsql.sync(connection)– sync replica or synced db with remotelibsql.replication_index(connection)– current replication indexlibsql.begin(on: connection)– start a transactionlibsql.commit(on: connection)– commit a transactionlibsql.rollback(on: connection)– rollback a transactionlibsql.transaction(on: connection, run: fn)– run a function inside a transactionlibsql.exec(sql, on: connection)– execute SQL without returning rowslibsql.exec_batch(sql, on:, with:)– execute a statement multiple times with different paramslibsql.prepare(sql, on: connection)– compile a prepared statementlibsql.exec_prepared(on: statement, with:)– execute a prepared statementlibsql.query_prepared(on: statement, with:, expecting:)– query via prepared statementlibsql.finalize(statement)– release a prepared statementlibsql.with_statement(sql, on:, run:)– prepare, run function, auto-finalizelibsql.query(sql, on:, with:, expecting:)– execute SQL with positional paramslibsql.query_named(sql, on:, with:, expecting:)– execute SQL with named paramslibsql.query_first(sql, on:, with:, expecting:)– return first row orNonelibsql.query_one(sql, on:, with:, expecting:)– expect exactly one rowlibsql.last_insert_rowid(connection)– last auto-generated rowidlibsql.changes(connection)– rows affected by last statementlibsql.total_changes(connection)– total changes since db openedlibsql.interrupt(connection)– cancel a long-running querylibsql.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
-
Remote
libsql://connections -
Named parameters (
:name,@name,$name) - Transactions
- Batch execution
- Prepared statement caching
- Embedded replica sync
- Synced database (offline writes + push sync)
- Precompiled NIF binaries (no local Rust needed for supported platforms)
License
Apache-2.0