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 usersUser::get_by_id()— fetch a user by primary keyUser::get_by_email()— fetch a user by the unique email fieldUser::all()— query all usersuser.update()— a builder for modifying a useruser.delete()— remove a userUser::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 — set up a project and run your first query
- Defining Models — struct fields, types, and table mapping
- Keys and Auto-Generation — primary keys, auto-generated values, composite keys
- Schema Management — create and reset database tables
- Creating Records — insert one or many records
- Querying Records — find, filter, and iterate over results
- Updating Records — modify existing records
- Deleting Records — remove records
- Indexes and Unique Constraints — add indexes and unique constraints
- Field Options — column names, types, defaults, and JSON serialization
- BelongsTo — define and use many-to-one relationships
- HasMany — define and use one-to-many relationships
- HasOne — define and use one-to-one relationships
- Preloading Associations — eager loading to avoid extra queries
- Filtering with Expressions — comparisons, AND/OR, and more
- Sorting, Limits, and Pagination — order results and paginate
- Embedded Types — store structs and enums inline
- Batch Operations — multiple queries in one round-trip
- Transactions — atomic operations
- Database Setup — connection URLs, table creation, and supported databases
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 wrote | Toasty generated |
|---|---|
struct User | User::create() — a builder to insert rows |
#[key] on id | User::get_by_id() — fetch by primary key |
#[auto] on id | Auto-generates an ID when you create a user |
#[unique] on email | User::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 type | Database type |
|---|---|
bool | Boolean |
String | Text |
i8, i16, i32, i64 | Integer (1, 2, 4, 8 bytes) |
u8, u16, u32, u64 | Unsigned integer |
uuid::Uuid | UUID |
Vec<u8> | Binary / Blob |
Option<T> | Nullable version of T |
With optional feature flags:
| Feature | Rust type | Database type |
|---|---|---|
rust_decimal | rust_decimal::Decimal | Decimal |
bigdecimal | bigdecimal::BigDecimal | Decimal |
jiff | jiff::Timestamp | Timestamp |
jiff | jiff::civil::Date | Date |
jiff | jiff::civil::Time | Time |
jiff | jiff::civil::DateTime | DateTime |
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()orUser::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] behavior | Explicit form |
|---|---|---|
uuid::Uuid | Generates 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:
| Attribute | Generated methods |
|---|---|
#[key] on single field | get_by_<field>(), filter_by_<field>(), delete_by_<field>() |
#[key] on multiple fields | get_by_<a>_and_<b>(), filter_by_<a>_and_<b>(), delete_by_<a>_and_<b>() |
#[key(a, b)] on struct | Same 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:
| Method | Returns | Description |
|---|---|---|
User::all() | Query builder | All records |
User::filter(expr) | Query builder | Records matching expression |
User::filter_by_id(id) | Query builder | Records matching key |
User::filter_by_id_batch(ids) | Query builder | Records matching any key in list |
User::filter_by_email(email) | Query builder | Records 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:
| Method | Returns |
|---|---|
.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 (consumesself), 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:
| Attribute | Meaning | Database effect (SQL) |
|---|---|---|
#[unique] | Each record has a distinct value | CREATE UNIQUE INDEX — the database rejects duplicates |
#[index] | Multiple records may share a value | CREATE 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:
| Method | Description |
|---|---|
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 syntax | Database type |
|---|---|
boolean | Boolean |
int, i8, i16, i32, i64 | Integer (various sizes) |
uint, u8, u16, u32, u64 | Unsigned integer |
text | Text |
varchar(N) | Variable-length string with max length |
numeric, numeric(P, S) | Decimal with optional precision and scale |
binary(N), blob | Binary data |
timestamp(P) | Timestamp with precision |
date | Date |
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 name | Field type | #[auto] expands to |
|---|---|---|
created_at | jiff::Timestamp | #[default(jiff::Timestamp::now())] — set once on create |
updated_at | jiff::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 type | Description |
|---|---|
jiff::Timestamp | An instant in time (UTC) |
jiff::civil::Date | A date without time |
jiff::civil::Time | A time of day without date |
jiff::civil::DateTime | A 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:
Nonemaps to SQLNULLin the databaseSome(value)maps to the JSON string representation
Without nullable:
Nonemaps to the JSON text"null"(a non-null string)Some(value)maps to the JSON string representation
Attribute summary
| Attribute | Purpose | Applies on |
|---|---|---|
#[column("name")] | Custom column name | — |
#[column(type = ...)] | Explicit column type | — |
#[default(expr)] | Default value | Create only |
#[update(expr)] | Automatic value | Create and update |
#[auto] on created_at | Shorthand for #[default(jiff::Timestamp::now())] | Create only |
#[auto] on updated_at | Shorthand for #[update(jiff::Timestamp::now())] | Create and update |
#[serialize(json)] | Store as JSON text | Create and update |
#[serialize(json, nullable)] | Store as JSON text with SQL NULL support | Create 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.
| Type | Foreign key on | Parent has | Child has | Example |
|---|---|---|---|---|
| BelongsTo | This model | — | One parent | A post belongs to a user |
| HasMany | Other model | Many children | — | A user has many posts |
| HasOne | Other model | One child | — | A 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.
Data consistency on delete and unlink
When you delete a parent record or disassociate a child, Toasty automatically maintains consistency based on the foreign key’s nullability:
| Action | FK is required (u64) | FK is optional (Option<u64>) |
|---|---|---|
| Delete parent | Child is deleted | Child stays, FK set to NULL |
Unset relation (e.g., update().profile(None)) | Child is deleted | Child stays, FK set to NULL |
| Delete child | Parent is unaffected | Parent 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… | Use | FK goes on |
|---|---|---|
| A post has one author | Post → BelongsTo<User> + User → HasMany<Post> | posts.user_id |
| A user has one profile | User → HasOne<Profile> + Profile → BelongsTo<User> | profiles.user_id |
| A comment belongs to a post | Comment → BelongsTo<Post> + Post → HasMany<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.
Accessing the related record
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:
| Method | Returns | Description |
|---|---|---|
post.user() | Relation accessor | Returns an accessor for the associated user |
.get(&mut db) | Result<User> | Loads the associated user from the database |
Post::create().user(&user) | Create builder | Sets the foreign key from a parent reference |
Post::create().user_id(id) | Create builder | Sets the foreign key directly |
Post::fields().user() | Field path | Used 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 type | Effect 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:
| Method | Returns | Description |
|---|---|---|
user.posts() | Relation accessor | Accessor scoped to this user’s posts |
.exec(&mut db) | Result<Vec<Post>> | All posts belonging to this user |
.create() | Create builder | Create 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 builder | Filter 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:
| Method | Description |
|---|---|
.post(Post::create()...) | Add one child to create alongside the parent |
.posts(...) | Add multiple children to create alongside the parent |
On the fields accessor:
| Method | Description |
|---|---|
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(())
}
}
Accessing the related record
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 type | Effect 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 toNULL, leaving the child record in place.
What gets generated
For a User model with #[has_one] profile: HasOne<Option<Profile>>, Toasty
generates:
| Method | Returns | Description |
|---|---|---|
user.profile() | Relation accessor | Accessor for the associated profile |
.get(&mut db) | Result<Option<Profile>> | Load the associated profile |
.create() | Create builder | Create a profile with the foreign key pre-filled |
User::create().profile(...) | Create builder | Associate a profile on creation |
user.update().profile(...) | Update builder | Replace or associate a profile |
user.update().profile(None) | Update builder | Disassociate the profile |
User::fields().profile() | Field path | Used 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 pattern | When to use | Queries |
|---|---|---|
user.posts().exec(&mut db) | Relation was not preloaded | Executes 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
| Syntax | Description |
|---|---|
.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.
| Expression | Description | SQL 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 list | IN (...) |
.is_none() | Null check (Option fields) | IS NULL |
.is_some() | Not-null check (Option fields) | IS NOT NULL |
.and(expr) | Both conditions true | AND |
.or(expr) | Either condition true | OR |
.not() / !expr | Negate condition | NOT |
.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:
| Method | Meaning |
|---|---|
.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.
Navigating pages
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:
| Method | Description |
|---|---|
.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:
| Method | Returns | Description |
|---|---|---|
.next(&mut db) | Result<Option<Page>> | Fetch next page |
.prev(&mut db) | Result<Option<Page>> | Fetch previous page |
.has_next() | bool | Whether a next page exists |
.has_prev() | bool | Whether a previous page exists |
.items | Vec<M> | The records in this page |
.len() | usize | Number of items (via Deref to slice) |
.iter() | iterator | Iterate 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.