Row-level locks
PostgreSQL SELECT ... FOR UPDATE / SHARE / NO KEY UPDATE on find, findOne, and findAll via SqlOperationOptions.
On PostgreSQL, find, findOne, and findAll accept a row-level lock in their third argument (SqlOperationOptions). The package wraps the generated SELECT with Drizzle's .for(...) clause. On MySQL the lock options are no-ops. The option is silently ignored, and no FOR UPDATE is generated.
When to use
Read-modify-write patterns where you need to prevent another transaction from mutating the row between your read and your update:
- Fetching an account balance, then decrementing it.
- Locking a
postsrow while computing a slug. - Anything where a stale read would cause a lost-update bug.
If you only need to fetch data and write it later, prefer executeSqlTransaction and call select(...).for('update') directly. Locks are scoped to the transaction and released on commit or rollback.
Options
Two fields on SqlOperationOptions. Both Postgres-only:
| Field | Type | SQL it produces |
|---|---|---|
lock | 'update' | 'share' | 'none' | FOR UPDATE / FOR SHARE / (omitted) |
forNoKeyUpdate | boolean | FOR NO KEY UPDATE |
lock: 'none' is the default. Pass it explicitly to override a service-level default. lock and forNoKeyUpdate are mutually exclusive; forNoKeyUpdate wins if both are set.
FOR NO KEY UPDATE is like FOR UPDATE but does not block other transactions from inserting FK-target rows. Useful when you're locking a parent row but children may legitimately appear (e.g. appending audit-log entries).
Examples
const account = await accounts.find(id, { lock: 'update' });
// SELECT ... FOR UPDATEconst activeRows = await accounts.findAll(
{ status: 'active' },
{ page: 1, limit: 50 },
{ lock: 'share' },
);
// SELECT ... FROM accounts WHERE ... ORDER BY ... LIMIT 50 OFFSET 0 FOR SHAREconst parent = await posts.findOne({ slug }, { forNoKeyUpdate: true });
// SELECT ... FOR NO KEY UPDATE
// other transactions can still INSERT child rows referencing this parentCaveats
- MySQL. The options are accepted but ignored. No
FOR UPDATEis added. If you need MySQL locking, drop down to a custom method. - No assertions under contention. The package's unit tests cover the code path (the option reaches Drizzle's
.for(...)); contention behavior is the database's contract, not ours. Verify with a concurrent integration test before relying on it. - Read methods only.
create/update/delete/softDelete/restorehave their own transaction model. They don't take alockoption. UseexecuteSqlTransactionand afind(..., { lock })inside it if you need to lock across reads and writes.