Skip to main content

Embed

Derive Macro Embed 

Source
#[derive(Embed)]
{
    // Attributes available to this derive:
    #[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 Embed trait implementation (id and schema methods).
  • A Fields struct returned by <Type>::fields() for building filter expressions on individual fields.
  • An Update struct used by the parent model’s update builder for partial field updates.

§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 Embed trait implementation (id and schema methods).
  • A Fields struct with is_<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(uuid::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:

RuleLabel
lowercasepreferredsupplier
UPPERCASEPREFERREDSUPPLIER
PascalCasePreferredSupplier
camelCasepreferredSupplier
snake_casepreferred_supplier
SCREAMING_SNAKE_CASEPREFERRED_SUPPLIER
kebab-casepreferred-supplier
SCREAMING-KEBAB-CASEPREFERRED-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 are stored as i64 and 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_species

Fields 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.

§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, relations, #[auto], #[default], or #[update] attributes.

§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?;