Production access patterns
A document list shows titles and comment counts. Opening that screen shouldn't load every document body or ask the database for each comment count separately.
Build the response you need with projections and one grouped count, then keep edit commands in short transactions. This follows the enterprise document example.
Read the response shape
A document card needs an ID, title and comment count. Its large body and settings need not be fetched:
let rows = client.document()
.id_starts_with("production-")
.id_gt(after)
.order_by(Document::id)
.limit(limit)
.select((Document::id, Document::title))
.all().await?;Here client is a ScopedClient supplied by the authenticated request boundary. Projection preserves its
tenant and read restrictions, so a smaller response shape doesn't expose private
documents or matching IDs from another tenant.
Use the last returned ID as after for this forward-only, ID-ordered list. A general feed with configurable
ordering should use the generated keyset/page APIs described in querying collections.
Apply filters and limits in the database instead of collecting an unbounded list and slicing it afterwards.
Aggregate once for the page
Return early for an empty page. Otherwise fetch comment counts for all returned IDs together:
let counts: Vec<CommentCount> = client.comment()
.document_id_in(rows.iter().map(|(id, _)| id.clone()))
.group_by(Comment::document_id)
.count("comments")
.all().await?;CommentCount is a small Deserialize struct with document_id: String and comments: u64. Missing groups
mean zero readable comments. Both queries retain the caller's scope. One grouped count
avoids a separate count call for every card. An empty page needs no count query.
Generated with_* and with_*_count methods are alternatives when the response needs hydrated relationships.
Choose the response shape first, rather than invoking a full configuration/runtime loader to retrieve one name.
Decrypt credentials only where their use requires plaintext.
Pass the transaction into helpers
A rename command can receive a scoped client, update the document, and append its
mandatory history in one short application transaction. Its audit adapter holds explicit root authority
and joins that same transaction through system_in. The handler does not receive that authority.
If history fails, the title update rolls back. Keep external delivery or model work outside this database transaction so a slow external request doesn't hold its connection.
Bound worker reads
For queue-like claiming, the existing row-lock API selects a bounded batch inside a transaction:
let rows = tx.document()
.order_by(Document::id)
.limit(2)
.for_update()
.skip_locked()
.all().await?;Use a serializable scoped transaction on PostgreSQL for this permission-controlled
locking pattern. Other backends have different locking support. See
transactions. A worker must persist its claim or state change before releasing the transaction.
SKIP LOCKED is not a consistent general-purpose listing strategy.
Reuse facts without retaining authority indefinitely
Resolve stable request identity/configuration facts once and pass them through. Batch equivalent team and resource lookups within a read operation. Keep permission decisions scoped to the tenant, effective caller, resource and action. Later mutations and tool calls still need their required fresh authorization.
Use runtime tracing to observe database work. Logical query budgets catch accidental per-item loaders. They do not measure every SQL statement or network round trip.
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-42-production-access"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-lesson-42-production-access"
path = "main.rs"
[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.7.2", default-features = false, features = ["sqlite", "migrate", "extensions"] }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
serde = { version = "1.0.229", features = ["derive", "rc"] }
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 Post {
id BigInt @id @default(autoincrement())
title String
body String
comments Comment[]
}
model Comment {
id BigInt @id @default(autoincrement())
body String
post_id BigInt
post Post @relation(fields: [post_id], references: [id], onDelete: Cascade)
}main.rs
use anyhow::Result;
#[allow(dead_code, unused_imports)]
#[rustfmt::skip]
#[path = "generated/mod.rs"]
mod generated;
use generated::client::Client;
use generated::{comment::Comment, post::Post};
#[derive(serde::Deserialize)]
struct CommentCount {
post_id: i64,
comments: i64,
}
async fn example(url: &str) -> Result<()> {
let client = Client::connect(url).await?;
client
.transaction(async |tx| {
let post = tx
.create_post()
.title("Launch notes")
.body("A long body that a card does not need")
.save()
.await?;
tx.create_comment().post_id(post.id).body("Reviewed").save().await?;
Ok(())
})
.await?;
let rows = client
.post()
.order_by(Post::id)
.limit(25)
.select((Post::id, Post::title))
.all()
.await?;
if rows.is_empty() {
return Ok(());
}
let counts: Vec<CommentCount> = client
.comment()
.post_id_in(rows.iter().map(|(id, _)| *id))
.group_by(Comment::post_id)
.count("comments")
.all()
.await?;
for (id, title) in rows {
let count = counts.iter().find(|row| row.post_id == id).map_or(0, |row| row.comments);
println!("{title}: {count} comments");
}
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))
}