From 5c1a045f82fcc26065f80c45b6bb27516b6b52c6 Mon Sep 17 00:00:00 2001 From: NagIzaazShaik <176337212+shaikn6@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:09:48 -0400 Subject: [PATCH] docs(transactions): note lost-update risk in read-modify-write The bumpCounter example reads a row and writes counter+1 in a transaction without locking. Under READ COMMITTED (the default) two concurrent runs can both read the same value and lose an update. Add a short section pointing at SELECT ... FOR UPDATE and SERIALIZABLE + retry. Addresses #3485 --- docs/howto/transactions.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/howto/transactions.md b/docs/howto/transactions.md index b80aee7558..11979502e4 100644 --- a/docs/howto/transactions.md +++ b/docs/howto/transactions.md @@ -98,4 +98,30 @@ func bumpCounter(ctx context.Context, db *pgx.Conn, queries *tutorial.Queries, i } return tx.Commit(ctx) } -``` \ No newline at end of file +``` + +## Concurrent read-modify-write + +The `bumpCounter` example reads a row and then writes a value derived from it. +Under the default isolation level (`READ COMMITTED`), two transactions running +`bumpCounter` concurrently can both read the same `counter`, both write +`counter + 1`, and one update is lost. + +When a write depends on a value read earlier in the same transaction, lock the +row on read with `SELECT ... FOR UPDATE` (supported by PostgreSQL and +MySQL/InnoDB): + +```sql +-- name: GetRecordForUpdate :one +SELECT * FROM records +WHERE id = $1 +FOR UPDATE; +``` + +Call `GetRecordForUpdate` in place of `GetRecord`; the second transaction then +blocks until the first commits, and reads the updated value. + +Alternatively, run the transaction at `SERIALIZABLE` isolation and retry on +serialization failures. sqlc does not manage isolation levels or retries — set +the isolation level when you begin the transaction and handle the retry loop in +your application code.