VibORM

Transactions & Batching

Execute multiple operations atomically with transactions or batch mode

VibORM provides two ways to execute multiple operations atomically:

  1. Dynamic Transactions - Operations can depend on each other (callback API)
  2. Batch Mode - Independent operations executed atomically (array API)

Dynamic Transactions

Use the callback API when operations need to depend on each other:

await client.$transaction(async (tx) => {
  // Create a user
  const user = await tx.user.create({
    data: { name: "Alice", email: "alice@example.com" },
  });

  // Use the created user's ID in the next operation
  await tx.post.create({
    data: {
      title: "Hello World",
      authorId: user.id,  // Depends on previous operation
    },
  });
});

Transaction Options

await client.$transaction(
  async (tx) => {
    // ... operations
  },
  {
    isolationLevel: "serializable",  // PostgreSQL only
    timeout: 5000,                    // Max duration in ms
  }
);

Batch Mode (Prisma-style)

Use the array API for independent operations that should execute atomically:

const [users, posts, count] = await client.$transaction([
  client.user.findMany({ where: { active: true } }),
  client.post.findMany({ where: { published: true } }),
  client.user.count(),
]);

Batch Writes

const [user1, user2] = await client.$transaction([
  client.user.create({ data: { name: "Alice", email: "alice@example.com" } }),
  client.user.create({ data: { name: "Bob", email: "bob@example.com" } }),
]);

console.log(user1.name); // "Alice"
console.log(user2.name); // "Bob"

Mixed Operations

const [newUser, allPosts, userCount] = await client.$transaction([
  client.user.create({ data: { name: "Charlie", email: "charlie@example.com" } }),
  client.post.findMany(),
  client.user.count(),
]);

How Operations Work

All model operations in VibORM return a PendingOperation object that implements PromiseLike. This means:

// Direct await - executes immediately
const users = await client.user.findMany();

// Store without awaiting - creates a pending operation
const findUsersOp = client.user.findMany();

// Execute later via await
const users = await findUsersOp;

// Or batch multiple operations
const [users, posts] = await client.$transaction([
  findUsersOp,
  client.post.findMany(),
]);

Driver Support

Different drivers have different capabilities for atomic execution:

DriverDynamic TransactionsBatch ModeHow Batch Works
pgFullFullTransaction wrapper
postgresFullFullTransaction wrapper
pgliteFullFullTransaction wrapper
mysql2FullFullTransaction wrapper
sqlite3FullFullTransaction wrapper
libsqlFullFullTransaction wrapper
bun-sqliteFullFullTransaction wrapper
planetscaleFullFullTransaction wrapper
d1Sequential*FullNative batch() API
d1-httpSequential*FullHTTP batch endpoint
neon-httpSequential*Fulltransaction() function

*Drivers marked "Sequential" for dynamic transactions will execute operations one-by-one without isolation. Use batch mode for atomic operations on these drivers.

Understanding the Difference

Dynamic transactions provide:

  • Atomicity (all-or-nothing)
  • Isolation (changes not visible until commit)
  • Read-your-writes (can use results from previous operations)

Batch mode provides:

  • Atomicity (all-or-nothing)
  • No isolation (other connections may see partial state)
  • No read-your-writes (operations must be independent)

Choosing the Right Approach

Use Dynamic Transactions When:

  • Operations depend on each other's results
  • You need isolation from other connections
  • You're using a driver that supports transactions
// Good: user.id is needed for posts
await client.$transaction(async (tx) => {
  const user = await tx.user.create({ data: { name: "Alice" } });
  await tx.post.createMany({
    data: [
      { title: "Post 1", authorId: user.id },
      { title: "Post 2", authorId: user.id },
    ],
  });
});

Use Batch Mode When:

  • Operations are independent
  • You want better performance (single round-trip on some drivers)
  • You're using a driver without full transaction support (D1, Neon-HTTP)
// Good: independent operations
const [users, posts, stats] = await client.$transaction([
  client.user.findMany(),
  client.post.findMany({ where: { published: true } }),
  client.user.aggregate({ _count: true, _avg: { age: true } }),
]);

Error Handling

Both transaction modes provide automatic rollback when any operation fails. The transaction is rolled back at the database level before the error is thrown to your code.

Migrations and Atomicity

When running migrations, VibORM automatically uses the best available method for atomic execution:

  1. Native batch - For D1, D1-HTTP, Neon-HTTP
  2. Transaction wrapper - For drivers with transaction support
  3. Sequential - Fallback with warning (shouldn't happen with current drivers)

This ensures migrations are atomic (all-or-nothing) regardless of the driver being used.

See Migration Atomicity for more details.

On this page