Rado GitHub

Transactions & batch

Group statements so they succeed or fail together.

Transactions

const result = await db.transaction(async tx => {
  const [user] = await tx.insert(User).values({name: 'Ada'}).returning()

  await tx.insert(Post).values({
    authorId: user.id,
    title: 'First post'
  })

  return user
})

The callback receives a transaction tx with the full database API. If the callback throws, everything rolls back; if it returns, everything commits and the return value is yours.

On synchronous drivers the callback is synchronous too. No await:

const result = db.transaction(tx => {
  const [user] = tx.insert(User).values({name: 'Ada'}).returning()
  tx.insert(Post).values({authorId: user.id, title: 'First post'}).run()
  return user
})

Rolling back deliberately

tx.rollback() aborts the transaction by throwing. Pass a value to get it back out:

import {Rollback} from 'rado'

try {
  await db.transaction(async tx => {
    const [user] = await tx.insert(User).values({name: 'Ada'}).returning()
    const ok = await someBusinessCheck(user)
    if (!ok) tx.rollback({reason: 'check failed'})
  })
} catch (err) {
  if (err instanceof Rollback) {
    err.data // {reason: 'check failed'}
  } else {
    throw err
  }
}

(Rollback is also exported as TransactionRollbackError.)

Nested transactions

Transactions nest via savepoints. An inner failure rolls back to the savepoint without dooming the outer transaction:

await db.transaction(async tx => {
  await tx.insert(User).values({name: 'Ada'})

  try {
    await tx.transaction(async inner => {
      await inner.insert(Post).values({authorId: 1, title: 'risky'})
      inner.rollback()
    })
  } catch {
    // inner rolled back; Ada's insert survives
  }
})

Transaction options

Dialect-specific options are passed as the second argument. For example on PostgreSQL:

await db.transaction(
  async tx => {...},
  {isolationLevel: 'serializable', accessMode: 'read write'}
)

Batch

db.batch runs an array of queries in one transaction (and, where the driver supports it, a single round-trip). Results come back per query:

const [users, , posts] = await db.batch([
  db.select().from(User),
  db.insert(User).values({name: 'Ada'}),
  db.select().from(Post)
])

On Cloudflare D1, which has no interactive transactions, use batch for atomicity. It maps directly onto D1's native batch API.

Creating and dropping in bulk

db.create and db.drop accept any number of tables, views, schemas and enums, and run as a batch:

await db.create(appSchema, User, Post, ActiveUsers)
await db.drop(ActiveUsers, Post, User)