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

Field Options

Field-level attributes control column names, explicit types, default values, and update expressions.

Custom column names

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

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

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

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

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

Explicit column types

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

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

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

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

You can combine a custom name with an explicit type:

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

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

Supported type values:

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

Not all databases support all column types. Toasty validates explicit column types against the database’s capabilities when you call db.push_schema(). If a type is not supported, schema creation fails with an error. For example, varchar is supported by PostgreSQL and MySQL but not by SQLite, Turso, 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 = toasty::create!(Post { title: "Hello World" })
    .exec(&mut db)
    .await?;

assert_eq!(post.view_count, 0);

// Override the default by setting it explicitly
let post = toasty::create!(Post {
    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] applies to a create builder and to the create branch of an upsert. It does not change an existing record on the update branch. A shared upsert mutation applies to this value when it creates a record; for example, #[default(10)] with subtract(3) inserts seven.

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.

An upsert applies #[update] on both its create and update branches.

#![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 = toasty::create!(Post { 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 = toasty::create!(Post { 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, and #[update] applies on update. An upsert selects the corresponding value for each branch:

#![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 = toasty::create!(Post { title: "Hello" })
    .exec(&mut db)
    .await?;

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

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

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

Timestamps with #[auto]

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

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

    title: String,

    #[auto]
    created_at: jiff::Timestamp,

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

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

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

On key fields, bare #[auto] defers to the type’s default auto-generation strategy (e.g., auto-increment for integers, UUID v7 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.9", features = ["sqlite", "jiff"] }

Date and time fields

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

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

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

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

    name: String,

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

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

JSON-encoded fields

toasty::Json<T> and serde_json::Value fields require #[column(type = text)], varchar(...), json, or jsonb. See JSON Encoding for feature setup, storage support, null representations, setters, deferred loading, and query limitations.

Attribute summary

AttributePurposeApplies on
#[column("name")]Custom column name
#[column(type = ...)]Explicit column type
#[default(expr)]Default valueCreate and the create branch of upsert
#[update(expr)]Automatic valueCreate, update, and both upsert branches
#[auto] on created_atShorthand for #[default(jiff::Timestamp::now())]Create and the create branch of upsert
#[auto] on updated_atShorthand for #[update(jiff::Timestamp::now())]Create, update, and both upsert branches

See Concurrency Control for the #[version] attribute.

Runnable example: cms-article-fields covers field options — defaults, auto timestamps, Json<T>, a queryable Vec<scalar>, and deferred columns.