Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
50: Connecting to Databases with sqlx or Diesel
I've always found the database layer in Rust to be a bit of a crossroads. You either want a heavyweight ORM that manages your types for you, or you want to write raw SQL but still feel that "if it compiles, it works" safety we love in Rust. For this session, I want to show you how I handle this. We're going to build a simple inventory tracker for a rare book shop—something with a few columns like title, author, and price.
The First Connection Attempt
I'll start with sqlx. I prefer it these days because it's async and it doesn't force me to learn a proprietary query language; it just uses SQL. First, I'll throw sqlx and tokio into my Cargo.toml with the sqlite and runtime-tokio-rustls features.
#[tokio::main]
async fn main() -> Result <(), sqlx::Error> {
let pool = sqlx::SqlitePool::connect("sqlite:books.db").await?;
println!("Connected!");
Ok(())
}
I run this, and... it works. But it's boring. The real magic of sqlx isn't just connecting; it's the compile-time verification. I want the compiler to tell me if my SQL is wrong before I even run the binary. To do that, I need a .env file with a DATABASE_URL, and I need to actually have the database file existing on my disk.
Where did the table go?
Now I'll try to fetch a book. I'm going to use the query! macro. This is where things usually get interesting. I'll write this:
let book = sqlx::query!("SELECT title, author FROM books WHERE id = ?", 1)
.fetch_one(&pool)
.await?;
I hit cargo build and immediately get a wall of red text. The compiler is complaining that it can't find the books table. This is a "failure" that I actually love. Most languages wouldn't tell me this until the app crashed in production. sqlx is literally reaching out to my local development database during compilation to check if that column author actually exists.
I'll jump into my terminal, run a quick CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT, price REAL);, and try building again. Now it compiles. The macro has mapped the SQLite TEXT type directly to a Rust String. No manual mapping, no boilerplate structs for the initial fetch.
The Friction of Type Mapping
But wait. What happens when I want to put this data into a struct? I can't just use query! forever if I'm passing this data around my app. I'll try creating a Book struct and using query_as!.
struct Book {
title: String,
author: String,
}
let book = sqlx::query_as!(Book, "SELECT title, author FROM books WHERE id = ?", 1)
.fetch_one(&pool)
.await?;
This works, but here is a quirk I've run into a dozen times: SQLite columns can be nullable. If my table definition allows NULL for author, sqlx will insist that my struct field be Option<String>. If I try to use a plain String, the compiler will scream at me. I have two choices: change my database schema to NOT NULL or wrap my Rust fields in Option. I'll go with NOT NULL because a book without an author is just a mystery I'm not in the mood to solve today.
Choosing Between sqlx and Diesel
You might be wondering, "Why not Diesel?" I'll be honest: Diesel is a beast. It's an actual ORM. If you're coming from ActiveRecord in Ruby or Hibernate in Java, Diesel will feel more familiar. It generates a huge amount of code based on your schema, and it's incredibly fast because it's mostly synchronous (though async support is evolving).
The trade-off is the learning curve. With sqlx, if you know SQL, you're 90% there. With Diesel, you have to learn "the Diesel way" of writing queries in Rust syntax. If you have a massive project with hundreds of tables and you want a strict, type-safe DSL to manage them, go with Diesel. But for most of my projects, I prefer the transparency of raw SQL checked at compile time. It keeps the "magic" to a minimum.
📋 Practical Task
Build a Persistent Rare Book Catalog
Your task is to extend the book shop example into a working CLI tool. You will need to set up a local SQLite database and implement the following functionality using sqlx:
- The Schema: Create a table named
bookswith columns forid(primary key),title(text, not null),author(text, not null), andprice(real, not null). - The Insertion: Write a function
add_book(pool: &SqlitePool, title: &str, author: &str, price: f64)that inserts a new record into the database. - The Retrieval: Write a function
get_expensive_books(pool: &SqlitePool, min_price: f64)that returns aVecof books that cost more than the specified price. - The Compile Check: Ensure you are using the
query!orquery_as!macros so that the project fails to compile if you intentionally misspell a column name in your SQL strings.
Test your implementation by adding three books and printing only those that cost over $50.00.
There are no comments for now.