Transactions
Atomic operations with raw-query and builder-friendly transaction APIs.
KuraDB provides two transaction styles:
kura.db.transactionfor imperative transaction callbackskura.db.raw.transactionfor declarative lists of raw SQL queries
The imperative callback now supports both:
- the original raw
query(statement, values)function - a second
txargument with transaction-scoped builders
Preferred Lua style
Use kura.db.transaction.await(...) when you want an awaitable transaction flow:
local transfer = kura.db.transaction.await(function(query, tx)
local accounts = kura.db.tables.accounts
local from = tx.select({ accounts.id, accounts.balance })
:from(accounts)
:where(op.eq(accounts.id, fromId))
:forUpdate()
:single()
:await()
local to = tx.select({ accounts.id, accounts.balance })
:from(accounts)
:where(op.eq(accounts.id, toId))
:forUpdate()
:single()
:await()
if from.balance < amount then
return false
end
tx.update(accounts)
:set({ balance = from.balance - amount })
:where(op.eq(accounts.id, fromId))
:await()
tx.update(accounts)
:set({ balance = to.balance + amount })
:where(op.eq(accounts.id, toId))
:await()
return {
fromBalance = from.balance - amount,
toBalance = to.balance + amount,
}
end)The imperative callback uses payload-or-false return semantics:
return falserolls the transaction back and returnsfalsereturn nilcommits the transaction and returnstruereturn <payload>commits the transaction and returns that payload
accounts is just an example table. The generated table names available inside kura.db.tables always come from your own schema.ts.
forUpdate requires tx
:forUpdate() is only allowed on builders created from the transaction-scoped tx object. Calling kura.db.select():forUpdate() outside a transaction raises an error so row locks are not used accidentally in non-transactional code.
Backwards-compatible callback transactions
The original callable style still works:
local result = kura.db.transaction(function(query)
local rows = query(
'SELECT balance FROM "public"."accounts" WHERE id = $1',
{ fromId }
)
if rows[1].balance < amount then
return false
end
query('UPDATE "public"."accounts" SET balance = balance - $1 WHERE id = $2', { amount, fromId })
query('UPDATE "public"."accounts" SET balance = balance + $1 WHERE id = $2', { amount, toId })
return { transferred = amount }
end)If you want builder ergonomics, accept the second tx argument:
local result = kura.db.transaction(function(query, tx)
local accounts = kura.db.tables.accounts
local row = tx.select({ accounts.id, accounts.balance })
:from(accounts)
:where(op.eq(accounts.id, fromId))
:single()
:await()
return row ~= nil
end)If you do not need a payload, simply omit the return value and the transaction will commit with true:
local ok = kura.db.transaction.await(function(query, tx)
local accounts = kura.db.tables.accounts
tx.update(accounts)
:set({ lastSeenAt = os.time() })
:where(op.eq(accounts.id, accountId))
:await()
end)Declarative raw transactions
Use kura.db.raw.transaction.await(...) to pass a list of raw SQL queries that should all succeed or all fail:
local ok = kura.db.raw.transaction.await({
{
query = 'UPDATE "public"."accounts" SET balance = balance - $1 WHERE id = $2',
values = { amount, fromId },
},
{
query = 'UPDATE "public"."accounts" SET balance = balance + $1 WHERE id = $2',
values = { amount, toId },
},
})What the transaction builder supports
Inside the tx object you can currently use:
tx.select(...)tx.insert(...)tx.update(...)tx.delete(...)
Those builders compile SQL the same way as normal kura.db.* builders, but execute against the current transaction connection instead of the global pool.
TypeScript
import { kuradb } from '@your-scope/kuradb';
const transfer = await kuradb.startTransaction(async (query) => {
const rows = await query(
'SELECT balance FROM "public"."accounts" WHERE id = $1',
[fromId]
);
if (rows[0].balance < amount) return false;
await query('UPDATE "public"."accounts" SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
await query('UPDATE "public"."accounts" SET balance = balance + $1 WHERE id = $2', [amount, toId]);
return { amount };
});kuradb.startTransaction(...) follows the same contract as Lua:
return falserolls back and resolves tofalsereturn undefinedcommits and resolves totruereturn payloadcommits and resolves to that payload
Transaction options
Both transaction APIs accept an options table:
kura.db.transaction.await(function(query, tx)
return true
end, {
isolationLevel = 'SERIALIZABLE',
readOnly = false,
})| Option | Type | Description |
|---|---|---|
isolationLevel | string | READ COMMITTED, REPEATABLE READ, READ UNCOMMITTED, SERIALIZABLE |
readOnly | boolean | Read-only transaction |
deferrable | boolean | Deferrable transaction, typically with SERIALIZABLE |
Current limitations
The transaction builder does not yet expose PostgreSQL-specific expression helpers such as increment, decrement, JSON path updates, or generic SQL fragments. For advanced write expressions inside a transaction, use the raw query(...) callback directly.