rent
Concepts

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.

On this page