A typed, dialect-aware SQL query builder for Rust.
Generic over a Dialect (PostgreSQL / MySQL / SQLite), with typed bind
parameters, automatic identifier escaping, dialect-correct placeholders
($N for Postgres, ? for MySQL/SQLite), and an sqlx handoff for execution.
v2.0 is a ground-up redesign. Upgrading from 1.x? See CHANGELOG — the API is entirely new (typed binds,
Dialect-generic, noserde_json::Valuein the core).
- User guide — full documentation site (queries, binds, errors, sqlx, cookbook).
- API reference — rustdoc on docs.rs.
- Changelog — release notes and breaking changes.
- Dialect-generic — one builder, three dialects; mixing them is a compile error.
- Typed binds — pass real Rust values (
i64,&str,Option<T>, …) viaIntoBind. - Injection-safe — identifiers always escaped, values always bound.
- Full query surface — SELECT/INSERT/UPDATE/DELETE, WHERE (+
or/andgroups), JOINs, CTEs (WITH/RECURSIVE),UNION, GROUP BY/HAVING/ORDER BY, LIMIT/OFFSET. - Upsert + RETURNING —
on_conflict_merge/_do_nothing, dialect-correct. - Row locking —
for_update/for_share+skip_locked/no_wait(no-op on SQLite). - Aggregates —
select_count/sum/avg/min/max(+_as) andselect_as. db()qualification — multi-tenant (one connection, many databases).- Typed fetch —
fetch_all::<T>/fetch_one/count/fetch_scalarviasqlx. - Dynamic —
when/when_else,paginate,distinct/distinct_on,ilike, jsonb.
[dependencies]
# pick the driver(s) you need; the builder itself is driver-agnostic
chain-builder = { version = "3", features = ["sqlx_postgres"] }
sqlx = { version = "0.9", features = ["postgres", "runtime-tokio-rustls"] }Driver features: sqlx_mysql (default), sqlx_sqlite, sqlx_postgres — enable any
combination. json enables Value::Json.
use chain_builder::{QueryBuilder, Postgres, Order};
let (sql, binds) = QueryBuilder::<Postgres>::table("users")
.db("mydb")
.select(["id", "name"])
.where_eq("status", "active")
.where_in("role", ["admin", "staff"])
.order_by("created_at", Order::Desc)
.paginate(2, 20)
.to_sql();
// SELECT "id", "name" FROM "mydb"."users"
// WHERE "status" = $1 AND "role" IN ($2, $3)
// ORDER BY "created_at" DESC LIMIT $4 OFFSET $5// upsert + RETURNING
use chain_builder::{QueryBuilder, Postgres};
let q = QueryBuilder::<Postgres>::table("users")
.insert([("email", "a@b.c"), ("name", "A")])
.on_conflict_merge(["email"])
.returning(["id"]);
// INSERT INTO "users" ("email", "name") VALUES ($1, $2)
// ON CONFLICT ("email") DO UPDATE SET "name" = EXCLUDED."name" RETURNING "id"
// execute with sqlx (feature sqlx_postgres)
// let rows: Vec<UserRow> = q.fetch_all(&pool).await?;Same builder, different dialect → MySQL backticks + ?:
use chain_builder::{QueryBuilder, MySql};
QueryBuilder::<MySql>::table("users").select(["id"]).where_eq("status", "active");
// SELECT `id` FROM `users` WHERE `status` = ?Two axes of SQL-injection safety:
- Values are always sent as bound parameters (
?/$N), never inlined. - Identifiers (table/column/alias names) are dialect-escaped automatically;
qualified names (
users.id) are escaped segment-by-segment ("users"."id"),*preserved. Pass bare names — do not pre-quote (it would be double-escaped).
The *_raw methods (select_raw, where_raw, group_by_raw, order_by_raw,
on_raw, having_raw) and the having* helpers are verbatim escape hatches —
never pass untrusted input through them.
sqlx_mysql(default) — MySQL driver +SqlxDialect for MySqlsqlx_sqlite— SQLite driver +SqlxDialect for Sqlitesqlx_postgres— PostgreSQL driver +SqlxDialect for Postgresjson—Value::Json+IntoBind for serde_json::Valueuuid—Value::Uuid+IntoBind for uuid::Uuidchrono— date/timeValuevariants +IntoBindforchronotypesdecimal—Value::Decimal+IntoBind for rust_decimal::Decimal
The query builder (to_sql()) works with no driver feature; a driver feature is
only needed for the sqlx handoff (to_sqlx_query, fetch_*). All three drivers
can be enabled simultaneously.
MIT License - see LICENSE for details.