FluentORM provides a generic Transaction system that works with any model, allowing you to perform multiple model operations atomically within a single database transaction.
const Transaction = @import("transaction.zig").Transaction;
const Executor = @import("executor.zig").Executor;
// Begin a transaction
var tx = try Transaction.begin(pool);
defer tx.deinit(); // Auto-rollback if not committed
// Use any model with tx.executor()
const user_id = try Users.insert(tx.executor(), allocator, .{
.email = "alice@example.com",
.name = "Alice",
.password_hash = "hash",
}).unwrap();
// Query within the same transaction
var query = Users.query();
const user = try query
.where(.{ .field = .id, .operator = .eq, .value = .{ .string = "$1" } })
.first(tx.executor(), allocator, .{user_id})
.unwrap();
// Operations on different models
_ = try Posts.insert(tx.executor(), allocator, .{
.user_id = user_id,
.title = "First Post",
.content = "Hello!",
}).unwrap();
// Raw SQL
try tx.exec("UPDATE stats SET user_count = user_count + 1", .{});
// Commit all changes atomically
try tx.commit();Acquires a connection from the pool and starts a transaction (BEGIN).
var tx = try Transaction.begin(pool);Returns an Executor that can be passed to any model method. This is how you use models within the transaction.
const user_id = try Users.insert(tx.executor(), allocator, data).unwrap();
const posts = try Posts.query().fetch(tx.executor(), allocator, .{}).unwrap();Commits the transaction (COMMIT) and releases the connection back to the pool.
try tx.commit();Errors:
error.TransactionAlreadyCommitted- Already committederror.TransactionAlreadyRolledBack- Already rolled back
Rolls back the transaction (ROLLBACK) and releases the connection.
try tx.rollback();Automatically rolls back if not committed or rolled back. Use with defer:
var tx = try Transaction.begin(pool);
defer tx.deinit(); // Safe cleanupExecute raw SQL within the transaction:
try tx.exec("UPDATE counters SET value = value + 1 WHERE name = $1", .{"visits"});Execute a raw query within the transaction:
var result = try tx.query("SELECT * FROM logs WHERE level = $1", .{"error"});
defer result.deinit();The Executor is a union type that abstracts over *pg.Pool and *pg.Conn:
pub const Executor = union(enum) {
pool: *pg.Pool,
conn: *pg.Conn,
};// From a pool (for non-transactional operations)
const exec = Executor.fromPool(pool);
// From a transaction
const exec = tx.executor();All model methods accept Executor as their first argument:
// Non-transactional
const user = try Users.findById(Executor.fromPool(pool), allocator, id).unwrap();
// Transactional
const user = try Users.findById(tx.executor(), allocator, id).unwrap();fn createUserWithProfile(pool: *pg.Pool, allocator: Allocator) !void {
var tx = try Transaction.begin(pool);
defer tx.deinit(); // Rollback on error
const user_id = try Users.insert(tx.executor(), allocator, .{...}).unwrap();
// If this fails, deinit() will rollback the user insert
try Profiles.insert(tx.executor(), allocator, .{
.user_id = user_id,
...
}).unwrap();
try tx.commit();
}var tx = try Transaction.begin(pool);
defer tx.deinit();
// Work with multiple models
const order_id = try Orders.insert(tx.executor(), allocator, order_data).unwrap();
for (items) |item| {
try OrderItems.insert(tx.executor(), allocator, .{
.order_id = order_id,
.product_id = item.product_id,
.quantity = item.quantity,
}).unwrap();
}
// Update inventory
for (items) |item| {
try tx.exec(
"UPDATE products SET stock = stock - $1 WHERE id = $2",
.{item.quantity, item.product_id}
);
}
try tx.commit();var tx = try Transaction.begin(pool);
defer tx.deinit();
// Insert and verify
const user_id = try Users.insert(tx.executor(), allocator, .{...}).unwrap();
var query = Users.query();
defer query.deinit();
const user = try query
.where(.{ .field = .id, .operator = .eq, .value = .{ .string = "$1" } })
.first(tx.executor(), allocator, .{user_id})
.unwrap();
if (user == null) {
return error.InsertVerificationFailed;
}
try tx.commit();If you were using the old model-specific Transaction API:
Before (deprecated):
var tx = try Transaction(Users).begin(conn);
const user_id = try tx.insert(allocator, data);After (new generic API):
var tx = try Transaction.begin(pool);
defer tx.deinit();
const user_id = try Users.insert(tx.executor(), allocator, data).unwrap();
try tx.commit();Key changes:
Transactionis no longer parameterized by model type- Pass
poolinstead ofconntobegin() - Use
tx.executor()with any model's methods - Explicit
tx.commit()required
Comprehensive transaction tests are implemented covering:
- Basic commit and rollback
- Multi-model operations
- Query operations within transactions
- Error handling with
Resulttypes - Auto-rollback on defer