Types and storage
Generated clients support booleans, signed and unsigned integers, floats, strings, bytes, blobs, times, JSON, UUIDs, enums, and user-defined encoded values.
Typed JSON maps or structs serialize at the client boundary. Field codecs can transform values before storage; Rent includes an AES-256-GCM pattern with ciphertext-at-rest and wrong-key tests. Blob repositories support lazy reads, reference checks, and dual-write migration. PostgreSQL domains can use native schema types with a portable fallback. Views are inspectable migration objects and can have generated read models.
Enum names and stored values
An enum variant's schema name can differ from its stored database value:
enum PostStatus {
DRAFT @map("draft")
PUBLISHED @map("published")
}
model Post {
id BigInt @id
status PostStatus @default(DRAFT)
}Use the declared, unquoted variant in @default, even when it has a mapping. Here the default stored value is
draft. The same rule applies to optional enum fields and JSON-backed enum-list defaults such as
@default([DRAFT, PUBLISHED]). Empty list defaults are valid. Unknown variants, quoted database strings,
and defaults with the wrong shape are rejected at schema validation with a source location.
Every variant needs a distinct, nonempty stored value. Names and mappings remain associated with the shared enum declaration throughout schema normalization, including fields inside reusable composite types.
Rent generates a shared Rust type in generated::enums. Schema variants become Rust-style names, while
database values and JSON use the declared mappings:
use generated::enums::PostStatus;
let post = client
.create_post()
.id(1)
.status(PostStatus::Draft)
.save()
.await?;
let published = client
.post()
.status_in([PostStatus::Published])
.all()
.await?;PostStatus::Draft.as_str() returns "draft". Parse external input with
"draft".parse::<PostStatus>()?; an undeclared stored value returns UnknownEnumValue. Arbitrary strings
are not accepted by generated enum setters or filters. Database reads and JSON deserialization reject
unknown values instead of silently substituting a variant. Deploy readers that understand a new variant
before writing it during a rolling deployment.
Optional fields use Option<PostStatus> and lists use Vec<PostStatus>, including inside composite values.
Filters accept owned or borrowed enum collections. Defaults still come from the database when a field is
omitted. Explicit @rent.rustType overrides retain the application's chosen type and conversion contracts.
Generated enums do not by themselves change an existing column's SQL storage type.
JSON and list defaults
Typed JSON lists
Scalar lists use JSON-compatible columns, with typed Rust values at the client boundary:
| RSL type | Generated Rust type | JSON representation |
|---|---|---|
Uuid[] / UUID[] | Vec<Uuid> | Canonical UUID strings |
DateTime[] | Vec<DateTime<Utc>> | UTC timestamp strings |
Decimal[] | Vec<Decimal> | Decimal strings preserving precision |
Bytes[] / Blob[] | Vec<Vec<u8>> | Arrays of byte arrays |
Json[] | Vec<Value> | Arrays of arbitrary JSON values |
Rent re-exports these types through rent::uuid, rent::chrono, rent::rust_decimal, and rent::serde_json.
Primitive lists similarly use Vec<String>, Vec<bool>, or the declared numeric type. Empty arrays are valid;
null elements require a JSON element type. These lists are not PostgreSQL native SQL arrays. Use relationships
for collections of related records. Writes replace the whole collection rather than appending individual items.
Validation checks built-in list defaults against the generated Rust serialization contract. Invalid UUIDs, timestamps without offsets, incompatible element shapes, and out-of-range numbers produce a source-located diagnostic. UUID and timestamp defaults are canonicalized; decimal defaults should be quoted to avoid floating-point rounding. JSON-list strings remain strings even if they contain JSON text. Custom Rust type overrides use the application's serializer and require application-level validation.
Timestamp lists preserve JSON string precision instead of inheriting the SQL timestamp column's microsecond
limit. They do not gain native timestamp indexing or date operations merely by declaring DateTime[].
The rich-values tutorial demonstrates all five collections.
Declaring defaults
List defaults use array literals. For a Json field, put a JSON document inside a quoted string:
model Post {
id BigInt @id
tags String[] @default([])
weights Float[] @default([1.25, 1000])
settings Json @default("{\"commentsEnabled\":true}")
label Json @default("\"draft\"")
previous Json @default("null")
}The settings default is an object, label is a JSON string, and previous is JSON null, not SQL NULL.
Omit these fields when creating a record to use their database defaults. Changing a default through a migration
affects later inserts; it does not replace values already stored in existing rows. These defaults work on
PostgreSQL, MySQL, MariaDB, and SQLite. List elements must be literal values; functions inside a list are rejected.
Automatic values
Choose whether Rent or the database supplies a missing value:
model Post {
id Uuid @id @default(uuid(7))
body String
created_at DateTime @default(now())
edited_at DateTime @updatedAt
}uuid() and uuid(4) generate random UUID v4 values; uuid(7) generates time-ordered UUID v7 values.
Both work with Uuid and String fields, including keys. Rent evaluates them when a create executes,
not while its builder is assembled. Bulk and nested creates evaluate missing defaults separately for each
record. Supplying a value explicitly keeps it; clearing a nullable field keeps SQL NULL.
@updatedAt uses the application's UTC clock. Rent supplies it on creation and refreshes it when an update
supplies scalar fields, even if their values equal the stored values. Empty unversioned updates and changes
confined to a join table do not touch the parent row.
A versioned compare-and-swap update increments the version, so it also refreshes @updatedAt. Explicit
timestamps take precedence, including explicit nullable clears. With local timestamp storage, Rent writes
the current UTC wall-clock value without a timezone. Use timestamp storage, not a calendar-date or time-only
column, for this attribute.
@updatedAt is client behavior, not a database trigger: raw SQL and writes from another application must
set it themselves if they need the same convention.
By contrast, literal defaults, now(), autoincrement(), and dbgenerated("SQL") are database defaults.
They apply to inserts performed outside Rent too. Combining @default(now()) with @updatedAt uses the
database clock for creation and the application clock for updates. Keep application and database clocks
synchronized. Unsupported default functions or arguments produce a schema-location diagnostic.
Default values pass through mutation hooks and policies like explicit values. Transactions roll back their
database writes, but do not undo clock reads, generated UUIDs, or other application-side effects. Keep custom
Rust-schema default_function and update_default_function callbacks synchronous, side-effect-free, and
non-panicking. They take no arguments and return the field's Rust value type. Update callbacks run once per
nonempty update operation, not once per matched database row.
Try the rich-values tutorial for a runnable UUID and timestamp example.
Dates and times
RSL DateTime generates DateTime<Utc>. Parse external input once; the SDK keeps the typed value through
setters, filters, projections, cursors, and mutation hooks:
use rent::chrono::{DateTime, Utc};
let happened_at = DateTime::parse_from_rfc3339("2026-09-12T18:04:56+05:30")?
.with_timezone(&Utc);
let values = client
.advanced_value()
.happened_at_ge(happened_at)
.all()
.await?;Use Utc::now() for the current instant, or @default(now()) to let the database supply it. Literal defaults
are validated against the field's calendar semantics. PostgreSQL uses native timestamp with time zone;
MySQL and MariaDB use DATETIME(6) containing UTC. SQLite stores UTC text with fixed fractional precision,
so equal instants compare equally and fractions sort chronologically. Existing noncanonical text must be
normalized explicitly when adopting that storage contract; changing a Rust type does not rewrite stored rows.
Different concepts have different Rust types:
| Meaning | Rust type | PostgreSQL override |
|---|---|---|
| Absolute instant | DateTime<Utc> | timestamp with time zone (default) |
| Calendar date | NaiveDate | date |
| Time of day, not a duration | NaiveTime | time(6) without time zone |
| Local date and time, without a timezone | NaiveDateTime | timestamp(6) without time zone |
model Appointment {
id BigInt @id
booked_at DateTime @default(now())
day DateTime @rent.type(postgres: "date", mysql: "date")
starts_at DateTime @rent.type(postgres: "time(6) without time zone", mysql: "time(6)")
local_at DateTime @rent.type(postgres: "timestamp(6) without time zone", mysql: "datetime(6)")
}Rust schemas can declare these Chrono types directly, imported from rent::chrono. Nullable fields become
Option<T>. Do not assign a timezone to a local wall-clock value by appending Z; resolve its timezone in
your application first. A time-with-timezone-only column is not a substitute for a dated instant.
Server bindings retain microseconds; sub-microsecond fractions are truncated consistently. SQLite retains nanoseconds. Returned records contain the stored precision; a column explicitly declared with fewer fractional digits may round further. Typed values support years 1 through 9999 on PostgreSQL and SQLite, and 1000 through 9999 on MySQL and MariaDB. Leap-second inputs and unsupported ranges are rejected before executing the statement.
The lower-level Argument::time(...) accepts RFC 3339 strings with Z or an explicit offset. Its validation
errors explain the format without exposing the input. Use native Chrono arguments for typed calendar semantics
and canonical SQLite storage. A rejected argument does not abort an existing database transaction.
UUIDs
Declare a Uuid field to get rent::uuid::Uuid in your generated models, setters, and filters. PostgreSQL
uses its native uuid column and parameter type. MySQL, MariaDB, and SQLite use canonical UUID text.
use rent::uuid::Uuid;
let user = client
.user()
.id_eq(Uuid::parse_str("67e5501a-2f2b-4c77-92cc-001122334455")?)
.only()
.await?;UUIDs can be primary keys, relationship keys, parts of compound keys, and keyset cursor values. Generated
eager loaders retain UUID bind types when reading related records. Hooks receive MutationValue::Uuid
so a rewritten UUID remains a UUID parameter.
For an existing text-backed PostgreSQL column, declare that storage choice explicitly:
model User {
id Uuid @id @rent.type(postgres: "text")
}The Rust API still uses Uuid; generated writes and predicates use text parameters for that column. Changing
the Rust API to use UUIDs is not a reason to silently rewrite an existing database column. Use a reviewed
migration when you intentionally change its storage type.