Musq is an asynchronous SQLite toolkit for Rust.
Musq bundles its own SQLite, runs each connection on a dedicated worker thread behind an async API, enables foreign key enforcement by default, and ships with sqlite-vec vector search built in.
//! Quickstart example from the README.
use musq::{FromRow, Musq, sql, sql_as, values};
/// User record fetched from the database.
#[derive(Debug, FromRow)]
pub struct User {
/// User ID.
pub id: i32,
/// User name.
pub name: String,
}
#[tokio::main]
async fn main() -> musq::Result<()> {
// Create an in-memory database pool
let pool = Musq::new().open_in_memory().await?;
// Create a table
sql!("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);")?
.execute(&pool)
.await?;
// Insert a user
let id = 1;
let name = "Alice";
let user_values = values! { "id": id, "name": name }?;
sql!("INSERT INTO users {insert:user_values}")?
.execute(&pool)
.await?;
// Fetch the user and map it to our struct
let user: User = sql_as!("SELECT id, name FROM users WHERE id = {id}")?
.fetch_one(&pool)
.await?;
println!("Fetched user: {user:?}");
Ok(())
}Musq is the options builder. open() and open_in_memory() return a Pool;
queries execute directly on the pool, on a Connection, a PoolConnection, or
a Transaction — all interchangeably.
use musq::{JournalMode, Musq};
let pool = Musq::new()
.max_connections(10)
.create_if_missing(true)
.journal_mode(JournalMode::Wal)
.open("app.db")
.await?;Defaults: foreign keys on, busy timeout 5s, 10 pool connections, journal mode
left unchanged (set JournalMode::Wal explicitly for WAL). Any pragma can be
set with .pragma(key, value).
sql! builds a query from a format!-like string. sql_as! does the same and
maps rows to a FromRow type. Interpolated values are always bound as
parameters, never spliced into the SQL.
use musq::{FromRow, sql, sql_as};
#[derive(FromRow, Debug)]
struct User {
id: i32,
name: String,
}
let id = 1;
let name = "Bob";
sql!("INSERT INTO users (id, name) VALUES ({id}, {name})")?
.execute(&pool)
.await?;
let user: User = sql_as!("SELECT id, name FROM users WHERE id = {id}")?
.fetch_one(&pool)
.await?;Placeholders:
| Placeholder | Expansion |
|---|---|
{expr}, {} |
one bound parameter |
{values:list} |
?, ?, ? — one parameter per element, for IN (...) |
{ident:expr} |
one quoted identifier |
{idents:list} |
comma-separated quoted identifiers |
{insert:values} |
(col, ...) VALUES (?, ...) |
{set:values} |
col = ?, ... |
{where:values} |
col = ? AND ...; col IS NULL for NULLs; 1=1 when empty |
{upsert:values, exclude: a, b} |
col = excluded.col, ... for ON CONFLICT ... DO UPDATE SET |
{raw:expr} |
verbatim SQL; taints the query |
let table_name = "users";
let user_ids = vec![1, 2, 3];
let columns = ["id", "name"];
let users: Vec<User> = sql_as!(
"SELECT {idents:columns} FROM {ident:table_name} WHERE id IN ({values:user_ids})"
)?
.fetch_all(&pool)
.await?;Run queries with .execute(), .fetch_one(), .fetch_optional(),
.fetch_all(), or .fetch() (a Stream). Compose dynamic queries with
Query::join, or drop down to QueryBuilder for full control.
Values is an insertion-ordered column/value map consumed by the {insert:},
{set:}, {where:}, and {upsert:} placeholders. Build one with the
values! macro or fluently with Values::new().val(k, v)?. Each value is
encoded immediately, so construction returns Result.
use musq::{Values, sql, sql_as, values};
let user_data = values! { "id": 1, "name": "Alice", "status": "active" }?;
sql!("INSERT INTO users {insert:user_data}")?
.execute(&pool)
.await?;
let changes = Values::new()
.val("name", "Alicia")?
.val("status", "inactive")?;
sql!("UPDATE users SET {set:changes} WHERE id = 1")?
.execute(&pool)
.await?;
let filters = values! { "status": "inactive" }?;
let user: User = sql_as!("SELECT id, name FROM users WHERE {where:filters}")?
.fetch_one(&pool)
.await?;
let upsert = values! { "id": 1, "name": "Alicia", "status": "active" }?;
sql!(
"INSERT INTO users {insert:upsert} ON CONFLICT(id) DO UPDATE SET {upsert:upsert, exclude: id}"
)?
.execute(&pool)
.await?;Option::None encodes as SQL NULL everywhere ({where:} renders it as
col IS NULL), and musq::Null is an untyped NULL literal:
async fn add_user(
pool: &musq::Pool,
name: &str,
phone: Option<String>,
) -> musq::Result<()> {
let user_data = values! {
"name": name,
"phone": phone, // Option: None encodes as NULL
"email": musq::Null, // untyped NULL literal
}?;
sql!("INSERT INTO users {insert:user_data}")?
.execute(pool)
.await?;
Ok(())
}DB-side computed values come from musq::expr (now_rfc3339_utc, jsonb,
jsonb_text, jsonb_serde, raw):
use musq::expr;
let changes = values! {
"updated_at": expr::now_rfc3339_utc(),
"payload": expr::jsonb(r#"{"event":"hello"}"#),
}?;
sql!("UPDATE events SET {set:changes} WHERE id = 1")?
.execute(&pool)
.await?;expr::raw(...) taints the resulting query; prefer the curated helpers.
Pool::begin and Connection::begin return a Transaction. Calling begin
on an existing transaction creates a savepoint. Dropping an uncommitted
transaction rolls it back. Connection::transaction runs a closure and
commits or rolls back based on its result.
let mut tx = pool.begin().await?;
sql!("INSERT INTO users (id, name) VALUES ({id}, {name})")?
.execute(&tx)
.await?;
tx.commit().await?;The Encode and Decode traits convert between Rust and SQLite types:
| Rust type | SQLite type |
|---|---|
bool |
BOOLEAN |
i8, i16, i32, i64 |
INTEGER |
u8, u16, u32 |
INTEGER |
f32, f64 |
REAL |
&str, String, Arc<String> |
TEXT |
&[u8], Vec<u8>, Arc<Vec<u8>> |
BLOB |
bstr::BString |
BLOB |
time::OffsetDateTime |
DATETIME |
time::PrimitiveDateTime |
DATETIME |
time::Date |
DATE |
time::Time |
TIME |
VecF32, VecInt8, VecBit |
BLOB |
Option<T> maps None to NULL. For large strings and blobs, prefer owned
or shared types (String, Vec<u8>, Arc<T>) to avoid copies; blobs can be
decoded directly into Arc<Vec<u8>>. bstr::BString handles text-like BLOBs
that may not be valid UTF-8.
#[derive(musq::Codec)] implements both Encode and Decode for enums and
newtype structs (Encode and Decode can also be derived individually).
Enums store as snake-cased strings ("open", "closed") by default:
#[derive(musq::Codec, Debug, PartialEq)]
enum Status {
Open,
Closed,
}Or as integers with repr:
#[derive(musq::Codec, Debug, PartialEq)]
#[musq(repr = "i32")]
enum Priority {
Low = 1,
Medium = 2,
High = 3,
}Newtype structs store as their inner value:
#[derive(musq::Codec, Debug, PartialEq)]
struct UserId(i32);#[derive(musq::Json)] stores any serde-compatible type as JSON text:
#[derive(musq::Json, serde::Serialize, serde::Deserialize, Debug, PartialEq)]
struct Metadata {
tags: Vec<String>,
version: i32,
}#[derive(FromRow)] maps columns to fields by name. Attributes:
#[musq(rename = "...")]— map a field to a differently named column#[musq(rename_all = "...")]— on the struct; case-convert all field names#[musq(default)]— useDefault::default()when the column is absent#[musq(skip)]— always useDefault::default()#[musq(try_from = "T")]— decode asT, then convert withTryFrom#[musq(deserialize_with = "path")]— decode with a customfn(prefix: &str, row: &Row) -> Result<T>#[musq(flatten)],#[musq(flatten, prefix = "...")]— embed a nestedFromRowstruct, optionally prefixing its column names; anOptionnested struct isNoneiff all of its columns are NULL
#[derive(FromRow)]
struct Address {
street: String,
city: String,
}
#[derive(FromRow)]
struct User {
id: i32,
// Reads the `street` and `city` columns.
#[musq(flatten)]
address: Address,
// Reads `billing_street` and `billing_city`; None iff both are NULL.
#[musq(flatten, prefix = "billing_")]
billing: Option<Address>,
}The default vec feature registers sqlite-vec on every connection and
provides the VecF32, VecInt8, and VecBit types. VecF32 binds directly
to vector functions and vec0 tables; VecInt8 and VecBit must be wrapped
in SQL as vec_int8(?) and vec_bit(?).
musq = { version = "0.0.4", default-features = false } # opt out of vecSee the end-to-end example: cargo run -p musq --example vec.
Musq supports exactly the SQLite release bundled by its libsqlite3-sys
dependency — currently SQLite 3.53.2 via libsqlite3-sys 0.38.1. Linking
against older or system SQLite libraries is not supported; leave
LIBSQLITE3_SYS_USE_PKG_CONFIG and SQLITE3_* environment variables unset.
Runtime introspection and control:
runtime_info()— SQLite version, source ID, and compile optionsdb_status(kind, reset)— per-connection status counters (page cache, lookaside, schema, statements, ...)wal_checkpoint(schema, mode)— run or inspect WAL checkpoints
Questions, ideas, feature requests: Discord.
Just like whales once used to be land-dwelling quadrupeds, Musq started life as a focused fork of SQLx.