Declaring a schema
#[derive(Table)] turns a plain struct into the row type, a table handle, an insert struct and a
&'static TableDef. The struct is the schema; migrations diff that metadata against a database.
table (alias name)
The SQL table name; without it the struct name becomes snake_case. The handle identifier comes
from the table name, so #[besu(table = "user_profiles")] generates user_profiles.
schema
PostgreSQL's schema, MySQL's database or SQLite's attached database; it qualifies every name, is
part of the table's identity, and renders CREATE TABLE "app"."users" (...). Besu never runs
CREATE SCHEMA, so leave it off for the default schema: PostgreSQL reports a table in
current_schema() as unqualified.
column (alias name)
The SQL column name; without it the field name is used verbatim.
primary_key (alias primary)
One field is an inline PRIMARY KEY; two or more become a table-level constraint in declaration
order. A primary key column may not be nullable.
auto_increment (alias autoincrement)
The database generates the value, which also removes the column from users.insert(..).
SQLite ignores it unless the column is also the primary key; MySQL requires it to be a key.
unique
A UNIQUE constraint on the column, rendered inline as "email" TEXT NOT NULL UNIQUE.
MySQL cannot index TEXT or BLOB without a prefix, so it writes
UNIQUE KEY `email` (`email`(255)): uniqueness over the first 255 characters.
index
A separate, non-unique, single-column index named {table}_{column}_idx, rendering
CREATE INDEX "users_created_at_idx" ON "users" ("created_at").
Composite and partial indexes have no attribute; see the constraints page.
Defaults: default, default_now, default_sql
A default makes the column optional in an insert; it does not change the Rust type, so the model
still holds bool, not Option<bool>.
default_now renders DEFAULT CURRENT_TIMESTAMP and default_sql renders its string verbatim,
unchecked. default takes a string, integer, float or boolean literal. MySQL wraps a default on
TEXT, BLOB or JSON in parentheses.
sql_type
Names a SqlType implementation instead of inferring one from the field's type.
The field's Rust type must be the SqlType's Rust type — String for Decimal, Char and
VarChar. A custom marker implements SqlType, and its Rust type needs Decode per driver.
references, on_delete, on_update
A single-column foreign key named {table}_{column}_fkey; references must be "table.column"
or "schema.table.column", and a missing dot is a compile error. The clause is
CONSTRAINT "posts_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users" ("id"), with the
ON DELETE and ON UPDATE actions appended.
Accepted actions are cascade, set_null/setnull, set_default/setdefault, restrict and
no_action/noaction; anything else falls back to no clause, so a typo is silent.
renamed_from
Declares that the column had another name, so push renames it instead of dropping and adding it.
It produces no DDL in CREATE TABLE; a push emits
ALTER TABLE "users" RENAME COLUMN "full_name" TO "name".
skip
Not a column at all: absent from the SELECT list, the insert struct and the TableDef. Decoding
fills it with Default::default(), so the field must implement Default.
What the derive generates
There is no separate inferred-select type: the struct is what a SELECT returns. A column reference
carries its name, SQL type and nullability, so users.id.name() is "id" and users.id.type_def()
is TypeDef::Int8.
UserInsert stores every column as a Set<T>, keeping "not provided" distinct from "explicitly
NULL", and From<User> for UserInsert converts a whole model, leaving the auto-increment unset.
Nullability
Nullability is a type parameter on Column (Column<T, S, N = NonNull>), not a wrapper around the
SQL type, so Option never nests: the derive strips one Option and rejects the inner one, making
Option<Option<T>> a compile error. A primary key or auto-increment column may not be nullable.
What is checked
- An unmapped field type, a generic or non-struct table, a nullable primary key, a bad
referencesstring, an unknown key and two same-named tables are compile errors. - Drift is a runtime failure, caught by
pushunless the change is destructive.
Gotchas
- The mapping reads only the last path segment, so a type alias can map to the wrong SQL type.
time::Dateis notchrono::NaiveDate. DateTimeis matched by name, not by its full type.chrono::DateTime<Local>is declaredTimestampTz, but no driver implementsDecodefor it. UseDateTime<Utc>.- Generated names are fixed:
{table}_{column}_idxand{table}_{column}_fkey. #[besu(skip)]removes the field from reading and writing. For a column the database fills in, usedefaultorauto_incrementinstead.
