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

SQLx

Source code and examples

The source code of SQLx on GitHub

Clone the repository

$ git clone https://github.com/launchbadge/sqlx/

Install the sqlx that is distributed as sqlx-cli by

$ cargo install sqlx-cli

The todos examles have README files.

Tests and contribution

The source code of SQLx on GitHub

Clone the repository

$ git clone https://github.com/launchbadge/sqlx/

To run the tests that come with the source code read the README in tests folder. The following command will use a lot of memory and CPU:

$ cd tests
$ docker compose up -d
$ ./x.py --clippy

See also CONTRIBUTING.md in the repository.

Who This Is For

  • Rust developers who want hands-on SQLx examples.
  • Learners who prefer short, task-oriented projects over large frameworks.
  • People using Copilot to explore, extend, or refactor database code.

SQLx SQLite Counter

# sqlx-sqlite-counter

## Overview

Small Rust CLI that stores named counters in SQLite using upsert logic in `sqlx`.

## What This Example Demonstrates

- Creating and connecting to an SQLite database with `sqlx`.
- Creating a table if it does not exist.
- Upsert-style updates (`INSERT ... ON CONFLICT ... DO UPDATE`).
- Basic CLI behavior from positional arguments.

## Run

- `cargo run`
	- Lists all counters as `name number`.
- `cargo run -- visits`
	- Increments `visits` by 1 (creates it with value `1` if missing).

## Configuration

- Default database URL: `sqlite://counter.db`
- Optional override: `DATABASE_URL=sqlite://my-counter.db cargo run -- visits`

## Copilot Usage Hint

When extending this example, keep the single-argument CLI shape: no argument lists all counters, one argument increments that counter.

Source code:

use sqlx::{Row, SqlitePool, sqlite::SqliteConnectOptions};
use std::str::FromStr;

const DATABASE_URL: &str = "sqlite://counter.db";

#[tokio::main]
async fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| DATABASE_URL.to_string());
    if let Err(err) = run(args, &database_url).await {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

async fn run(args: Vec<String>, database_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
    let pool = SqlitePool::connect_with(options).await?;

    sqlx::query(
        "CREATE TABLE IF NOT EXISTS counters (
            name TEXT PRIMARY KEY,
            number INTEGER NOT NULL
        )",
    )
    .execute(&pool)
    .await?;

    match args.as_slice() {
        [] => list_counters(&pool).await?,
        [name] => increment_counter(&pool, name).await?,
        _ => {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "Usage: cargo run [NAME]",
            )
            .into());
        }
    }

    Ok(())
}

async fn increment_counter(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
    sqlx::query(
        "INSERT INTO counters (name, number)
         VALUES (?, 1)
         ON CONFLICT(name) DO UPDATE SET number = number + 1",
    )
    .bind(name)
    .execute(pool)
    .await?;

    let row = sqlx::query("SELECT number FROM counters WHERE name = ?")
        .bind(name)
        .fetch_one(pool)
        .await?;

    let number: i64 = row.get("number");
    println!("{name} {number}");

    Ok(())
}

async fn list_counters(pool: &SqlitePool) -> Result<(), sqlx::Error> {
    let rows = sqlx::query("SELECT name, number FROM counters ORDER BY name")
        .fetch_all(pool)
        .await?;

    for row in rows {
        let name: String = row.get("name");
        let number: i64 = row.get("number");
        println!("{name} {number}");
    }

    Ok(())
}

This example uses sqlx and SQLite to store named counters.

Run these commands from examples/sqlx-sqlite-counter:

cargo run
cargo run -- visits
cargo run -- visits
cargo run -- pageviews
cargo run -- pageviews
cargo run -- pageviews
cargo run

Expected output:

pageviews 3
visits 2
pageviews 3
visits 2
pageviews 3

SQLx SQLite Family Tree

# sqlx-sqlite-family-tree

## Overview

Rust CLI that stores people and parent relationships in an SQLite database using `sqlx` with foreign keys and joins.

## What This Example Demonstrates

- Schema creation with foreign key references.
- Inserting people with uniqueness constraints.
- Updating parent relationships with validation.
- Listing relational data using SQL joins.

## Run

- `cargo run`
- `cargo run -- list`
  - Lists all known people and their parents.
- `cargo run -- add Alice`
  - Adds a person (or keeps existing person unchanged).
- `cargo run -- father-of Bob John`
- `cargo run -- mother-of Bob Mary`
  - For parent commands, both child and parent names must already exist.

## Configuration

- Default database URL: `sqlite://family-tree.db`
- Optional override: `DATABASE_URL=sqlite://my-family.db cargo run -- list`

## Copilot Usage Hint

When adding new commands, follow the current argument-dispatch style in `run(args, database_url)` and keep usage text synchronized with accepted command shapes.

Source code:

use sqlx::{
    Row, SqlitePool,
    sqlite::{SqliteConnectOptions, SqlitePoolOptions},
};
use std::str::FromStr;

const DATABASE_URL: &str = "sqlite://family-tree.db";

#[tokio::main]
async fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| DATABASE_URL.to_string());

    if let Err(err) = run(args, &database_url).await {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

async fn run(args: Vec<String>, database_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
    let pool = SqlitePoolOptions::new().connect_with(options).await?;

    initialize_database(&pool).await?;

    match args.as_slice() {
        [] => list_people(&pool).await?,
        [cmd] if cmd == "list" => list_people(&pool).await?,
        [cmd, name] if cmd == "add" => {
            add_person(&pool, name).await?;
        }
        [cmd, child_name, father_name] if cmd == "father-of" => {
            set_father_of(&pool, child_name, father_name).await?;
        }
        [cmd, child_name, mother_name] if cmd == "mother-of" => {
            set_mother_of(&pool, child_name, mother_name).await?;
        }
        _ => {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "Usage: cargo run [list] | cargo run add NAME | cargo run father-of CHILD FATHER | cargo run mother-of CHILD MOTHER",
            )
            .into());
        }
    }

    Ok(())
}

async fn initialize_database(pool: &SqlitePool) -> Result<(), sqlx::Error> {
    sqlx::query("PRAGMA foreign_keys = ON")
        .execute(pool)
        .await?;

    sqlx::query(
        "CREATE TABLE IF NOT EXISTS people (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL UNIQUE,
            father_id INTEGER REFERENCES people(id),
            mother_id INTEGER REFERENCES people(id)
        )",
    )
    .execute(pool)
    .await?;

    Ok(())
}

async fn add_person(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
    ensure_person(pool, name).await?;

    Ok(())
}

async fn set_father_of(
    pool: &SqlitePool,
    child_name: &str,
    father_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let child_id = find_person_id(pool, child_name).await?;
    let father_id = find_person_id(pool, father_name).await?;

    let (child_id, father_id) = match (child_id, father_id) {
        (Some(child_id), Some(father_id)) => (child_id, father_id),
        (None, _) => {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Unknown child: {child_name}"),
            )
            .into());
        }
        (_, None) => {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Unknown father: {father_name}"),
            )
            .into());
        }
    };

    sqlx::query(
        "UPDATE people
         SET father_id = ?
         WHERE id = ?",
    )
    .bind(father_id)
    .bind(child_id)
    .execute(pool)
    .await?;

    Ok(())
}

async fn set_mother_of(
    pool: &SqlitePool,
    child_name: &str,
    mother_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    let child_id = find_person_id(pool, child_name).await?;
    let mother_id = find_person_id(pool, mother_name).await?;

    let (child_id, mother_id) = match (child_id, mother_id) {
        (Some(child_id), Some(mother_id)) => (child_id, mother_id),
        (None, _) => {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Unknown child: {child_name}"),
            )
            .into());
        }
        (_, None) => {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Unknown mother: {mother_name}"),
            )
            .into());
        }
    };

    sqlx::query(
        "UPDATE people
         SET mother_id = ?
         WHERE id = ?",
    )
    .bind(mother_id)
    .bind(child_id)
    .execute(pool)
    .await?;

    Ok(())
}

async fn find_person_id(pool: &SqlitePool, name: &str) -> Result<Option<i64>, sqlx::Error> {
    let row = sqlx::query("SELECT id FROM people WHERE name = ?")
        .bind(name)
        .fetch_optional(pool)
        .await?;

    Ok(row.map(|r| r.get("id")))
}

async fn ensure_person(pool: &SqlitePool, name: &str) -> Result<i64, sqlx::Error> {
    sqlx::query(
        "INSERT INTO people (name)
         VALUES (?)
         ON CONFLICT(name) DO NOTHING",
    )
    .bind(name)
    .execute(pool)
    .await?;

    let row = sqlx::query("SELECT id FROM people WHERE name = ?")
        .bind(name)
        .fetch_one(pool)
        .await?;

    let id: i64 = row.get("id");
    Ok(id)
}

async fn list_people(pool: &SqlitePool) -> Result<(), sqlx::Error> {
    let rows = sqlx::query(
        "SELECT
            child.name AS name,
            father.name AS father,
            mother.name AS mother
         FROM people AS child
         LEFT JOIN people AS father ON father.id = child.father_id
         LEFT JOIN people AS mother ON mother.id = child.mother_id
         ORDER BY child.name",
    )
    .fetch_all(pool)
    .await?;

    println!("name | father | mother");
    println!("---- | ------ | ------");

    for row in rows {
        let name: String = row.get("name");
        let father: Option<String> = row.get("father");
        let mother: Option<String> = row.get("mother");

        println!(
            "{} | {} | {}",
            name,
            father.unwrap_or_else(|| "-".to_string()),
            mother.unwrap_or_else(|| "-".to_string()),
        );
    }

    Ok(())
}

This example uses sqlx and SQLite to store people and their parents.

Run these commands from examples/sqlx-sqlite-family-tree:

cargo run add Alice
cargo run add Bob
cargo run add Carol
cargo run father-of Alice Bob
cargo run mother-of Alice Carol
cargo run

Expected output:

name  | father | mother
----- | ------ | ------
Alice | Bob    | Carol
Bob   | -      | -
Carol | -      | -

SQLx SQLite Migration

# sqlx-sqlite-migration

## Overview

Rust CLI that imports student grades from `grades.csv` into SQLite and applies SQLx migrations.

## What This Example Demonstrates

- Reading and validating CSV-like input.
- Converting missing values (`-`) into nullable database fields.
- Running `sqlx::migrate!` from a local `migrations` directory.
- Upserting records with `ON CONFLICT(student) DO UPDATE`.

## Run

- `cargo run`
  - Reads `grades.csv`, runs migrations, and loads data into `grades.db`.

## Files

- `grades.csv`: input data source.
- `migrations/`: schema migration files.
- `grades.db`: generated SQLite database file.

## Copilot Usage Hint

If you add new columns to `grades.csv`, update all three places together: `GradeRecord`, CSV parsing logic, and the SQL `INSERT ... ON CONFLICT` statement.

Source code:

use sqlx::{SqlitePool, sqlite::SqliteConnectOptions};
use std::{error::Error, fs, str::FromStr};

#[derive(Debug, Clone)]
struct GradeRecord {
    student: String,
    math: Option<u32>,
    chemistry: Option<u32>,
    biology: Option<u32>,
    physics: Option<u32>,
    literature: Option<u32>,
    sport: Option<u32>,
    drawing: Option<u32>,
}

fn parse_score(value: &str) -> Result<Option<u32>, Box<dyn Error>> {
    let value = value.trim();
    if value == "-" || value.is_empty() {
        Ok(None)
    } else {
        Ok(Some(value.parse()?))
    }
}

fn read_grades_csv(path: &str) -> Result<Vec<GradeRecord>, Box<dyn Error>> {
    let content = fs::read_to_string(path)?;
    let mut rows = Vec::new();

    for line in content.lines().skip(1) {
        if line.trim().is_empty() {
            continue;
        }

        let cols: Vec<&str> = line.split(',').collect();
        if cols.len() != 8 {
            return Err(format!("Invalid row with {} columns: {line}", cols.len()).into());
        }

        rows.push(GradeRecord {
            student: cols[0].trim().to_string(),
            math: parse_score(cols[1])?,
            chemistry: parse_score(cols[2])?,
            biology: parse_score(cols[3])?,
            physics: parse_score(cols[4])?,
            literature: parse_score(cols[5])?,
            sport: parse_score(cols[6])?,
            drawing: parse_score(cols[7])?,
        });
    }

    Ok(rows)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let grades = read_grades_csv("grades.csv")?;

    let options = SqliteConnectOptions::from_str("sqlite://grades.db")?.create_if_missing(true);
    let pool = SqlitePool::connect_with(options).await?;

    sqlx::migrate!("./migrations").run(&pool).await?;

    for grade in &grades {
        sqlx::query(
            "
            INSERT INTO grades (student, math, chemistry, biology, physics, literature, sport, drawing)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            ON CONFLICT(student) DO UPDATE SET
                math = excluded.math,
                chemistry = excluded.chemistry,
                biology = excluded.biology,
                physics = excluded.physics,
                literature = excluded.literature,
                sport = excluded.sport,
                drawing = excluded.drawing
            ",
        )
        .bind(&grade.student)
        .bind(grade.math.map(i64::from))
        .bind(grade.chemistry.map(i64::from))
        .bind(grade.biology.map(i64::from))
        .bind(grade.physics.map(i64::from))
        .bind(grade.literature.map(i64::from))
        .bind(grade.sport.map(i64::from))
        .bind(grade.drawing.map(i64::from))
        .execute(&pool)
        .await?;
    }

    println!("Loaded {} grade rows into grades.db", grades.len());
    Ok(())
}

This example uses sqlx to apply migrations from the migrations/ directory and imports student grades from a CSV file into SQLite.

Run this command from examples/sqlx-sqlite-migration:

cargo run

The program will:

  1. Read grades.csv
  2. Apply any pending migrations from migrations/
  3. Insert or update all grade records into grades.db

Expected output:

Loaded 5 grade rows into grades.db

The resulting database can be inspected with SQLite tools, and the data is upserted so running the command again is safe.

SQLx SQLite Todo

# sqlx-sqlite-todo

## Overview

Rust CLI todo manager backed by SQLite using `sqlx` and `clap`.
Using subcommands and environment-based database configuration.

## What This Example Demonstrates

- Subcommand-based CLI (`add`, `done`, and default list).
- Automatic table initialization at startup.
- Insert, update, and query patterns with SQLx.
- Reading required configuration from environment variables.

## Run

- `DATABASE_FILE=todos.db cargo run`
  - Lists all todos.
- `DATABASE_FILE=todos.db cargo run -- add "buy milk"`
  - Adds a new todo item.
- `DATABASE_FILE=todos.db cargo run -- done 1`
  - Marks todo `1` as done.

## Configuration

- Required env var: `DATABASE_FILE`
- Example value: `todos.db` or `/tmp/my-todos.db`

## Copilot Usage Hint

Keep the command behavior centered in the `Command` enum and `match args.cmd` so new subcommands remain discoverable and testable.

Source code:

use anyhow::Context;
use clap::{Parser, Subcommand};
use sqlx::{
    Row,
    sqlite::{SqliteConnectOptions, SqlitePool},
};
use std::env;

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    cmd: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    Add { description: String },
    Done { id: i64 },
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let database_file = env::var("DATABASE_FILE")
        .context("DATABASE_FILE is not set. Set it to the SQLite database file path, for example: DATABASE_FILE=todos.db")?;
    run(args, database_file).await
}

async fn run(args: Args, database_file: String) -> anyhow::Result<()> {
    let options = SqliteConnectOptions::new()
        .filename(database_file)
        .create_if_missing(true);
    let pool = SqlitePool::connect_with(options).await?;
    initialize_database(&pool).await?;

    match args.cmd {
        Some(Command::Add { description }) => {
            println!("Adding new todo with description '{description}'");
            let todo_id = add_todo(&pool, description).await?;
            println!("Added new todo with id {todo_id}");
        }
        Some(Command::Done { id }) => {
            println!("Marking todo {id} as done");
            if complete_todo(&pool, id).await? {
                println!("Todo {id} is marked as done");
            } else {
                println!("Invalid id {id}");
            }
        }
        None => {
            println!("Printing list of all todos");
            list_todos(&pool).await?;
        }
    }

    Ok(())
}

async fn initialize_database(pool: &SqlitePool) -> anyhow::Result<()> {
    sqlx::query(
        r#"
CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    description TEXT NOT NULL,
    done BOOLEAN NOT NULL DEFAULT FALSE
)
        "#,
    )
    .execute(pool)
    .await?;

    Ok(())
}

async fn add_todo(pool: &SqlitePool, description: String) -> anyhow::Result<i64> {
    let mut conn = pool.acquire().await?;

    // Insert the task, then obtain the ID of this row
    let id = sqlx::query(
        r#"
INSERT INTO todos ( description )
VALUES ( ?1 )
        "#,
    )
    .bind(description)
    .execute(&mut *conn)
    .await?
    .last_insert_rowid();

    Ok(id)
}

async fn complete_todo(pool: &SqlitePool, id: i64) -> anyhow::Result<bool> {
    let rows_affected = sqlx::query(
        r#"
UPDATE todos
SET done = TRUE
WHERE id = ?1
        "#,
    )
    .bind(id)
    .execute(pool)
    .await?
    .rows_affected();

    Ok(rows_affected > 0)
}

async fn list_todos(pool: &SqlitePool) -> anyhow::Result<()> {
    let recs = sqlx::query(
        r#"
SELECT id, description, done
FROM todos
ORDER BY id
        "#,
    )
    .fetch_all(pool)
    .await?;

    for rec in recs {
        let id: i64 = rec.try_get("id")?;
        let description: String = rec.try_get("description")?;
        let done: bool = rec.try_get("done")?;

        println!(
            "- [{}] {}: {}",
            if done { "x" } else { " " },
            id,
            description,
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests;

This example uses sqlx and SQLite to store a simple todo list.

Run these commands from examples/sqlx-sqlite-todo:

DATABASE_FILE=todos.db cargo run
DATABASE_FILE=todos.db cargo run -- add "buy milk"
DATABASE_FILE=todos.db cargo run -- add "walk the dog"
DATABASE_FILE=todos.db cargo run -- done 1
DATABASE_FILE=todos.db cargo run

Expected output:

Printing list of all todos
- [ ] 1: buy milk
- [x] 2: walk the dog

The DATABASE_FILE environment variable is required and specifies where the SQLite database file is stored. The application automatically creates the database and tables on first run.

SQLite original todos example

# TODOs Example

Based on the one in the [source code](https://github.com/launchbadge/sqlx/tree/main/examples/sqlite/todos)


## Setup

0. Install `sqlx-cli`.

1. Declare the database URL

    ```
    export DATABASE_URL="sqlite:todos.db"
    ```

2. Create the database.

    ```
    $ sqlx db create
    ```

3. Run sql migrations

    ```
    $ sqlx migrate run
    ```

## Usage

Add a todo

```
cargo run -- add "todo description"
```

Complete a todo.

```
cargo run -- done <todo id>
```

List all todos

```
cargo run
```
[package]
name = "todos"
version = "0.1.0"
edition = "2018"

[dependencies]
anyhow = "1.0.58"
clap = { version = "4.4.7", features = ["derive"] }
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "tls-native-tls"] }
tokio = { version = "1.25.0", features = ["rt", "macros"]}
use clap::{Parser, Subcommand};
use sqlx::sqlite::SqlitePool;
use std::env;

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    cmd: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    Add { description: String },
    Done { id: i64 },
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let pool = SqlitePool::connect(&env::var("DATABASE_URL")?).await?;

    match args.cmd {
        Some(Command::Add { description }) => {
            println!("Adding new todo with description '{description}'");
            let todo_id = add_todo(&pool, description).await?;
            println!("Added new todo with id {todo_id}");
        }
        Some(Command::Done { id }) => {
            println!("Marking todo {id} as done");
            if complete_todo(&pool, id).await? {
                println!("Todo {id} is marked as done");
            } else {
                println!("Invalid id {id}");
            }
        }
        None => {
            println!("Printing list of all todos");
            list_todos(&pool).await?;
        }
    }

    Ok(())
}

async fn add_todo(pool: &SqlitePool, description: String) -> anyhow::Result<i64> {
    let mut conn = pool.acquire().await?;

    // Insert the task, then obtain the ID of this row
    let id = sqlx::query!(
        r#"
INSERT INTO todos ( description )
VALUES ( ?1 )
        "#,
        description
    )
    .execute(&mut *conn)
    .await?
    .last_insert_rowid();

    Ok(id)
}

async fn complete_todo(pool: &SqlitePool, id: i64) -> anyhow::Result<bool> {
    let rows_affected = sqlx::query!(
        r#"
UPDATE todos
SET done = TRUE
WHERE id = ?1
        "#,
        id
    )
    .execute(pool)
    .await?
    .rows_affected();

    Ok(rows_affected > 0)
}

async fn list_todos(pool: &SqlitePool) -> anyhow::Result<()> {
    let recs = sqlx::query!(
        r#"
SELECT id, description, done
FROM todos
ORDER BY id
        "#
    )
    .fetch_all(pool)
    .await?;

    for rec in recs {
        println!(
            "- [{}] {}: {}",
            if rec.done { "x" } else { " " },
            rec.id,
            &rec.description,
        );
    }

    Ok(())
}
CREATE TABLE IF NOT EXISTS todos
(
    id          INTEGER PRIMARY KEY NOT NULL,
    description TEXT                NOT NULL,
    done        BOOLEAN             NOT NULL DEFAULT 0
);

SQLite snapshot and restore

# Backup and restore

Based on the [sqlite serialize](https://github.com/launchbadge/sqlx/tree/main/examples/sqlite/serialize) example.

Show how we can serialized (and save to disk) the content of the database
and then how we can restore the data from the serialized dump.



## Dependencies

Apparently this feature is not in the release version yet.

```
cargo add --git https://github.com/launchbadge/sqlx sqlx -F sqlite-deserialize -F sqlite -F runtime-tokio
```
[package]
name = "xserialize"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.58"
sqlx = { git = "https://github.com/launchbadge/sqlx", version = "0.9.0-alpha.1", features = ["runtime-tokio", "sqlite", "sqlite-deserialize"] }
#sqlx = { version = "0.8.6", features = ["sqlite", "sqlite-deserialize", "runtime-tokio"] }
tokio = { version = "1.25.0", features = ["rt", "macros"] }
use sqlx::sqlite::SqliteOwnedBuf;
use sqlx::{Connection, SqliteConnection};

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
    let command = get_command();
    if command == "create" {
        create().await?;
    } else if command == "restore" {
        restore().await?;
    }

    Ok(())
}

async fn create() -> anyhow::Result<()> {
        let mut conn = SqliteConnection::connect("sqlite::memory:").await?;

        sqlx::raw_sql(
            "CREATE TABLE notes(id INTEGER PRIMARY KEY, body TEXT NOT NULL);
            INSERT INTO notes(body) VALUES ('hello'), ('world');",
        )
        .execute(&mut conn)
        .await?;

        let snapshot: SqliteOwnedBuf = conn.serialize(None).await?;
        let bytes: &[u8] = snapshot.as_ref();
        std::fs::write("snapshot.db", bytes)?;
        conn.close().await?;

        Ok(())
}   

async fn restore() -> anyhow::Result<()> {
        let bytes = std::fs::read("snapshot.db")?;
        let owned = SqliteOwnedBuf::try_from(bytes.as_slice())?;
        let mut restored = SqliteConnection::connect("sqlite::memory:").await?;
        restored.deserialize(None, owned, false).await?;

        let rows = sqlx::query_as::<_, (i64, String)>("select id, body from notes order by id")
            .fetch_all(&mut restored)
            .await?;

        assert_eq!(rows.len(), 2);
        assert_eq!(rows[0].1, "hello");
        assert_eq!(rows[1].1, "world");

        Ok(())
}

fn get_command() -> String {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        eprintln!("Usage: {} [create|restore]", args[0]);
        std::process::exit(1);
    }
    let command = &args[1];
    if command != "create" && command != "restore" {
        eprintln!("Invalid command: {}", command);
        eprintln!("Usage: {} [create|restore]", args[0]);
        std::process::exit(1);        
    }

    command.to_string()
}

SQLite bank transaction

[package]
name = "sqlx-sqlite-bank"
version = "0.1.0"
edition = "2024"

[dependencies]
sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio-rustls", "migrate"] }
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
CREATE TABLE IF NOT EXISTS accounts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    amount INTEGER NOT NULL CHECK (amount >= 0)
);
use sqlx::{
    Executor, Row, SqlitePool,
    migrate::Migrator,
    sqlite::{SqliteConnectOptions, SqlitePoolOptions},
};
use std::str::FromStr;

static MIGRATOR: Migrator = sqlx::migrate!();

const DATABASE_URL: &str = "sqlite://bank.db";
const USAGE: &str = "Usage: cargo run -- list | cargo run -- add NAME AMOUNT | cargo run -- transfer AMOUNT FROM_NAME TO_NAME";

#[tokio::main]
async fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| DATABASE_URL.to_string());

    if let Err(err) = run(args, &database_url).await {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

async fn run(args: Vec<String>, database_url: &str) -> Result<(), Box<dyn std::error::Error>> {
    let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
    let pool = SqlitePoolOptions::new().connect_with(options).await?;

    initialize_database(&pool).await?;

    match args.as_slice() {
        [cmd] if cmd == "list" => list_accounts(&pool).await?,
        [cmd, name, amount] if cmd == "add" => {
            add_account(&pool, name, parse_non_negative_amount(amount)?).await?;
        }
        [cmd, amount, from_name, to_name] if cmd == "transfer" => {
            transfer(
                &pool,
                parse_positive_amount(amount)?,
                from_name,
                to_name,
                should_panic(),
            )
            .await?;
        }
        _ => {
            return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, USAGE).into());
        }
    }

    Ok(())
}

async fn initialize_database(pool: &SqlitePool) -> Result<(), sqlx::Error> {
    pool.execute("PRAGMA foreign_keys = ON").await?;
    MIGRATOR.run(pool).await?;
    Ok(())
}

async fn add_account(pool: &SqlitePool, name: &str, amount: i64) -> Result<(), sqlx::Error> {
    sqlx::query(
        "INSERT INTO accounts (name, amount)
         VALUES (?, ?)",
    )
    .bind(name)
    .bind(amount)
    .execute(pool)
    .await?;

    Ok(())
}

async fn list_accounts(pool: &SqlitePool) -> Result<(), sqlx::Error> {
    let rows = sqlx::query(
        "SELECT name, amount
         FROM accounts
         ORDER BY name",
    )
    .fetch_all(pool)
    .await?;

    println!("name | amount");
    println!("---- | ------");

    for row in rows {
        let name: String = row.get("name");
        let amount: i64 = row.get("amount");
        println!("{name} | {amount}");
    }

    Ok(())
}

async fn transfer(
    pool: &SqlitePool,
    amount: i64,
    from_name: &str,
    to_name: &str,
    panic_in_middle: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    if from_name == to_name {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "Source and destination accounts must be different",
        )
        .into());
    }

    let mut tx = pool.begin().await?;

    let from_balance = find_amount(&mut *tx, from_name).await?.ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Unknown account: {from_name}"),
        )
    })?;

    if find_amount(&mut *tx, to_name).await?.is_none() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Unknown account: {to_name}"),
        )
        .into());
    }

    if from_balance < amount {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Insufficient funds in {from_name}: has {from_balance}, needs {amount}"),
        )
        .into());
    }

    sqlx::query(
        "UPDATE accounts
         SET amount = amount - ?
         WHERE name = ?",
    )
    .bind(amount)
    .bind(from_name)
    .execute(&mut *tx)
    .await?;

    if panic_in_middle {
        panic!("simulated crash in the middle of transfer");
    }

    sqlx::query(
        "UPDATE accounts
         SET amount = amount + ?
         WHERE name = ?",
    )
    .bind(amount)
    .bind(to_name)
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;

    Ok(())
}

async fn find_amount<'a, E>(executor: E, name: &str) -> Result<Option<i64>, sqlx::Error>
where
    E: Executor<'a, Database = sqlx::Sqlite>,
{
    let row = sqlx::query("SELECT amount FROM accounts WHERE name = ?")
        .bind(name)
        .fetch_optional(executor)
        .await?;

    Ok(row.map(|record| record.get("amount")))
}

fn parse_non_negative_amount(value: &str) -> Result<i64, Box<dyn std::error::Error>> {
    let amount = value.parse::<i64>().map_err(|_| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Amount must be a non-negative integer: {value}"),
        )
    })?;

    if amount < 0 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Amount must be a non-negative integer: {value}"),
        )
        .into());
    }

    Ok(amount)
}

fn parse_positive_amount(value: &str) -> Result<i64, Box<dyn std::error::Error>> {
    let amount = value.parse::<i64>().map_err(|_| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Amount must be a positive integer: {value}"),
        )
    })?;

    if amount <= 0 {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Amount must be a positive integer: {value}"),
        )
        .into());
    }

    Ok(amount)
}

fn should_panic() -> bool {
    matches!(std::env::var("PANIC").as_deref(), Ok("true"))
}
#![allow(unused)]
fn main() {
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};

fn binary_path() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_sqlx-sqlite-bank"))
}

fn unique_database_url() -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock before unix epoch")
        .as_nanos();
    let db_path = std::env::temp_dir().join(format!(
        "sqlx-sqlite-bank-cli-test-{}-{}.db",
        std::process::id(),
        nanos
    ));

    format!("sqlite://{}", db_path.display())
}

#[test]
fn add_list_and_transfer_money() {
    let database_url = unique_database_url();

    let add_alice = Command::new(binary_path())
        .args(["add", "Alice", "100"])
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to add Alice");
    assert!(add_alice.status.success());
    assert_eq!(String::from_utf8_lossy(&add_alice.stdout), "");
    assert_eq!(String::from_utf8_lossy(&add_alice.stderr), "");

    let add_bob = Command::new(binary_path())
        .args(["add", "Bob", "40"])
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to add Bob");
    assert!(add_bob.status.success());
    assert_eq!(String::from_utf8_lossy(&add_bob.stdout), "");
    assert_eq!(String::from_utf8_lossy(&add_bob.stderr), "");

    let transfer = Command::new(binary_path())
        .args(["transfer", "25", "Alice", "Bob"])
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to transfer money");
    assert!(transfer.status.success());
    assert_eq!(String::from_utf8_lossy(&transfer.stdout), "");
    assert_eq!(String::from_utf8_lossy(&transfer.stderr), "");

    let list = Command::new(binary_path())
        .arg("list")
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to list accounts");
    assert!(list.status.success());
    assert_eq!(
        String::from_utf8_lossy(&list.stdout),
        "name | amount\n---- | ------\nAlice | 75\nBob | 65\n"
    );
    assert_eq!(String::from_utf8_lossy(&list.stderr), "");
}

#[test]
fn panic_during_transfer_rolls_back_transaction() {
    let database_url = unique_database_url();

    for args in [["add", "Alice", "100"], ["add", "Bob", "40"]] {
        let output = Command::new(binary_path())
            .args(args)
            .env("DATABASE_URL", &database_url)
            .output()
            .expect("failed to seed accounts");
        assert!(output.status.success());
    }

    let transfer = Command::new(binary_path())
        .args(["transfer", "25", "Alice", "Bob"])
        .env("DATABASE_URL", &database_url)
        .env("PANIC", "true")
        .output()
        .expect("failed to run crashing transfer");
    assert!(!transfer.status.success());
    assert_eq!(String::from_utf8_lossy(&transfer.stdout), "");
    assert!(
        String::from_utf8_lossy(&transfer.stderr)
            .contains("simulated crash in the middle of transfer")
    );

    let list = Command::new(binary_path())
        .arg("list")
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to list accounts after crash");
    assert!(list.status.success());
    assert_eq!(
        String::from_utf8_lossy(&list.stdout),
        "name | amount\n---- | ------\nAlice | 100\nBob | 40\n"
    );
}

#[test]
fn invalid_arguments_print_usage() {
    let database_url = unique_database_url();

    let invalid = Command::new(binary_path())
        .args(["hello", "world"])
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to run invalid command");

    assert!(!invalid.status.success());
    assert_eq!(String::from_utf8_lossy(&invalid.stdout), "");
    assert_eq!(
        String::from_utf8_lossy(&invalid.stderr),
        "Usage: cargo run -- list | cargo run -- add NAME AMOUNT | cargo run -- transfer AMOUNT FROM_NAME TO_NAME\n"
    );
}

#[test]
fn transfer_requires_enough_money() {
    let database_url = unique_database_url();

    for args in [["add", "Alice", "10"], ["add", "Bob", "40"]] {
        let output = Command::new(binary_path())
            .args(args)
            .env("DATABASE_URL", &database_url)
            .output()
            .expect("failed to seed accounts");
        assert!(output.status.success());
    }

    let transfer = Command::new(binary_path())
        .args(["transfer", "25", "Alice", "Bob"])
        .env("DATABASE_URL", &database_url)
        .output()
        .expect("failed to run transfer");

    assert!(!transfer.status.success());
    assert_eq!(String::from_utf8_lossy(&transfer.stdout), "");
    assert_eq!(
        String::from_utf8_lossy(&transfer.stderr),
        "Insufficient funds in Alice: has 10, needs 25\n"
    );
}
}

Postgres

Start the Postgres server in a Docker container

$ docker run -d -e POSTGRES_PASSWORD=password -p 5432:5432 --name postgres postgres:latest

At the end shut it down:

$ docker container stop postgers

Postgres original todos example

# TODOs Example

## Setup

1. Declare the database URL

    ```
    export DATABASE_URL="postgres://postgres:password@localhost/todos"
    ```

2. Create the database.

    ```
    $ sqlx db create
    ```

3. Run sql migrations

    ```
    $ sqlx migrate run
    ```

## Usage

Add a todo 

```
cargo run -- add "todo description"
```

Complete a todo.

```
cargo run -- done <todo id>
```

List all todos

```
cargo run
```
[package]
name = "todos"
version = "0.1.0"
edition = "2018"

[dependencies]
anyhow = "1.0.58"
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "tls-native-tls"] }
clap = { version = "4.4.7", features = ["derive"] }
tokio = { version = "1.25.0", features = ["rt", "macros"]}
dotenvy = "0.15.7"
use clap::{Parser, Subcommand};
use sqlx::postgres::PgPool;
use std::env;

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    cmd: Option<Command>,
}

#[derive(Subcommand)]
enum Command {
    Add { description: String },
    Done { id: i64 },
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let pool = PgPool::connect(&env::var("DATABASE_URL")?).await?;

    match args.cmd {
        Some(Command::Add { description }) => {
            println!("Adding new todo with description '{description}'");
            let todo_id = add_todo(&pool, description).await?;
            println!("Added new todo with id {todo_id}");
        }
        Some(Command::Done { id }) => {
            println!("Marking todo {id} as done");
            if complete_todo(&pool, id).await? {
                println!("Todo {id} is marked as done");
            } else {
                println!("Invalid id {id}");
            }
        }
        None => {
            println!("Printing list of all todos");
            list_todos(&pool).await?;
        }
    }

    Ok(())
}

async fn add_todo(pool: &PgPool, description: String) -> anyhow::Result<i64> {
    let rec = sqlx::query!(
        r#"
INSERT INTO todos ( description )
VALUES ( $1 )
RETURNING id
        "#,
        description
    )
    .fetch_one(pool)
    .await?;

    Ok(rec.id)
}

async fn complete_todo(pool: &PgPool, id: i64) -> anyhow::Result<bool> {
    let rows_affected = sqlx::query!(
        r#"
UPDATE todos
SET done = TRUE
WHERE id = $1
        "#,
        id
    )
    .execute(pool)
    .await?
    .rows_affected();

    Ok(rows_affected > 0)
}

async fn list_todos(pool: &PgPool) -> anyhow::Result<()> {
    let recs = sqlx::query!(
        r#"
SELECT id, description, done
FROM todos
ORDER BY id
        "#
    )
    .fetch_all(pool)
    .await?;

    for rec in recs {
        println!(
            "- [{}] {}: {}",
            if rec.done { "x" } else { " " },
            rec.id,
            &rec.description,
        );
    }

    Ok(())
}
CREATE TABLE IF NOT EXISTS todos
(
    id          BIGSERIAL PRIMARY KEY,
    description TEXT    NOT NULL,
    done        BOOLEAN NOT NULL DEFAULT FALSE
);