#[derive(Model)]
{
// Attributes available to this derive:
#[key]
#[auto]
#[default]
#[update]
#[column]
#[index]
#[unique]
#[table]
#[has_many]
#[has_one]
#[belongs_to]
#[version]
#[shared]
#[document]
}
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
Modeltrait implementation, including the associatedQuery,Create, andUpdatebuilder types. - A
Loadimplementation for deserializing rows from the database. - The
Modeltrait’s schema-registration methods (id,schema,register) used to register the model at runtime. - Static query methods such as
all(),filter(expr),filter_by_<field>(), andget_by_<key>(). - Instance methods
update()anddelete(). - A
Fieldsstruct 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 fields are allowed using bracket syntax:
#[key(partition = [tenant, 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. User →
users).
#[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:
| Syntax | Behavior |
|---|---|
#[auto] on uuid::Uuid | UUID v7 (timestamp-sortable) |
#[auto(uuid(v4))] | UUID v4 (random) |
#[auto(uuid(v7))] | UUID v7 (explicit) |
#[auto] on integer types (i8–i64, u8–u64) | Auto-increment |
#[auto(increment)] | Auto-increment (explicit) |
#[auto] on a field named created_at | Expands to #[default(jiff::Timestamp::now())] |
#[auto] on a field named updated_at | Expands 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
| Syntax | Description |
|---|---|
boolean | Boolean |
i8, i16, i32, i64 | Signed integer (1/2/4/8 bytes) |
int(N) | Signed integer with N-byte width |
u8, u16, u32, u64 | Unsigned integer (1/2/4/8 bytes) |
uint(N) | Unsigned integer with N-byte width |
text | Unbounded text |
varchar(N) | Text with max length N |
numeric | Arbitrary-precision numeric |
numeric(P, S) | Numeric with precision P and scale S |
binary(N) | Fixed-size binary with N bytes |
blob | Variable-length binary |
timestamp(P) | Timestamp with P fractional-second digits |
date | Date 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.
§JSON-encoded fields via Json<T>
Wrap a serde-typed value in toasty::Json<T> to
store it as a JSON string in the database. Requires the serde feature
and that T implements serde::Serialize and serde::Deserialize.
tags: toasty::Json<Vec<String>>,For nullable JSON columns, wrap Json<T> in Option — None maps to
SQL NULL:
metadata: Option<toasty::Json<HashMap<String, String>>>,To instead store None as the JSON literal "null" (no SQL NULL),
wrap the other way: Json<Option<T>>.
§Relation attributes
Relation fields can be lazy or eager. Wrap the relation value in
toasty::Deferred<_> for lazy loading; ordinary queries leave the field
unloaded until the generated relation accessor or .include(...) loads it.
Use the relation value directly for eager loading; every query that returns
the model loads the relation as if the query included that field.
| Attribute | Lazy field type | Eager field type |
|---|---|---|
#[belongs_to] | toasty::Deferred<T> or toasty::Deferred<Option<T>> | T or Option<T> |
#[has_many] | toasty::Deferred<Vec<T>> | Vec<T> |
#[has_one] | toasty::Deferred<T> or toasty::Deferred<Option<T>> | T or Option<T> |
Toasty rejects schemas with eager-load cycles. If two relation paths point
back to each other, wrap at least one field in toasty::Deferred<_>.
§#[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::Deferred<User>,To load the relation with every Example query, omit Deferred:
#[belongs_to(key = user_id, references = id)]
user: User,| Parameter | Meaning |
|---|---|
key = <field> | Local field holding the foreign key value |
references = <field> | Field on the target model being referenced |
For composite foreign keys, pass arrays to key and references:
#[belongs_to(key = [org_id, tenant_id], references = [id, tenant_id])]
org: toasty::Deferred<Org>,The number of fields in key must equal the number of fields in
references.
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::Deferred<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::Deferred<Vec<Post>>,To load the collection with every Example query, use Vec<Post>:
#[has_many]
posts: Vec<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::Deferred<Vec<Person>>,§via — multi-step relations
Instead of pairing with a belongs_to, a has_many can reach its target
through a path of existing relations with via. The path is a dotted
chain of relation fields, read left to right starting from this model. A
via relation owns no foreign key — it is derived from the relations it
traverses — so it takes no pair:
// User → comments → article
#[has_many(via = comments.article)]
commented_articles: toasty::Deferred<Vec<Article>>,The target type is Article because the path comments.article ends
there. A via relation is read-only and yields distinct targets — a target
reached through several intermediates appears once. Query, filter, and order
it like any other relation. Preloading it with .include() or projecting it
with .select() is supported on SQL backends; both are not yet available on
DynamoDB.
§#[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::Deferred<Profile>,To load the relation with every Example query, omit Deferred:
#[has_one]
profile: Profile,Wrap in Option for an optional association:
#[has_one]
profile: toasty::Deferred<Option<Profile>>,The eager optional form is Option<Profile>.
§via — multi-step relations
Like #[has_many], a #[has_one] can reach its target through a path of
existing relations with via (see the #[has_many] via section above for
the full rules). Declare it when the path is expected to reach at most one
target:
// User → account → subscription
#[has_one(via = account.subscription)]
subscription: toasty::Deferred<Option<Subscription>>,§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], and#[update]cannot be used on relation fields (BelongsTo,HasMany,HasOne).- A field can have at most one relation attribute.
- Eager relation fields cannot form a cycle. Use
toasty::Deferred<_>on at least one edge of a bidirectional relation. Selfcan 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::Deferred<Vec<Post>>,
}
#[derive(Debug, toasty::Model)]
struct Post {
#[key]
#[auto]
id: i64,
title: String,
tags: toasty::Json<Vec<String>>,
#[index]
user_id: i64,
#[belongs_to(key = user_id, references = id)]
user: toasty::Deferred<User>,
}