Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Persistence with sqlx

Goal: you can store and query data with sqlx on SQLite: typed parameterized queries, schema-as-code, and constraints enforced by the database itself.

Why sqlx (and not an ORM)

House decision: explicit SQL, compile-time checked when used with the query macro, no DSL to learn. Sinau LMS runs on exactly this. The lab uses the runtime API (sqlx::query(...).bind(...)) so everything works offline; the production pattern adds the query! macro checked against a real schema at build time.

Connect + schema-as-code

#![allow(unused)]
fn main() {
let opts = SqliteConnectOptions::from_str("sqlite:/tmp/app.db")?.create_if_missing(true);
let pool = SqlitePoolOptions::new().max_connections(5).connect_with(opts).await?;
sqlx::query("CREATE TABLE IF NOT EXISTS notes (...)").execute(&pool).await?;
}

A Pool is cloneable and shared — hand one to your axum State. Real projects put the DDL into ordered migrations/*.sql (that is literally Sinau’s server/migrations/ layout); the idea is the same: schema is versioned code, not a GUI adventure.

The query patterns you need

#![allow(unused)]
fn main() {
// insert and get the id back — RETURNING saves a round trip
let id: i64 = sqlx::query("INSERT INTO notes (title) VALUES (?1) RETURNING id")
    .bind(title).fetch_one(&pool).await?.get(0);

// list — map rows by position (or .try_get by column name)
let rows = sqlx::query("SELECT id, title FROM notes ORDER BY id")
    .fetch_all(&pool).await?;

// optional single row
let maybe = sqlx::query("... WHERE id = ?1").bind(id).fetch_optional(&pool).await?;
}

Always .bind() values — never format them into the string. Positional ?1 ?2 keeps the mapping visible.

Constraints: the DB has the last word

title TEXT NOT NULL CHECK (length(trim(title)) > 0)

The app validates for friendly 400s (M5); the constraint guarantees it even if a future code path forgets. Defense in depth — the same reason Sinau’s migrations carry real REFERENCES ... ON DELETE CASCADE clauses.

Lab

labs/m6-notes-db — embedded SQLite file DB, RETURNING inserts, ordered list, optional find, and a demo of the CHECK constraint rejecting empty titles. Tests (4): sequential ids, insert-order listing, constraint rejection (empty AND whitespace), missing-row None.

Done when: tests green, and you can explain what fetch_one vs fetch_optional vs fetch_all each guarantee — and which one you reach for when “not found” is a normal outcome.