Add PostgreSQL FOR UPDATE getters

Add a PostgreSQL-specific getRecordForUpdate helper that appends FOR
UPDATE to the generated SELECT statement so callers can lock a row
inside an explicit transaction.

generateProcsForModels now always emits a direct-connection
get<RecordName>ForUpdate proc that accepts db_postgres.DbConn. There is
intentionally no dbType overload for this API, because reacquiring a
connection via withConnection would defeat the lock's transactional
scope.

The source docs and README now document the new helper and show the
intended usage pattern inside inTransaction:

  db.inTransaction:
    var item = conn.getTodoItemForUpdate(todoId)
    item.priority += 1
    discard conn.updateTodoItem(item)
This commit is contained in:
2026-03-24 22:04:39 -05:00
parent 71cb5a7cff
commit 2301da8143
2 changed files with 35 additions and 3 deletions

View File

@@ -109,6 +109,7 @@ This will generate procedures like the following in two flavors:
.. code-block:: Nim .. code-block:: Nim
proc getTodoItem*(db: TodoDB, id: UUID): TodoItem; proc getTodoItem*(db: TodoDB, id: UUID): TodoItem;
proc getTodoItem*[D: DbConnType](conn: D, id: UUID): TodoItem; proc getTodoItem*[D: DbConnType](conn: D, id: UUID): TodoItem;
proc getTodoItemForUpdate*(conn: db_postgres.DbConn, id: UUID): TodoItem;
proc tryGetTodoItem*(db: TodoDB, id: UUID): Option[TodoItem]; proc tryGetTodoItem*(db: TodoDB, id: UUID): Option[TodoItem];
proc tryGetTodoItem*[D: DbConnType](conn: D, id: UUID): Option[TodoItem]; proc tryGetTodoItem*[D: DbConnType](conn: D, id: UUID): Option[TodoItem];
proc getTodoItemIfItExists*(db: TodoDB, id: UUID): Option[TodoItem]; proc getTodoItemIfItExists*(db: TodoDB, id: UUID): Option[TodoItem];
@@ -170,6 +171,8 @@ This will generate procedures like the following in two flavors:
Use the `dbType` flavor when the caller does not already have a connection. Use the `dbType` flavor when the caller does not already have a connection.
Use the connection flavor inside `withConnection` or `inTransaction`. Use the connection flavor inside `withConnection` or `inTransaction`.
The generated `get<RecordName>ForUpdate` helper is PostgreSQL-specific and
is only available for direct PostgreSQL connections.
Warning: do not call the `dbType` flavor from inside `inTransaction`. Warning: do not call the `dbType` flavor from inside `inTransaction`.
Those overloads call `withConnection` and may acquire a different Those overloads call `withConnection` and may acquire a different
@@ -178,7 +181,7 @@ transaction.
.. code-block:: Nim .. code-block:: Nim
db.inTransaction: db.inTransaction:
var item = conn.getTodoItem(todoId) var item = conn.getTodoItemForUpdate(todoId)
item.priority += 1 item.priority += 1
discard conn.updateTodoItem(item) discard conn.updateTodoItem(item)

View File

@@ -109,6 +109,7 @@
## .. code-block:: Nim ## .. code-block:: Nim
## proc getTodoItem*(db: TodoDB, id: UUID): TodoItem; ## proc getTodoItem*(db: TodoDB, id: UUID): TodoItem;
## proc getTodoItem*[D: DbConnType](conn: D, id: UUID): TodoItem; ## proc getTodoItem*[D: DbConnType](conn: D, id: UUID): TodoItem;
## proc getTodoItemForUpdate*(conn: db_postgres.DbConn, id: UUID): TodoItem;
## proc tryGetTodoItem*(db: TodoDB, id: UUID): Option[TodoItem]; ## proc tryGetTodoItem*(db: TodoDB, id: UUID): Option[TodoItem];
## proc tryGetTodoItem*[D: DbConnType](conn: D, id: UUID): Option[TodoItem]; ## proc tryGetTodoItem*[D: DbConnType](conn: D, id: UUID): Option[TodoItem];
## proc getTodoItemIfItExists*(db: TodoDB, id: UUID): Option[TodoItem]; ## proc getTodoItemIfItExists*(db: TodoDB, id: UUID): Option[TodoItem];
@@ -169,6 +170,8 @@
## ##
## Use the `dbType` flavor when the caller does not already have a connection. ## Use the `dbType` flavor when the caller does not already have a connection.
## Use the connection flavor inside `withConnection` or `inTransaction`. ## Use the connection flavor inside `withConnection` or `inTransaction`.
## The generated `get<RecordName>ForUpdate` helper is PostgreSQL-specific and
## is only available for direct PostgreSQL connections.
## ##
## Warning: do not call the `dbType` flavor from inside `inTransaction`. ## Warning: do not call the `dbType` flavor from inside `inTransaction`.
## Those overloads call `withConnection` and may acquire a different ## Those overloads call `withConnection` and may acquire a different
@@ -177,7 +180,7 @@
## ##
## .. code-block:: Nim ## .. code-block:: Nim
## db.inTransaction: ## db.inTransaction:
## var item = conn.getTodoItem(todoId) ## var item = conn.getTodoItemForUpdate(todoId)
## item.priority += 1 ## item.priority += 1
## discard conn.updateTodoItem(item) ## discard conn.updateTodoItem(item)
## ##
@@ -334,7 +337,7 @@
## .. _pool.DbConnPool: fiber_orm/pool.html#DbConnPool ## .. _pool.DbConnPool: fiber_orm/pool.html#DbConnPool
## ##
import std/[json, macros, options, sequtils, strutils] import std/[json, macros, options, sequtils, strutils]
import db_connector/db_common import db_connector/[db_common, db_postgres]
import uuids import uuids
from std/unicode import capitalize from std/unicode import capitalize
@@ -451,6 +454,23 @@ template getRecord*[D: DbConnType](db: D, modelType: type, id: typed): untyped =
rowToModel(modelType, row) rowToModel(modelType, row)
template getRecordForUpdate*(db: db_postgres.DbConn, modelType: type, id: typed): untyped =
## Fetch a record by id and lock it with `FOR UPDATE`.
##
## This is PostgreSQL-specific and should only be used inside a transaction.
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
" WHERE id = ? FOR UPDATE"
logQuery("getRecordForUpdate", sqlStmt, [("id", $id)])
let row = db.getRow(sql(sqlStmt), @[$id])
if allIt(row, it.len == 0):
raise newException(NotFoundError, "no " & modelName(modelType) & " record for id " & $id)
rowToModel(modelType, row)
template tryGetRecord*[D: DbConnType](db: D, modelType: type, id: typed): untyped = template tryGetRecord*[D: DbConnType](db: D, modelType: type, id: typed): untyped =
## Fetch a record by id. ## Fetch a record by id.
let sqlStmt = let sqlStmt =
@@ -642,6 +662,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
## .. code-block:: Nim ## .. code-block:: Nim
## proc getTodoItem*(db: TodoDB, id: idType): TodoItem; ## proc getTodoItem*(db: TodoDB, id: idType): TodoItem;
## proc getTodoItem*[D: DbConnType](conn: D, id: idType): TodoItem; ## proc getTodoItem*[D: DbConnType](conn: D, id: idType): TodoItem;
## proc getTodoItemForUpdate*(conn: db_postgres.DbConn, id: idType): TodoItem;
## proc tryGetTodoItem*(db: TodoDB, id: idType): Option[TodoItem]; ## proc tryGetTodoItem*(db: TodoDB, id: idType): Option[TodoItem];
## proc tryGetTodoItem*[D: DbConnType](conn: D, id: idType): Option[TodoItem]; ## proc tryGetTodoItem*[D: DbConnType](conn: D, id: idType): Option[TodoItem];
## proc getTodoItemIfItExists*(db: TodoDB, id: idType): Option[TodoItem]; ## proc getTodoItemIfItExists*(db: TodoDB, id: idType): Option[TodoItem];
@@ -677,6 +698,8 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
## The `dbType` overloads are convenience wrappers around `withConnection`. ## The `dbType` overloads are convenience wrappers around `withConnection`.
## Inside `inTransaction`, prefer the overloads that take `conn: D` where ## Inside `inTransaction`, prefer the overloads that take `conn: D` where
## `D: DbConnType` so all operations use the transaction connection. ## `D: DbConnType` so all operations use the transaction connection.
## The generated `get<RecordName>ForUpdate` helper is PostgreSQL-specific and
## is only available for direct PostgreSQL connections.
## ##
## .. _Database Object: #database-object ## .. _Database Object: #database-object
result = newStmtList() result = newStmtList()
@@ -688,6 +711,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
let modelName = $(t.getType[1]) let modelName = $(t.getType[1])
let getName = ident("get" & modelName) let getName = ident("get" & modelName)
let getForUpdateName = ident("get" & modelName & "ForUpdate")
let tryGetName = ident("tryGet" & modelName) let tryGetName = ident("tryGet" & modelName)
let getIfExistsName = ident("get" & modelName & "IfItExists") let getIfExistsName = ident("get" & modelName & "IfItExists")
let getAllName = ident("getAll" & pluralize(modelName)) let getAllName = ident("getAll" & pluralize(modelName))
@@ -704,6 +728,9 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
proc `getName`*[D: DbConnType](conn: D, id: `idType`): `t` = proc `getName`*[D: DbConnType](conn: D, id: `idType`): `t` =
result = getRecord(conn, `t`, id) result = getRecord(conn, `t`, id)
proc `getForUpdateName`*(conn: db_postgres.DbConn, id: `idType`): `t` =
result = getRecordForUpdate(conn, `t`, id)
proc `tryGetName`*(db: `dbType`, id: `idType`): Option[`t`] = proc `tryGetName`*(db: `dbType`, id: `idType`): Option[`t`] =
db.withConnection conn: result = tryGetRecord(conn, `t`, id) db.withConnection conn: result = tryGetRecord(conn, `t`, id)
@@ -1039,6 +1066,8 @@ template inTransaction*(db, body: untyped) =
## overloads that take `conn: D` where `D: DbConnType`. Do not call the ## overloads that take `conn: D` where `D: DbConnType`. Do not call the
## overloads that take the outer database object, because those call ## overloads that take the outer database object, because those call
## `withConnection` again and may acquire a different connection. ## `withConnection` again and may acquire a different connection.
## If you need to lock a PostgreSQL row before modifying it, use the
## generated `get<RecordName>ForUpdate` helper.
db.withConnection conn: db.withConnection conn:
conn.exec(sql"BEGIN TRANSACTION") conn.exec(sql"BEGIN TRANSACTION")
try: try: