Rust Transaction Management
In TeaQL Rust, transaction management separates business graph definitions from underlying connection scopes. By using the provider model, TeaQL ensures that complex, nested entity mutations (e.g. creating a parent order along with 10 child line items) are committed as a single atomic transaction and automatically rolled back on any failure.
Automatic Transaction Boundary for Graph Writes
When committing complex entity graphs via entity.save(&ctx), TeaQL Rust automatically manages the transaction scope.
entity.save(&ctx) call
-> calls executor.begin_transaction()
-> parses/constructs GraphMutationPlan
-> runs batch mutations (inserts/updates/deletes)
-> ON SUCCESS: calls executor.commit_transaction()
-> ON FAILURE: calls executor.rollback_transaction()
Transaction Requirement
Because graph writes involve multiple SQL statements (often across different tables), a transactional executor is strictly required. If the database provider does not support transactions, entity.save(&ctx) will immediately return a runtime error:
// Internally in the runtime
if matches!(boundary, GraphTransactionBoundary::Unsupported) {
return Err(RepositoryError::Runtime(RuntimeError::Graph(
"entity.save(&ctx) requires a transactional executor".to_owned(),
)));
}
Under the Hood: The QueryExecutor Trait
Database providers (like SQLx or rusqlite) implement the QueryExecutor trait in teaql-runtime. This trait defines three main transaction hooks:
pub trait QueryExecutor {
type Error: std::error::Error + Send + Sync + 'static;
fn fetch_all(&self, query: &CompiledQuery) -> Result<Vec<Record>, Self::Error>;
fn execute(&self, query: &CompiledQuery) -> Result<u64, Self::Error>;
// Transaction Hooks
fn begin_transaction(&self) -> Result<GraphTransactionBoundary, Self::Error> {
Ok(GraphTransactionBoundary::Unsupported)
}
fn commit_transaction(&self) -> Result<(), Self::Error> {
Ok(())
}
fn rollback_transaction(&self) -> Result<(), Self::Error> {
Ok(())
}
}
Transaction Boundary States
begin_transaction returns a GraphTransactionBoundary enum to inform the runtime of the active scope:
Started: A new transaction has successfully started.AlreadyActive: A transaction is already active (subsequent save operations will merge into this transaction).Unsupported: The database executor does not support transactions (likeMemoryRepositoryunder simple setups).
Provider Support
Each built-in Rust database provider implements connection-scoped transaction boundaries:
1. SQLx Providers (PostgreSQL, SQLite, MySQL)
SQLx providers wrap the database pool and use thread-safe, mutex-guarded connections to lock a dedicated transaction connection.
- Begin: Allocates a connection from the pool, runs
BEGIN, and locks it in the executor state. - Commit: Releases the lock, commits the SQLx transaction, and returns the connection to the pool.
- Rollback: Releases the lock, rolls back all nested operations, and returns the connection.
2. rusqlite Provider (SQLite Sync)
The rusqlite synchronous provider manages transactions directly on the underlying SQLite raw connection, locking access during graph writes to ensure sync isolation on embedded and edge environments.
Concrete Example: Auto-Rollback in Action
Below is an example of an application-level graph save operation. If any child node fails validation or database limits, the entire parent and sibling nodes are automatically rolled back.
use my_domain::{Q, Order, LineItem};
use teaql_runtime::UserContext;
async fn create_order_with_items(ctx: &UserContext) -> Result<(), Box<dyn std::error::Error>> {
// 1. Construct a complex, nested entity graph
let mut order = Order::new()
.update_code("SO-2026-9999")
.update_total_amount(150.00);
// Add first item (Valid)
let item_1 = LineItem::new()
.update_sku("SKU-1001")
.update_quantity(2);
order.add_line_item_list(item_1);
// Add second item (Invalid: trigger duplicate key or custom domain error)
let item_2 = LineItem::new()
.update_sku("SKU-INVALID-KEY")
.update_quantity(-5); // Negative quantity violates checker rules!
order.add_line_item_list(item_2);
// 2. Persist the graph
// Under the hood, this will:
// - Call begin_transaction()
// - Try to insert order and valid item
// - Fail when checker or database rejects item_2
// - Automatically trigger rollback_transaction()
match order.save(ctx).await {
Ok(saved_order) => {
println!("Order successfully saved!");
}
Err(err) => {
// No partial records exist in the database.
// The order and item_1 are guaranteed to be cleaned up.
eprintln!("Failed to save order graph: {}. Rolled back successfully.", err);
}
}
Ok(())
}
Summary
By leveraging the QueryExecutor trait and request-scoped UserContext, TeaQL Rust ensures:
- Atomicity: Zero partial writes exist for complex entity relationship trees on database engines.
- Safety: Automatic rollback requires no hand-written
COMMITorROLLBACKlogic in your application. - Flexibility: You can swap between SQLx PostgreSQL, SQLite, or synchronous rusqlite backends without changing a single line of your graph transaction code.