Transactions
- Transactions
Overview
wasm-dbms supports ACID transactions, allowing you to group multiple database operations into a single atomic unit. Either all operations succeed and are committed together, or none of them take effect.
Key features:
- Atomicity: All operations in a transaction succeed or fail together
- Consistency: Data integrity constraints are maintained
- Isolation: Transactions are isolated from each other
- Durability: Committed changes persist
Transaction Lifecycle
Begin Transaction
Start a new transaction using DbmsContext::begin_transaction():
#![allow(unused)]
fn main() {
use wasm_dbms::prelude::*;
// Begin a new transaction
let tx_id = ctx.begin_transaction();
println!("Started transaction: {}", tx_id);
}
The returned transaction ID is used to create a transactional database instance.
Perform Operations
Create a WasmDbmsDatabase with the transaction ID and perform operations:
#![allow(unused)]
fn main() {
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
// Insert within transaction
database.insert::<User>(user)?;
// Update within transaction
database.update::<User>(update)?;
// Delete within transaction
database.delete::<User>(DeleteBehavior::Restrict, Some(filter))?;
// Select within transaction (sees uncommitted changes)
let users = database.select::<User>(query)?;
}
Note: Operations within a transaction are visible to subsequent operations in the same transaction, but not to other callers until committed.
Commit
Commit the transaction to make all changes permanent:
#![allow(unused)]
fn main() {
// Commit the transaction
database.commit()?;
println!("Transaction committed successfully");
}
After commit:
- All changes become visible to other callers
- The transaction ID becomes invalid
- Changes persist in storage
Rollback
Rollback the transaction to discard all changes:
#![allow(unused)]
fn main() {
// Rollback the transaction
database.rollback()?;
println!("Transaction rolled back");
}
After rollback:
- All changes within the transaction are discarded
- The transaction ID becomes invalid
- The database state is as if the transaction never happened
Who May Use a Transaction
The engine identifies a transaction by its id and nothing else. Any code that
holds the id can read and write through it, commit it, or roll it back;
DbmsContext does not record who opened it. This keeps the engine free of
platform concepts such as users, principals, or tenants.
When transactions cross a trust boundary, for example a canister serving many principals or a host serving many tenants, the application layer that embeds the engine decides who may use an id. The Embedding wasm-dbms guide shows how to keep a ledger from transaction id to identity and check it before every transactional call. Single-tenant programs do not need one.
ACID Properties
Atomicity
All operations in a transaction are treated as a single unit. If any operation fails, the entire transaction can be rolled back:
#![allow(unused)]
fn main() {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
// First operation succeeds
database.insert::<User>(user1)?;
// Second operation fails (e.g., primary key conflict)
let result = database.insert::<User>(user2_duplicate);
if result.is_err() {
// Rollback everything - user1 is also discarded
database.rollback()?;
}
}
Consistency
Transactions maintain data integrity:
- Primary key uniqueness is enforced
- Foreign key constraints are checked
- Validators run on all data
- Sanitizers are applied
#![allow(unused)]
fn main() {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
// This will fail if referenced user doesn't exist
let post = PostInsertRequest {
id: 1.into(),
title: "My Post".into(),
author_id: 999.into(), // Non-existent user
};
let result = database.insert::<Post>(post);
// Returns Err(DbmsError::Query(QueryError::BrokenForeignKeyReference))
}
Isolation
Changes made within a transaction are not visible to other callers until committed:
#![allow(unused)]
fn main() {
// Database A starts a transaction
let tx_id = ctx.begin_transaction();
let mut db_a = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
db_a.insert::<User>(new_user)?;
// Database B queries - does NOT see the new user
let db_b = WasmDbmsDatabase::oneshot(&ctx, my_schema);
let users = db_b.select::<User>(query)?;
assert!(!users.iter().any(|u| u.id == new_user.id));
// Database A commits
db_a.commit()?;
// Now Database B can see the user
let users = db_b.select::<User>(query)?;
assert!(users.iter().any(|u| u.id == new_user.id));
}
Durability
Committed transactions persist in the provider’s logical memory. With the
WASI file provider, writes reach the file during normal provider operations.
With WasiKeyValueMemoryProvider, call ctx.flush() after the commit to
publish the checkpoint; a transaction commit alone does not guarantee that a
fresh component instance sees the data after restart. See the key-value provider guide.
Error Handling
Handling Failures
When an operation fails within a transaction, you should typically rollback:
#![allow(unused)]
fn main() {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
fn process_order(database: &impl Database) -> Result<(), DbmsError> {
// Multiple operations that should succeed together
database.insert::<Order>(order)?;
database.update::<Inventory>(update)?;
database.insert::<OrderItem>(item)?;
Ok(())
}
match process_order(&database) {
Ok(()) => {
database.commit()?;
println!("Order processed successfully");
}
Err(e) => {
database.rollback()?;
println!("Order failed, rolled back: {:?}", e);
}
}
}
Transaction Errors
| Error | Cause |
|---|---|
TransactionNotFound | Invalid transaction ID or transaction already completed |
NoActiveTransaction | Attempting to commit/rollback without an active transaction |
#![allow(unused)]
fn main() {
use wasm_dbms_api::prelude::{DbmsError, TransactionError};
match database.commit() {
Ok(()) => println!("Committed"),
Err(DbmsError::Transaction(TransactionError::NoActiveTransaction)) => {
println!("No active transaction to commit");
}
Err(e) => println!("Other error: {:?}", e),
}
}
Best Practices
1. Keep transactions short
Long-running transactions hold resources and block other operations:
#![allow(unused)]
fn main() {
// GOOD: Prepare data outside transaction
let users_to_insert = prepare_users();
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
for user in users_to_insert {
database.insert::<User>(user)?;
}
database.commit()?;
// BAD: Doing expensive work inside transaction
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
for raw_data in large_dataset {
let user = expensive_parsing(raw_data); // Don't do this in transaction
database.insert::<User>(user)?;
}
database.commit()?;
}
2. Always handle rollback
Ensure transactions are either committed or rolled back:
#![allow(unused)]
fn main() {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
let result = (|| -> Result<(), DbmsError> {
database.insert::<User>(user1)?;
database.insert::<User>(user2)?;
Ok(())
})();
match result {
Ok(()) => database.commit()?,
Err(_) => database.rollback()?,
}
}
3. Use transactions for related operations
Group operations that should succeed or fail together:
#![allow(unused)]
fn main() {
// GOOD: Related operations in transaction
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
database.insert::<Order>(order)?;
database.insert::<Payment>(payment)?;
database.update::<Inventory>(inv_update)?;
database.commit()?;
// BAD: Unrelated operations in transaction (unnecessary)
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
database.insert::<UserPreferences>(prefs)?;
database.insert::<AuditLog>(log)?; // Unrelated
database.commit()?;
}
4. Don’t mix transactional and non-transactional operations
#![allow(unused)]
fn main() {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(&ctx, my_schema, tx_id);
// GOOD: All operations use the transaction
database.insert::<Order>(order)?;
database.insert::<OrderItem>(item)?;
// BAD: Mixing transaction and non-transaction
let oneshot = WasmDbmsDatabase::oneshot(&ctx, my_schema);
database.insert::<Order>(order)?;
oneshot.insert::<AuditLog>(log)?; // Not in transaction!
}
Examples
Bank Transfer
Transfer money between accounts atomically:
#![allow(unused)]
fn main() {
fn transfer(
ctx: &DbmsContext<impl MemoryProvider>,
from_account: u32,
to_account: u32,
amount: Decimal,
) -> Result<(), DbmsError> {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(ctx, my_schema, tx_id);
// Deduct from source account
let deduct = AccountUpdateRequest::builder()
.decrease_balance(amount)
.filter(Filter::eq("id", Value::Uint32(from_account.into())))
.build();
database.update::<Account>(deduct)?;
// Add to destination account
let add = AccountUpdateRequest::builder()
.increase_balance(amount)
.filter(Filter::eq("id", Value::Uint32(to_account.into())))
.build();
database.update::<Account>(add)?;
// Record the transfer
let transfer_record = TransferInsertRequest {
id: Uuid::new_v4().into(),
from_account: from_account.into(),
to_account: to_account.into(),
amount,
timestamp: DateTime::now(),
};
database.insert::<Transfer>(transfer_record)?;
// Commit atomically
database.commit()?;
Ok(())
}
}
Order Processing
Process an order with inventory update:
#![allow(unused)]
fn main() {
fn process_order(
ctx: &DbmsContext<impl MemoryProvider>,
order: OrderInsertRequest,
items: Vec<OrderItemInsertRequest>,
) -> Result<u32, Box<dyn std::error::Error>> {
let tx_id = ctx.begin_transaction();
let mut database = WasmDbmsDatabase::from_transaction(ctx, my_schema, tx_id);
// Insert the order
database.insert::<Order>(order.clone())?;
// Insert order items and update inventory
for item in items {
// Insert order item
database.insert::<OrderItem>(item.clone())?;
// Decrease inventory
let inv_update = InventoryUpdateRequest::builder()
.decrease_quantity(item.quantity)
.filter(Filter::eq("product_id", Value::Uint32(item.product_id)))
.build();
let updated = database.update::<Inventory>(inv_update)?;
if updated == 0 {
// Product not in inventory, rollback
database.rollback()?;
return Err("Product not found in inventory".into());
}
}
// All successful, commit
database.commit()?;
Ok(order.id.into())
}
}