Skip to content
Tutorials

Define the schema in RSL

Maya and Ken's writing app needs users, posts, and comments. Begin by describing those records and how they connect.

Start with a single schema file at the default path, rent/schema.rsl. Larger projects can split the schema across multiple .rsl files in a configured directory. Start the project and generate an initial client explicitly:

cargo new writing-app
cd writing-app
rent init User Post Comment

Replace the initial declarations with the schema below, then run rent generate. This example keeps the complete schema in one file:

datasource db {
  provider = "sqlite"
}

model User {
  id    BigInt @id
  name  String
  posts Post[]

}

model Post {
  id        BigInt    @id
  title     String
  author_id BigInt
  author    User      @relation(fields: [author_id], references: [id])
  comments  Comment[]

}

model Comment {
  id      BigInt @id
  body    String
  post_id BigInt
  post    Post   @relation(fields: [post_id], references: [id], onDelete: Cascade)

}

Rent infers the target and cardinality from the RSL field type and connects User.posts to Post.author. Running rent generate validates references and replaces the generated Rust client atomically. Later chapters add versioning, many-to-many joins, tenant fields, rich values, and advanced indexes only when those concepts are taught.

Complete example

Download this project, or copy the files below into an empty directory.

Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.

Use a separate local database for this example:

export DATABASE_URL='sqlite://lesson.db?mode=rwc'
rent generate
rent migrate dev --name initial
cargo run

Run the included test with cargo nextest run. It creates an isolated temporary database.

Cargo.toml
[package]
name = "rent-lesson-00-schema"
version = "0.1.0"
edition = "2024"

[workspace]

[[bin]]
name = "rent-lesson-00-schema"
path = "main.rs"

[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6", default-features = false, features = ["sqlite", "migrate", "extensions"] }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
tempfile = "3.27.0"

# Used by the generated client after `rent generate`.
[package.metadata.cargo-machete]
ignored = ["rent"]
rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"

[database]
dialect = "sqlite"
backends = ["sqlite"]
schema.rsl
datasource db {
  provider = "sqlite"
}

model User {
  id    BigInt  @id
  name  String
  posts Post[]
}

model Post {
  id        BigInt     @id
  title     String
  author_id BigInt
  author    User       @relation(fields: [author_id], references: [id])
  comments  Comment[]
}

model Comment {
  id      BigInt  @id
  body    String
  post_id BigInt
  post    Post    @relation(fields: [post_id], references: [id], onDelete: Cascade)
}
main.rs
use anyhow::{Result, ensure};
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;

async fn example(url: &str) -> Result<()> {
    let db = generated::client::Client::connect(url).await?;
    let maya = db.create_user().id(1).name("Maya").save().await?;
    let post = db.create_post().id(1).title("Launch notes").author_id(maya.id).save().await?;
    db.create_comment()
        .id(1)
        .body("Ready for review")
        .post_id(post.id)
        .save()
        .await?;
    let author = db.post().id_eq(post.id).query_author().only().await?;
    ensure!(author.name == "Maya");
    println!("{} wrote {}", author.name, post.title);
    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    example(&std::env::var("DATABASE_URL")?).await
}
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests {
    #[tokio::test]
    async fn example_runs() -> anyhow::Result<()> {
        let (_directory, url) = super::test_support::database()?;
        super::example(&url).await
    }
}
test_support.rs
use anyhow::{Context, Result, ensure};
use std::process::Command;

pub fn database() -> Result<(tempfile::TempDir, String)> {
    let directory = tempfile::tempdir()?;
    std::fs::write(directory.path().join("schema.rsl"), include_str!("schema.rsl"))?;
    std::fs::write(directory.path().join("rent.toml"), r#"schema = "schema.rsl""#)?;
    let url = format!("sqlite://{}?mode=rwc", directory.path().join("app.db").display());
    let executable = std::env::var("RENT_CLI").unwrap_or_else(|_| "rent".into());
    let output = Command::new(executable)
        .arg("migrate")
        .arg("dev")
        .arg("--root")
        .arg(directory.path())
        .arg("--to")
        .arg("schema.rsl")
        .arg("--database-url")
        .arg(&url)
        .output()
        .context("run the Rent CLI")?;
    ensure!(
        output.status.success(),
        "migration failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    Ok((directory, url))
}

On this page