Custom types
A custom column type is a marker type that names a SQL type, plus a Rust type that carries the value. The marker is zero-sized and compile-time only; the Rust type is what rows and parameters are made of.
Naming an existing marker
#[besu(sql_type = "..")] takes a path to a SqlType implementation and uses its DEF for
DDL, casts and migration diffing. The field type should be that marker's Rust type; the
derive does not check it, which matters for reading rows. The attribute is described in
Declaring a schema.
VarChar<64> renders VARCHAR(64) on PostgreSQL and MySQL and TEXT on SQLite, whose
storage classes have no length. Decimal<10, 2> renders NUMERIC(10,2) and DECIMAL(10,2),
but no driver decodes that back into a String yet, so a decimal column can be declared and
created and cannot be read on PostgreSQL or MySQL. On SQLite it is stored as TEXT and works.
The path is resolved where the derive is used, so crate::types::Money and
besu::VarChar<64> both work.
Declaring your own marker
A marker is a struct with no fields and two associated items: the canonical DEF and the
Rust type. Nothing else is required, and a marker over TEXT is complete on its own.
This is the cheap custom type: the value stays a String, every driver already decodes a
string, and the marker only exists to give the column a name in your own code. Filtering still
needs an IntoSqlValue<Slug> implementation, because the comparison helpers are implemented
per (Rust type, SQL type) pair rather than by conversion:
A type with its own Rust value
When the value is not a String, the marker's Rust is your own type and three things are
needed: the marker, a conversion into Value for binding, and one Decode implementation per
driver for reading.
SqlType::Rust requires Into<Value>, which the From implementation supplies. The bound is
on the marker, so a type that cannot be bound as a parameter cannot be a column type.
Decoding is per driver because a row is the driver's own type. This is the PostgreSQL one:
Two rules make a Decode correct: read exactly one column, and advance *offset by exactly
Self::COLUMNS (one, by default). Option<InetAddr> then works for a nullable column and for
the outer side of a join without another implementation.
What is checked
Gotchas
- There are no codecs. Parity lists codecs as absent, and the reason is structural: a row
is a driver type, so decoding is a driver responsibility. One
impl Decode<Dr>per driver is the design, not a stopgap. - Naming the row type means depending on sqlx. The driver re-exports nothing for this; the
implnamessqlx::postgres::PgRow, so thesqlxcrate has to be a direct dependency of the crate that writes the impl. Customis rendered verbatim.TypeDef::Custom("inet")producesinetin DDL and in aCAST; a typo becomes a database error, and a type the server does not have cannot be created.- Introspection keeps an unknown type by name. PostgreSQL reports
inetasinetand MySQL reports an unknown type as its own spelling, so a hand-declaredCustomname round-trips throughpushwhen it matches exactly. - The value's text form is your encoding. A type that binds as
Value::Textbut is decoded from a native binary representation — a UUID, for instance — will not round-trip unless the two halves agree. - A custom marker does not add validation.
Slugis aStringat run time; enforcing the shape is your code, animpl TryFrom, or aCHECKconstraint in a migration.
