Skip to content
Tutorials

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 run

The 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
    }
}

On this page