Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Keys and Auto-Generation

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

Single-field keys

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

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

    name: String,
}
}

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

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

Keys without #[auto]

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

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

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

Other key types

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

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

    name: String,
}
}

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

Auto-generated values

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

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

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

Auto strategies

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

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

You can specify the strategy explicitly:

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

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

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

UUID v7 vs v4

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

UUID v4 values are random with no ordering.

Integer auto-increment

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

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

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

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

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

Composite keys

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

Multiple #[key] fields

Mark each key field with #[key]:

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

    #[key]
    course_id: u64,

    grade: Option<String>,
}
}

This generates lookup methods that take both fields:

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

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

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

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

    course_id: u64,

    grade: Option<String>,
}
}

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

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

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

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

    name: String,
}
}

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

Partition and local keys

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

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

    title: String,

    user_id: u64,
}
}

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

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

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

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

What gets generated

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

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