From harness-claude
Executes atomic Drizzle ORM database operations with db.transaction(), nested transactions via savepoints, rollbacks, and batch updates for multi-query consistency.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Execute atomic Drizzle operations with db.transaction(), nested transactions, and rollback semantics
Executes atomic database operations using Prisma $transaction: sequential arrays, interactive callbacks with dependent reads/writes, nested creates/updates, and batch ops with rollback.
Provides Drizzle ORM patterns for schema definition, CRUD operations, relations, queries, transactions, and migrations. Supports PostgreSQL, MySQL, SQLite, MSSQL, CockroachDB.
Guides Drizzle ORM type-safe schema design, relational queries, prepared statements, migrations, and transactions. Use for database schema, queries, migrations, and performance optimization in TypeScript.
Share bugs, ideas, or general feedback.
Execute atomic Drizzle operations with db.transaction(), nested transactions, and rollback semantics
const result = await db.transaction(async (tx) => {
const [sender] = await tx
.update(accounts)
.set({ balance: sql`${accounts.balance} - ${amount}` })
.where(eq(accounts.id, senderId))
.returning();
if (sender.balance < 0) {
tx.rollback(); // Rolls back the entire transaction
}
const [receiver] = await tx
.update(accounts)
.set({ balance: sql`${accounts.balance} + ${amount}` })
.where(eq(accounts.id, receiverId))
.returning();
return { sender, receiver };
});
Use tx for all queries inside the callback. Using db instead of tx runs queries outside the transaction.
Rollback — call tx.rollback() or throw an error:
await db.transaction(async (tx) => {
await tx.insert(orders).values({ userId, total });
const inventory = await tx.query.products.findFirst({
where: eq(products.id, productId),
});
if (!inventory || inventory.stock < quantity) {
throw new Error('Insufficient stock'); // Triggers rollback
}
await tx
.update(products)
.set({ stock: sql`${products.stock} - ${quantity}` })
.where(eq(products.id, productId));
});
await db.transaction(async (tx) => {
await tx.insert(orders).values(orderData);
try {
await tx.transaction(async (tx2) => {
await tx2.insert(notifications).values(notifData);
// If this fails, only the inner savepoint rolls back
});
} catch {
// Order still committed; notification failed silently
}
});
await db.transaction(
async (tx) => {
// ...
},
{
isolationLevel: 'serializable',
accessMode: 'read write',
deferrable: true,
}
);
await db.transaction(async (tx) => {
for (const item of items) {
await tx
.update(inventory)
.set({ stock: sql`${inventory.stock} - ${item.qty}` })
.where(eq(inventory.sku, item.sku));
}
});
Drizzle transactions use the underlying database driver's transaction support. The tx object is a transaction-scoped Drizzle client with the same query API as db.
tx.rollback(): This throws a special error that Drizzle catches to trigger rollback. Do not catch this error inside the transaction callback. The transaction function returns never after tx.rollback().
Nested transactions use savepoints. On PostgreSQL and MySQL, nested tx.transaction() calls translate to SAVEPOINT / ROLLBACK TO SAVEPOINT. SQLite does not support savepoints in all drivers.
Isolation levels (PostgreSQL):
read uncommitted — rarely used, same as read committed on PostgreSQLread committed (default) — each statement sees committed data as of statement startrepeatable read — all statements see committed data as of transaction startserializable — full serializability, detects and aborts conflicting transactionsConnection handling: A transaction holds one database connection for its entire duration. Keep transactions short to avoid connection pool exhaustion. Avoid external HTTP calls, file I/O, or long computations inside transactions.
Trade-offs:
tx.rollback() uses exceptions for control flow, which can be surprising — do not wrap it in try/catch inside the transactionhttps://orm.drizzle.team/docs/transactions