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:
@@ -109,6 +109,7 @@ This will generate procedures like the following in two flavors:
|
||||
.. code-block:: Nim
|
||||
proc getTodoItem*(db: TodoDB, 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*[D: DbConnType](conn: D, 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 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`.
|
||||
Those overloads call `withConnection` and may acquire a different
|
||||
@@ -178,7 +181,7 @@ transaction.
|
||||
|
||||
.. code-block:: Nim
|
||||
db.inTransaction:
|
||||
var item = conn.getTodoItem(todoId)
|
||||
var item = conn.getTodoItemForUpdate(todoId)
|
||||
item.priority += 1
|
||||
discard conn.updateTodoItem(item)
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
## .. code-block:: Nim
|
||||
## proc getTodoItem*(db: TodoDB, 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*[D: DbConnType](conn: D, 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 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`.
|
||||
## Those overloads call `withConnection` and may acquire a different
|
||||
@@ -177,7 +180,7 @@
|
||||
##
|
||||
## .. code-block:: Nim
|
||||
## db.inTransaction:
|
||||
## var item = conn.getTodoItem(todoId)
|
||||
## var item = conn.getTodoItemForUpdate(todoId)
|
||||
## item.priority += 1
|
||||
## discard conn.updateTodoItem(item)
|
||||
##
|
||||
@@ -334,7 +337,7 @@
|
||||
## .. _pool.DbConnPool: fiber_orm/pool.html#DbConnPool
|
||||
##
|
||||
import std/[json, macros, options, sequtils, strutils]
|
||||
import db_connector/db_common
|
||||
import db_connector/[db_common, db_postgres]
|
||||
import uuids
|
||||
|
||||
from std/unicode import capitalize
|
||||
@@ -451,6 +454,23 @@ template getRecord*[D: DbConnType](db: D, modelType: type, id: typed): untyped =
|
||||
|
||||
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 =
|
||||
## Fetch a record by id.
|
||||
let sqlStmt =
|
||||
@@ -642,6 +662,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
|
||||
## .. code-block:: Nim
|
||||
## proc getTodoItem*(db: TodoDB, 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*[D: DbConnType](conn: D, 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`.
|
||||
## Inside `inTransaction`, prefer the overloads that take `conn: D` where
|
||||
## `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
|
||||
result = newStmtList()
|
||||
@@ -688,6 +711,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
|
||||
|
||||
let modelName = $(t.getType[1])
|
||||
let getName = ident("get" & modelName)
|
||||
let getForUpdateName = ident("get" & modelName & "ForUpdate")
|
||||
let tryGetName = ident("tryGet" & modelName)
|
||||
let getIfExistsName = ident("get" & modelName & "IfItExists")
|
||||
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` =
|
||||
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`] =
|
||||
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 the outer database object, because those call
|
||||
## `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:
|
||||
conn.exec(sql"BEGIN TRANSACTION")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user