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 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 | -      | -