better-drizzle

This page is PostgreSQL and MySQL only.

Row locks

Control concurrent row access with FOR UPDATE, FOR SHARE, skipLocked, noWait, table targeting, and transaction-only enforcement.

Use a row lock when a read decides a write that must not race another transaction: claiming a job, reserving stock, moving money. The lock option on every read helper wraps Drizzle's FOR UPDATE, FOR SHARE, and related clauses.

Locks are supported on PostgreSQL and MySQL. SQLite has no row-level locking and throws LOCK_NOT_SUPPORTED. For complete patterns (job queues, cursor pages, includes, lock errors), see locking patterns.

Lock rows in a read

The simplest form is a string shorthand:

const users = await client.users.findMany({
	where: { active: true },
	lock: 'update',
});

This generates SELECT ... FOR UPDATE.

The object form gives access to every option:

const users = await client.users.findMany({
	where: { active: true },
	lock: {
		mode: 'update',
		skipLocked: true,
	},
});

skipLocked and noWait

These control what happens when a locked row is already held by another transaction:

  • skipLocked: true - skip rows that are locked, return only unlocked rows
  • noWait: true - fail immediately if any requested row is locked

They are mutually exclusive. Enabling both throws an error.

// Skip locked rows
const available = await client.jobs.findMany({
	where: { status: 'pending' },
	lock: {
		mode: 'update',
		skipLocked: true,
	},
});

With noWait, the query fails right away instead of waiting for the lock:

// Fail fast if locked
try {
	const row = await client.jobs.findFirst({
		where: { id: 1 },
		lock: {
			mode: 'update',
			noWait: true,
		},
	});
} catch (error) {
	// LOCK_TIMEOUT - the row was locked by another transaction
}

Lock specific tables (PostgreSQL only)

On PostgreSQL you can scope the lock to specific tables using tables. This is useful in joins where you only want to lock certain tables:

const posts = await client.posts.findMany({
	where: { published: true, author: { is: { active: true } } },
	include: { author: true },
	lock: {
		mode: 'update',
		tables: ['posts'],
	},
});

tables accepts both TypeScript table keys and database table names. Duplicate table references (by database name) are deduplicated automatically.

PostgreSQL only

tables is only supported on PostgreSQL. Using it on MySQL or SQLite throws LOCK_NOT_SUPPORTED.

Enforcing transaction-only locks

Outside a transaction, the lock is released as soon as the statement finishes, so it protects nothing. You can enforce that locks only run inside explicit transactions:

const client = better(db, {
	locks: {
		transactionsOnly: true,
	},
});

With this config, any lock outside client.transaction(...) throws LOCK_REQUIRES_TRANSACTION:

// This throws
const users = await client.users.findMany({
	lock: 'update',
});

// This works
await client.transaction(async (tx) => {
	return tx.users.findMany({
		lock: 'update',
	});
});

Reference

Lock modes

ModeSQL (PostgreSQL)SQL (MySQL)
'update'FOR UPDATEFOR UPDATE
'share'FOR SHAREFOR SHARE
'noKeyUpdate'FOR NO KEY UPDATEnot supported
'keyShare'FOR KEY SHAREnot supported

noKeyUpdate and keyShare are PostgreSQL-specific. Using them on MySQL throws LOCK_NOT_SUPPORTED.

Methods that accept lock

Locks work on every read helper that accepts QueryArgs:

MethodAccepts lock
findManyYes
findFirstYes
findOneYes
findUniqueYes
paginateYes
cursorYes
countNo
existsNo
all write operationsNo

Dialect support

FeaturePostgreSQLMySQLSQLite
lock: 'update'YesYesrejected
lock: 'share'YesYesrejected
lock: 'noKeyUpdate'Yesrejectedrejected
lock: 'keyShare'Yesrejectedrejected
skipLockedYesYesN/A
noWaitYesYesN/A
tablesYesrejectedN/A
transactionsOnlyYesYesN/A

On this page