rgoussu@goussu: ~/library/rust/storage
~/library/rust/storage cat relational-databases.md

Relational databases from Rust — sqlx, diesel & sea-orm

# The relational spectrum — sqlx's compile-time-checked SQL, diesel's type-safe DSL, sea-orm's async ORM, tokio-postgres underneath — plus pooling, transactions and migrations.

Conceptsaved 2026-08-09 #rust#storage#databases#sqlx#diesel#sea-orm#postgres

Overview

Three philosophies split the Rust relational world. sqlx says write SQL — and verifies every query against a live database at compile time, mapping rows to structs by derive. diesel says don't write SQL — its Rust DSL makes invalid queries unrepresentable, checked against a schema snapshot, no database needed at build time. sea-orm says you want an ORM — entities, ActiveModel mutations, relations, async on top of sqlx's drivers. Underneath sit the raw drivers (tokio-postgres, rusqlite) when you want the wire protocol and nothing else. The ecosystem's center of gravity is sqlx, and its checked-SQL move is the genuinely novel contribution — the position Go's sqlc approximates offline and JPA doesn't attempt.

Key points

  • sqlx: query!/query_as! macros connect to DATABASE_URL at build time, ask the database to prepare the statement, and typecheck columns/parameters against your structs — SQL typos and schema drift become compile errors. Async, works with Postgres/MySQL/SQLite, ships its own pool. Offline mode (cargo sqlx prepare.sqlx/ metadata, committed) keeps CI database-free.
  • The sqlx caveats: dynamic SQL falls back to unchecked query() (or the sea-query builder); the macros' DB connection requirement surprises new contributors; nullability inference occasionally needs as "col!" overrides.
  • diesel: the mature heavyweight — schema.rs generated by diesel CLI, queries as composable typed expressions, excellent performance, sync by design (diesel-async exists). Its compile errors are legendarily verbose; its guarantees are the strongest of the three.
  • sea-orm: entity/ActiveModel codegen from the database, relations, pagination, mock-testing support — the closest thing to a JVM-style ORM experience, chosen by teams that want conventions over SQL; built on sqlx so it inherits the async pool.
  • tokio-postgres when raw: pipelined, fast, LISTEN/NOTIFY support; pool with deadpool-postgres/bb8; rusqlite is the SQLite equivalent (sync, embedded).
  • Transactions: let mut tx = pool.begin().await? → pass &mut *tx to queries → tx.commit().await?; dropping the value rolls back — cleanup-by-RAII making the forgotten-rollback bug structurally impossible.
  • Migrations: sqlx's embedded migrator (sqlx migrate + migrate!() at startup), refinery standalone, diesel's own — plain SQL files by convention; #[sqlx::test] runs them per test database.
  • Choosing: sqlx by default; diesel when maximum compile-time safety on complex queries pays; sea-orm when the team wants entity conventions; raw driver for pipelining-critical paths and LISTEN/NOTIFY plumbing.

Examples

#[derive(sqlx::FromRow)]
struct User { id: Uuid, email: String, created_at: OffsetDateTime }

async fn find(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<User>> {
    sqlx::query_as!(User,
        r#"SELECT id, email, created_at FROM users WHERE id = $1"#, id)
        .fetch_optional(pool)
        .await
}

async fn transfer(pool: &PgPool, from: Uuid, to: Uuid, cents: i64) -> sqlx::Result<()> {
    let mut tx = pool.begin().await?;
    sqlx::query!("UPDATE accounts SET balance = balance - $1 WHERE id = $2", cents, from)
        .execute(&mut *tx).await?;
    sqlx::query!("UPDATE accounts SET balance = balance + $1 WHERE id = $2", cents, to)
        .execute(&mut *tx).await?;
    tx.commit().await?          // drop without commit = rollback
}

Related