Savepoints
Saving a user is required, but adding an optional commenter may fail. A savepoint marks the place you can return to without discarding the earlier work.
Generated writes already recover their own failed changes inside a transaction.
See the previous example.
When working directly with the driver, give the recoverable part a name. Starting
with an empty users table:
let mut transaction = pool.begin().await?;
transaction
.execute("INSERT INTO users (id, name) VALUES (1, 'Maya')")
.await?;
transaction.savepoint("optional_commenter").await?;
transaction
.execute("INSERT INTO users (id, name) VALUES (2, 'Ken')")
.await?;
transaction
.rollback_to_savepoint("optional_commenter")
.await?;
transaction.release_savepoint("optional_commenter").await?;
transaction.commit().await?;The commit keeps Maya and discards Ken, because Ken was inserted after the savepoint. Names are validated before Rent emits SQL. Use savepoints for recoverable sub-steps. Use the closure API when the whole operation should succeed or fail together.
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.
rent generate
cargo runThe project includes its database setup. Each lesson uses a small, self-contained schema. You can run it without applying the earlier lessons.
Cargo.toml
[package]
name = "rent-lesson-06-savepoints"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-lesson-06-savepoints"
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"] }
rent-schema-language = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6" }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
@@map("users")
}main.rs
use anyhow::{Result, ensure};
use rent::driver::DatabasePool;
use rent::sql::Dialect;
use rent::sql::ast::{Select, TableRef};
const SCHEMA: &str = include_str!("schema.rsl");
async fn exercise() -> Result<()> {
rent_schema_language::parse_schema("schema.rsl", SCHEMA)?;
let pool = DatabasePool::connect(Dialect::Sqlite, "sqlite::memory:").await?;
pool.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
.await?;
let mut transaction = pool.begin().await?;
transaction.execute("INSERT INTO users (id, name) VALUES (1, 'Maya')").await?;
transaction.savepoint("optional_commenter").await?;
transaction.execute("INSERT INTO users (id, name) VALUES (2, 'Ken')").await?;
transaction.rollback_to_savepoint("optional_commenter").await?;
transaction.release_savepoint("optional_commenter").await?;
transaction.commit().await?;
let names = pool
.fetch_strings(&Select::new(Dialect::Sqlite, TableRef::new("users"), ["name"]).render())
.await?;
ensure!(names == ["Maya"]);
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
exercise().await
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn example_runs() -> anyhow::Result<()> {
super::exercise().await
}
}