sqlx
sqlx — Async SQL Toolkit for Rust
sqlx is an async-first, pure-Rust SQL toolkit with compile-time query checking. It supports PostgreSQL, MySQL, and SQLite without a DSL — you write raw SQL, and sqlx validates it against a live database at compile time.
Crate Architecture
| Module | Purpose |
|---|---|
sqlx::postgres |
PostgreSQL driver (PgPool, PgConnection, PgListener) |
sqlx::mysql |
MySQL driver (MySqlPool, MySqlConnection) |
sqlx::sqlite |
SQLite driver (SqlitePool, SqliteConnection) |
sqlx::any |
Database-agnostic driver (AnyPool) — no compile-time checking |
sqlx::query_builder |
Runtime dynamic query construction (QueryBuilder) |
sqlx::migrate |
Migration framework (migrate! macro, Migrator) |
sqlx::types |
Type wrappers (Json<T>, Text<T>) |
Quick Start: Minimal Dependency Setup
Always include at least one database driver and a runtime feature. Without a runtime feature, the pool will panic at runtime.
# Cargo.toml
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "chrono", "uuid"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
dotenvy = "0.15"
# .env
DATABASE_URL=postgres://postgres:password@localhost:5432/mydb
use sqlx::PgPool;
#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
dotenvy::dotenv().ok();
let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await?;
let users = sqlx::query_as!(User, "SELECT id, name FROM users")
.fetch_all(&pool).await?;
Ok(())
}
Critical: DATABASE_URL must be set at compile time for query!() macros to work. Set it in .env or use SQLX_OFFLINE=true with prepared metadata (see Offline Mode section).
Quick Reference: Which Guide Do I Need?
- Compile-time query macros, type overrides, offline mode -> Read
references/compile-time-checking.md - Connection pools, PgConnectOptions, pool tuning -> Read
references/connection-pooling.md - Rust-to-SQL type mapping, FromRow, custom types -> Read
references/type-mapping.md - Transactions, nested transactions, savepoints -> Read
references/transactions-migrations.md - #[sqlx::test], fixtures, test patterns -> Read
references/testing-mocking.md - PostgreSQL specifics (PgListener, COPY, arrays, ranges) -> Read
references/database-specifics.md - Error handling, sqlx::Error enum -> Read
references/error-handling.md - Feature flags, Cargo.toml patterns -> Read
references/feature-flags.md
Core Patterns at a Glance
1. Query with Compile-Time Checking (Preferred)
The query!() macro connects to the database at compile time, validates SQL syntax, checks that parameter types match, and verifies return column types.
// Returns an anonymous struct with typed fields (no FromRow needed)
let record = sqlx::query!("SELECT id, name, email FROM users WHERE id = $1", user_id)
.fetch_one(&pool)
.await?;
// record.id: i64, record.name: String, record.email: String
// query_as! maps to a named struct (needs #[derive(FromRow)])
let user: User = sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = $1", user_id)
.fetch_one(&pool)
.await?;
// query_scalar! returns a single column value
let count: i64 = sqlx::query_scalar!("SELECT COUNT(*)::BIGINT FROM users")
.fetch_one(&pool)
.await?;
2. Runtime Query Functions (No Compile-Time DB Needed)
Use these when you cannot set DATABASE_URL at compile time. They are not type-checked.
// query() returns anonymous PgRow — access columns by name or index
// Note: `sqlx::Row` trait must be in scope for .get()
use sqlx::Row;
let row = sqlx::query("SELECT id, name FROM users WHERE id = $1")
.bind(1)
.fetch_one(&pool)
.await?;
let id: i64 = row.get("id");
let name: String = row.get("name");
3. Fetch Strategies
Every query supports multiple fetch methods. Choose based on expected result count:
// .fetch_one() — Exactly one row. Error if 0 rows (RowNotFound) or >1 rows
// .fetch_optional() — 0 or 1 row. Returns Option<T>. None if no rows.
// .fetch_all() — All rows as Vec<T>. Empty vec if no rows.
// .fetch(_) — Returns a Stream (row by row, lazy). Use with futures::StreamExt.
4. Connection Pool Configuration
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;
let pool = PgPoolOptions::new()
.max_connections(20)
.min_connections(5)
.acquire_timeout(Duration::from_secs(3))
.idle_timeout(Duration::from_secs(600))
.max_lifetime(Duration::from_secs(1800))
.connect("postgres://user:pass@localhost/db")
.await?;
5. Transactions
Transactions implement Executor, so you can pass them to any function that accepts a pool or connection.
use sqlx::Acquire;
// Manual transaction
let mut tx = pool.begin().await?;
sqlx::query!("INSERT INTO users (name) VALUES ($1)", "Alice")
.execute(&mut *tx)
.await?;
sqlx::query!("INSERT INTO audit_log (action) VALUES ($1)", "user created")
.execute(&mut *tx)
.await?;
tx.commit().await?;
// Dropping tx without commit() triggers automatic rollback.
// Closure-based (auto-commit on Ok, auto-rollback on Err)
// Requires acquiring a connection first — .transaction() is on Connection, not Pool.
let mut conn = pool.acquire().await?;
let result = conn.transaction::<_, _, sqlx::Error>(|tx| {
Box::pin(async move {
sqlx::query!("INSERT INTO users (name) VALUES ($1)", "Bob")
.execute(&mut **tx).await?;
Ok(())
})
}).await?;
// Nested transactions (SAVEPOINTs)
let mut tx = pool.begin().await?;
let mut nested = tx.begin().await?; // Creates a SAVEPOINT — requires `Acquire`
nested.rollback().await?; // Only rolls back to savepoint
tx.commit().await?; // Outer transaction unaffected
6. Generic Functions over Executor
Write functions that work with pools, connections, and transactions alike using the Executor trait:
use sqlx::{Executor, PgExecutor};
async fn find_user_by_id<'e, E>(executor: E, id: i64) -> sqlx::Result<User>
where
E: PgExecutor<'e>,
{
sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
.fetch_one(executor)
.await
}
// Works with pool, connection, or transaction:
let user = find_user_by_id(&pool, 1).await?;
let user = find_user_by_id(&mut tx, 1).await?;
7. FromRow Derive
Map SQL rows to Rust structs with #[derive(FromRow)]. The struct field names must match SQL column names.
use sqlx::FromRow;
#[derive(Debug, FromRow)]
struct User {
id: i64,
name: String,
email: String,
created_at: chrono::DateTime<chrono::Utc>,
}
// Column renaming when names don't match
#[derive(Debug, FromRow)]
struct User {
id: i64,
#[sqlx(rename = "full_name")]
name: String,
}
// Flatten for JOINs (combines fields from multiple tables)
#[derive(Debug, FromRow)]
struct UserWithPost {
#[sqlx(flatten)]
user: User,
#[sqlx(flatten)]
post: Post,
}
// Skip columns from the query that you don't need
#[derive(Debug, FromRow)]
struct UserSummary {
id: i64,
name: String,
#[sqlx(skip)]
_ignored_field: String,
}
8. JSON Columns
use sqlx::types::Json;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct User {
id: i64,
#[sqlx(json)]
metadata: Json<Metadata>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Metadata {
role: String,
preferences: std::collections::HashMap<String, String>,
}
// Insert with JSON value
sqlx::query!(
"INSERT INTO users (name, metadata) VALUES ($1, $2)",
"Alice",
Json(Metadata { role: "admin".into(), preferences: Default::default() })
).execute(&pool).await?;
// Read — Json<T> automatically deserializes
let user: User = sqlx::query_as(
"SELECT id, name, metadata FROM users WHERE id = $1"
).bind(1).fetch_one(&pool).await?;
println!("Role: {}", user.metadata.0.role);
9. QueryBuilder for Dynamic Queries
When SQL structure depends on runtime conditions, use QueryBuilder instead of string concatenation.
use sqlx::QueryBuilder;
let mut qb = QueryBuilder::new("SELECT * FROM users WHERE ");
// Conditionally add clauses
if let Some(name) = &filter.name {
qb.push("name LIKE ").push_bind(format!("%{}%", name));
}
if filter.active_only {
if filter.name.is_some() {
qb.push(" AND ");
}
qb.push("active = ").push_bind(true);
}
let users: Vec<User> = qb.build_query_as::<User>()
.fetch_all(&pool)
.await?;
10. Migrations
# Create a new migration
sqlx migrate add create_users
# Run all pending migrations
sqlx migrate run
# Rollback the last migration
sqlx migrate revert
# Show migration status
sqlx migrate info
Embed and run migrations in code:
let migrator = sqlx::migrate!(); // Reads from ./migrations by default
migrator.run(&pool).await?;
11. #[sqlx::test] Attribute
Automatically creates an isolated test database, runs migrations, and cleans up afterward.
#[sqlx::test(migrations = "./migrations")]
async fn test_create_user(pool: PgPool) -> sqlx::Result<()> {
sqlx::query!("INSERT INTO users (name) VALUES ($1)", "Alice")
.execute(&pool).await?;
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE name = $1", "Alice")
.fetch_one(&pool).await?;
assert_eq!(user.name, "Alice");
Ok(())
}
Placeholder Syntax by Database
This is a frequent source of errors. Different databases use different placeholders:
| Database | Placeholder | Example |
|---|---|---|
| PostgreSQL | $1, $2, $3... |
WHERE id = $1 AND name = $2 |
| MySQL | ? |
WHERE id = ? AND name = ? |
| SQLite | ? |
WHERE id = ? AND name = ? |
Compile-time macros handle this automatically based on DATABASE_URL. Runtime queries must use the correct syntax for the target database.
Key Imports Reference
use sqlx::{
// Query functions (runtime, no compile-time checking)
query, query_as, query_scalar,
// Traits
Executor, FromRow, Decode, Encode, Type,
// Pool types
PgPool, MySqlPool, SqlitePool, AnyPool, Pool,
// Connection types
PgConnectOptions, Transaction,
// Error types
Error, Result,
// JSON support
types::Json,
// PostgreSQL extras
postgres::{PgConnectOptions, PgListener, PgPoolOptions, PgSslMode},
// MySQL extras
mysql::{MySqlConnectOptions, MySqlPoolOptions},
// SQLite extras
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteJournalMode},
};
Common Pitfalls
-
Missing DATABASE_URL at compile time —
query!()macros connect to the DB duringcargo build. SetDATABASE_URLin.envor use offline mode withcargo sqlx prepare. -
Missing runtime feature flag — Always include
runtime-tokioorruntime-async-stdin features. Without it, pool creation will panic. -
Missing database feature flag — Just adding
sqlx = "0.8"without a DB feature only enablesany,json,macros,migrate, andderive. Always add e.g.features = ["postgres"]. -
Wrong placeholder syntax — PostgreSQL uses
$1, MySQL/SQLite use?. Compile-time macros enforce this; runtime queries do not. -
Transaction dereference — Execute queries on a transaction with
&mut *tx(not&tx). -
SQLite boolean mapping — Booleans are stored as INTEGER (0/1). Use integers in SQL comparisons.
-
Slow compile times with many queries — Use offline mode, or consider
query_unchecked!()for non-critical queries. -
RETURNING clause — PostgreSQL supports
RETURNING *in INSERT/UPDATE. MySQL does not (useLAST_INSERT_ID()instead).
Reference Files
For detailed information on any topic, read the appropriate reference file:
references/compile-time-checking.md— query!, query_as!, query_scalar!, type overrides, unchecked variants, offline mode, .sqlx/ directory, query filesreferences/connection-pooling.md— Pool options, connect options, PgConnectOptions, SSL modes, after_connect hooks, pool lifecyclereferences/type-mapping.md— Full Rust-to-SQL type table, feature-flag types, FromRow attributes, custom Encode/Decode, transparent types, enum mappingreferences/transactions-migrations.md— Transactions (manual, closure, nested), reversible migrations, migrate! macro, migration CLI commandsreferences/testing-mocking.md— #[sqlx::test] macro, fixtures, repository pattern for mockability, in-memory SQLite for testsreferences/database-specifics.md— PostgreSQL (PgListener, COPY, arrays, ranges, composite types), MySQL specifics, SQLite (in-memory, journal mode, PRAGMA, extensions)references/error-handling.md— sqlx::Error enum, database error codes, RowNotFound, decode errors, pool errorsreferences/feature-flags.md— Complete feature flag table, Cargo.toml patterns per use case, TLS options, runtime options