#[derive(Embed)]
{
// Attributes available to this derive:
#[belongs_to]
#[column]
#[document]
#[index]
#[unique]
#[shared]
}
Expand description
Derive macro that turns a struct or enum into an embedded type stored inline in a parent model’s table.
Embedded types do not have their own tables or primary keys. Their
fields are flattened into the parent model’s columns. Use Embed for
value objects (addresses, coordinates, metadata) and enums
(status codes, contact info variants).
§Structs
An embedded struct’s fields become columns in the parent table, prefixed
with the field name. For example, an address: Address field with
street and city produces columns address_street and
address_city.
#[derive(toasty::Embed)]
struct Address {
street: String,
city: String,
}
#[derive(toasty::Model)]
struct User {
#[key]
#[auto]
id: i64,
name: String,
address: Address,
}Applying #[derive(Embed)] to a struct generates:
- An
Embedtrait implementation (idandschemamethods). - A
Fieldsstruct returned by<Type>::fields()for building filter expressions on individual fields. - An
Updatestruct used by the parent model’s update builder for partial field updates.
A field accessor is named after the field it reads. A newtype’s field is
unnamed, so its accessor is inner(). It returns a path to the single
column the newtype maps to, which compares against the wrapped type:
#[derive(toasty::Embed)]
struct Email(String);
#[derive(toasty::Model)]
struct User {
#[key]
#[auto]
id: i64,
email: Email,
}
let query = User::filter(User::fields().email().inner().eq("alice@example.com"));Multi-field structs do not get the ordering methods — multi-column values have no ordering shared across backends:
// Error: no method `ge` on the fields struct of a multi-field embed
let _ = Pin::filter(Pin::fields().location().ge(Point { x: 0, y: 0 }));The same applies to sorting — asc/desc exist only on newtype fields:
// Error: no method `asc` on the fields struct of a multi-field embed
let _ = Pin::all().order_by(Pin::fields().location().asc());A tuple-newtype can wrap a non-indexable type, but the wrapper can only participate in an index when its inner type can. Toasty checks that requirement when a model uses the wrapper in an index or unique constraint.
#[derive(toasty::Embed)]
struct Outer(Point);§Nesting
Embedded structs can contain other embedded types. Columns are flattened with chained prefixes:
#[derive(toasty::Embed)]
struct Location {
lat: i64,
lon: i64,
}
#[derive(toasty::Embed)]
struct Address {
street: String,
city: Location,
}When Address is embedded as address in a parent model, this
produces columns address_street, address_city_lat, and
address_city_lon.
§Enums
An embedded enum stores a discriminant value identifying the active
variant. By default, Toasty derives a string label for each variant by
converting its Rust name to snake_case. Use
#[column(rename_all = "...")] on the enum to select another naming
convention, or #[column(variant = "...")] on a variant to set one label.
Unit-only enum:
#[derive(toasty::Embed)]
enum Status {
Pending,
InProgress,
Archived,
}A unit-only enum occupies a single column in the parent table. The
example stores the labels pending, in_progress, and archived.
Data-carrying enum:
#[derive(toasty::Embed)]
enum ContactInfo {
Email { address: String },
Phone { number: String },
}A data-carrying enum stores the discriminant column plus one nullable
column per variant field. For example, a contact: ContactInfo field
produces columns contact (discriminant), contact_address, and
contact_number. Only the columns belonging to the active variant
contain values; the rest are NULL.
Mixed enum (unit and data variants together):
#[derive(toasty::Embed)]
enum Status {
Pending,
Failed { reason: String },
Done,
}Applying #[derive(Embed)] to an enum generates:
- An
Embedtrait implementation (idandschemamethods). - A
Fieldsstruct withis_<variant>()methods and comparison methods (eq,ne,in_list). - For data-carrying variants, per-variant handle types with a
matches(closure)method for pattern matching and field access.
§Newtype Auto proxying
A tuple-newtype embedded struct (one unnamed field) automatically
implements Auto whenever its inner type does — no annotation
required. Toasty emits a NewtypeOf marker carrying the inner type
and a blanket Auto impl resolves through it:
#[derive(toasty::Embed)]
struct UserId(toasty::stmt::Uuid);
#[derive(toasty::Model)]
struct User {
#[key]
#[auto]
id: UserId,
name: String,
}Newtypes wrapping non-Auto types stay non-Auto; nesting works
transparently (Outer(Inner(u64)) proxies through both layers).
§Attributes
§#[column(...)] — customize the database column
On struct fields, overrides the column name and/or type:
#[derive(toasty::Embed)]
struct Address {
#[column("addr_street")]
street: String,
#[column(type = varchar(255))]
city: String,
}See Model for the full list of supported column
types.
Changing stored enum discriminants. On an enum,
#[column(rename_all = "...")] changes how Toasty derives string labels
for variants without an explicit label:
#[derive(toasty::Embed)]
#[column(rename_all = "SCREAMING_SNAKE_CASE")]
enum PartyKind {
Customer,
PreferredSupplier,
}This example uses the labels CUSTOMER and PREFERRED_SUPPLIER. Without
rename_all, Toasty uses snake_case.
The supported rules and their result for PreferredSupplier are:
| Rule | Label |
|---|---|
lowercase | preferredsupplier |
UPPERCASE | PREFERREDSUPPLIER |
PascalCase | PreferredSupplier |
camelCase | preferredSupplier |
snake_case | preferred_supplier |
SCREAMING_SNAKE_CASE | PREFERRED_SUPPLIER |
kebab-case | preferred-supplier |
SCREAMING-KEBAB-CASE | PREFERRED-SUPPLIER |
Use #[column(variant = "...")] to set individual labels:
#[derive(toasty::Embed)]
enum PartyKind {
#[column(variant = "customer")]
Customer,
#[column(variant = "preferred-supplier")]
PreferredSupplier,
}An explicit variant label takes precedence over rename_all when an enum
uses both attributes.
String-label enums use Toasty’s enum storage by default. Use
#[column(type = enum("type_name"))] to set the database enum type name,
or #[column(type = text)] or #[column(type = varchar(N))] to use a
plain string column. rename_all changes variant labels only; it does not
change the enum type name.
To store integers instead, assign an integer to every variant:
#[derive(toasty::Embed)]
enum Priority {
#[column(variant = 10)]
Low,
#[column(variant = 20)]
High,
}An enum cannot mix string and integer discriminants. Integer discriminants
use i64 storage by default. Add an integer enum-level override such as
#[column(type = u8)] to request narrower storage. The type applies to
flattened discriminant columns, through transparent field wrappers, and to
each element of Vec<unit-enum>. The same attribute on a model field
overrides the enum default for that use; on a collection it selects the
element type. Every discriminant must fit the selected type. Enum embeds
inside #[document] fields are not supported. Integer-discriminant enums do
not support rename_all. All discriminant values must be unique. String
labels may contain at most 63 bytes.
§#[index] — add a database index
Creates a non-unique index on the field’s flattened column.
#[derive(toasty::Embed)]
struct Contact {
#[index]
country: String,
}§#[unique] — add a unique constraint
Creates a unique index on the field’s flattened column. The database enforces uniqueness.
#[derive(toasty::Embed)]
struct Contact {
#[unique]
email: String,
}§#[shared(ident)] — share a column across enum variants
Declares a shared logical field on the enum. Variant fields declaring
the same identifier are backed by a single nullable column instead of
one column per variant. The identifier — not the Rust field names,
which may differ per variant — names the field: the column name derives
from it ({enum_field}_{ident}), and enum-level #[index] /
#[unique] attributes reference it.
#[derive(toasty::Embed)]
enum Creature {
#[column(variant = 1)]
Human {
#[shared(name)]
full_name: String,
profession: String,
},
#[column(variant = 2)]
Animal {
#[shared(name)]
nickname: String,
species: String,
},
}
// Columns: creature, creature_name (shared), creature_profession,
// creature_speciesFields sharing an identifier must have the same type. To rename the
shared column, add #[column("...")] to any one member of the group
(if several declare it, they must agree):
#[shared(name)]
#[column("legacy_name")]
name: String,§Enum-level #[index(...)] / #[unique(...)]
On the enum itself, #[index(...)] and #[unique(...)] create an
index over variant-field columns. Each reference is a shared field
identifier or a variant::field path naming a variant field that owns
its column; the two forms compose into composite indices.
#[derive(toasty::Embed)]
#[unique(name)]
#[index(name, human::profession)]
enum Creature {
#[column(variant = 1)]
Human {
#[shared(name)]
name: String,
profession: String,
},
#[column(variant = 2)]
Animal {
#[shared(name)]
name: String,
},
}An index on a shared column covers rows of every variant: with
#[unique(name)] above, a Human named “Bob” and an Animal named
“Bob” conflict. Rows of variants that do not declare the shared field
store NULL and never conflict. For this reason, field-level
#[index] / #[unique] on a #[shared] field is a compile error
pointing at the enum-level form.
§#[belongs_to(...)] — relations stored in embedded types
A field of an embedded struct or enum variant may declare
#[belongs_to], with the same parameters as the model-level attribute
(see Model). The differences:
keyreferences a sibling field of the same struct or variant.- The field type must be
toasty::Deferred<..>; the always-loaded form is not supported. - There is no
.include(): load the referenced model with an ordinaryget_by_*/find_by_*on the stored key. - Writes set the key field explicitly and leave the relation unloaded
(
Deferred::default()); setting it from a model value is not supported. - A
has_manyon the target cannot pair with it.
#[derive(Debug, toasty::Embed)]
enum Owner {
Human {
#[index]
id: toasty::stmt::Uuid,
#[belongs_to(key = id)]
human: toasty::Deferred<Human>,
},
// ... other owner kinds
}§Using embedded types in a model
Reference an embedded type as a field on a Model
struct. The parent model’s create and update builders gain a setter for
the embedded field. Partial updates of individual sub-fields use
stmt::patch:
use toasty::stmt;
// Full replacement
user.update()
.address(Address { street: "456 Oak Ave".into(), city: "Seattle".into() })
.exec(&mut db).await?;
// Partial update — updates city, leaves street unchanged
user.update()
.address(stmt::patch(Address::fields().city(), "Portland"))
.exec(&mut db).await?;Embedded struct fields are queryable through the parent model’s
fields() accessor:
let users = User::filter(User::fields().address().city().eq("Seattle"))
.exec(&mut db).await?;§Constraints
- Embedded structs must have named fields (tuple structs are not supported).
- Generic parameters are not supported.
- Enum discriminants must all be strings or all be integers. Integer discriminants must be specified on every variant.
#[column(rename_all = "...")]applies only to string labels.- Enum variants may be unit variants or have named fields. Tuple variants are not supported.
- Embedded types cannot have primary keys,
has_many/has_onerelations,#[auto],#[default], or#[update]attributes.#[belongs_to]is supported; the field must betoasty::Deferred<..>.
§Full example
#[derive(Debug, PartialEq, toasty::Embed)]
#[column(rename_all = "SCREAMING_SNAKE_CASE")]
enum Priority {
Low,
Normal,
High,
}
#[derive(Debug, toasty::Embed)]
struct Metadata {
version: i64,
status: String,
priority: Priority,
}
#[derive(Debug, toasty::Model)]
struct Document {
#[key]
#[auto]
id: i64,
title: String,
#[unique]
slug: String,
meta: Metadata,
}
// Create
let mut doc = Document::create()
.title("Design doc")
.slug("design-doc")
.meta(Metadata {
version: 1,
status: "draft".to_string(),
priority: Priority::Normal,
})
.exec(&mut db).await?;
// Query by embedded field
let drafts = Document::filter(
Document::fields().meta().status().eq("draft")
).exec(&mut db).await?;
// Partial update
use toasty::stmt;
doc.update()
.meta(stmt::apply([
stmt::patch(Metadata::fields().version(), 2),
stmt::patch(Metadata::fields().status(), "published"),
]))
.exec(&mut db).await?;