Documentation Index
Fetch the complete documentation index at: https://mintlify.com/nimdeveloper/better-duck/llms.txt
Use this file to discover all available pages before exploring further.
better-duck-diesel implements Diesel’s AnsiTransactionManager, which means transactional safety
works the same way it does against PostgreSQL or SQLite. Wrap a block of DML operations in
conn.transaction(|conn| { ... }) and the outcome is fully atomic: return Ok to commit, return
Err to roll back. Every DML error propagated out of the closure automatically triggers a rollback
before the error is surfaced to the caller.
Basic Transactions
Pass a closure that borrows the connection and returns a Result. The closure receives a &mut DuckDbConnection that is tied to the in-progress transaction — use it as you would the outer
connection.
use better_duck_diesel::DuckDbConnection;
use diesel::{connection::SimpleConnection, prelude::*};
diesel::table! {
products (id) {
id -> Integer,
name -> Text,
price -> Double,
}
}
let mut conn = DuckDbConnection::establish(":memory:")?;
conn.batch_execute(
"CREATE TABLE products (id INTEGER PRIMARY KEY, name VARCHAR NOT NULL, price DOUBLE NOT NULL)",
)?;
conn.transaction(|conn| {
diesel::insert_into(products::table)
.values((
products::id.eq(3),
products::name.eq("doohickey"),
products::price.eq(4.99),
))
.execute(conn)?;
// Returning Ok(()) commits the transaction.
Ok(())
})?;
Returning Err from any point inside the closure — including via the ? operator — causes an
immediate rollback and no rows are persisted:
let result = conn.transaction(|conn| -> QueryResult<()> {
diesel::insert_into(products::table)
.values((products::id.eq(4), products::name.eq("ephemeral"), products::price.eq(1.00)))
.execute(conn)?;
// Force a rollback.
Err(diesel::result::Error::RollbackTransaction)
});
assert!(result.is_err());
// The row with id=4 was never committed.
Nested Transactions
AnsiTransactionManager uses SQL SAVEPOINT / RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT
for nested transactions. Calling conn.transaction(...) inside an already-open transaction
automatically issues a savepoint rather than a bare BEGIN. Rolling back the inner transaction
releases only its savepoint; the outer transaction remains open and can still commit.
conn.transaction(|outer| {
diesel::insert_into(products::table)
.values((products::id.eq(10), products::name.eq("outer row"), products::price.eq(5.00)))
.execute(outer)?;
// Inner savepoint — this is rolled back.
let _ = outer.transaction(|inner| -> QueryResult<()> {
diesel::insert_into(products::table)
.values((products::id.eq(11), products::name.eq("inner row"), products::price.eq(3.00)))
.execute(inner)?;
Err(diesel::result::Error::RollbackTransaction)
});
// The outer transaction still sees its own row (id=10) but not the inner one (id=11).
let n: i64 = products::table.count().first(outer)?;
assert_eq!(n, 1);
QueryResult::Ok(())
})?;
// After the outer commits, only "outer row" is present.
Error Handling
Any QueryResult::Err returned from inside the closure — including constraint violations raised by
DuckDB — rolls the transaction back before propagating the error to the caller. You can match on the
error to distinguish constraint violations from other failures:
use diesel::result::{DatabaseErrorKind, Error as DieselError};
let result = conn.transaction(|conn| -> QueryResult<()> {
// Insert a row.
diesel::insert_into(products::table)
.values((products::id.eq(1), products::name.eq("first"), products::price.eq(9.00)))
.execute(conn)?;
// Insert again with the same primary key — violates the PK constraint.
diesel::insert_into(products::table)
.values((products::id.eq(1), products::name.eq("duplicate"), products::price.eq(9.00)))
.execute(conn)?;
Ok(())
});
match result {
Err(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, info)) => {
eprintln!("PK conflict rolled back: {}", info.message());
}
Err(e) => return Err(e),
Ok(()) => {}
}
Because the whole transaction is rolled back on any error, the table is left in a consistent state
regardless of where inside the closure the error occurred.
With Async Connections
Transactions must not span an .await point. A conn.transaction(|conn| { ... }) closure
is synchronous — you cannot .await a future inside it, and you must not hold a transaction open
across a thread-yield boundary. Use better_duck_core’s AsyncPool::with() to check out a
connection and run the entire transactional block on a blocking thread via spawn_blocking. See
the Async page for the full pattern.
If you are using the async facade from better-duck-core, run your transactional logic inside
with() so that the blocking work executes entirely on a dedicated thread:
// Pseudocode — full example in the Async section.
async_pool.with(|conn| {
conn.transaction(|conn| {
diesel::insert_into(products::table)
.values(/* ... */)
.execute(conn)?;
Ok(())
})
}).await?;
The closure passed to with() is not async, which prevents accidentally awaiting
inside a live transaction.