rent
Concepts

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 typeGenerated Rust typeJSON 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:

MeaningRust typePostgreSQL override
Absolute instantDateTime<Utc>timestamp with time zone (default)
Calendar dateNaiveDatedate
Time of day, not a durationNaiveTimetime(6) without time zone
Local date and time, without a timezoneNaiveDateTimetimestamp(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.

On this page