Schema language
RSL is parsed independently by Rent and follows the familiar declarative model-schema conventions. Its top-level declarations are datasource, generator, model, view, enum, type, and Rent's extension block.
Fields and relationships
model Post {
id BigInt @id @default(autoincrement())
slug String @unique
body String? @map("post_body")
published Boolean @default(false)
metadata Json?
happenedAt DateTime @default(now()) @map("happened_at")
authorId BigInt
author User @relation(fields: [authorId], references: [id])
tags Tag[] @relation("PostTags")
@@map("posts")
}Built-in types include String, Boolean, Int, BigInt, Float, Decimal, DateTime, Json, and Bytes. RSL adds
UInt, explicit integer widths, Float32, Blob, and Uuid where Rent's Rust and storage semantics are more precise; Bool
and Float64 are accepted aliases. Native PostgreSQL extension types are declared as pgvector.Vector,
postgis.Point, and citext.CiText, and an unmodeled PostgreSQL column type as Unsupported("<sql type>"). Enums
store a native PostgreSQL enum type when they declare @@rent.native; @@map and @@schema then override the SQL
type name and schema, which default to the enum name and public. A question mark
means nullable and square brackets mean a list.
Use Int8, Int16, Int32 (also Int), and Int64 (also BigInt) for signed Rust integers;
UInt8, UInt16, UInt32, and UInt64 (also UInt) generate the corresponding unsigned types.
Nullable forms such as UInt64? generate Option<u64>. Integer decoding rejects negative unsigned values,
fractional numeric values, and values outside the declared Rust range instead of truncating them.
UInt64 uses numeric(20) on PostgreSQL and decimal(20,0) on MySQL/MariaDB, supporting the full u64 range.
SQLite's integer storage is signed, so unsigned values must not exceed i64::MAX (9,223,372,036,854,775,807);
writes outside that range return an error. Choose UInt32 when that smaller range fits your application.
Field attributes describe identity, defaults, uniqueness, relationships, storage names, timestamps, ignored fields,
and native database types. @default(uuid()) and @default(uuid(7)) supply UUIDs in Rent;
@updatedAt supplies creation and update timestamps. @default(now()) uses the database clock.
See automatic values for overrides, bulk writes,
empty updates, and transaction behavior.
Rent-specific field behavior uses the rent namespace:
- @rent.version enables optimistic locking.
- @rent.sensitive marks a field as sensitive on the schema descriptor.
- @rent.immutable removes update setters.
- @rent.manual marks an application-assigned ID.
- @rent.defaultSql("...") preserves a database default expression.
- @rent.generated("...") declares a generated column.
- @rent.rustType("path::Type") overrides the generated Rust value type.
- @rent.type(postgres: "...", mysql: "...", sqlite: "...") preserves exact storage types.
- @rent.previousName("...") preserves a column's identity across a rename.
- @rent.join(table: "...", columns: [...], constraints: [...]) fixes many-to-many storage names.
- @rent.through(name: "...", model: "...") adds a direct traversal through an explicit join model.
- @rent.order(field: "...", direction: ...) sets the default ordering of a many-to-many traversal.
Models and indexes
Model attributes describe table names (@@map), PostgreSQL and MySQL schemas (@@schema), compound primary keys
(@@id), unique constraints, indexes, full-text indexes, ignored models, check constraints (@@rent.check),
row-level security (@@rent.rowLevelSecurity and @@rent.policy), triggers (@@rent.trigger), and a table's
previous name (@@rent.previousName). Views declare their SQL with @@rent.sql and @@rent.materialized. Index field arguments such as sort and ops, and options such as map and type,
are retained during lowering.
An identity may use any required field. Use @@id when the database identity spans more than one field. Rent
generates a reusable <Model>Id Rust type for a compound identity, so update and delete operations remain typed.
model Membership {
userId BigInt
groupId BigInt
role String
@@id([userId, groupId])
@@map("memberships")
}Reusable composite types become generated Rust value types and are stored in JSON-compatible columns:
type Address {
street String
city String @map("city_name") @default("Ferris")
}
model User {
id BigInt @id @default(autoincrement())
address Address
}@map on a value-object member names the JSON key, not a separate database column. Missing members receive
their literal @default when Rent decodes the object. Explicit values take precedence; explicit JSON null
only works for optional members. Reading an older object applies defaults in memory without rewriting its
stored JSON. A required member with no default remains an error when absent.
Value objects can contain other value objects and lists. Recursive structures need a list boundary such as
children Node[]; child Node? alone still produces an infinitely sized Rust value and is rejected. Defaults
must also terminate: use an explicit empty list at a recursive boundary. Database constraints, generated SQL,
and default callbacks belong on model fields, not on JSON members. Use mapped member keys inside JSON defaults.
Unknown attributes, arguments, and block properties are errors. This keeps misspellings from silently changing the schema. See the RSL diagnostics catalog for every stable error code.
model Document {
id BigInt @id
tenantId BigInt
title String
embedding Json
@@unique([tenantId, title], map: "documents_tenant_title_key")
@@index([embedding(ops: JsonbPathOps)], map: "documents_embedding_idx", type: Gin)
@@rent.check(name: "documents_title_nonempty", expression: "length(title) > 0")
@@rent.rowLevelSecurity
@@rent.policy(name: "tenant_documents", command: "ALL", roles: [app], using: "tenant_id = current_setting('app.tenant')")
@@map("documents")
}Commands
rent validate
rent validate --syntax-only
rent format
rent format --check
rent describe
rent generateRSL diagnostics include a stable RSL code, source path, line, and column. A schema path may name one file or a directory of .rsl files.