Guides
Run transactions
Use adapter-owned transaction scopes and savepoints, and keep work inside its active scope.
Use a transactional adapter when several queries must share one commit or rollback boundary:
The examples assume queries named readQuery, firstMutation,
secondMutation, and optionalMutation, built with the read and mutation
guides.
import { qubu } from "qubu"
import type { TransactionalQueryAdapter } from "qubu"
declare const transactionalAdapter: TransactionalQueryAdapter
const db = qubu(transactionalAdapter)
const result = await db.transaction(async (transaction) => {
await transaction.execute(firstMutation)
await transaction.execute(secondMutation)
return transaction.rows(readQuery)
})
The adapter owns connection pinning, commit, rollback, and release. Qubu creates the scoped client and returns the callback result; it emits no transaction SQL itself.
A scoped client's methods follow its adapter's capabilities: execute() and
rows() are always available; EXPLAIN and streaming require their respective
capabilities. TransactionOptions.signal is passed to the adapter. Isolation
levels and other driver-specific settings remain adapter-specific.
Roll back part of a transaction
The pg, mysql2, and node:sqlite adapters expose transaction() on scoped
clients through the shared NestedTransactionalQueryAdapter capability.
Other adapters retain their existing transaction surface. For example, with a
bound pg client, catch a nested failure to keep earlier work:
import { pgAdapter } from "@qubu/adapter-pg"
import { Pool } from "pg"
const pgDb = qubu(pgAdapter(new Pool()))
await pgDb.transaction(async (outer) => {
await outer.execute(firstMutation)
try {
await outer.transaction(async (inner) => {
await inner.execute(optionalMutation)
})
} catch (error) {
// The nested work was rolled back; the outer transaction can continue.
}
await outer.execute(secondMutation)
})
Each nested callback uses a uniquely named savepoint on the same connection.
Success releases it; failure rolls back to it and releases it. Letting the
failure escape also rolls back the outer transaction. Failed savepoint creation
or recovery makes the entire transaction unsafe to commit, even if the callback
catches the error. Primary and cleanup failures are retained in AggregateError.
Finish work before leaving a scope
Await every query and nested transaction before returning. These three adapters reject:
- Calls on a finished scoped client.
- Overlapping sibling scopes.
- A child scope started while its parent has pending queries.
- Parent queries while a child is active.
If a callback finishes with work pending, the adapter waits for that work and rolls back.
Use the active scoped client for all work on a directly supplied connection. Its root client rejects unrelated operations during the transaction. A pg pool can still run independent queries on other connections.
Your application remains responsible for raw driver calls and separately constructed adapters.
EXPLAIN and result decoding remain available at every depth. Nested transaction
hooks identify their enclosing transaction with parentId; queries identify
their immediate scope. Cancellation does not interrupt savepoint recovery.
Lock selected rows
For locking reads inside a transaction, add rowLock() to the SELECT:
import { eq, from, integer, rowLock, select, table, where } from "qubu"
const jobs = table("jobs", { id: integer() })
const claimedJob = select(
{ id: jobs.id },
from(jobs),
where(eq(jobs.id, 7)),
rowLock("update", "skip-locked"),
)
PostgreSQL supports all four Qubu modes. MySQL supports update and share;
key-share and no-key-update fail during rendering. The standard and SQLite
dialects reject row-locking queries. Qubu does not start a transaction for a
locking query or validate every database restriction on locking syntax.