Rust 86 🦀 Asynchronous SQL Database Drivers (sqlx and diesel)
Rust’s database story splits along a single axis: whether the SQL is written by hand or generated by a type-safe DSL. sqlx is a SQL toolkit—you write the query, and the macro sqlx::query! checks it against a live database (or a cached schema) at compile time, verifying column names and types without generating any query code . diesel is an ORM and query builder—you write Rust expressions using a generated schema module, and the compiler validates the query shape and types before any database connection exists . Both give compile-time safety. They differ in where the safety comes from and what the developer must learn.
The async story is the second axis. sqlx was built for async from the start, with runtime features for Tokio and async-std and native integration with connection pools . diesel is synchronous by default; async support comes through the separate diesel-async crate, which works but adds a dependency and a mental layer that native-async drivers do not have . For a new Tokio-based service, sqlx is the path of least resistance. For a mature project with a stable schema and a team that values the DSL’s guarantees over async ergonomics, diesel remains the more robust choice.
This chapter covers the architecture of both drivers, the compile-time verification mechanisms, the async and connection-pool models, the query-building approaches for static and dynamic queries, and the decision criteria that determine which driver fits a given project. It assumes familiarity with Rust’s async model and SQL fundamentals.
Key point: sqlx checks the SQL you write; diesel generates the SQL you describe. sqlx requires a database (or an offline cache) at compile time; diesel requires a generated schema file. sqlx is async-native; diesel is sync-first with an async adapter. The choice is between writing SQL with compile-time checks and writing a Rust DSL with compile-time guarantees.
Why two database drivers dominate the Rust ecosystem
The compile-time safety mechanisms are fundamentally different. sqlx::query! is a procedural macro that connects to the database during compilation and asks the database to prepare the statement. The database returns the result set’s column names and types, and the macro generates a Rust struct that matches them. If the query references a column that does not exist, or returns a type that cannot be decoded into the struct field, the compilation fails . diesel uses a table! macro generated from the database schema by diesel print-schema. The macro produces Rust types for every table and column, and the query DSL is built on those types. A query that references a non-existent column fails because the Rust expression does not compile—the database is never consulted .
The async models differ in origin. sqlx supports Tokio and async-std through feature flags, and its connection pool (PgPool, MySqlPool, SqlitePool) is designed for async use . diesel is a synchronous library. The diesel-async crate provides async support by wrapping diesel‘s synchronous connection in a background thread or by using async-capable connection types, but the core query builder and type system remain synchronous . A project that is fully async will find sqlx‘s API more natural because the await points are in the driver, not in an adapter layer.
Dynamic queries are easier in sqlx. diesel‘s DSL is designed for static query shapes. A search endpoint with optional filters requires either Box<dyn QueryDsl> or conditional branching that produces different query types, which the type system resists. sqlx‘s QueryBuilder constructs SQL strings piece by piece with parameter binding, which is the natural way to handle dynamic filters . The trade-off is that the query builder’s output is not checked by the query! macro; it is runtime SQL, with the safety that comes from parameterization but not from compile-time validation.
The trade-off. sqlx requires a database connection during compilation, or an offline cache generated by cargo sqlx prepare. This is the most common complaint: CI pipelines need a database, and new developers must set one up before the project compiles . diesel does not need a database at compile time, but it requires running diesel migration run and committing the generated schema.rs before the code compiles. Both have a setup step; they differ in whether that step involves a live database.
a. sqlx: SQL with compile-time verification
The core of sqlx is the query! family of macros. The macro takes a SQL string with $1-style placeholders, sends it to the database during compilation, and generates an anonymous struct whose fields match the result columns. The developer never writes the struct; the macro produces it.
use sqlx::PgPool;
async fn get_user(pool: &PgPool, id: i32) -> sqlx::Result<()> {
// Compile-time check: column names and types must match.
let user = sqlx::query!(
"SELECT id, email, created_at FROM users WHERE id = $1",
id
)
.fetch_one(pool)
.await?;
// The macro generated fields: user.id, user.email, user.created_at.
println!("{}: {}", user.id, user.email);
Ok(())
}
The query_as! variant takes an explicit struct path instead of generating an anonymous one. This is useful when the result needs to be returned from a function or stored in a collection. The macro still verifies that the struct’s field types match the query’s output types .
The offline mode is the mechanism for CI and for developers who do not want a database running locally. cargo sqlx prepare connects to the database once, caches the query metadata in a .sqlx directory, and commits it. Subsequent builds with SQLX_OFFLINE=true use the cache instead of connecting . The friction is that the cache must be regenerated whenever a query changes, and a stale cache produces a build failure that is not immediately obvious.
For dynamic queries, QueryBuilder builds the SQL incrementally. It handles parameter numbering and escaping, and it produces a Query that can be executed. The builder does not provide compile-time checking, but it does provide safe parameter binding .
use sqlx::QueryBuilder;
let mut builder = QueryBuilder::new("SELECT id, email FROM users WHERE 1=1");
if let Some(email) = filter_email {
builder.push(" AND email = ").push_bind(email);
}
if let Some(active) = filter_active {
builder.push(" AND active = ").push_bind(active);
}
let query = builder.build();
The connection pool is created with PgPoolOptions and is cloneable and shared across handlers. sqlx handles the pool’s lifecycle and health checks .
b. diesel: the DSL and the generated schema
diesel starts with a schema file generated by the CLI. diesel print-schema reads the database and produces src/schema.rs, which contains table! macros that define Rust types for every table and column. diesel migration run regenerates the schema automatically after each migration .
// src/schema.rs (generated)
diesel::table! {
users (id) {
id -> Int4,
email -> Varchar,
created_at -> Timestamp,
}
}
Queries are written using the DSL imported from the schema module. The compiler checks that the columns exist and that the types match.
use diesel::prelude::*;
use crate::schema::users;
fn get_user(conn: &mut PgConnection, user_id: i32) -> QueryResult<User> {
users::table
.filter(users::id.eq(user_id))
.select(User::as_select())
.first(conn)
}
The Queryable derive on the User struct maps the query result to the struct fields. The Selectable derive generates the select clause that matches the struct . The Insertable derive maps a struct to an insert statement, which is useful for forms and API payloads .
Relations are handled through the Associations derive and the belonging_to method. For a one-to-many relation, belonging_to constructs a WHERE foreign_key IN (...) query that loads all children for a set of parents in one query, avoiding the N+1 problem that naive ORMs produce . The grouped_by method then groups the results in memory.
let all_books = books::table.select(Book::as_select()).load(conn)?;
let pages = Page::belonging_to(&all_books)
.select(Page::as_select())
.load(conn)?;
let pages_per_book = pages.grouped_by(&all_books);
The synchronous nature of diesel means that database calls block the current thread. In an async context, this is a problem unless the call is wrapped in spawn_blocking or the diesel-async crate is used. diesel-async provides AsyncPgConnection and async pool support, but the query DSL remains synchronous and the async layer is an adapter, not a native async API .
c. The decision criteria and the hybrid pattern
The choice is not about which library is “better.” It is about which safety mechanism and async model fit the project.
| Factor | sqlx | diesel |
|---|---|---|
| SQL style | Hand-written SQL | Rust DSL |
| Compile-time check | Against live DB or offline cache | Against generated schema |
| Async | Native | Via diesel-async |
| Dynamic queries | QueryBuilder | Awkward; requires type erasure |
| Learning curve | Moderate (SQL knowledge) | High (DSL + type system) |
| DB at compile time | Required (or cache) | Not required |
| Relations | Manual SQL | belonging_to, grouped_by |
The hybrid pattern is common in larger applications: use sqlx for the bulk of the data access because it is async-native and the team knows SQL, and use diesel for the parts of the domain where the schema is stable and the compile-time guarantees of the DSL are valuable. The two libraries can coexist in the same project, though they maintain separate connection pools and migration systems.
The practical recommendation from the 2026 comparisons is consistent. For a new Tokio-based API, sqlx is the faster path to working code . For a long-lived project with a stable schema, a team that values the DSL’s refactoring safety, and a willingness to accept the synchronous core, diesel is the more robust choice . The benchmark differences are negligible; the database is the bottleneck, not the driver .
Complete Example Session
// ============================================
// PART 1: SQLX — DEPENDENCY AND POOL
// ============================================
// Cargo.toml
// sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
use sqlx::PgPool;
async fn create_pool() -> PgPool {
PgPool::connect("postgres://user:pass@localhost/db").await.unwrap()
}
// ============================================
// PART 2: SQLX — COMPILE-TIME CHECKED QUERY
// ============================================
async fn find_user(pool: &PgPool, id: i32) -> sqlx::Result<()> {
let user = sqlx::query!(
"SELECT id, email, created_at FROM users WHERE id = $1",
id
)
.fetch_one(pool)
.await?;
// Fields generated by macro: user.id, user.email, user.created_at.
println!("{}", user.email);
Ok(())
}
// ============================================
// PART 3: SQLX — QUERY_AS! WITH EXPLICIT STRUCT
// ============================================
#[derive(Debug)]
struct UserRow {
id: i32,
email: String,
}
async fn find_user_struct(pool: &PgPool, id: i32) -> sqlx::Result<UserRow> {
sqlx::query_as!(
UserRow,
"SELECT id, email FROM users WHERE id = $1",
id
)
.fetch_one(pool)
.await
}
// ============================================
// PART 4: SQLX — DYNAMIC QUERY WITH QUERYBUILDER
// ============================================
use sqlx::QueryBuilder;
async fn search_users(
pool: &PgPool,
email_filter: Option<&str>,
active_filter: Option<bool>,
) -> sqlx::Result<Vec<UserRow>> {
let mut builder = QueryBuilder::new("SELECT id, email FROM users WHERE 1=1");
if let Some(email) = email_filter {
builder.push(" AND email = ").push_bind(email);
}
if let Some(active) = active_filter {
builder.push(" AND active = ").push_bind(active);
}
builder.build_query_as::<UserRow>().fetch_all(pool).await
}
// ============================================
// PART 5: SQLX — OFFLINE CACHE
// ============================================
// cargo sqlx prepare
// SQLX_OFFLINE=true cargo build
// ============================================
// PART 6: DIESEL — SCHEMA GENERATION
// ============================================
// diesel setup
// diesel migration generate create_users
// diesel migration run
// Generates src/schema.rs with table! macros.
// ============================================
// PART 7: DIESEL — MODEL AND QUERY
// ============================================
// use diesel::prelude::*;
// use crate::schema::users;
//
// #[derive(Queryable, Selectable)]
// #[diesel(table_name = users)]
// struct User {
// id: i32,
// email: String,
// }
//
// fn find_user(conn: &mut PgConnection, uid: i32) -> QueryResult<User> {
// users::table
// .filter(users::id.eq(uid))
// .select(User::as_select())
// .first(conn)
// }
// ============================================
// PART 8: DIESEL — INSERT WITH INSERTABLE
// ============================================
// #[derive(Insertable)]
// #[diesel(table_name = users)]
// struct NewUser<'a> {
// email: &'a str,
// }
//
// diesel::insert_into(users::table)
// .values(&NewUser { email: "a@b.com" })
// .execute(conn)?;
// ============================================
// PART 9: DIESEL — RELATIONS
// ============================================
// #[derive(Associations)]
// #[diesel(belongs_to(Book))]
// struct Page { book_id: i32, ... }
//
// let pages = Page::belonging_to(&books)
// .select(Page::as_select())
// .load(conn)?;
// ============================================
// PART 10: THE DECISION
// ============================================
// New Tokio API, SQL-first team → sqlx.
// Stable schema, DSL safety, sync acceptable → diesel.
// Dynamic filters are core → sqlx QueryBuilder.
// No DB at compile time → diesel.
// Async is non-negotiable → sqlx.
Quick Reference
Driver Comparison
| Aspect | sqlx | diesel |
|---|---|---|
| Type | SQL toolkit | ORM + query builder |
| Compile-time check | Live DB or offline cache | Generated schema.rs |
| Query style | Hand-written SQL | Rust DSL |
| Async | Native (Tokio, async-std) | Via diesel-async |
| Connection pool | PgPool, built-in | r2d2, deadpool |
| Dynamic queries | QueryBuilder | Type-erasure friction |
| Migrations | sqlx-cli | diesel_cli |
Compile-Time Check Mechanism
| Driver | Mechanism | Requires DB |
|---|---|---|
sqlx | query! macro prepares statement | Yes (or cache) |
diesel | table! macro from schema | No |
Setup Commands
| Driver | Command | Purpose |
|---|---|---|
sqlx | cargo sqlx prepare | Generate offline cache |
sqlx | sqlx migrate add | Create migration |
diesel | diesel setup | Initialize project |
diesel | diesel migration run | Run migrations, regenerate schema |
diesel | diesel print-schema | Generate schema.rs |
Decision Factors
| Factor | Favors sqlx | Favors diesel |
|---|---|---|
| New async project | ✓ | |
| Stable schema, long-lived | ✓ | |
| Dynamic filters | ✓ | |
| No DB at compile time | ✓ | |
| Team knows SQL | ✓ | |
| DSL/ORM ergonomics | ✓ |
Best Practices
✅ Do This:
// Use sqlx::query! for compile-time verified SQL. // ✅
sqlx::query!("SELECT id FROM users WHERE id = $1", id)
// Generate offline cache for CI. // ✅
cargo sqlx prepare
// Use QueryBuilder for dynamic filters. // ✅
builder.push(" AND email = ").push_bind(email);
// Run diesel migration run to regenerate schema. // ✅
diesel migration run
// Use diesel-async for async diesel. // ✅
diesel-async = { version = "0.5", features = ["postgres"] }
❌ Don’t Do This:
// Use sqlx::query! without a database or cache. // ❌
// Compilation fails.
// Use diesel DSL for dynamic query shapes. // ❌
// Type system resists; use QueryBuilder.
// Forget to regenerate schema after migration. // ❌
// diesel DSL compiles against stale types.
// Assume diesel is async-native. // ❌
// It is synchronous; use diesel-async for async.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
sqlx build fails in CI | No database, no cache | cargo sqlx prepare, commit .sqlx |
diesel column not found | Schema not regenerated | diesel migration run |
Dynamic filters hard in diesel | DSL requires static shape | Use sqlx QueryBuilder |
Async blocking in diesel | Synchronous connection in async context | diesel-async or spawn_blocking |
Stale .sqlx cache | Query changed, cache not regenerated | Re-run cargo sqlx prepare |
Real-World Examples
1. SQLx Compile-Time Query
sqlx::query!("SELECT id, email FROM users WHERE id = $1", id)
2. SQLx QueryAs Struct
sqlx::query_as!(UserRow, "SELECT id, email FROM users WHERE id = $1", id)
3. SQLx QueryBuilder
builder.push(" AND email = ").push_bind(email);
4. SQLx Offline Cache
cargo sqlx prepare
SQLX_OFFLINE=true cargo build
5. Diesel Schema Generation
diesel migration run
6. Diesel Query
users::table.filter(users::id.eq(uid)).select(User::as_select()).first(conn)
7. Diesel Insertable
diesel::insert_into(users::table).values(&NewUser { email }).execute(conn)?;
8. Diesel BelongingTo
Page::belonging_to(&books).select(Page::as_select()).load(conn)?;
9. Diesel Async
diesel-async = { version = "0.5", features = ["postgres", "deadpool"] }
10. Pool Creation (SQLx)
let pool = PgPool::connect("postgres://...").await?;
Visual
Compile-Time Verification
┌──────────────────────────────────────────────┐
│ SQLX DIESEL │
│ │
│ query! macro table! macro │
│ │ │ │
│ ▼ ▼ │
│ Connects to DB Generated schema │
│ during compile. from diesel CLI. │
│ │ │ │
│ ▼ ▼ │
│ DB prepares Rust types for │
│ statement. tables/columns. │
│ │ │ │
│ ▼ ▼ │
│ Macro generates DSL expressions │
│ struct matching type-check against │
│ result columns. schema types. │
│ │ │ │
│ ▼ ▼ │
│ Compile fails if Compile fails if │
│ column/type mismatch. column not in │
│ schema. │
│ │
│ Requires DB (or cache). No DB required. │
│ │
└──────────────────────────────────────────────┘
Async Model
┌──────────────────────────────────────────────┐
│ SQLX DIESEL │
│ │
│ Async native. Sync core. │
│ Tokio/async-std diesel-async │
│ runtime features. adapter for async. │
│ │
│ .await on queries. .await via │
│ AsyncPgConnection. │
│ │
│ Pool: PgPool Pool: r2d2/deadpool │
│ (async). (async via adapter).│
│ │
│ Natural for Extra dependency │
│ Tokio services. for async services. │
│ │
└──────────────────────────────────────────────┘
Query Style
┌──────────────────────────────────────────────┐
│ SQLX DIESEL │
│ │
│ Write SQL string: Write Rust DSL: │
│ "SELECT id FROM users::table │
│ users WHERE id = $1" .filter(id.eq(1)) │
│ .select(...) │
│ │
│ Familiar to SQL Requires learning │
│ developers. DSL and type system.│
│ │
│ Dynamic: QueryBuilder Dynamic: type │
│ (piece-by-piece SQL). erasure friction. │
│ │
└──────────────────────────────────────────────┘
Decision Flow
┌──────────────────────────────────────────────┐
│ WHICH DRIVER? │
│ │
│ New project + Tokio + SQL-first? │
│ └── sqlx │
│ │
│ Stable schema + long-lived + DSL safety? │
│ └── diesel │
│ │
│ Dynamic filters are core? │
│ └── sqlx QueryBuilder │
│ │
│ No database at compile time? │
│ └── diesel │
│ │
│ Async is non-negotiable? │
│ └── sqlx │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
sqlx type | Async SQL toolkit |
diesel type | ORM and query builder |
sqlx check | Live DB or offline cache at compile time |
diesel check | Generated schema.rs at compile time |
sqlx async | Native (Tokio, async-std) |
diesel async | Via diesel-async adapter |
sqlx dynamic queries | QueryBuilder |
diesel dynamic queries | Type-erasure friction |
sqlx setup | cargo sqlx prepare |
diesel setup | diesel migration run |
Key takeaways:
sqlxchecks the SQL you write;dieselgenerates the SQL you describe.sqlx::query!connects to the database (or a cache) and verifies column names and types.diesel‘stable!macro produces Rust types from a generated schema, and the DSL is checked against those types .sqlxis async-native;dieselis sync-first. For a Tokio-based service,sqlxavoids the adapter layer. For a project that can tolerate synchronous calls or is willing to usediesel-async,dieseloffers the more robust DSL .- Dynamic queries favor
sqlx.QueryBuilderconstructs SQL piece by piece with parameter binding.diesel‘s DSL is designed for static shapes; dynamic filters require type erasure that fights the type system . sqlxrequires a database or cache at compile time. Thequery!macro prepares the statement against a live connection. The offline cache (cargo sqlx prepare) is the CI workaround, but it must be regenerated when queries change .dieselrequires a generated schema but no database. Theschema.rsfile is committed, and the DSL compiles against it. The trade-off is that the schema must be regenerated after migrations .- The benchmark difference is negligible. Both drivers produce efficient SQL. The database is the bottleneck, not the driver. The choice should be driven by async fit, query style, and compile-time workflow, not by micro-benchmarks .
Remember: Rust’s database drivers are not interchangeable. sqlx is for teams that think in SQL and want compile-time checks without a DSL. diesel is for teams that want the type system to validate the query shape and are willing to learn the DSL and accept a synchronous core. sqlx is async by construction; diesel is sync with an async adapter. sqlx needs a database at compile time; diesel needs a generated schema. The choice is between writing SQL with checks and writing a Rust DSL with guarantees. Both are safe; they are safe in different ways.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!