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 CommentReplace 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 runRun 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))
}