Skip to Content
Course content

174: SQLx Compile-Time Checked Queries

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent way too many hours of my career chasing down bugs that were essentially just typos in a SQL string. You know the feeling: you write a query, it looks perfect, the Rust compiler gives you a thumbs up, and then—boom—the app crashes in production because you wrote user_id instead of userId.

The Danger of Runtime SQL Errors

Let's look at how we usually handle this. I'm building a simple task manager, and I've got a tasks table. I'll start with the standard sqlx::query approach, which is what most people do first.

async fn get_task_title(pool: &PgPool, id: i32) -> Result {
    let row = sqlx::query("SELECT title FROM tasks WHERE id = $1")
        .bind(id)
        .fetch_one(pool)
        .await?;

    let title: String = row.get("title");
    Ok(title)
}

This looks fine. But watch what happens if I make a tiny mistake. I'll change title to tittle in the SELECT statement. I hit cargo build, and... it compiles perfectly. The compiler has no idea what's inside that string. It just sees a string. The error only manifests when this function actually runs, which is the worst possible time to find out you can't spell.

Letting the Compiler Read the Database

This is where SQLx does something almost magical. It provides macros—specifically query!—that actually connect to your database during the compilation process. It sends the SQL to the DB, asks "Is this valid?", and checks the types of the columns being returned.

I'll try replacing my query with the macro version:

async fn get_task_title(pool: &PgPool, id: i32) -> Result {
    let row = sqlx::query!("SELECT tittle FROM tasks WHERE id = $1", id)
        .fetch_one(pool)
        .await?;

    Ok(row.tittle) 
}

Now, when I run cargo build, the compiler screams at me. It doesn't just say "there's an error"; it tells me exactly what's wrong: column "tittle" does not exist. This is incredible. The Rust compiler is now acting as a SQL linter.

Wait, I just realized I'm getting a new error: can't find DATABASE_URL in environment. This is the "gotcha" with compile-time checks. Since the compiler needs to talk to the DB to verify the SQL, it needs a connection string. I'll create a .env file in the project root:

DATABASE_URL=postgres://user:password@localhost/task_db

Once that's in place, the build succeeds (after I fix my typo, obviously). But there's a catch: I don't want my CI/CD pipeline to need a live database just to compile the code. SQLx handles this with "offline mode." I can run cargo sqlx prepare, and it saves a sqlx-data.json file. This file acts as a cached snapshot of the database schema, allowing the macros to verify queries without a live connection.

Mapping to Structs without the Boilerplate

One thing that always annoyed me about query! is that it returns an anonymous record (a struct created on the fly). If I want to pass that data around my application, I usually want it in a proper struct Task. I used to manually map the fields, but query_as! does the heavy lifting for me.

Let's refine my task fetcher to use a struct:

#[derive(Debug)]
struct Task {
    id: i32,
    title: String,
    is_completed: bool,
}

async fn get_task(pool: &PgPool, id: i32) -> Result {
    // Note the 'as Task' part isn't needed; the macro handles the mapping
    // based on the struct fields and the column names.
    let task = sqlx::query_as!(
        Task, 
        "SELECT id, title, is_completed FROM tasks WHERE id = $1", 
        id
    )
    .fetch_one(pool)
    .await?;

    Ok(task)
}

The beauty here is that if I add a new column to the Task struct but forget to add it to the SELECT statement, the compiler will flag it. It ensures that my Rust types and my database schema are in perfect sync. If the DB column is nullable, SQLx will force the Rust field to be an Option<T>. It effectively eliminates a whole class of "type mismatch" runtime crashes.




📋 Practical Task

Implementing a Type-Safe Project Task Filter

You are tasked with adding a filtering feature to the task manager. You need to create a function that retrieves all tasks that are NOT yet completed, but only for a specific project ID.

  • Database Schema: Assume a table tasks with columns id (int), project_id (int), title (text), and is_completed (bool).
  • Requirement 1: Define a Task struct that matches these columns.
  • Requirement 2: Write an async fn get_incomplete_tasks_for_project that takes a &PgPool and a project_id: i32.
  • Requirement 3: Use the sqlx::query_as! macro to fetch the results. The query should filter by project_id and ensure is_completed is false.
  • Requirement 4: Ensure the function returns a Result<Vec<Task>, sqlx::Error>.

Your goal is to ensure that if you were to accidentally misspell project_id as proj_id in the query, the code would fail to compile rather than crashing at runtime.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.