Turso
An Elixir library that wraps the turso Rust
crate (v0.8.2) via Rustler NIFs, exposed
through a DBConnection pool.
It supports local file databases (and ":memory:"), Turso Cloud sync
via embedded replicas, and turso's built-in vector search and full-text
search SQL features, with correct ResourceArc lifetime management and a
working connection pool. Ecto support is optional via Ecto.Adapters.Turso.
Native binaries
Version 0.5 builds the NIF from source while its renamed Turso.* API is
prepared for a new set of precompiled artifacts. A working Rust toolchain
(cargo) matching the BEAM architecture is therefore required.
Future releases may provide precompiled binaries for:
| OS | Architectures |
|---|---|
| Linux | aarch64, x86_64 |
| macOS | aarch64, x86_64 |
| FreeBSD | x86_64 |
| Windows | x86_64 |
Releases also publish static NIF archives for static BEAM builds:
| Target | Archive |
|---|---|
| Linux amd64 musl | ex_turso-vVERSION-static-x86_64-unknown-linux-musl.tar.gz |
| Linux arm64 musl | ex_turso-vVERSION-static-aarch64-unknown-linux-musl.tar.gz |
Each archive contains libex_turso.a, which exports ex_turso_nif_init for
OTP's ex_turso static NIF library name. Use it when configuring OTP:
./configure --enable-static-nifs=/path/to/libex_turso.a:ex_turso
To build the archive from source instead:
TARGET=x86_64-unknown-linux-musl # or aarch64-unknown-linux-musl
rustup target add "$TARGET"
cargo build --manifest-path native/ex_turso/Cargo.toml \
--target "$TARGET" \
--release \
--locked
nm -g --defined-only "native/ex_turso/target/$TARGET/release/libex_turso.a" \
| grep ' ex_turso_nif_init$'
Use cross build instead of cargo build when building the arm64 musl archive
from a non-arm64 host.
Installation
def deps do
[
{:ex_turso, "~> 3.0"}
]
end
Usage
Start a pool under your supervision tree:
children = [
{Turso, database: "my_app.db", name: MyApp.DB}
]
Supervisor.start_link(children, strategy: :one_for_one)
Then query and execute against the registered name:
{:ok, _} = Turso.execute(MyApp.DB, "CREATE TABLE users (id INTEGER, name TEXT)")
{:ok, _} = Turso.execute(MyApp.DB, "INSERT INTO users VALUES (?, ?)", [1, "Alice"])
{:ok, %Turso.Result{rows: [%{"name" => "Alice"}]}} =
Turso.query(MyApp.DB, "SELECT name FROM users WHERE id = ?", [1])
Transactions go through DBConnection:
DBConnection.transaction(MyApp.DB, fn conn ->
{:ok, _} = Turso.execute(conn, "UPDATE users SET name = ? WHERE id = ?", ["Bob", 1])
end)
Use database: ":memory:" for an in-memory database (one per pool connection).
Always pass values as bound parameters (?) rather than interpolating them
into the SQL string — statements are logged when a query errors.
Ecto
Turso includes an optional SQL adapter for Ecto. Add ecto_sql in the
application that uses Ecto:
def deps do
[
{:ex_turso, "~> 3.0"},
{:ecto_sql, "~> 3.14"}
]
end
Define your repo with Ecto.Adapters.Turso:
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Turso
end
Configure it with the same database options used by Turso:
config :my_app, MyApp.Repo,
database: "my_app.db",
pool_size: 5
The adapter supports regular Ecto.Repo schema/query operations and
ecto_sql migrations using Turso's SQLite-compatible SQL dialect. Streaming
and multi-result queries are not supported by the current native connection.
Large CHECK expressions
Native SQL execution uses a dedicated stack so large, balanced CHECK
expressions can run with the default BEAM dirty IO scheduler stack settings.
No larger +sssdio setting is required.
Turso 0.8.2 still enforces a maximum expression tree depth of 100. Schema authors must group long chains of predicates into balanced expressions to stay within that limit. ExTurso does not rewrite application SQL or lift the parser limit.
Rebuilding a table for unsupported column changes
Turso does not safely support every ALTER COLUMN operation. For changes that
require replacing a table, use
Ecto.Adapters.Turso.Migration.rebuild_table!/3 from explicit up/0 and
down/0 migrations. The helper requires the complete target table definition
and an explicit identifier-only copy mapping.
The :create callback receives an already quoted temporary table identifier:
defmodule MyApp.Repo.Migrations.AllowNullableOwnerId do
use Ecto.Migration
@disable_ddl_transaction true
def up do
execute(fn -> rebuild(null: true) end)
end
def down do
execute(fn -> rebuild(null: false) end)
end
defp rebuild(opts) do
Ecto.Adapters.Turso.Migration.rebuild_table!(
repo(),
:github_import_runs,
create: fn temporary_table -> create_sql(temporary_table, opts) end,
copy: [:id, :source_owner_github_id, :source_repository_id],
recreate: [:indexes, :triggers]
)
end
defp create_sql(temporary_table, opts) do
owner_nullability = if opts[:null], do: "", else: " NOT NULL"
"""
CREATE TABLE #{temporary_table} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_owner_github_id INTEGER#{owner_nullability},
source_repository_id INTEGER,
CONSTRAINT github_import_runs_owner_check
CHECK (source_owner_github_id IS NOT NULL OR source_repository_id IS NOT NULL)
)
"""
end
end
The helper pins one connection, temporarily disables foreign-key enforcement,
creates and copies into the replacement table inside a transaction, restores
explicit indexes, triggers, and the AUTOINCREMENT sequence, runs foreign-key
and integrity checks, and restores the connection's original foreign-key
setting on every exit path. It rejects existing transactions, qualified table
names, generated or hidden columns, and copy mappings that do not preserve the
primary key. :recreate defaults to both indexes and triggers; an optional
:validate callback receives the checked-out transaction connection for
additional application checks. Built-in foreign-key and integrity checks
cannot be disabled. Application-specific backfills must be performed
separately.
Full-text search
Turso enables Turso's embedded full-text search index support for local databases. Use Turso's FTS index syntax:
{:ok, _} = Turso.execute(MyApp.DB, "CREATE TABLE docs (id INTEGER PRIMARY KEY, content TEXT)")
{:ok, _} = Turso.execute(MyApp.DB, "CREATE INDEX docs_fts ON docs USING fts (content)")
{:ok, %Turso.Result{rows: rows}} =
Turso.query(MyApp.DB, "SELECT id FROM docs WHERE (content) MATCH ?", ["search term"])
SQLite's FTS5 virtual table syntax, such as
CREATE VIRTUAL TABLE docs_fts USING fts5(content), is not exposed by the
embedded turso crate v0.8.2 API. Use CREATE INDEX ... USING fts with MATCH
queries instead.
Turso 0.8.2 uses a new FTS storage format. FTS indexes created by older versions such as 0.7.2 must be rebuilt explicitly from their base tables; they are not migrated automatically. Recreate each affected index with its original columns and options:
{:ok, _} = Turso.execute(MyApp.DB, "DROP INDEX docs_fts")
{:ok, _} = Turso.execute(MyApp.DB, "CREATE INDEX docs_fts ON docs USING fts (content)")
The base table's rows are preserved. Rebuilding enables MATCH queries and writes that maintain the index with the new storage format.
Turso Cloud sync
Pass :remote_url and :auth_token to open the local file as an embedded
replica of a Turso Cloud database (supports turso://, libsql://, and https:// schemes):
children = [
{Turso,
database: "replica.db",
remote_url: "turso://my-db.turso.io",
auth_token: fn -> System.fetch_env!("TURSO_AUTH_TOKEN") end,
name: MyApp.DB}
]
auth_token accepts a string or a zero-arity function; prefer the function so
the token does not appear in supervisor child specs and crash reports.
Trigger a bidirectional sync (pull then push) with:
:ok = Turso.sync(MyApp.DB)
Sync is rejected inside a transaction and on databases not configured with
:remote_url/:auth_token.
Errors
Failures return {:error, %Turso.Error{message: message, code: code}}. The
code classifies the failure: :busy (locked, retryable), :constraint,
:invalid_param (unsupported bound parameter type), :misuse, :error, or
:io/:corrupt — the last two mark the connection as broken, so the pool
drops it and opens a fresh one.
Architecture
| Layer | Module / file | Role |
|---|---|---|
| Native | native/ex_turso/src/lib.rs |
Rustler NIFs over turso, driven by a global Tokio runtime |
| NIF decls | Turso.Native |
Loads the compiled NIF |
| Pooling | Turso.Connection |
DBConnection behaviour implementation |
| Query | Turso.Query |
Statement struct + DBConnection.Query protocol |
| Ecto | Ecto.Adapters.Turso |
Optional ecto_sql adapter |
| Public API | Turso |
start_link/1, child_spec/1, query/3, execute/3 |