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.