Model

Derive Macro Model 

Source
#[derive(Model)]
{
    // Attributes available to this derive:
    #[key]
    #[auto]
    #[default]
    #[update]
    #[column]
    #[index]
    #[unique]
    #[table]
    #[has_many]
    #[has_one]
    #[belongs_to]
    #[serialize]
}
Expand description

Derive macro that turns a struct into a Toasty model backed by a database table.

For a tutorial-style introduction, see the Toasty guide.

§Overview

Applying #[derive(Model)] to a named struct generates:

  • A Model trait implementation, including the associated Query, Create, and Update builder types.
  • A Load implementation for deserializing rows from the database.
  • A Register implementation for schema registration at runtime.
  • Static query methods such as all(), filter(expr), filter_by_<field>(), and get_by_<key>().
  • Instance methods update() and delete().
  • A Fields struct returned by <Model>::fields() for building typed filter expressions.

The struct must have named fields and no generic parameters.

§Struct-level attributes

§#[key(...)] — primary key

Defines the primary key at the struct level. Mutually exclusive with field-level #[key].

Simple form — every listed field becomes a partition key:

#[derive(Model)]
#[key(name)]
struct Widget {
    name: String,
    value: i64,
}

Composite key with partition/local scoping:

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

The partition fields determine data distribution (relevant for DynamoDB); local fields scope within a partition. For SQL databases both behave as a regular composite primary key.

Multiple partition and local entries are allowed:

#[key(partition = tenant, partition = org, local = id)]

When using named partition/local syntax, at least one of each is required. You cannot mix the simple and named forms.

§#[table = "name"] — custom table name

Overrides the default table name. Without this attribute the table name is the pluralized, snake_case form of the struct name (e.g. Userusers).

#[derive(Model)]
#[table = "legacy_users"]
struct User {
    #[key]
    #[auto]
    id: i64,
    name: String,
}

§Field-level attributes

§#[key] — mark a field as a primary key column

Marks one or more fields as the primary key. When used on multiple fields each becomes a partition key column (equivalent to listing them in #[key(...)] at the struct level).

Cannot be combined with a struct-level #[key(...)] attribute.

#[derive(Model)]
struct User {
    #[key]
    #[auto]
    id: i64,
    name: String,
}

§#[auto] — automatic value generation

Tells Toasty to generate this field’s value automatically. The strategy depends on the field type and optional arguments:

SyntaxBehavior
#[auto] on uuid::UuidUUID v7 (timestamp-sortable)
#[auto(uuid(v4))]UUID v4 (random)
#[auto(uuid(v7))]UUID v7 (explicit)
#[auto] on integer types (i8i64, u8u64)Auto-increment
#[auto(increment)]Auto-increment (explicit)
#[auto] on a field named created_atExpands to #[default(jiff::Timestamp::now())]
#[auto] on a field named updated_atExpands to #[update(jiff::Timestamp::now())]

The created_at/updated_at expansion requires the jiff feature and a field type compatible with jiff::Timestamp.

Cannot be combined with #[default] or #[update] on the same field.

§#[default(expr)] — default value on create

Sets a default value that is used when the field is not explicitly provided during creation. The expression is any valid Rust expression.

#[default(0)]
view_count: i64,

#[default("draft".to_string())]
status: String,

The default can be overridden by calling the corresponding setter on the create builder.

Cannot be combined with #[auto] on the same field. Can be combined with #[update] (the default applies on create; the update expression applies on subsequent updates).

§#[update(expr)] — value applied on create and update

Sets a value that Toasty applies every time a record is created or updated, unless the field is explicitly set on the builder.

#[update(jiff::Timestamp::now())]
updated_at: jiff::Timestamp,

Cannot be combined with #[auto] on the same field.

§#[index] — add a database index

Creates a non-unique index on the field. Toasty generates a filter_by_<field> method for indexed fields.

#[index]
email: String,

§#[unique] — add a unique constraint

Creates a unique index on the field. Like #[index], this generates filter_by_<field>. The database enforces uniqueness.

#[unique]
email: String,

§#[column(...)] — customize the database column

Overrides the column name and/or type for a field.

Custom name:

#[column("user_email")]
email: String,

Custom type:

#[column(type = varchar(255))]
email: String,

Both:

#[column("user_email", type = varchar(255))]
email: String,

§Supported column types

SyntaxDescription
booleanBoolean
i8, i16, i32, i64Signed integer (1/2/4/8 bytes)
int(N)Signed integer with N-byte width
u8, u16, u32, u64Unsigned integer (1/2/4/8 bytes)
uint(N)Unsigned integer with N-byte width
textUnbounded text
varchar(N)Text with max length N
numericArbitrary-precision numeric
numeric(P, S)Numeric with precision P and scale S
binary(N)Fixed-size binary with N bytes
blobVariable-length binary
timestamp(P)Timestamp with P fractional-second digits
dateDate without time
time(P)Time with P fractional-second digits
datetime(P)Date and time with P fractional-second digits
"custom"Arbitrary type string passed through to the driver

Cannot be used on relation fields.

§#[serialize(json)] — serialize complex types as JSON

Stores the field as a JSON string in the database. Requires the serde feature and that the field type implements serde::Serialize and serde::Deserialize.

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

For Option<T> fields, add nullable so that None maps to SQL NULL rather than the JSON string "null":

#[serialize(json, nullable)]
metadata: Option<HashMap<String, String>>,

Cannot be used on relation fields.

§Relation attributes

§#[belongs_to(...)] — foreign-key reference

Declares a many-to-one (or one-to-one) association through a foreign key stored on this model.

#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<User>,
ParameterMeaning
key = <field>Local field holding the foreign key value
references = <field>Field on the target model being referenced

For composite foreign keys, repeat key/references pairs:

#[belongs_to(key = org_id, references = id, key = tenant_id, references = tenant_id)]
org: toasty::BelongsTo<Org>,

The number of key entries must equal the number of references entries.

Wrap the target type in Option for an optional (nullable) foreign key:

#[index]
manager_id: Option<i64>,

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

§#[has_many] — one-to-many association

Declares a collection of related models. The target model must have a #[belongs_to] field pointing back to this model.

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

Toasty generates an accessor method (e.g. .posts()) and an insert helper (e.g. .insert_post()), where the insert helper name is the auto-singularized field name.

§pair — disambiguate self-referential or multiple relations

When the target model has more than one #[belongs_to] pointing to the same model (or points to itself), use pair to specify which belongs_to field this has_many corresponds to:

#[has_many(pair = parent)]
children: toasty::HasMany<Person>,

§#[has_one] — one-to-one association

Declares a single related model. The target model must have a #[belongs_to] field pointing back to this model.

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

Wrap in Option for an optional association:

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

§Constraints

  • The struct must have named fields (tuple structs are not supported).
  • Generic parameters are not supported.
  • Every root model must have a primary key, defined either by a struct-level #[key(...)] or by one or more field-level #[key] attributes, but not both.
  • #[auto] cannot be combined with #[default] or #[update] on the same field.
  • #[column], #[default], #[update], and #[serialize] cannot be used on relation fields (BelongsTo, HasMany, HasOne).
  • A field can have at most one relation attribute.
  • Self can be used as a type in relation fields for self-referential models.

§Full example

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

    #[unique]
    email: String,

    name: String,

    #[default(jiff::Timestamp::now())]
    created_at: jiff::Timestamp,

    #[update(jiff::Timestamp::now())]
    updated_at: jiff::Timestamp,

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

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

    title: String,

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

    #[index]
    user_id: i64,

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