Transactions in Query
@ydbjs/query provides two helpers: begin and transaction.
begin(options?, fn)— creates a transaction, runsfn(tx, signal), commits on success and rolls back on error.transaction(options?, fn)— alias tobegin.
Options:
isolation?: 'serializableReadWrite' | 'snapshotReadOnly'— isolation level (defaultserializableReadWrite).idempotent?: boolean— enables retries for conditionally retryable errors. Ensure business logic is idempotent.
signal: AbortSignal allows canceling long operations inside the callback.
Basic usage
ts
const result = await sql.begin(async (tx) => {
await tx`UPDATE users SET active = false WHERE ...`
return await tx`SELECT * FROM users WHERE active = false`
})Isolation and idempotency options
ts
await sql.begin(
{
isolation: 'snapshotReadOnly',
idempotent: true,
},
async (tx) => {
return await tx`SELECT COUNT(*) FROM users`
}
)Topic integration (without using)
When inside sql.transaction(...), use tx‑aware Topic clients without using/manual disposal:
ts
import { createTopicTxReader } from '@ydbjs/topic/reader'
import { createTopicTxWriter } from '@ydbjs/topic/writer'
await sql.transaction(async (tx, signal) => {
const reader = createTopicTxReader(tx, driver, {
topic: '/Root/my-topic',
consumer: 'svc-a',
})
for await (const batch of reader.read({ signal })) {
// processing
}
const writer = createTopicTxWriter(tx, driver, {
topic: '/Root/my-topic',
producer: 'p1',
})
writer.write(new TextEncoder().encode('message'))
})Reader registers updateOffsetsInTransaction on tx.onCommit; Writer triggers flush on tx.onCommit. Manual disposal inside a transaction is not required.
Timeouts and retries
- Set timeouts per query or per handler.
- See “Advanced → Retries and Idempotency”.
Best practices
- Keep transactions short to reduce blocking/conflicts.
- Prefer
snapshotReadOnlyfor read‑only operations when possible. - Idempotent operations make retries easier; use idempotency keys if needed.
- Avoid mixing heavy Topic operations with large SQL reads in a single transaction unless necessary.