The libSQL Rust crate contains everything you need to work with Turso and works flawlessly with popular async runtimes like tokio.

Installing

Install the crate in your project using the following command:

cargo add libsql

Conditional compilation

The libsql rust client supports conditionally compiling certain features to reduce compile times depending on what features you would like to use.

The following features are available:

FeatureDescription
remoteEnables the HTTP-only client, allowing communication with a remote sqld server using pure Rust. Does not require compiling C code for SQLite. Suitable for projects that only need to interact with a remote database.
coreEnables the local database only, incorporating the C SQLite3 code into the build. This is the foundation for local database operations but does not include additional features like replication or encryption.
replicationCombines core with additional code required for replication, enabling the embedded replica features.
encryptionEnables encryption at rest support, adding the necessary code to compile encryption capabilities and expose functions for configuring it. This is optional and not enabled by default, catering to projects that require enhanced data security.
[dependencies]
libsql = { version = "...", features = ["encryption"] }

Using core and replication features require a c compiler. The encryption feature requires cmake to be installed on your system.

Initializing

Make sure add the crate to your project at the top of your file:

use libsql::Builder;

let url = env::var("LIBSQL_URL").expect("LIBSQL_URL must be set");
let token = env::var("LIBSQL_AUTH_TOKEN").unwrap_or_default();

let mut db = Builder::new_remote_replica("local.db", &url, &token).build().await.unwrap();
let conn = db.connect().unwrap();

In-Memory Databases

libSQL supports connecting to in-memory databases for cases where you don’t require persistence:

use libsql::Builder;

let db = Builder::new_local(":memory:").build().await.unwrap();
let conn = db.connect().unwrap();

Local Development

You can work locally using an SQLite file using new_local:

use libsql::Builder;

let mut db = Builder::new_local("local.db").build().await.unwrap();
let conn = db.connect().unwrap();

Embedded Replicas

You can work with embedded replicas using new_remote_replica that can sync from the remote URL and delegate writes to the remote primary database:

use libsql::Builder;

let url = env::var("LIBSQL_URL").expect("LIBSQL_URL must be set");
let token = env::var("LIBSQL_AUTH_TOKEN").unwrap_or_default();

let mut db = Builder::new_remote_replica("local.db", &url, &token).build().await.unwrap();
let conn = db.connect().unwrap();

Manual Sync

The sync function allows you to sync manually the local database with the remote counterpart:

use libsql::Builder;

let url = env::var("LIBSQL_URL").expect("LIBSQL_URL must be set");
let token = env::var("LIBSQL_AUTH_TOKEN").unwrap_or_default();

let mut db = Builder::new_remote_replica("local.db", &url, &token).build().await.unwrap();

let conn = db.connect().unwrap();

db.sync().await.unwrap(); // Call sync manually to update local database

If you require full control over how frames get from your instance of sqld (libSQL Server), you can do this using new_local_replica and sync_frames. Reach out to us on Discord if you want to learn more.

Periodic Sync

The periodic_sync function allows you to set an interval for automatic synchronization of the database in the background:

use libsql::Builder;
use std::time::Duration;

let url = env::var("LIBSQL_URL").expect("LIBSQL_URL must be set");
let token = env::var("LIBSQL_AUTH_TOKEN").unwrap_or_default();

let mut db = Builder::new_remote_replica("local.db", &url, &token)
  .periodic_sync(Duration::from_secs(300)) // Sync every 5 minutes
  .build()
  .await.unwrap();

let conn = db.connect().unwrap();

Read Your Own Writes

The read_your_writes function configures the database connection to ensure that writes are immediately visible to subsequent read operations initiated by the same connection. This is enabled by default, and is particularly important in distributed systems to ensure consistency from the perspective of the writing process.

You can disable this behavior by passing false to the function:

use libsql::Builder;

let url = env::var("LIBSQL_URL").expect("LIBSQL_URL must be set");
let token = env::var("LIBSQL_AUTH_TOKEN").unwrap_or_default();

let mut db = Builder::new_remote_replica("local.db", &url, &token)
  .read_your_writes(false) // Disable reading your own writes
  .build()
  .await.unwrap();

let conn = db.connect().unwrap();

Encryption

To enable encryption on a SQLite file (new_local or new_remote_replica), make sure you have the encryption feature enabled, and pass the encryption_config:

Rust
use libsql::Builder;
use bytes::Bytes;

let url = env::var("LIBSQL_URL").expect("LIBSQL_URL must be set");
let token = env::var("LIBSQL_AUTH_TOKEN").unwrap_or_default();

let cipher = Cipher::YourChosenCipher;
let encryption_key_bytes = Bytes::from("your_secure_encryption_key_here");

let encryption_config = EncryptionConfig {
    cipher,
    encryption_key: encryption_key_bytes,
};

let mut db = Builder::new_remote_replica("local.db", &url, &token)
  .encryption_config(encryption_config) // Apply encryption configuration
  .build()
  .await
  .unwrap();

let conn = db.connect().unwrap();

Encrypted databases appear as raw data and cannot be read as standard SQLite databases. You must use the libSQL client for any operations — learn more.

Simple query

You can pass a string to execute() to invoke a SQL statement, as well as optional arguments:

conn.execute("SELECT * FROM users", ()).await?;

Prepared Statements

You can prepare a cached statement using prepare() and then execute it with query():

let stmt = db_conn.prepare("SELECT * FROM users").await?;

db_conn.query(&stmt, [&1]).await?;

Placeholders

libSQL supports the use of positional and named placeholders within SQL statements:

conn.execute("SELECT * FROM users WHERE id = ?1", params![1]).await?;

Deserialization

You can use the de::from_row function to deserialize a row into a struct:

use libsql::{de, Builder};

let mut stmt = conn
  .prepare("SELECT * FROM users WHERE id = ?1")
  .await
  .unwrap();
let row = stmt
  .query([1])
  .await
  .unwrap()
  .next()
  .await
  .unwrap()
  .unwrap();

#[derive(Debug, serde::Deserialize)]
struct User {
  name: String,
  age: i64,
  vision: f64,
  avatar: Vec<u8>,
}

let user = de::from_row::<User>(&row).unwrap();

Batch Transactions

A batch consists of multiple SQL statements executed sequentially within an implicit transaction. The backend handles the transaction: success commits all changes, while any failure results in a full rollback with no modifications.

conn.execute_batch(r#"
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL
  );

  INSERT INTO users (name) VALUES ('Alice');
  INSERT INTO users (name) VALUES ('Bob');
"#).await?;

Interactive Transactions

Interactive transactions in SQLite ensure the consistency of a series of read and write operations within a transaction’s scope. These transactions give you control over when to commit or roll back changes, isolating them from other client activity.

  • transaction() — with default transaction behavior (DEFFERED)
  • transaction_with_behavior() — with custom transaction behavior
let mut tx = db_conn.transaction().await?;

tx.execute("INSERT INTO users (name) VALUES (?1)", ["Iku"]).await?;
tx.execute("INSERT INTO users (name) VALUES (?1)", ["Iku 2"]).await?;

tx.commit().await?;