A complete example

Let's put the pieces together. We have teams, each with zero or more players, and two tables that already exist:

CREATE TABLE teams (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE players (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    team_id BIGINT NOT NULL REFERENCES teams(id),
    name TEXT NOT NULL
);

This is PostgreSQL SQL; another backend needs its own identity and foreign-key syntax. Configure a postgresql datasource as in Configure datasources, then write the models and the read:

use canyon_sql::{
    core::Canyon,
    crud::Read,
    macros::{canyon_entity, CanyonMapper, Crud, Fields},
    CanyonResult,
};

#[derive(Debug, Fields, Crud, CanyonMapper)]
#[canyon_entity(table_name = "teams")]
struct Team {
    #[primary_key]
    id: i64,
    name: String,
}

#[derive(Debug, Fields, Crud, CanyonMapper)]
#[canyon_entity(table_name = "players")]
struct Player {
    #[primary_key]
    id: i64,
    #[foreign_key(references = Team::id)]
    team_id: i64,
    name: String,
}

#[tokio::main]
async fn main() -> CanyonResult<()> {
    Canyon::init().await?;

    let teams = Team::find_all().await?;
    for team in teams {
        let players = Player::find_all_by_team(&team).await?;
        println!("{} has {} players", team.name, players.len());
    }

    Ok(())
}

Follow the loop in main: Canyon reads every team, then reads that team's players. Two declarations make this possible:

  • #[foreign_key(references = Team::id)] generates the Rust lookup methods.
  • REFERENCES teams(id) makes PostgreSQL enforce the relationship.

A team with no players prints 0. If either query or row mapping fails, ? returns that error from main; the loop does not quietly skip the team.

You can grow this example in several directions:

  • Filter teams with Team::select_query()? and TeamFieldValue.
  • Add writes with the Insert, Update, or Delete traits.
  • Put writes behind a repository adapter. Read that chapter's table-name limitation first: this example uses teams, not the default team.

The schema, permissions, transaction boundaries, and meaning of a missing row remain application decisions. Canyon handles the repeated database plumbing around them.