diesel-libsql
Community project -- not affiliated with or maintained by the Diesel or Turso/libSQL teams.
A Diesel ORM backend for libSQL -- Turso's SQLite-compatible database.
Use Diesel's typed query builder, migrations, and connection management against local SQLite files, remote Turso databases, and embedded replicas. Supports both sync and native async, with OpenTelemetry instrumentation and connection pooling built in.
Why diesel-libsql?
Diesel's built-in SQLite backend uses the C SQLite API directly. That works for local files, but libSQL extends SQLite in ways the C API can't reach:
| diesel-sqlite | diesel-libsql | |
|---|---|---|
Local file / :memory: |
Yes | Yes |
| Remote Turso (HTTP) | No | Yes |
| Embedded replicas | No | Yes |
ALTER TABLE ALTER COLUMN |
No | Yes |
| Native async | No | Yes |
| Encryption at rest | No | Yes |
| OpenTelemetry spans | Manual | Built-in |
Installation
[]
= "0.1"
= { = "2.3", = ["sqlite"] }
Pick the features you need:
# Async connection (native, not spawn_blocking)
= { = "0.1", = ["async"] }
# Async + deadpool connection pool
= { = "0.1", = ["deadpool"] }
# Async + bb8 connection pool
= { = "0.1", = ["bb8"] }
# Sync connection pool
= { = "0.1", = ["r2d2"] }
# OpenTelemetry instrumentation
= { = "0.1", = ["otel"] }
# Encryption at rest (requires cmake)
= { = "0.1", = ["encryption"] }
Quick start
Local
use *;
use LibSqlConnection;
let mut conn = establish?;
sql_query
.execute?;
Remote Turso
use *;
use LibSqlConnection;
// Token in URL
let mut conn = establish?;
// Or set LIBSQL_AUTH_TOKEN env var and omit from URL
let mut conn = establish?;
Async
use ;
use AsyncLibSqlConnection;
let mut conn = establish.await?;
sql_query
.execute
.await?;
The async connection talks directly to libsql's native async API -- no spawn_blocking wrapper.
Connection URLs
| Format | Mode |
|---|---|
:memory: |
In-memory database |
/path/to/db.sqlite |
Local file |
libsql://host?authToken=TOKEN |
Remote Turso |
http://127.0.0.1:8081 |
Local Turso dev server (turso dev) |
For remote URLs, the auth token can be in the URL (?authToken=...) or in the LIBSQL_AUTH_TOKEN environment variable.
Embedded replicas
Read locally, write to a remote primary. Microsecond reads with eventual consistency.
use LibSqlConnection;
// Simple
let mut conn = establish_replica?;
// With configuration
use ReplicaBuilder;
use Duration;
let mut conn = new
.sync_interval // auto-sync every 5 minutes
.read_your_writes // see your own writes immediately
.establish?;
// Manual sync
conn.sync?;
ALTER TABLE ALTER COLUMN
libSQL lets you change column types and constraints after table creation -- something standard SQLite can't do.
conn.alter_column?;
This generates ALTER TABLE users ALTER COLUMN name TO name TEXT NOT NULL DEFAULT 'unknown'.
Note: changes only apply to new inserts and updates. Existing rows are not retroactively modified.
Transaction modes
Standard transaction() uses BEGIN DEFERRED. For write-heavy workloads, use explicit locking:
// Acquire a reserved lock immediately (prevents SQLITE_BUSY on write)
conn.immediate_transaction?;
// Acquire an exclusive lock (blocks all other connections)
conn.exclusive_transaction?;
Connection pooling
Sync (r2d2)
use LibSqlConnectionManager;
let manager = new;
let pool = builder.max_size.build?;
let mut conn = pool.get?;
Async (deadpool)
use ;
let pool = builder
.max_size
.build?;
let mut conn = pool.get.await?;
Async (bb8)
use ;
let pool = builder
.max_size
.build
.await?;
let mut conn = pool.get.await?;
Pooling is most valuable for remote Turso connections (reuses HTTP sessions, avoids repeated TLS handshakes) and embedded replicas (concurrent read access). For local-only file databases, a single connection is often sufficient.
Migrations
Diesel migrations work out of the box. For local development, diesel_cli works directly since libSQL database files are SQLite-compatible:
For remote Turso or when using libSQL-specific SQL (like ALTER COLUMN), use programmatic migrations:
use ;
const MIGRATIONS: EmbeddedMigrations = embed_migrations!;
let mut conn = establish?;
conn.run_pending_migrations?;
This is also the recommended pattern for production deployments -- migrations are compiled into your binary.
Production deployment strategies
For remote Turso databases, you have flexibility in when and where migrations run:
Run at app startup (simplest):
// In main(), before serving traffic
conn.run_pending_migrations?;
Run as an init container (recommended for Kubernetes):
# Migrations run once before the app container starts.
# In a multi-replica Deployment, each pod's init container runs
# independently — run_pending_migrations is idempotent, so this is safe.
spec:
initContainers:
- name: migrate
image: ghcr.io/your-org/your-app:latest
command:
env:
- name: LIBSQL_URL
value: "libsql://your-db.turso.io"
- name: LIBSQL_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: turso-creds
key: token
containers:
- name: app
image: ghcr.io/your-org/your-app:latest
# ...
Init containers guarantee migrations complete before your app serves traffic. For large or destructive migrations, you can also run them as a standalone k8s Job before triggering the Deployment rollout.
Encryption at rest
Requires the encryption feature (and cmake at build time):
let mut conn = establish_encrypted?;
Uses AES-256-CBC with per-page encryption and HMAC-SHA512 authentication.
OpenTelemetry
Attach OtelInstrumentation to emit spans for every query, connection, and transaction:
use ;
let mut conn = establish?;
// Query text on by default (parameterized SQL only, no bind values)
conn.set_instrumentation;
// Disable query text if you don't want table/column names in traces
conn.set_instrumentation;
Spans follow OTel database semantic conventions:
| Attribute | Default | Notes |
|---|---|---|
db.system = "sqlite" |
Always | |
db.operation.name |
Always | SELECT, INSERT, etc. |
db.query.text |
On | Parameterized SQL only (WHERE name = ?). Disable with with_query_text(false). |
server.address |
Always | Auth tokens automatically redacted |
error.type |
On failure |
Security: db.query.text contains only parameterized SQL — bind parameter values are never included, only ? placeholders. This is safe by default. Disable with with_query_text(false) if you don't want table/column names in traces. Connection URLs are automatically redacted to strip auth tokens. Works with both sync and async connections.
Feature flags
| Flag | Description | Dependencies |
|---|---|---|
r2d2 |
Sync connection pooling | r2d2 |
async |
Native async connection | diesel-async, futures-util |
deadpool |
Async pool via deadpool (implies async) |
deadpool |
bb8 |
Async pool via bb8 (implies async) |
bb8 |
otel |
OpenTelemetry span instrumentation | opentelemetry |
encryption |
AES-256 encryption at rest | libsql/encryption (needs cmake) |
How it works
diesel-libsql defines a new LibSql backend type for Diesel. It reuses Diesel's SqliteType for type metadata and generates identical SQL (backtick quoting, ? bind params), but has its own value types (LibSqlValue, LibSqlBindCollector) that work with libsql's Rust API instead of the C SQLite API.
The async connection implements diesel_async::AsyncConnection natively -- queries go directly through libsql's async methods with no sync bridge or spawn_blocking.
Status
This is a community-maintained crate. It is not an official project of Diesel or Turso. Bug reports and contributions are welcome via GitHub issues.
Known issues
Two low-severity vulnerabilities exist in transitive dependencies of the libsql crate (not in diesel-libsql itself). Both require upstream fixes in libsql:
rustls-webpki< 0.103.10 — CRL matching logic bug. Blocked on libsql updating itsrustlsdependency.libsql-sqlite3-parser<= 0.13.0 — crash on invalid UTF-8. No patched version available yet.
These affect remote/replica connections only (local file mode does not use rustls).
License
MIT — see LICENSE.