From 2301da8143d518c472934f9142aba28c9e82754c Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Tue, 24 Mar 2026 22:04:39 -0500 Subject: [PATCH] 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 getForUpdate 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) --- README.rst | 5 ++++- src/fiber_orm.nim | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7a3677f..1997a45 100644 --- a/README.rst +++ b/README.rst @@ -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 `getForUpdate` 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) diff --git a/src/fiber_orm.nim b/src/fiber_orm.nim index dc35f36..0e0a723 100644 --- a/src/fiber_orm.nim +++ b/src/fiber_orm.nim @@ -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 `getForUpdate` 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 `getForUpdate` 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 `getForUpdate` helper. db.withConnection conn: conn.exec(sql"BEGIN TRANSACTION") try: