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

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

Custom column names

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

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

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

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

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

Explicit column types

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

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

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

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

You can combine a custom name with an explicit type:

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

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

Supported type values:

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

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

Default values

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

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

assert_eq!(post.view_count, 0);

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

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

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

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

Update expressions

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

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

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

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

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

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

Combining #[default] and #[update]

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

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

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

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

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

Timestamps with #[auto]

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

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

    title: String,

    #[auto]
    created_at: jiff::Timestamp,

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

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

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

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

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

Timestamp fields require the jiff feature:

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

Date and time fields

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

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

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

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

    name: String,

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

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

JSON serialization

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

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

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

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

    title: String,

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

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

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

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

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

Nullable JSON fields

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

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

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

With nullable:

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

Without nullable:

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

Attribute summary

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