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

Introduction

Toasty is an async ORM for Rust. It supports both SQL databases (SQLite, PostgreSQL, MySQL) and NoSQL databases (DynamoDB).

You define your models as Rust structs and annotate them with #[derive(toasty::Model)]. Toasty infers the database schema from your annotated structs — field types map to column types, and attributes like #[key], #[unique], and #[index] control the schema. You can customize the mapping with attributes for table names, column names, and column types. Toasty’s derive macro also generates query builders, create/update builders, and relationship accessors at compile time.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[unique]
    email: String,
}
}

From this definition, Toasty generates:

  • User::create() — a builder for inserting new users
  • User::get_by_id() — fetch a user by primary key
  • User::get_by_email() — fetch a user by the unique email field
  • User::all() — query all users
  • user.update() — a builder for modifying a user
  • user.delete() — remove a user
  • User::fields() — field accessors for building filter expressions

The rest of this guide walks through each feature with examples. By the end, you will know how to define models, set up relationships, query data, and use Toasty’s more advanced features like embedded types, batch operations, and transactions.

What this guide covers

Getting Started

This chapter walks through creating a project, defining a model, and running your first queries.

Create a new project

cargo new my-app
cd my-app

Add the following dependencies to Cargo.toml:

[dependencies]
toasty = { version = "0.1", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] }

The sqlite feature enables the SQLite driver. Toasty also supports postgresql, mysql, and dynamodb — swap the feature flag to use a different database.

Define a model

Replace the contents of src/main.rs with:

use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[unique]
    email: String,
}

async fn __example() -> toasty::Result<()> {
#[tokio::main]
async fn main() -> toasty::Result<()> {
    // Build a Db handle, registering all models
    let mut db = toasty::Db::builder()
        .register::<User>()
        .connect("sqlite::memory:")
        .await?;

    // Create tables based on registered models
    db.push_schema().await?;

    // Create a user
    let user = User::create()
        .name("Alice")
        .email("alice@example.com")
        .exec(&mut db)
        .await?;

    println!("Created: {:?}", user.name);

    // Fetch the user back by primary key
    let found = User::get_by_id(&mut db, &user.id).await?;
    println!("Found: {:?}", found.email);

    Ok(())
}
Ok(())
}

Run it:

cargo run

You should see:

Created: "Alice"
Found: "alice@example.com"

What just happened

The #[derive(toasty::Model)] macro read the User struct and generated several types and methods at compile time:

You wroteToasty generated
struct UserUser::create() — a builder to insert rows
#[key] on idUser::get_by_id() — fetch by primary key
#[auto] on idAuto-generates an ID when you create a user
#[unique] on emailUser::get_by_email() — fetch by email

You did not write any of these methods. They come from the derive macro. The rest of this guide shows everything the macro can generate and how to use it.

Connecting to a database

Db::builder() creates a builder where you register your models and then connect to a database. Every model must be registered before connecting so that Toasty can infer the full database schema — tables, columns, indexes, and relationships between models.

let mut db = toasty::Db::builder()
    .register::<User>()
    .register::<Post>()
    .connect("sqlite::memory:")
    .await?;

The connection URL determines which database driver to use. See Database Setup for connection URLs for each supported database.

Creating tables

db.push_schema() creates all tables and indexes defined by your registered models. See Schema Management for more on managing your database schema.

Defining Models

A model is a Rust struct annotated with #[derive(toasty::Model)]. Each struct maps to a database table and each field maps to a column.

#![allow(unused)]
fn main() {
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,
    email: String,
}
}

This defines a User model that maps to a users table with three columns. In SQLite, the generated table looks like:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL
);

Each struct field becomes a column. Required fields (String, u64, etc.) map to NOT NULL columns. The #[key] attribute marks the primary key, and #[auto] tells Toasty to auto-generate the value on insert.

Supported field types

Toasty supports these Rust types as model fields:

Rust typeDatabase type
boolBoolean
StringText
i8, i16, i32, i64Integer (1, 2, 4, 8 bytes)
u8, u16, u32, u64Unsigned integer
uuid::UuidUUID
Vec<u8>Binary / Blob
Option<T>Nullable version of T

With optional feature flags:

FeatureRust typeDatabase type
rust_decimalrust_decimal::DecimalDecimal
bigdecimalbigdecimal::BigDecimalDecimal
jiffjiff::TimestampTimestamp
jiffjiff::civil::DateDate
jiffjiff::civil::TimeTime
jiffjiff::civil::DateTimeDateTime

Enable feature flags in your Cargo.toml:

[dependencies]
toasty = { version = "0.1", features = ["sqlite", "jiff"] }

Optional fields

Wrap a field in Option<T> to make it nullable. An Option<T> field maps to a nullable column in the database — the column allows NULL values instead of requiring NOT NULL.

#![allow(unused)]
fn main() {
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,
    bio: Option<String>,
}
}

The bio field produces a nullable column:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    bio TEXT          -- nullable, allows NULL
);

When creating a record, optional fields default to NULL if not set:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    bio: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// bio will be NULL in the database, None in Rust
let user = User::create()
    .name("Alice")
    .exec(&mut db)
    .await?;
Ok(())
}
}

Table names

Toasty auto-pluralizes the struct name to derive the table name. User becomes users, Post becomes posts.

Override the table name with #[table]:

#![allow(unused)]
fn main() {
#[derive(Debug, toasty::Model)]
#[table = "people"]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,
}
}

This maps to a table named people instead of the default users.

What gets generated

For a model with basic fields (no relationships or indexes), #[derive(Model)] generates:

Static methods on the model:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
fn __example() {
// Returns a create builder
let _ =
User::create();

// Returns a builder for bulk inserts
let _ =
User::create_many();

// Returns a query builder for all records
let _ =
User::all();

// Returns a query builder with a filter applied
let _ =
User::filter(User::fields().name().eq("Alice"));

// Returns field accessors (for building filter expressions)
let _ =
User::fields();
}
}

Instance methods:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
fn __example(mut user: User) {
// Returns an update builder for this record
let _ =
user.update();

// Returns a delete builder for this record
let _ =
user.delete();
}
}

Builders:

  • The create builder returned by User::create() has a setter method for each non-auto field. Chain setters and call .exec(&mut db) to insert:

    #![allow(unused)]
    fn main() {
    use toasty::Model;
    #[derive(Debug, toasty::Model)]
    struct User {
        #[key]
        #[auto]
        id: u64,
        name: String,
        email: String,
    }
    async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
    let user = User::create()
        .name("Alice")
        .email("alice@example.com")
        .exec(&mut db)
        .await?;
    Ok(())
    }
    }
  • The update builder returned by user.update() has a setter for each field. Only set the fields you want to change:

    #![allow(unused)]
    fn main() {
    use toasty::Model;
    #[derive(Debug, toasty::Model)]
    struct User {
        #[key]
        #[auto]
        id: u64,
        name: String,
        email: String,
    }
    async fn __example(mut user: User, mut db: toasty::Db) -> toasty::Result<()> {
    user.update()
        .name("Bob")
        .exec(&mut db)
        .await?;
    Ok(())
    }
    }
  • The query builder returned by User::all() or User::filter() has methods like .exec(), .first(), .get(), and .collect::<Vec<_>>() to execute the query.

What types can you pass to setters?

Builder setters accept more than just the exact field type. For a String field, you can pass a String, a &str, or even an Option<String>. For numeric fields, you can pass the value directly or a reference. This works through Toasty’s IntoExpr trait, which handles the conversion automatically.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
fn __example() {
// String literal (&str)
let _ =
User::create().name("Alice");

// Owned String
let name = "Bob".to_string();
let _ =
User::create().name(name);

// Reference to a String
let name = "Carol".to_string();
let _ =
User::create().name(&name);
}
}

You don’t need to call .to_string() or .clone() to satisfy the setter — pass the value in whatever form you have it.

Additional methods are generated when you add attributes like #[key], #[unique], and #[index]. The next chapters cover these.

Keys and Auto-Generation

Every model needs a primary key. Toasty uses the #[key] attribute to mark which field (or fields) form the primary key, and #[auto] to optionally auto-generate values for key fields.

Single-field keys

Mark a field with #[key] to make it the primary key:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,
}
}

This generates User::get_by_id() to fetch a user by primary key:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::get_by_id(&mut db, &1).await?;
Ok(())
}
}

Keys without #[auto]

The #[auto] attribute is optional. Without it, you are responsible for providing the key value when creating a record and for ensuring uniqueness:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Country {
    #[key]
    code: String,

    name: String,
}
}
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Country {
    #[key]
    code: String,
    name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let country = Country::create()
    .code("US")
    .name("United States")
    .exec(&mut db)
    .await?;
Ok(())
}
}

Other key types

The primary key field can be any supported type. UUID is a common choice:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: uuid::Uuid,

    name: String,
}
}

When #[auto] is used on a uuid::Uuid field, Toasty generates a UUID v7 (time-ordered) by default. See auto strategies for other options.

Auto-generated values

The #[auto] attribute tells Toasty to generate the field’s value automatically. You don’t set auto fields when creating a record — Toasty fills them in.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// No need to set `id` — it's auto-generated
let user = User::create()
    .name("Alice")
    .exec(&mut db)
    .await?;

// The generated id is available on the returned value
println!("id: {}", user.id);
Ok(())
}
}

Auto strategies

The behavior of #[auto] depends on the field type:

Field type#[auto] behaviorExplicit form
uuid::UuidGenerates a UUID v7#[auto(uuid(v7))]
u64, i64, etc.Auto-incrementing integer#[auto(increment)]

You can specify the strategy explicitly:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct ExampleA {
    #[key]
// UUID v7 (time-ordered, the default for Uuid)
#[auto(uuid(v7))]
id: uuid::Uuid,
    name: String,
}

#[derive(Debug, toasty::Model)]
struct ExampleB {
    #[key]
// UUID v4 (random)
#[auto(uuid(v4))]
id: uuid::Uuid,
    name: String,
}

#[derive(Debug, toasty::Model)]
struct ExampleC {
    #[key]
// Auto-incrementing integer
#[auto(increment)]
id: i64,
    name: String,
}
}

UUID v7 vs v4

UUID v7 values are time-ordered — UUIDs created later sort after earlier ones. This is the default for uuid::Uuid fields because time-ordered keys perform better in database indexes.

UUID v4 values are random with no ordering.

Integer auto-increment

Integer keys use the database’s auto-increment feature:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto(increment)]
    id: i64,

    title: String,
}
}
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto(increment)]
    id: i64,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let post = Post::create()
    .title("Hello World")
    .exec(&mut db)
    .await?;

println!("post id: {}", post.id); // 1, 2, 3, ...
Ok(())
}
}

Auto-increment requires database support. SQLite, PostgreSQL, and MySQL all support auto-incrementing columns. DynamoDB does not.

Composite keys

A composite key uses two or more fields as the primary key. Toasty supports three ways to define composite keys.

Multiple #[key] fields

Mark each key field with #[key]:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Enrollment {
    #[key]
    student_id: u64,

    #[key]
    course_id: u64,

    grade: Option<String>,
}
}

This generates lookup methods that take both fields:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Enrollment {
    #[key]
    student_id: u64,
    #[key]
    course_id: u64,
    grade: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let enrollment = Enrollment::get_by_student_id_and_course_id(
    &mut db, &1, &101
).await?;
Ok(())
}
}

Model-level #[key(...)]

Instead of annotating each field, you can list the key fields in a single #[key(...)] attribute on the struct:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
#[key(student_id, course_id)]
struct Enrollment {
    student_id: u64,

    course_id: u64,

    grade: Option<String>,
}
}

#[key(student_id, course_id)] on the struct is equivalent to putting #[key] on both student_id and course_id. It generates the same lookup methods:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
#[key(student_id, course_id)]
struct Enrollment {
    student_id: u64,
    course_id: u64,
    grade: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let enrollment = Enrollment::get_by_student_id_and_course_id(
    &mut db, &1, &101
).await?;
Ok(())
}
}

This also works for single-field keys — #[key(code)] on the struct is equivalent to #[key] on the code field:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
#[key(code)]
struct Country {
    code: String,

    name: String,
}
}

You cannot mix plain field names with partition/local syntax in the same #[key(...)] attribute. Use one style or the other.

Partition and local keys

For databases like DynamoDB that use partition and sort keys, use the #[key(partition = ..., local = ...)] attribute on the struct:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
#[key(partition = user_id, local = id)]
struct Todo {
    #[auto]
    id: u64,

    title: String,

    user_id: u64,
}
}

The partition field determines which partition the record is stored in. The local field uniquely identifies the record within that partition.

With partition/local keys, Toasty generates methods to query by both fields or by the partition key alone:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
#[key(partition = user_id, local = id)]
struct Todo {
    #[auto]
    id: u64,
    title: String,
    user_id: u64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Get a specific todo
let todo = Todo::get_by_user_id_and_id(
    &mut db, &1, &42
).await?;

// Get all todos for a user
let todos = Todo::filter_by_user_id(&1)
    .exec(&mut db)
    .await?;
Ok(())
}
}

What gets generated

For a model with #[key], Toasty generates these methods:

AttributeGenerated methods
#[key] on single fieldget_by_<field>(), filter_by_<field>(), delete_by_<field>()
#[key] on multiple fieldsget_by_<a>_and_<b>(), filter_by_<a>_and_<b>(), delete_by_<a>_and_<b>()
#[key(a, b)] on structSame as #[key] on multiple fields
#[key(partition = a, local = b)]get_by_<a>_and_<b>(), filter_by_<a>(), filter_by_<a>_and_<b>(), delete_by_<a>_and_<b>()

Schema Management

This section still needs to be written. It will cover creating and resetting database tables.

Creating Records

Toasty generates a create builder for each model. Call <YourModel>::create(), set fields with chained methods, and call .exec(&mut db) to insert the record.

Creating a single record

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;

println!("Created user with id: {}", user.id);
Ok(())
}
}

User::create() returns a builder with a setter method for each non-auto field. Chain the setters to provide values, then call .exec(&mut db) to insert the row. The returned User instance has all fields set, including auto-generated ones like id.

The generated SQL looks like:

INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');

Required vs optional fields

Required fields (String, u64, etc.) must be set before calling .exec(). Optional fields (Option<T>) default to NULL if not set.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,
    bio: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {

// bio defaults to None (NULL in the database)
let user = User::create()
    .name("Alice")
    .exec(&mut db)
    .await?;

assert!(user.bio.is_none());

// Or set it explicitly
let user = User::create()
    .name("Bob")
    .bio("Likes Rust")
    .exec(&mut db)
    .await?;

assert_eq!(user.bio.as_deref(), Some("Likes Rust"));
Ok(())
}
}

Creating many records

Use toasty::batch() to insert multiple records at once. Pass an array of create builders for the same model, or a tuple for mixed models. Call .exec() to insert them all.

Array syntax (same model)

When creating multiple records of the same model, pass an array. The return type is Vec<User>:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users = toasty::batch([
    User::create().name("Alice").email("alice@example.com"),
    User::create().name("Bob").email("bob@example.com"),
    User::create().name("Carol").email("carol@example.com"),
])
.exec(&mut db)
.await?;

assert_eq!(users.len(), 3);
Ok(())
}
}

This also works with a Vec of builders, which is useful when the number of records is determined at runtime:

let names = ["Alice", "Bob", "Carol"];
let builders: Vec<_> = names
    .iter()
    .enumerate()
    .map(|(i, n)| User::create().name(*n).email(format!("user{i}@example.com")))
    .collect();

let users = toasty::batch(builders).exec(&mut db).await?;

Tuple syntax (mixed models)

When creating records of different models, use a tuple. The return type matches the tuple structure:

let (user, post): (User, Post) = toasty::batch((
    User::create().name("Alice"),
    Post::create().title("Hello World"),
))
.exec(&mut db)
.await?;

Nested creation

When models have relationships, you can create a parent and its children in a single call. This is covered in more detail in the relationship chapters (BelongsTo, HasMany, HasOne), but here is a preview.

Given a User that has many Todo items:

let user = User::create()
    .name("Alice")
    .todo(Todo::create().title("Buy groceries"))
    .todo(Todo::create().title("Write docs"))
    .exec(&mut db)
    .await?;

Toasty creates the user first, then creates each todo with the user’s ID automatically set as the foreign key. Toasty makes a best effort to execute nested creation atomically — either all records are inserted or none are. Whether full atomicity is guaranteed depends on your database’s capabilities, so check your database’s transaction and consistency documentation.

What gets generated

For a model like User, #[derive(Model)] generates:

  • User::create() — returns a builder with a setter for each non-auto field

The create builder’s setter methods accept flexible input types through the IntoExpr trait. For a String field, you can pass &str, String, or &String. For numeric fields, you can pass the value directly or by reference. See Defining Models — What types can you pass to setters? for details.

Querying Records

Toasty generates several ways to retrieve records: by primary key, by indexed fields, or by building queries with filters.

Get by primary key

<YourModel>::get_by_id() fetches a single record by its primary key. It returns the record directly, or an error if no record exists with that key.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::get_by_id(&mut db, &1).await?;
println!("Found: {}", user.name);
Ok(())
}
}

The method name matches the key field name. A model with #[key] code: String generates get_by_code(). Composite keys generate combined names like get_by_student_id_and_course_id().

Get all records

<YourModel>::all() returns a query for all records of that model.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users = User::all().exec(&mut db).await?;

for user in &users {
    println!("{}: {}", user.id, user.name);
}
Ok(())
}
}

Executing queries

Queries returned by all(), filter(), and filter_by_*() are not executed until you call a terminal method. Toasty provides three terminal methods:

.exec() — collect all results

Returns all matching records as a Vec:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users: Vec<User> = User::all().exec(&mut db).await?;
Ok(())
}
}

.first() — get the first result or None

Returns Option<User>Some if at least one record matches, None if the query returns no results:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let maybe_user = User::all().first(&mut db).await?;

match maybe_user {
    Some(user) => println!("Found: {}", user.name),
    None => println!("No users found"),
}
Ok(())
}
}

.get() — get exactly one result

Returns the record directly, or an error if no record matches. Use this when you expect the query to return exactly one result:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db, user_id: u64) -> toasty::Result<()> {
let user = User::filter_by_id(user_id).get(&mut db).await?;
Ok(())
}
}

Filtering by indexed fields

Toasty generates filter_by_* methods for indexed and key fields. These return a query builder that you can execute with any terminal method.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// filter_by_id returns a query builder
let user = User::filter_by_id(1).get(&mut db).await?;

// filter_by_email is generated because email has #[unique]
let user = User::filter_by_email("alice@example.com")
    .get(&mut db)
    .await?;
Ok(())
}
}

The difference between get_by_* and filter_by_*: get_by_* methods execute immediately and return the record. filter_by_* methods return a query builder that you can further customize before executing.

Filtering with expressions

For queries beyond simple field equality, use <YourModel>::filter() with field expressions. The Filtering with Expressions chapter covers this in detail. Here is a quick example:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users = User::filter(User::fields().name().eq("Alice"))
    .exec(&mut db)
    .await?;
Ok(())
}
}

Chaining filters

You can chain .filter() on an existing query to add more conditions:

let users = User::filter_by_name("Alice")
    .filter(User::fields().age().gt(25))
    .exec(&mut db)
    .await?;

Each .filter() call adds an AND condition to the query.

Batch key lookups

For primary key fields, Toasty generates a filter_by_<key>_batch() method that fetches multiple records by key in a single query:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users = User::filter_by_id_batch([&1, &2, &3])
    .exec(&mut db)
    .await?;
Ok(())
}
}

This is more efficient than calling get_by_id() in a loop.

What gets generated

For a model with #[key] on id and #[unique] on email, Toasty generates:

MethodReturnsDescription
User::all()Query builderAll records
User::filter(expr)Query builderRecords matching expression
User::filter_by_id(id)Query builderRecords matching key
User::filter_by_id_batch(ids)Query builderRecords matching any key in list
User::filter_by_email(email)Query builderRecords matching unique field
User::get_by_id(&mut db, &id)Result<User>One record by key (immediate)
User::get_by_email(&mut db, email)Result<User>One record by unique field (immediate)

Query builders support these terminal methods:

MethodReturns
.exec(&mut db)Result<Vec<User>>
.first(&mut db)Result<Option<User>>
.get(&mut db)Result<User>

Updating Records

Toasty generates an update builder for each model. You can update a record through an instance method, through a query, or with a generated convenience method.

Updating an instance

Call .update() on a mutable model instance, set the fields you want to change, and call .exec(&mut db):

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut user = User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;

user.update()
    .name("Alice Smith")
    .exec(&mut db)
    .await?;

// The instance is updated in place
assert_eq!(user.name, "Alice Smith");
Ok(())
}
}

Only set the fields you want to change. Fields you don’t set keep their current values. The .update() method takes &mut self, so you need a mutable binding. After .exec() completes, the instance reflects the new values.

The generated SQL looks like:

UPDATE users SET name = 'Alice Smith' WHERE id = 1;

Updating multiple fields

Chain multiple setters to update several fields at once:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut user = User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;
user.update()
    .name("Alice Smith")
    .email("alice.smith@example.com")
    .exec(&mut db)
    .await?;
Ok(())
}
}

Updating by query

You can update records without first loading them by building a query and calling .update() on it:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db, user_id: u64) -> toasty::Result<()> {
User::filter_by_id(user_id)
    .update()
    .name("Bob")
    .exec(&mut db)
    .await?;
Ok(())
}
}

This executes the update directly without loading the record first. The query determines which records to update, and the chained setters specify the new values.

Update by indexed field

Toasty generates update_by_* convenience methods for key and indexed fields:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db, user_id: u64) -> toasty::Result<()> {
User::update_by_id(user_id)
    .name("Bob")
    .exec(&mut db)
    .await?;
Ok(())
}
}

This is shorthand for User::filter_by_id(user_id).update(). Toasty generates update_by_* for each field that has #[key], #[unique], or #[index].

Setting optional fields to None

To clear an optional field, pass None with the appropriate type annotation:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    bio: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut user = User::create()
    .name("Alice")
    .bio("Likes Rust")
    .exec(&mut db)
    .await?;
user.update()
    .bio(Option::<String>::None)
    .exec(&mut db)
    .await?;

assert!(user.bio.is_none());
Ok(())
}
}

What gets generated

For a model with #[key] on id and #[unique] on email, Toasty generates:

  • user.update() — instance method (takes &mut self), returns an update builder. After .exec(), the instance is reloaded with the new values.
  • User::update_by_id(id) — returns an update builder for the record matching the given key.
  • User::update_by_email(email) — returns an update builder for the record matching the given email.
  • Any query builder’s .update() method — converts the query into an update builder.

The update builder has a setter method for each field. Only the fields you set are included in the UPDATE statement.

Deleting Records

Toasty provides several ways to delete records: from an instance, by primary key, or through a query.

Deleting an instance

Call .delete() on a model instance, then .exec(&mut db):

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;

let user_id = user.id;

user.delete().exec(&mut db).await?;

// The record no longer exists
let result = User::get_by_id(&mut db, &user_id).await;
assert!(result.is_err());
Ok(())
}
}

The .delete() method consumes the instance (takes self, not &self). After deleting, you can no longer use the instance.

The generated SQL looks like:

DELETE FROM users WHERE id = 1;

Deleting by primary key

Use the generated delete_by_* method to delete a record by its key without loading it first:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;
User::delete_by_id(&mut db, user.id).await?;
Ok(())
}
}

This executes the delete directly — no SELECT query is issued first.

Deleting by query

Build a query and call .delete() on it to delete all matching records:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;
User::filter_by_email("alice@example.com")
    .delete()
    .exec(&mut db)
    .await?;
Ok(())
}
}

You can use any query builder — filter_by_*, filter(), or all() — and chain .delete() to convert it into a delete operation.

What gets generated

For a model with #[key] on id and #[unique] on email, Toasty generates:

  • user.delete() — instance method (consumes self), returns a delete statement. Call .exec(&mut db) to execute.
  • User::delete_by_id(&mut db, id) — deletes the record matching the given key. Executes immediately.
  • User::delete_by_email(&mut db, email) — deletes the record matching the given email. Executes immediately.
  • Any query builder’s .delete() method — converts the query into a delete statement.

Indexes and Unique Constraints

Toasty supports two field-level attributes for indexing: #[unique] and #[index]. Both create database indexes, but they differ in what gets generated.

Unique fields

Add #[unique] to a field to create a unique index. On databases that support unique constraints (SQLite, PostgreSQL, MySQL), the database enforces that no two records can have the same value for this field. Toasty applies the constraint on a best-effort basis — if the underlying database does not support unique indexes (e.g., DynamoDB on non-key attributes), the #[unique] attribute still generates the same query methods but uniqueness is not enforced at the storage level.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[unique]
    email: String,
}
}

This generates a unique index on the email column:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_users_email ON users (email);

Attempting to insert a duplicate value returns an error:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
User::create()
    .name("Alice")
    .email("alice@example.com")
    .exec(&mut db)
    .await?;

// This fails — email must be unique
let result = User::create()
    .name("Bob")
    .email("alice@example.com")
    .exec(&mut db)
    .await;

assert!(result.is_err());
Ok(())
}
}

Generated methods for unique fields

Because a unique field identifies at most one record, Toasty generates a get_by_* method that returns a single record:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Get a single user by email (errors if not found)
let user = User::get_by_email(&mut db, "alice@example.com").await?;
Ok(())
}
}

Toasty also generates filter_by_*, update_by_*, and delete_by_* methods:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[unique]
    email: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Filter — returns a query builder
let user = User::filter_by_email("alice@example.com")
    .get(&mut db)
    .await?;

// Update by email
User::update_by_email("alice@example.com")
    .name("Alice Smith")
    .exec(&mut db)
    .await?;

// Delete by email
User::delete_by_email(&mut db, "alice@example.com").await?;
Ok(())
}
}

Indexed fields

Add #[index] to a field to tell Toasty that this field is a query target. On SQL databases, Toasty creates a database index on the column, which lets the database find matching rows without scanning the entire table. On DynamoDB, the attribute maps to a secondary index.

Unlike #[unique], #[index] does not enforce uniqueness — multiple records can share the same value.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[index]
    country: String,
}
}

This generates a non-unique index:

CREATE INDEX idx_users_country ON users (country);

Generated methods for indexed fields

Because an indexed field may match multiple records, the generated methods work with collections rather than single records:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// filter_by_country returns a query builder (may match many records)
let users = User::filter_by_country("US")
    .exec(&mut db)
    .await?;

// Update all records matching the index
User::update_by_country("US")
    .country("United States")
    .exec(&mut db)
    .await?;

// Delete all records matching the index
User::delete_by_country(&mut db, "US").await?;
Ok(())
}
}

Toasty also generates a get_by_* method for indexed fields. It returns the matching record directly, but errors if no record matches or if more than one record matches:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::get_by_country(&mut db, "US").await?;
Ok(())
}
}

Choosing between #[unique] and #[index]

Both attributes tell Toasty that a field is a query target and generate the same set of methods: get_by_*, filter_by_*, update_by_*, and delete_by_*.

The difference is in the constraint they express:

AttributeMeaningDatabase effect (SQL)
#[unique]Each record has a distinct valueCREATE UNIQUE INDEX — the database rejects duplicates
#[index]Multiple records may share a valueCREATE INDEX — no uniqueness enforcement

Use #[unique] for fields that identify a single record — email addresses, usernames, slugs. Use #[index] for fields you query frequently but that naturally repeat — country, status, category.

What gets generated

For a model with #[unique] on email and #[index] on country:

MethodDescription
User::get_by_email(&mut db, email)One record by unique field
User::filter_by_email(email)Query builder for unique field
User::update_by_email(email)Update builder for unique field
User::delete_by_email(&mut db, email)Delete by unique field
User::get_by_country(&mut db, country)One record by indexed field
User::filter_by_country(country)Query builder for indexed field
User::update_by_country(country)Update builder for indexed field
User::delete_by_country(&mut db, country)Delete by indexed field

These methods follow the same patterns as key-generated methods. See Querying Records, Updating Records, and Deleting Records for details on terminal methods and builders.

Field Options

Toasty provides several field-level attributes to control how fields map to database columns: custom column names, explicit types, default values, update expressions, and JSON serialization.

Custom column names

By default, a Rust field name maps directly to a column name. Use #[column("name")] to override this:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    #[column("display_name")]
    name: String,
}
}

The field is still accessed as user.name in Rust, but the database column is named display_name:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    display_name TEXT NOT NULL
);

Explicit column types

Toasty infers the column type from the Rust field type. Use #[column(type = ...)] to specify an explicit database type instead:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    #[column(type = varchar(100))]
    name: String,
}
}

This creates a VARCHAR(100) column instead of TEXT. The database rejects values that exceed the specified length.

You can combine a custom name with an explicit type:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    #[column("display_name", type = varchar(100))]
    name: String,
}
}

Supported type values:

Type syntaxDatabase type
booleanBoolean
int, i8, i16, i32, i64Integer (various sizes)
uint, u8, u16, u32, u64Unsigned integer
textText
varchar(N)Variable-length string with max length
numeric, numeric(P, S)Decimal with optional precision and scale
binary(N), blobBinary data
timestamp(P)Timestamp with precision
dateDate
time(P)Time with precision
datetime(P)Date and time with precision

Not all databases support all column types. Toasty validates explicit column types against the database’s capabilities when you call db.push_schema(). If a type is not supported, schema creation fails with an error. For example, varchar is supported by PostgreSQL and MySQL but not by SQLite or DynamoDB — using #[column(type = varchar(100))] with SQLite produces an error like "unsupported feature: VARCHAR type is not supported by this database". If the requested size exceeds the database’s maximum, Toasty reports that as well.

Default values

Use #[default(expr)] to set a default value applied when creating a record. If you don’t set the field on the create builder, Toasty evaluates the expression and uses the result.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
#[default(0)]
view_count: i64,
}
}
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
    #[default(0)]
    view_count: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// view_count defaults to 0
let post = Post::create()
    .title("Hello World")
    .exec(&mut db)
    .await?;

assert_eq!(post.view_count, 0);

// Override the default by setting it explicitly
let post = Post::create()
    .title("Popular Post")
    .view_count(100)
    .exec(&mut db)
    .await?;

assert_eq!(post.view_count, 100);
Ok(())
}
}

The expression inside #[default(...)] is any valid Rust expression. It runs at insert time, not at compile time.

#[default] only applies on create. It has no effect on updates.

Update expressions

Use #[update(expr)] to set an expression that applies on both create and update. Each time the record is created or updated, Toasty evaluates the expression and sets the field — unless you explicitly override it.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
#[update(jiff::Timestamp::now())]
updated_at: jiff::Timestamp,
}
}
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
    #[update(jiff::Timestamp::now())]
    updated_at: jiff::Timestamp,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// updated_at is set automatically on create
let mut post = Post::create()
    .title("Hello World")
    .exec(&mut db)
    .await?;

// updated_at is refreshed automatically on update
post.update()
    .title("Updated Title")
    .exec(&mut db)
    .await?;
Ok(())
}
}

You can override the automatic value by setting the field explicitly:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
    #[update(jiff::Timestamp::now())]
    updated_at: jiff::Timestamp,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut post = Post::create()
    .title("Hello World")
    .exec(&mut db)
    .await?;
let explicit_ts = jiff::Timestamp::from_second(946684800).unwrap();
post.update()
    .title("Backdated")
    .updated_at(explicit_ts)
    .exec(&mut db)
    .await?;

assert_eq!(post.updated_at, explicit_ts);
Ok(())
}
}

Combining #[default] and #[update]

You can use both attributes on the same field. #[default] applies on create, #[update] applies on update:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
// On create: "draft". On update: "edited".
#[default("draft".to_string())]
#[update("edited".to_string())]
status: String,
}
}
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
    #[default("draft".to_string())]
    #[update("edited".to_string())]
    status: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut post = Post::create()
    .title("Hello")
    .exec(&mut db)
    .await?;

assert_eq!(post.status, "draft");

post.update().title("Updated").exec(&mut db).await?;

assert_eq!(post.status, "edited");
Ok(())
}
}

Timestamps with #[auto]

For timestamp fields named created_at or updated_at, #[auto] provides a shorthand:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    title: String,

    #[auto]
    created_at: jiff::Timestamp,

    #[auto]
    updated_at: jiff::Timestamp,
}
}

When #[auto] appears without arguments on a non-key field, Toasty uses a heuristic based on the field name and type to determine the behavior:

Field nameField type#[auto] expands to
created_atjiff::Timestamp#[default(jiff::Timestamp::now())] — set once on create
updated_atjiff::Timestamp#[update(jiff::Timestamp::now())] — refreshed on every create and update

On key fields, bare #[auto] defers to the type’s default auto-generation strategy (e.g., auto-increment for integers, UUID v4 for uuid::Uuid). See Keys and Auto-Generation for details.

This is the recommended way to add timestamps to your models. The created_at field is set when the record is first inserted and never changes. The updated_at field is refreshed each time the record is updated.

Timestamp fields require the jiff feature:

[dependencies]
toasty = { version = "0.1", features = ["sqlite", "jiff"] }

Date and time fields

With the jiff feature enabled, you can use these types for date and time fields:

Rust typeDescription
jiff::TimestampAn instant in time (UTC)
jiff::civil::DateA date without time
jiff::civil::TimeA time of day without date
jiff::civil::DateTimeA date and time without timezone

You can control the storage precision with #[column(type = ...)]:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[column(type = timestamp(3))]
    starts_at: jiff::Timestamp,

    #[column(type = time(0))]
    reminder_time: jiff::civil::Time,
}
}

JSON serialization

Use #[serialize(json)] to store a Rust value as a JSON string in the database. The field type must implement serde::Serialize and serde::Deserialize.

use toasty::Model;
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Metadata {
    version: u32,
    labels: Vec<String>,
}

#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    title: String,

    #[serialize(json)]
    tags: Vec<String>,

    #[serialize(json)]
    meta: Metadata,
}

Toasty serializes the value to a JSON string on insert and update, and deserializes it back when reading. The default database column type is TEXT. You can override this with #[column(type = ...)] if needed — for example, #[column(type = varchar(1000))] to limit the stored JSON size on databases that support varchar.

use toasty::Model;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Metadata {
    version: u32,
    labels: Vec<String>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
    #[serialize(json)]
    tags: Vec<String>,
    #[serialize(json)]
    meta: Metadata,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let post = Post::create()
    .title("Hello")
    .tags(vec!["rust".to_string(), "toasty".to_string()])
    .meta(Metadata {
        version: 1,
        labels: vec!["alpha".to_string()],
    })
    .exec(&mut db)
    .await?;

assert_eq!(post.tags, vec!["rust", "toasty"]);
assert_eq!(post.meta.version, 1);
Ok(())
}

Nullable JSON fields

By default, #[serialize(json)] creates a NOT NULL column. An Option<T> field with #[serialize(json)] serializes None as the JSON text "null" — the column still stores a non-null string.

To allow SQL NULL in the column, add the nullable modifier:

#![allow(unused)]
fn main() {
use toasty::Model;
use std::collections::HashMap;
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    title: String,
#[serialize(json, nullable)]
metadata: Option<HashMap<String, String>>,
}
}

With nullable:

  • None maps to SQL NULL in the database
  • Some(value) maps to the JSON string representation

Without nullable:

  • None maps to the JSON text "null" (a non-null string)
  • Some(value) maps to the JSON string representation

Attribute summary

AttributePurposeApplies on
#[column("name")]Custom column name
#[column(type = ...)]Explicit column type
#[default(expr)]Default valueCreate only
#[update(expr)]Automatic valueCreate and update
#[auto] on created_atShorthand for #[default(jiff::Timestamp::now())]Create only
#[auto] on updated_atShorthand for #[update(jiff::Timestamp::now())]Create and update
#[serialize(json)]Store as JSON textCreate and update
#[serialize(json, nullable)]Store as JSON text with SQL NULL supportCreate and update

Relationships

Models rarely exist in isolation. A blog has users, posts, and comments. An e-commerce site has customers, orders, and products. Relationships define how these models connect to each other.

In Toasty, you declare relationships on your model structs using attributes like #[belongs_to], #[has_many], and #[has_one]. Toasty uses these declarations to generate methods for traversing between models, creating related records, and maintaining data consistency when records are deleted or updated.

How relationships work at the database level

Relationships are implemented through foreign keys — a column in one table that stores the primary key of a row in another table. For example, a posts table has a user_id column that references the users table:

users                    posts
┌────┬───────┐          ┌────┬──────────┬─────────┐
│ id │ name  │          │ id │ title    │ user_id │
├────┼───────┤          ├────┼──────────┼─────────┤
│  1 │ Alice │◄─────────│  1 │ Hello    │       1 │
│  2 │ Bob   │◄────┐    │  2 │ World    │       1 │
└────┴───────┘     └────│  3 │ Goodbye  │       2 │
                        └────┴──────────┴─────────┘

The posts table holds the foreign key (user_id). Each post points to exactly one user. A user can have many posts.

This single pattern — a foreign key column in one table referencing the primary key of another — underlies all three relationship types in Toasty.

Relationship types

Toasty supports three relationship types. They differ in how many records each side of the relationship holds, and which model contains the foreign key.

TypeForeign key onParent hasChild hasExample
BelongsToThis modelOne parentA post belongs to a user
HasManyOther modelMany childrenA user has many posts
HasOneOther modelOne childA user has one profile

Which model gets which attribute?

The model whose table contains the foreign key column declares #[belongs_to]. The model on the other side declares #[has_many] or #[has_one].

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    // User's table has no FK — declares has_many
    #[has_many]
    posts: toasty::HasMany<Post>,
}

#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    // Post's table has the FK — declares belongs_to
    #[index]
    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,

    title: String,
}
}

Relationship pairs

Most relationships are bidirectional — declared on both models. The User above has #[has_many] posts and the Post has #[belongs_to] user. Toasty matches these two sides into a pair automatically by looking at the model types — field names do not factor into the matching. If there is ambiguity (for example, a model with two BelongsTo relations pointing to the same parent type), use pair to link them explicitly:

// On User: the child's relation field is named "owner", not "user"
#[has_many(pair = owner)]
posts: toasty::HasMany<Post>,

You can define one-sided relationships with only #[belongs_to] on the child and no corresponding #[has_many] or #[has_one] on the parent. This is useful when you need to navigate from child to parent but not the reverse. The opposite is not allowed — a #[has_many] or #[has_one] field always requires a matching #[belongs_to] on the target model, because Toasty needs the foreign key definition to know how the models connect.

Required vs optional relationships

The nullability of the foreign key field controls whether the relationship is required or optional.

Required: non-nullable foreign key

#[index]
user_id: u64,

#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<User>,

Every post must have a user. The user_id column is NOT NULL in the database.

Optional: nullable foreign key

#[index]
user_id: Option<u64>,

#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<Option<User>>,

A post can exist without a user. The user_id column allows NULL.

This distinction matters beyond just data modeling — it determines what happens when a relationship is broken, as the next section explains.

When you delete a parent record or disassociate a child, Toasty automatically maintains consistency based on the foreign key’s nullability:

ActionFK is required (u64)FK is optional (Option<u64>)
Delete parentChild is deletedChild stays, FK set to NULL
Unset relation (e.g., update().profile(None))Child is deletedChild stays, FK set to NULL
Delete childParent is unaffectedParent is unaffected

The logic: a required foreign key means the child cannot exist without its parent. If the parent goes away, the child must go too. An optional foreign key means the child can stand on its own, so Toasty sets the FK to NULL and leaves the child in place.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .post(Post::create().title("Hello"))
    .exec(&mut db)
    .await?;

let posts = user.posts().exec(&mut db).await?;
assert_eq!(1, posts.len());

// user_id is required (u64), so deleting the user deletes the post too
user.delete().exec(&mut db).await?;

assert!(Post::get_by_id(&mut db, &posts[0].id).await.is_err());
Ok(())
}
}

If user_id were Option<u64> instead, the post would survive the deletion with user_id set to None.

This behavior is applied at the application level by Toasty’s query engine, not by database-level foreign key constraints. Toasty inspects the schema and generates the appropriate cascade deletes or null-setting updates automatically.

Choosing the right relationship type

You want to express…UseFK goes on
A post has one authorPostBelongsTo<User> + UserHasMany<Post>posts.user_id
A user has one profileUserHasOne<Profile> + ProfileBelongsTo<User>profiles.user_id
A comment belongs to a postCommentBelongsTo<Post> + PostHasMany<Comment>comments.post_id

When deciding between HasOne and HasMany, ask: “Can the parent have more than one?” If yes, use HasMany. If exactly one (or zero), use HasOne. The foreign key placement is the same either way — it always goes on the child.

When deciding between HasOne and BelongsTo for a one-to-one relationship, ask: “Which model is the dependent one — the one that doesn’t make sense without the other?” Put the FK on the dependent model with BelongsTo, and declare HasOne on the independent model.

Composite foreign keys

When a parent model has a composite primary key, the #[belongs_to] attribute accepts multiple key/references pairs — one for each column in the composite key:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    #[has_many]
    todos: toasty::HasMany<Todo>,
}

#[derive(Debug, toasty::Model)]
#[key(partition = user_id, local = id)]
struct Todo {
    #[auto]
    id: uuid::Uuid,

    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,

    title: String,
}
}

In this example, Todo uses a composite primary key (user_id + id). The user_id field serves double duty: it is part of the Todo’s own primary key and the foreign key pointing to User.

When the parent itself has a composite primary key, list each column pair:

#[belongs_to(key = org_id, references = org_id, key = team_id, references = id)]
team: toasty::BelongsTo<Team>,

The number of key entries must match the number of references entries. Toasty pairs them positionally: the first key maps to the first references, the second to the second, and so on.

Composite foreign key fields should be indexed together so that Toasty can query efficiently:

#[index(fields(org_id, team_id))]

What the following chapters cover

Each relationship type has its own chapter with full details on definition, querying, creating, and updating:

  • BelongsTo — defining foreign keys, accessing the parent, setting the relation on create
  • HasMany — querying children, creating through the relation, inserting and removing, scoped queries
  • HasOne — required vs optional, creating and updating the child, replace and unset behavior
  • Preloading Associations — avoiding extra queries by loading relations upfront with .include()

BelongsTo

A BelongsTo relationship connects a child model to a parent model through a foreign key. The child stores the parent’s ID in one of its own fields.

Defining a BelongsTo relationship

A BelongsTo relationship requires two things on the child model: a foreign key field and a BelongsTo<T> relation field. The #[belongs_to] attribute tells Toasty which field holds the foreign key and which field on the parent it references.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[has_many]
    posts: toasty::HasMany<Post>,
}

#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    #[index]
    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,

    title: String,
}
}

The user_id field is the foreign key — it stores the id of the associated User. The #[belongs_to(key = user_id, references = id)] attribute tells Toasty that user_id on Post maps to id on User.

The foreign key field should have #[index] so that Toasty can efficiently look up posts by user. In the database, this creates:

CREATE TABLE posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL
);
CREATE INDEX idx_posts_user_id ON posts (user_id);

The parent model (User) typically declares a #[has_many] field pointing back at the child. See HasMany for details.

Optional BelongsTo

If a child does not always have a parent, make the foreign key Option<T> and wrap the relation type in Option:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    #[index]
    user_id: Option<u64>,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,

    title: String,
}
}

The user_id column is now nullable. A post can exist without a user.

Call the relation method on the child instance to get the parent. The method name matches the relation field name.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let post = Post::create().title("Hello").user_id(1).exec(&mut db).await?;
// Load the associated user from the database
let user = post.user().get(&mut db).await?;
println!("Author: {}", user.name);
Ok(())
}
}

For an optional BelongsTo, .get() returns Option<User>:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let post = Post::create().title("Hello").exec(&mut db).await?;
match post.user().get(&mut db).await? {
    Some(user) => println!("Author: {}", user.name),
    None => println!("No author"),
}
Ok(())
}
}

Each call to .user().get() executes a database query. To avoid repeated queries, use preloading.

Setting the relation on create

You can associate a child with a parent in two ways: by passing a reference to the parent, or by setting the foreign key directly.

By parent reference

Pass a reference to an existing parent record:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().name("Alice").exec(&mut db).await?;

let post = Post::create()
    .title("Hello World")
    .user(&user)
    .exec(&mut db)
    .await?;

assert_eq!(post.user_id, user.id);
Ok(())
}
}

Toasty extracts the parent’s primary key and sets the foreign key field automatically.

By foreign key value

Set the foreign key field directly:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().name("Alice").exec(&mut db).await?;
let post = Post::create()
    .title("Hello World")
    .user_id(user.id)
    .exec(&mut db)
    .await?;
Ok(())
}
}

This is useful when you have the parent’s ID but not the full record.

Customizing the pair name

By default, Toasty matches a #[has_many] field on the parent to a #[belongs_to] field on the child by the singularized parent model name. If the child’s relation field has a different name, use #[has_many(pair = field_name)] on the parent:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    #[has_many(pair = owner)]
    todos: toasty::HasMany<Todo>,
}

#[derive(Debug, toasty::Model)]
struct Todo {
    #[key]
    #[auto]
    id: u64,

    #[index]
    owner_id: u64,

    #[belongs_to(key = owner_id, references = id)]
    owner: toasty::BelongsTo<User>,

    title: String,
}
}

Here the child’s relation field is named owner instead of user, so the parent specifies pair = owner to establish the connection.

What gets generated

For a Post model with #[belongs_to] user: BelongsTo<User>, Toasty generates:

MethodReturnsDescription
post.user()Relation accessorReturns an accessor for the associated user
.get(&mut db)Result<User>Loads the associated user from the database
Post::create().user(&user)Create builderSets the foreign key from a parent reference
Post::create().user_id(id)Create builderSets the foreign key directly
Post::fields().user()Field pathUsed with .include() for preloading

HasMany

A HasMany relationship connects a parent model to multiple child records. The parent declares a HasMany<T> field, and the child stores a foreign key pointing back to the parent via BelongsTo.

Defining a HasMany relationship

Add a #[has_many] field of type HasMany<T> on the parent model. The child model must have a corresponding #[belongs_to] field.

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[has_many]
    posts: toasty::HasMany<Post>,
}

#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,

    #[index]
    user_id: u64,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,

    title: String,
}
}

The #[has_many] attribute does not add any columns to the parent’s table. The relationship is stored entirely in the child’s foreign key column (user_id).

Querying children

Call the relation method on a parent instance to get an accessor for its children:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().name("Alice").exec(&mut db).await?;
let posts: Vec<Post> = user.posts().exec(&mut db).await?;

for post in &posts {
    println!("{}", post.title);
}
Ok(())
}
}

The generated SQL is:

SELECT * FROM posts WHERE user_id = ?;

All queries through the relation accessor are automatically scoped to the parent. user.posts() only returns posts belonging to that user.

Creating through the relation

Create a child record through the parent’s relation accessor. Toasty automatically sets the foreign key:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().name("Alice").exec(&mut db).await?;
let post = user
    .posts()
    .create()
    .title("Hello World")
    .exec(&mut db)
    .await?;

assert_eq!(post.user_id, user.id);
Ok(())
}
}

You don’t need to set user_id — Toasty fills it in from the parent.

Nested creation

Create a parent and its children in a single call using the singular form of the relation name on the create builder:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .post(Post::create().title("First post"))
    .post(Post::create().title("Second post"))
    .exec(&mut db)
    .await?;

let posts = user.posts().exec(&mut db).await?;
assert_eq!(2, posts.len());
Ok(())
}
}

Toasty creates the user first, then creates each post with the user’s ID as the foreign key.

Inserting and removing children

Use .insert() and .remove() to link and unlink existing records.

Inserting

Associate an existing child record with a parent:

let post = Post::create().title("Orphan post").user_id(0).exec(&mut db).await?;

// Associate the post with a user
user.posts().insert(&mut db, &post).await?;

This updates the child’s foreign key to point to the parent. You can also insert multiple records at once:

user.posts().insert(&mut db, &[post1, post2, post3]).await?;

If the child is already associated with a different parent, .insert() moves it to the new parent.

Removing

Disassociate a child from a parent:

user.posts().remove(&mut db, &post).await?;

What happens to the child depends on whether the foreign key is required or optional:

Foreign key typeEffect of .remove()
Required (user_id: u64)Deletes the child record
Optional (user_id: Option<u64>)Sets the foreign key to NULL

When the foreign key is required, the child cannot exist without a parent, so Toasty deletes it. When the foreign key is optional, the child remains in the database with a null foreign key.

Scoped queries

The relation accessor supports scoped queries — filtering, updating, and deleting within the parent’s children.

Filtering with .query()

// Find posts with a specific condition
let drafts = user
    .posts()
    .query(Post::fields().published().eq(false))
    .exec(&mut db)
    .await?;

Looking up by ID within the scope

let post = user.posts().get_by_id(&mut db, &post_id).await?;

This only returns the post if it belongs to the user. If the post exists but belongs to a different user, this returns an error.

Updating through the scope

user.posts()
    .filter_by_id(post_id)
    .update()
    .title("New title")
    .exec(&mut db)
    .await?;

Deleting through the scope

user.posts()
    .filter_by_id(post_id)
    .delete()
    .exec(&mut db)
    .await?;

Filtering parents by children

You can filter parent records based on conditions on their children using .any() on the relation field:

// Find users who have at least one published post
let users = User::filter(
    User::fields()
        .posts()
        .any(Post::fields().published().eq(true))
)
.exec(&mut db)
.await?;

What gets generated

For a User model with #[has_many] posts: HasMany<Post>, Toasty generates:

On the parent instance:

MethodReturnsDescription
user.posts()Relation accessorAccessor scoped to this user’s posts
.exec(&mut db)Result<Vec<Post>>All posts belonging to this user
.create()Create builderCreate a post with the foreign key pre-filled
.get_by_id(&mut db, &id)Result<Post>Get a post by ID within the scope
.query(expr)Query builderFilter posts within the scope
.insert(&mut db, &post)Result<()>Associate an existing post with the user
.remove(&mut db, &post)Result<()>Disassociate a post from the user

On the create builder:

MethodDescription
.post(Post::create()...)Add one child to create alongside the parent
.posts(...)Add multiple children to create alongside the parent

On the fields accessor:

MethodDescription
User::fields().posts()Field path for preloading and filtering
User::fields().posts().any(expr)Filter parents by child conditions

HasOne

A HasOne relationship connects a parent model to a single child record. Like HasMany, the foreign key lives on the child model, but HasOne enforces that at most one child exists per parent.

Defining a HasOne relationship

Add a #[has_one] field of type HasOne<T> on the parent model. The child model must have a corresponding #[belongs_to] field with a #[unique] foreign key (since each parent maps to at most one child):

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,

    name: String,

    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}

#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,

    #[unique]
    user_id: Option<u64>,

    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,

    bio: String,
}
}

The child’s foreign key has #[unique] instead of #[index], which guarantees that only one profile can reference a given user. In the database:

CREATE TABLE profiles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER,
    bio TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_profiles_user_id ON profiles (user_id);

Optional vs required HasOne

The type parameter on HasOne controls whether the parent must have a child.

Optional: HasOne<Option<Profile>>

The parent may or may not have a child. Creating a parent without a child is allowed:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    bio: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// A user without a profile — this is fine
let user = User::create().name("Alice").exec(&mut db).await?;

assert!(user.profile().get(&mut db).await?.is_none());
Ok(())
}
}

Required: HasOne<Profile>

The parent must have a child. Creating a parent requires providing a child:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    #[has_one]
    profile: toasty::HasOne<Profile>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    bio: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Must provide a profile when creating the user
let user = User::create()
    .profile(Profile::create().bio("Hello"))
    .exec(&mut db)
    .await?;

let profile = user.profile().get(&mut db).await?;
assert_eq!(profile.bio, "Hello");
Ok(())
}
}

Call the relation method on the parent instance to load the child:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    bio: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .profile(Profile::create().bio("A person"))
    .exec(&mut db)
    .await?;
// For HasOne<Option<Profile>> — returns Option<Profile>
let profile = user.profile().get(&mut db).await?;

if let Some(profile) = profile {
    println!("Bio: {}", profile.bio);
}
Ok(())
}
}

For a required HasOne<Profile>, .get() returns Profile directly (not wrapped in Option).

Each call to .profile().get() executes a database query. To avoid this, use preloading.

Creating through the relation

Create a child for an existing parent through the relation accessor:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    bio: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().name("Alice").exec(&mut db).await?;

let profile = user
    .profile()
    .create()
    .bio("A person")
    .exec(&mut db)
    .await?;

assert_eq!(profile.user_id, Some(user.id));
Ok(())
}
}

Or create the parent and child together:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    bio: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .profile(Profile::create().bio("A person"))
    .exec(&mut db)
    .await?;
Ok(())
}
}

Updating the relation

Replacing with a new child

Create a new child and associate it with the parent in an update:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
    bio: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut user = User::create()
    .name("Alice")
    .profile(Profile::create().bio("Old bio"))
    .exec(&mut db)
    .await?;
user.update()
    .profile(Profile::create().bio("New bio"))
    .exec(&mut db)
    .await?;

let profile = user.profile().get(&mut db).await?.unwrap();
assert_eq!(profile.bio, "New bio");
Ok(())
}
}

Associating an existing child

Pass a reference to an existing child record:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().exec(&mut db).await?;
let profile = Profile::create().exec(&mut db).await?;

User::filter_by_id(user.id)
    .update()
    .profile(&profile)
    .exec(&mut db)
    .await?;
Ok(())
}
}

Unsetting the relation

For an optional HasOne, pass None to disassociate the child:

user.update().profile(None).exec(&mut db).await?;

// The profile no longer belongs to the user
assert!(user.profile().get(&mut db).await?.is_none());

What happens to the child when you unset the relation depends on the child’s foreign key:

Child’s foreign key typeEffect of unsetting
Required (user_id: u64)Deletes the child record
Optional (user_id: Option<u64>)Sets the foreign key to NULL

Deleting behavior

When you delete a parent, the behavior depends on the child’s foreign key type:

  • Required foreign key (user_id: u64): Toasty deletes the child record, since it cannot exist without a parent.
  • Optional foreign key (user_id: Option<u64>): Toasty sets the foreign key to NULL, leaving the child record in place.

What gets generated

For a User model with #[has_one] profile: HasOne<Option<Profile>>, Toasty generates:

MethodReturnsDescription
user.profile()Relation accessorAccessor for the associated profile
.get(&mut db)Result<Option<Profile>>Load the associated profile
.create()Create builderCreate a profile with the foreign key pre-filled
User::create().profile(...)Create builderAssociate a profile on creation
user.update().profile(...)Update builderReplace or associate a profile
user.update().profile(None)Update builderDisassociate the profile
User::fields().profile()Field pathUsed with .include() for preloading

Preloading Associations

Preloading (also called eager loading) loads related records alongside the main query, avoiding extra database round-trips when you access associations.

The N+1 problem

Without preloading, accessing a relation on each record in a list causes one query per record:

// 1 query: load all users
let users = User::all().exec(&mut db).await?;

for user in &users {
    // N queries: one per user to load their posts
    let posts = user.posts().exec(&mut db).await?;
    println!("{}: {} posts", user.name, posts.len());
}

If there are 100 users, this executes 101 queries. Preloading reduces this to a fixed number of queries regardless of how many records you load.

Using .include()

Add .include() to a query to preload a relation. Pass the field path from the model’s fields() accessor:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .post(Post::create().title("Hello"))
    .exec(&mut db)
    .await?;
let user = User::filter_by_id(user.id)
    .include(User::fields().posts())
    .get(&mut db)
    .await?;

// Access preloaded posts — no additional query
let posts: &[Post] = user.posts.get();
assert_eq!(1, posts.len());
Ok(())
}
}

The .include() call tells Toasty to load the associated posts as part of the query. After preloading, access the data through the field directly (user.posts.get()) instead of the method (user.posts().exec()).

Preloaded vs unloaded access

There are two ways to access a relation, depending on whether it was preloaded:

Access patternWhen to useQueries
user.posts().exec(&mut db)Relation was not preloadedExecutes a query
user.posts.get()Relation was preloaded with .include()No query

Calling .get() on an unloaded relation panics. Only use .get() when you know the relation was preloaded.

Preloading BelongsTo

Preload a parent record from the child side:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    posts: toasty::HasMany<Post>,
}
#[derive(Debug, toasty::Model)]
struct Post {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .post(Post::create().title("Hello"))
    .exec(&mut db)
    .await?;
let post_id = user.posts().exec(&mut db).await?[0].id;
let post = Post::filter_by_id(post_id)
    .include(Post::fields().user())
    .get(&mut db)
    .await?;

// Access the preloaded user
let user: &User = post.user.get();
assert_eq!("Alice", user.name);
Ok(())
}
}

Preloading HasOne

Preload a single child record from the parent side:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    bio: String,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create()
    .name("Alice")
    .profile(Profile::create().bio("A person"))
    .exec(&mut db)
    .await?;
let user = User::filter_by_id(user.id)
    .include(User::fields().profile())
    .get(&mut db)
    .await?;

// Access the preloaded profile
let profile = user.profile.get().as_ref().unwrap();
assert_eq!("A person", profile.bio);
Ok(())
}
}

If no related record exists, the preloaded value is None rather than causing a panic:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_one]
    profile: toasty::HasOne<Option<Profile>>,
}
#[derive(Debug, toasty::Model)]
struct Profile {
    #[key]
    #[auto]
    id: u64,
    bio: String,
    #[unique]
    user_id: Option<u64>,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<Option<User>>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let user = User::create().name("No Profile").exec(&mut db).await?;

let user = User::filter_by_id(user.id)
    .include(User::fields().profile())
    .get(&mut db)
    .await?;

// Preloaded and empty — .get() returns None, not a panic
assert!(user.profile.get().is_none());
Ok(())
}
}

Multiple includes

Chain multiple .include() calls to preload several relations in one query:

let user = User::filter_by_id(user_id)
    .include(User::fields().posts())
    .include(User::fields().comments())
    .get(&mut db)
    .await?;

// Both are preloaded
let posts: &[Post] = user.posts.get();
let comments: &[Comment] = user.comments.get();

You can mix relation types — preload HasMany, HasOne, and BelongsTo relations in the same query:

let user = User::filter_by_id(user_id)
    .include(User::fields().profile())   // HasOne
    .include(User::fields().posts())     // HasMany
    .get(&mut db)
    .await?;

Preloading with collection queries

.include() works with .exec() (collection queries), not just .get() (single record). All records in the result have their relations preloaded:

let users = User::all()
    .include(User::fields().posts())
    .exec(&mut db)
    .await?;

for user in &users {
    // No additional query per user
    let posts: &[Post] = user.posts.get();
    println!("{}: {} posts", user.name, posts.len());
}

Summary

SyntaxDescription
.include(Model::fields().relation())Preload a relation in the query
model.relation.get()Access preloaded HasMany data (returns &[T])
model.relation.get()Access preloaded BelongsTo data (returns &T)
model.relation.get()Access preloaded HasOne data (returns &T or &Option<T>)
model.relation.is_unloaded()Check if a relation was preloaded

Filtering with Expressions

The filter_by_* methods generated for indexed fields cover simple equality lookups. For anything else — comparisons, combining conditions with AND/OR, checking for null — use Model::filter() with field expressions.

ExpressionDescriptionSQL equivalent
.eq(value)Equal= value
.ne(value)Not equal!= value
.gt(value)Greater than> value
.ge(value)Greater than or equal>= value
.lt(value)Less than< value
.le(value)Less than or equal<= value
.in_list([...])Value in listIN (...)
.is_none()Null check (Option fields)IS NULL
.is_some()Not-null check (Option fields)IS NOT NULL
.and(expr)Both conditions trueAND
.or(expr)Either condition trueOR
.not() / !exprNegate conditionNOT
.any(expr)Any related record matches (HasMany)IN (SELECT ...)

Field paths

Every model has a fields() method that returns typed accessors for each field. These accessors produce field paths that you pass to comparison methods:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
fn __example() {
// User::fields() returns a struct with one method per field
let name_path = User::fields().name();
let country_path = User::fields().country();
}
}

Field paths are the building blocks for filter expressions. Call a comparison method on a path to get an Expr<bool>, then pass that expression to Model::filter().

Equality and inequality

.eq() tests whether a field equals a value. .ne() tests whether it does not:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Find users named "Alice"
let users = User::filter(User::fields().name().eq("Alice"))
    .exec(&mut db)
    .await?;

// Find users not from the US
let users = User::filter(User::fields().country().ne("US"))
    .exec(&mut db)
    .await?;
Ok(())
}
}

Ordering comparisons

Four methods compare field values by order:

MethodMeaning
.gt(value)Greater than
.ge(value)Greater than or equal
.lt(value)Less than
.le(value)Less than or equal
#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
    #[key]
    #[auto]
    id: u64,
    kind: String,
    timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Events after timestamp 1000
let events = Event::filter(Event::fields().timestamp().gt(1000))
    .exec(&mut db)
    .await?;

// Events at or before timestamp 500
let events = Event::filter(Event::fields().timestamp().le(500))
    .exec(&mut db)
    .await?;
Ok(())
}
}

Membership with in_list

.in_list() tests whether a field’s value is in a given list, equivalent to SQL’s IN clause:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[index]
    country: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let users = User::filter(
    User::fields().country().in_list(["US", "CA", "MX"]),
)
.exec(&mut db)
.await?;
Ok(())
}
}

Null checks

For Option<T> fields, use .is_none() and .is_some() to filter by whether the value is null:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    bio: Option<String>,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users who have not set a bio
let users = User::filter(User::fields().bio().is_none())
    .exec(&mut db)
    .await?;

// Users who have set a bio
let users = User::filter(User::fields().bio().is_some())
    .exec(&mut db)
    .await?;
Ok(())
}
}

These methods are only available on paths to Option<T> fields. Calling .is_none() on a non-optional field is a compile error.

Combining with AND

.and() combines two expressions so both must be true:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
    #[key]
    #[auto]
    id: u64,
    kind: String,
    timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let events = Event::filter(
    Event::fields()
        .kind()
        .eq("info")
        .and(Event::fields().timestamp().gt(1000)),
)
.exec(&mut db)
.await?;
Ok(())
}
}

Chain multiple .and() calls to add more conditions:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
    #[key]
    #[auto]
    id: u64,
    kind: String,
    timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let events = Event::filter(
    Event::fields()
        .kind()
        .eq("info")
        .and(Event::fields().timestamp().gt(1000))
        .and(Event::fields().timestamp().lt(2000)),
)
.exec(&mut db)
.await?;
Ok(())
}
}

You can also add AND conditions by chaining .filter() on a query. Each .filter() call adds another AND condition:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Event {
    #[key]
    #[auto]
    id: u64,
    kind: String,
    timestamp: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Equivalent to the previous example
let events = Event::filter(Event::fields().kind().eq("info"))
    .filter(Event::fields().timestamp().gt(1000))
    .filter(Event::fields().timestamp().lt(2000))
    .exec(&mut db)
    .await?;
Ok(())
}
}

Combining with OR

.or() combines two expressions so at least one must be true:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    age: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users named "Alice" or aged 35
let users = User::filter(
    User::fields()
        .name()
        .eq("Alice")
        .or(User::fields().age().eq(35)),
)
.exec(&mut db)
.await?;
Ok(())
}
}

Expressions evaluate left to right through method chaining. Each method wraps everything before it. a.or(b).and(c) produces (a OR b) AND c:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    age: i64,
    active: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// (name = "Alice" OR age = 35) AND active = true
let users = User::filter(
    User::fields()
        .name()
        .eq("Alice")
        .or(User::fields().age().eq(35))
        .and(User::fields().active().eq(true)),
)
.exec(&mut db)
.await?;
Ok(())
}
}

To group differently, build sub-expressions and pass them as arguments. Here, a.or(b.and(c)) produces a OR (b AND c):

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    age: i64,
    active: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// name = "Alice" OR (age = 35 AND active = true)
let users = User::filter(
    User::fields().name().eq("Alice").or(User::fields()
        .age()
        .eq(35)
        .and(User::fields().active().eq(true))),
)
.exec(&mut db)
.await?;
Ok(())
}
}

Negation with NOT

.not() negates an expression. The ! operator works too:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    age: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Users not named "Alice"
let users = User::filter(User::fields().name().eq("Alice").not())
    .exec(&mut db)
    .await?;

// Same thing with the ! operator
let users = User::filter(!User::fields().name().eq("Alice"))
    .exec(&mut db)
    .await?;
Ok(())
}
}

NOT works on compound expressions too:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    age: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// NOT (name = "Alice" OR name = "Bob")
let users = User::filter(
    !(User::fields()
        .name()
        .eq("Alice")
        .or(User::fields().name().eq("Bob"))),
)
.exec(&mut db)
.await?;
Ok(())
}
}

Filtering on associations

For HasMany relations, .any() tests whether at least one related record matches a condition. This generates a subquery:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct User {
    #[key]
    #[auto]
    id: u64,
    name: String,
    #[has_many]
    todos: toasty::HasMany<Todo>,
}
#[derive(Debug, toasty::Model)]
struct Todo {
    #[key]
    #[auto]
    id: u64,
    #[index]
    user_id: u64,
    #[belongs_to(key = user_id, references = id)]
    user: toasty::BelongsTo<User>,
    title: String,
    complete: bool,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Find users who have at least one incomplete todo
let users = User::filter(
    User::fields()
        .todos()
        .any(Todo::fields().complete().eq(false)),
)
.exec(&mut db)
.await?;
Ok(())
}
}

The path User::fields().todos() refers to the HasMany relation. Calling .any() on it takes a filter expression on the child model (Todo) and produces a filter expression on the parent (User).

Sorting, Limits, and Pagination

Queries return results in an unspecified order by default. Use .order_by() to sort results, .limit() and .offset() to restrict the result set, and .paginate() to walk through results one page at a time.

Sorting with order_by

Call .order_by() on a query with a field path and a direction — .asc() for ascending or .desc() for descending:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Ascending: smallest order first
let items = Item::all()
    .order_by(Item::fields().order().asc())
    .exec(&mut db)
    .await?;

// Descending: largest order first
let items = Item::all()
    .order_by(Item::fields().order().desc())
    .exec(&mut db)
    .await?;
Ok(())
}
}

Item::fields().order() returns a field path. Calling .asc() or .desc() on it produces an ordering expression that .order_by() accepts.

Sorting works with filters too:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
    category: String,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let items = Item::filter(Item::fields().category().eq("books"))
    .order_by(Item::fields().order().desc())
    .exec(&mut db)
    .await?;
Ok(())
}
}

Limiting results

.limit(n) caps the number of records returned:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// At most 5 items
let items = Item::all().limit(5).exec(&mut db).await?;
Ok(())
}
}

If the query matches fewer records than the limit, all matching records are returned.

Combine .order_by() with .limit() to get the top or bottom N records:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Top 7 items by order (highest first)
let items = Item::all()
    .order_by(Item::fields().order().desc())
    .limit(7)
    .exec(&mut db)
    .await?;
Ok(())
}
}

Offset

.offset(n) skips the first n results. It requires .limit() to be called first:

#![allow(unused)]
fn main() {
use toasty::Model;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Skip the first 5, then return the next 7
let items = Item::all()
    .order_by(Item::fields().order().asc())
    .limit(7)
    .offset(5)
    .exec(&mut db)
    .await?;
Ok(())
}
}

Limit and offset work for simple cases, but cursor-based pagination (below) is a better fit for paging through large result sets. Offset-based pagination gets slower as the offset increases because the database still reads and discards the skipped rows. It can also produce inconsistent results when rows are inserted or deleted between page fetches. See Markus Winand’s “No Offset” for an in-depth explanation.

Cursor-based pagination

.paginate(per_page) splits results into pages. It requires .order_by() and returns a Page instead of a Vec:

#![allow(unused)]
fn main() {
use toasty::Model;
use toasty::Page;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let page: Page<_> = Item::all()
    .order_by(Item::fields().order().desc())
    .paginate(10)
    .exec(&mut db)
    .await?;

// Access items in the page
for item in page.iter() {
    println!("order: {}", item.order);
}

// Check how many items are in this page
println!("items: {}", page.len());
Ok(())
}
}

A Page dereferences to a slice, so you can index into it, iterate over it, and call slice methods like .len() and .iter() directly.

Page provides .next() and .prev() methods that fetch the next or previous page. Both return Option<Page>None when there are no more results in that direction:

#![allow(unused)]
fn main() {
use toasty::Model;
use toasty::Page;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let first_page: Page<_> = Item::all()
    .order_by(Item::fields().order().asc())
    .paginate(10)
    .exec(&mut db)
    .await?;

// Move to the next page
if let Some(second_page) = first_page.next(&mut db).await? {
    println!("page 2 has {} items", second_page.len());

    // Go back
    if let Some(back) = second_page.prev(&mut db).await? {
        println!("back to page 1: {} items", back.len());
    }
}
Ok(())
}
}

Use .has_next() and .has_prev() to check whether more pages exist without fetching them:

if page.has_next() {
    let next = page.next(&mut db).await?.unwrap();
}

Walking all pages

To process every record in pages:

#![allow(unused)]
fn main() {
use toasty::Model;
use toasty::Page;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
let mut page: Page<_> = Item::all()
    .order_by(Item::fields().order().asc())
    .paginate(10)
    .exec(&mut db)
    .await?;

loop {
    for item in page.iter() {
        println!("order: {}", item.order);
    }

    match page.next(&mut db).await? {
        Some(next) => page = next,
        None => break,
    }
}
Ok(())
}
}

Starting from a cursor position

Use .after() to start pagination after a specific value in the sort field:

#![allow(unused)]
fn main() {
use toasty::Model;
use toasty::Page;
#[derive(Debug, toasty::Model)]
struct Item {
    #[key]
    #[auto]
    id: u64,
    #[index]
    order: i64,
}
async fn __example(mut db: toasty::Db) -> toasty::Result<()> {
// Start after order=90 (descending), so the first item will be order=89
let page: Page<_> = Item::all()
    .order_by(Item::fields().order().desc())
    .paginate(10)
    .after(90)
    .exec(&mut db)
    .await?;
Ok(())
}
}

The value passed to .after() corresponds to the field used in .order_by().

Method summary

Methods available on query builders:

MethodDescription
.order_by(field.asc())Sort ascending by field
.order_by(field.desc())Sort descending by field
.limit(n)Return at most n records
.offset(n)Skip first n records (requires .limit())
.paginate(per_page)Cursor-based pagination (requires .order_by())

Methods available on Page:

MethodReturnsDescription
.next(&mut db)Result<Option<Page>>Fetch next page
.prev(&mut db)Result<Option<Page>>Fetch previous page
.has_next()boolWhether a next page exists
.has_prev()boolWhether a previous page exists
.itemsVec<M>The records in this page
.len()usizeNumber of items (via Deref to slice)
.iter()iteratorIterate items (via Deref to slice)

Embedded Types

This section still needs to be written. It will cover storing structs and enums inline.

Batch Operations

This section still needs to be written. It will cover running multiple queries in one round-trip.

Transactions

This section still needs to be written. It will cover atomic operations.

Database Setup

This section still needs to be written. It will cover connection URLs, table creation, and supported databases.