Schema options
Rent treats rent/schema.rsl as the source for both generated Rust clients and migrations. RSL uses familiar
declarative model syntax wherever the concepts match. Rent-only capabilities live under the rent namespace or in an
extension block, so the boundary stays visible.
Relationship storage
Ordinary foreign keys use @relation:
model Post {
id BigInt @id @default(autoincrement())
authorId BigInt
author User @relation(fields: [authorId], references: [id], map: "posts_author_fkey", onDelete: Cascade)
}onDelete and onUpdate accept Cascade, Restrict, NoAction, SetNull, and SetDefault. Compound foreign
keys use aligned fields and references arrays.
Implicit many-to-many relations receive deterministic storage names. Use @rent.join when the physical join table,
columns, or constraint symbols are part of the database contract:
model Post {
id BigInt @id
tags Tag[] @relation("PostTags") @rent.join(table: "post_labels", columns: [post_id, label_id], constraints: [post_labels_post_fkey, post_labels_label_fkey])
}
model Tag {
id BigInt @id
posts Post[] @relation("PostTags")
}When the join row has application data, model it normally and add a direct traversal with
@rent.through(name: "memberships", model: "Membership"). The name identifies the relation exposed on the
source model for join rows; model identifies the explicit join model. Generated code retains both the
Membership CRUD surface and the direct target traversal.
Database namespaces
Use @@schema to place models in explicit PostgreSQL schemas or MySQL/MariaDB databases. Generated
queries and migrations use that namespace, independent of the connection's default schema:
model Author {
id BigInt @id
name String
@@map("users")
@@schema("publishing")
}
model Reviewer {
id BigInt @id
name String
@@map("users")
@@schema("moderation")
}These are different tables with different generated Rust models. Relationships may cross namespaces;
eager loading, tenant policies, transactions, and bulk operations retain the declared storage locations.
A modeled join table uses its own @@schema; an implicit join table inherits the declaring model's schema.
Give relationship tables distinct storage names within each model's relationships.
SQLite does not support this server-namespace workflow. Remove explicit schema declarations for a SQLite schema rather than expecting them to select a same-named table in its default database.
Indexes
Index declarations support uniqueness, ordering, operator classes, mapped names, and access methods. Rent adds partial predicates, covering columns, and arbitrary SQL expressions:
model Post {
id BigInt @id
tenantId BigInt
slug String
title String
publishedAt DateTime?
embedding Unsupported("vector(1536)")
@@unique([tenantId, slug], map: "posts_tenant_slug_key")
@@index([publishedAt(sort: Desc)], map: "posts_published_idx", where: "published_at IS NOT NULL", include: [title])
@@index([embedding(ops: vector_cosine_ops)], map: "posts_embedding_hnsw", type: Hnsw)
@@index([], map: "posts_lower_title_idx", expression: "lower(title)")
}SQL expressions and predicates are trusted migration input. Keep them static and review rent migrate diff.
Defaults, generated columns, and exact SQL types
Use ordinary @default(...) for literals and supported functions. Rent extensions express database-owned behavior
and per-dialect storage precisely:
model Account {
id Uuid @id @rent.defaultSql("gen_random_uuid()")
balance Decimal @rent.type(postgres: "numeric(18,6)", mysql: "decimal(18,6)", sqlite: "numeric")
searchKey String @rent.generated("lower(email)")
email String
}Generated fields are readable and filterable but omitted from create and update builders. @rent.manual marks an
application-supplied primary key. @rent.rustType("path::Type") overrides the generated Rust representation when a
portable scalar is insufficient.
Rename identity
Mapped names describe current storage. Rename markers preserve identity across a migration:
model User {
id BigInt @id
name String @rent.previousName("display_name")
@@map("users")
@@rent.previousName("people")
}Checks, triggers, row-level security, and policies
model Document {
id BigInt @id
tenantId BigInt
@@rent.check(name: "documents_tenant_positive", expression: "tenant_id > 0")
@@rent.trigger(name: "documents_audit", event: "AFTER INSERT OR UPDATE", body: "EXECUTE FUNCTION audit_document()")
@@rent.rowLevelSecurity
@@rent.policy(name: "tenant_documents", command: "ALL", roles: [app], using: "tenant_id = current_setting('app.tenant')::bigint", check: "tenant_id = current_setting('app.tenant')::bigint")
}Policies are permissive by default; set permissive: false for a restrictive PostgreSQL policy.
Views
Views generate read-only clients. Rent requires the SQL definition because it also drives migrations:
view PostCount {
authorId BigInt @map("author_id")
count BigInt
@@map("post_counts")
@@rent.sql("SELECT author_id, count(*) AS count FROM posts GROUP BY 1")
@@rent.materialized
}Omit @@rent.materialized for an ordinary view.
Extensions
Extensions are first-class top-level declarations rather than model attributes:
extension pgcrypto {
name = "pgcrypto"
version = "1.3"
schema = "extensions"
}These declarations participate in the same migration diff, review, checksum, and apply workflow as tables and
indexes. Omit version when Rent may select the newest compatible installed version.