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
|
.. 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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user