From 71cb5a7cff246249c6c0d67d1a056a5813fb85c8 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Tue, 24 Mar 2026 21:48:51 -0500 Subject: [PATCH] Update documentation for new signature changes, bump version. --- README.rst | 81 +++++++++++++++++++++++----- fiber_orm.nimble | 2 +- src/fiber_orm.nim | 135 +++++++++++++++++++++++++++++++++++++++------- 3 files changed, 185 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index cb64fbc..7a3677f 100644 --- a/README.rst +++ b/README.rst @@ -83,7 +83,7 @@ Using Fiber ORM we can generate a data access layer with: .. code-block:: Nim # db.nim import std/[options] - import db_connectors/db_postgres + import db_connector/db_postgres import fiber_orm import ./models.nim @@ -100,32 +100,87 @@ Using Fiber ORM we can generate a data access layer with: generateLookup(TodoDB, TimeEntry, @["todoItemId"]) -This will generate the following procedures: +This will generate procedures like the following in two flavors: + +* a `dbType` flavor that acquires a connection via `withConnection` +* a connection flavor that operates directly on an existing + `conn: D` where `D: DbConnType` .. code-block:: Nim proc getTodoItem*(db: TodoDB, id: UUID): TodoItem; + proc getTodoItem*[D: DbConnType](conn: D, 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]; - proc getAllTodoItems*(db: TodoDB): seq[TodoItem]; + proc getTodoItemIfItExists*[D: DbConnType]( + conn: D, id: UUID): Option[TodoItem]; + proc getAllTodoItems*(db: TodoDB, + pagination = none[PaginationParams]()): PagedRecords[TodoItem]; + proc getAllTodoItems*[D: DbConnType](conn: D, + pagination = none[PaginationParams]()): PagedRecords[TodoItem]; proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; + proc createTodoItem*[D: DbConnType](conn: D, rec: TodoItem): TodoItem; proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool; + proc updateTodoItem*[D: DbConnType](conn: D, rec: TodoItem): bool; + proc createOrUpdateTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; + proc createOrUpdateTodoItem*[D: DbConnType]( + conn: D, rec: TodoItem): TodoItem; proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool; + proc deleteTodoItem*[D: DbConnType](conn: D, rec: TodoItem): bool; proc deleteTodoItem*(db: TodoDB, id: UUID): bool; + proc deleteTodoItem*[D: DbConnType](conn: D, id: UUID): bool; proc findTodoItemsWhere*(db: TodoDB, whereClause: string, - values: varargs[string, dbFormat]): seq[TodoItem]; + values: varargs[string, dbFormat], + pagination = none[PaginationParams]()): PagedRecords[TodoItem]; + proc findTodoItemsWhere*[D: DbConnType](conn: D, whereClause: string, + values: varargs[string, dbFormat], + pagination = none[PaginationParams]()): PagedRecords[TodoItem]; proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry; + proc getTimeEntry*[D: DbConnType](conn: D, id: UUID): TimeEntry; proc getTimeEntryIfItExists*(db: TodoDB, id: UUID): Option[TimeEntry]; - proc getAllTimeEntries*(db: TodoDB): seq[TimeEntry]; + proc getTimeEntryIfItExists*[D: DbConnType]( + conn: D, id: UUID): Option[TimeEntry]; + proc getAllTimeEntries*(db: TodoDB, + pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; + proc getAllTimeEntries*[D: DbConnType](conn: D, + pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry; + proc createTimeEntry*[D: DbConnType](conn: D, rec: TimeEntry): TimeEntry; proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool; + proc updateTimeEntry*[D: DbConnType](conn: D, rec: TimeEntry): bool; proc deleteTimeEntry*(db: TodoDB, rec: TimeEntry): bool; + proc deleteTimeEntry*[D: DbConnType](conn: D, rec: TimeEntry): bool; proc deleteTimeEntry*(db: TodoDB, id: UUID): bool; + proc deleteTimeEntry*[D: DbConnType](conn: D, id: UUID): bool; proc findTimeEntriesWhere*(db: TodoDB, whereClause: string, - values: varargs[string, dbFormat]): seq[TimeEntry]; + values: varargs[string, dbFormat], + pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; + proc findTimeEntriesWhere*[D: DbConnType](conn: D, whereClause: string, + values: varargs[string, dbFormat], + pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; - proc findTimeEntriesByTodoItemId(db: TodoDB, todoItemId: UUID): seq[TimeEntry]; + proc findTimeEntriesByTodoItemId*(db: TodoDB, todoItemId: UUID, + pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; + proc findTimeEntriesByTodoItemId*[D: DbConnType]( + conn: D, todoItemId: UUID, + pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; + +Use the `dbType` flavor when the caller does not already have a connection. +Use the connection flavor inside `withConnection` or `inTransaction`. + +Warning: do not call the `dbType` flavor from inside `inTransaction`. +Those overloads call `withConnection` and may acquire a different +connection, causing the statements to execute outside the active +transaction. + +.. code-block:: Nim + db.inTransaction: + var item = conn.getTodoItem(todoId) + item.priority += 1 + discard conn.updateTodoItem(item) Object-Relational Modeling ========================== @@ -133,11 +188,11 @@ Object-Relational Modeling Model Class ----------- -Fiber ORM uses simple Nim `object`s and `ref object`s as model classes. +Fiber ORM uses simple Nim objects and ref objects as model classes. Fiber ORM expects there to be one table for each model class. Name Mapping -```````````` +^^^^^^^^^^^^ Fiber ORM uses `snake_case` for database identifiers (column names, table names, etc.) and `camelCase` for Nim identifiers. We automatically convert model names to and from table names (`TodoItem` <-> `todo_items`), as well @@ -168,7 +223,7 @@ procedures in the `fiber_orm/util`_ module for details. .. _util: fiber_orm/util.html ID Field -```````` +^^^^^^^^ Fiber ORM expects every model class to have a field named `id`, with a corresponding `id` column in the model table. This field must be either a @@ -257,8 +312,10 @@ Many of the Fiber ORM macros expect a database object type to be passed. In the example above the `pool.DbConnPool`_ object is used as database object type (aliased as `TodoDB`). This is the intended usage pattern, but anything can be passed as the database object type so long as there is a -defined `withConn` template that provides an injected `conn: DbConn` object +defined `withConnection` template that provides an injected `conn: DbConn` object to the provided statement body. +The generated connection-flavor procedures are intended to work directly +with that `conn` value. For example, a valid database object implementation that opens a new connection for every request might look like this: @@ -269,7 +326,7 @@ connection for every request might look like this: type TodoDB* = object connString: string - template withConn*(db: TodoDB, stmt: untyped): untyped = + template withConnection*(db: TodoDB, stmt: untyped): untyped = let conn {.inject.} = open("", "", "", db.connString) try: stmt finally: close(conn) diff --git a/fiber_orm.nimble b/fiber_orm.nimble index cbe2710..6b9d764 100644 --- a/fiber_orm.nimble +++ b/fiber_orm.nimble @@ -1,6 +1,6 @@ # Package -version = "4.1.0" +version = "4.2.0" author = "Jonathan Bernard" description = "Lightweight Postgres ORM for Nim." license = "GPL-3.0" diff --git a/src/fiber_orm.nim b/src/fiber_orm.nim index 62fbae7..dc35f36 100644 --- a/src/fiber_orm.nim +++ b/src/fiber_orm.nim @@ -100,39 +100,86 @@ ## ## generateLookup(TodoDB, TimeEntry, @["todoItemId"]) ## -## This will generate the following procedures: +## This will generate procedures like the following in two flavors: +## +## * a `dbType` flavor that acquires a connection via `withConnection` +## * a connection flavor that operates directly on an existing +## `conn: D` where `D: DbConnType` ## ## .. code-block:: Nim ## proc getTodoItem*(db: TodoDB, id: UUID): TodoItem; -## +## proc getTodoItem*[D: DbConnType](conn: D, 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]; +## proc getTodoItemIfItExists*[D: DbConnType]( +## conn: D, id: UUID): Option[TodoItem]; ## proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; +## proc createTodoItem*[D: DbConnType](conn: D, rec: TodoItem): TodoItem; ## proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool; -## proc createOrUpdateTodoItem*(db: TodoDB, rec: TodoItem): bool; +## proc updateTodoItem*[D: DbConnType](conn: D, rec: TodoItem): bool; +## proc createOrUpdateTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; +## proc createOrUpdateTodoItem*[D: DbConnType]( +## conn: D, rec: TodoItem): TodoItem; ## proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool; +## proc deleteTodoItem*[D: DbConnType](conn: D, rec: TodoItem): bool; ## proc deleteTodoItem*(db: TodoDB, id: UUID): bool; +## proc deleteTodoItem*[D: DbConnType](conn: D, id: UUID): bool; ## ## proc getAllTodoItems*(db: TodoDB, -## pagination = none[PaginationParams]()): seq[TodoItem]; +## pagination = none[PaginationParams]()): PagedRecords[TodoItem]; +## proc getAllTodoItems*[D: DbConnType](conn: D, +## pagination = none[PaginationParams]()): PagedRecords[TodoItem]; ## ## proc findTodoItemsWhere*(db: TodoDB, whereClause: string, ## values: varargs[string, dbFormat], pagination = none[PaginationParams]() -## ): seq[TodoItem]; +## ): PagedRecords[TodoItem]; +## proc findTodoItemsWhere*[D: DbConnType](conn: D, whereClause: string, +## values: varargs[string, dbFormat], pagination = none[PaginationParams]() +## ): PagedRecords[TodoItem]; ## ## proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry; +## proc getTimeEntry*[D: DbConnType](conn: D, id: UUID): TimeEntry; ## proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry; +## proc createTimeEntry*[D: DbConnType](conn: D, rec: TimeEntry): TimeEntry; ## proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool; +## proc updateTimeEntry*[D: DbConnType](conn: D, rec: TimeEntry): bool; ## proc deleteTimeEntry*(db: TodoDB, rec: TimeEntry): bool; +## proc deleteTimeEntry*[D: DbConnType](conn: D, rec: TimeEntry): bool; ## proc deleteTimeEntry*(db: TodoDB, id: UUID): bool; +## proc deleteTimeEntry*[D: DbConnType](conn: D, id: UUID): bool; ## ## proc getAllTimeEntries*(db: TodoDB, -## pagination = none[PaginationParams]()): seq[TimeEntry]; +## pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; +## proc getAllTimeEntries*[D: DbConnType](conn: D, +## pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; ## ## proc findTimeEntriesWhere*(db: TodoDB, whereClause: string, ## values: varargs[string, dbFormat], pagination = none[PaginationParams]() -## ): seq[TimeEntry]; +## ): PagedRecords[TimeEntry]; +## proc findTimeEntriesWhere*[D: DbConnType](conn: D, whereClause: string, +## values: varargs[string, dbFormat], pagination = none[PaginationParams]() +## ): PagedRecords[TimeEntry]; ## -## proc findTimeEntriesByTodoItemId(db: TodoDB, todoItemId: UUID, -## pagination = none[PaginationParams]()): seq[TimeEntry]; +## proc findTimeEntriesByTodoItemId*(db: TodoDB, todoItemId: UUID, +## pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; +## proc findTimeEntriesByTodoItemId*[D: DbConnType]( +## conn: D, todoItemId: UUID, +## pagination = none[PaginationParams]()): PagedRecords[TimeEntry]; +## +## Use the `dbType` flavor when the caller does not already have a connection. +## Use the connection flavor inside `withConnection` or `inTransaction`. +## +## Warning: do not call the `dbType` flavor from inside `inTransaction`. +## Those overloads call `withConnection` and may acquire a different +## connection, causing the statements to execute outside the active +## transaction. +## +## .. code-block:: Nim +## db.inTransaction: +## var item = conn.getTodoItem(todoId) +## item.priority += 1 +## discard conn.updateTodoItem(item) ## ## Object-Relational Modeling ## ========================== @@ -140,11 +187,11 @@ ## Model Class ## ----------- ## -## Fiber ORM uses simple Nim `object`s and `ref object`s as model classes. +## Fiber ORM uses simple Nim objects and ref objects as model classes. ## Fiber ORM expects there to be one table for each model class. ## ## Name Mapping -## ```````````` +## ^^^^^^^^^^^^ ## Fiber ORM uses `snake_case` for database identifiers (column names, table ## names, etc.) and `camelCase` for Nim identifiers. We automatically convert ## model names to and from table names (`TodoItem` <-> `todo_items`), as well @@ -175,7 +222,7 @@ ## .. _util: fiber_orm/util.html ## ## ID Field -## ```````` +## ^^^^^^^^ ## ## Fiber ORM expects every model class to have a field named `id`, with a ## corresponding `id` column in the model table. This field must be either a @@ -266,6 +313,8 @@ ## anything can be passed as the database object type so long as there is a ## defined `withConnection` template that provides a `conn: DbConn` object ## to the provided statement body. +## The generated connection-flavor procedures are intended to work directly +## with that `conn` value. ## ## For example, a valid database object implementation that opens a new ## connection for every request might look like this: @@ -587,23 +636,47 @@ template findViaJoinTable*[D: DbConnType]( macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untyped = ## Generate all standard access procedures for the given model types. For a - ## `model class`_ named `TodoItem`, this will generate the following - ## procedures: + ## `model class`_ named `TodoItem`, this will generate `dbType` and + ## connection overloads for procedures like the following: ## ## .. code-block:: Nim ## proc getTodoItem*(db: TodoDB, id: idType): TodoItem; - ## proc getAllTodoItems*(db: TodoDB): TodoItem; + ## proc getTodoItem*[D: DbConnType](conn: D, 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]; + ## proc getTodoItemIfItExists*[D: DbConnType]( + ## conn: D, id: idType): Option[TodoItem]; + ## proc getAllTodoItems*(db: TodoDB): PagedRecords[TodoItem]; + ## proc getAllTodoItems*[D: DbConnType](conn: D): PagedRecords[TodoItem]; ## proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; + ## proc createTodoItem*[D: DbConnType](conn: D, rec: TodoItem): TodoItem; ## proc deleteTodoItem*(db: TodoDB, rec: TodoItem): bool; + ## proc deleteTodoItem*[D: DbConnType](conn: D, rec: TodoItem): bool; ## proc deleteTodoItem*(db: TodoDB, id: idType): bool; + ## proc deleteTodoItem*[D: DbConnType](conn: D, id: idType): bool; ## proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool; - ## proc createOrUpdateTodoItem*(db: TodoDB, rec: TodoItem): bool; + ## proc updateTodoItem*[D: DbConnType](conn: D, rec: TodoItem): bool; + ## proc createOrUpdateTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; + ## proc createOrUpdateTodoItem*[D: DbConnType]( + ## conn: D, rec: TodoItem): TodoItem; ## ## proc findTodoItemsWhere*( - ## db: TodoDB, whereClause: string, values: varargs[string]): TodoItem; + ## db: TodoDB, + ## whereClause: string, + ## values: varargs[string, dbFormat], + ## pagination = none[PaginationParams]()): PagedRecords[TodoItem]; + ## proc findTodoItemsWhere*[D: DbConnType]( + ## conn: D, + ## whereClause: string, + ## values: varargs[string, dbFormat], + ## pagination = none[PaginationParams]()): PagedRecords[TodoItem]; ## ## `dbType` is expected to be some type that has a defined `withConnection` ## procedure (see `Database Object`_ for details). + ## 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. ## ## .. _Database Object: #database-object result = newStmtList() @@ -710,7 +783,14 @@ macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyp ## ## .. code-block:: Nim ## proc findTodoItemsByOwnerAndPriority*(db: SampleDB, - ## owner: string, priority: int): seq[TodoItem] + ## owner: string, priority: int, + ## pagination = none[PaginationParams]()): PagedRecords[TodoItem] + ## proc findTodoItemsByOwnerAndPriority*[D: DbConnType](conn: D, + ## owner: string, priority: int, + ## pagination = none[PaginationParams]()): PagedRecords[TodoItem] + ## + ## Use the `db` overload for standalone calls and the `conn` overload inside + ## `withConnection` or `inTransaction`. let fieldNames = fields[1].mapIt($it) let procName = ident("find" & pluralize($modelType.getType[1]) & "By" & fieldNames.mapIt(it.capitalize).join("And")) var callParams = quote do: @[] @@ -798,11 +878,19 @@ macro generateJoinTableProcs*( ## This macro will generate the following procedures: ## ## .. code-block:: Nim - ## proc findTodoItemsByTimeEntry*(db: SampleDB, timeEntry: TimeEntry): seq[TodoItem] - ## proc findTimeEntriesByTodoItem*(db: SampleDB, todoItem: TodoItem): seq[TimeEntry] + ## proc getTodoItemsByTimeEntry*(db: SampleDB, timeEntry: TimeEntry, + ## pagination = none[PaginationParams]()): PagedRecords[TodoItem] + ## proc getTodoItemsByTimeEntry*[D: DbConnType](conn: D, timeEntry: TimeEntry, + ## pagination = none[PaginationParams]()): PagedRecords[TodoItem] + ## proc getTimeEntriesByTodoItem*(db: SampleDB, todoItem: TodoItem, + ## pagination = none[PaginationParams]()): PagedRecords[TimeEntry] + ## proc getTimeEntriesByTodoItem*[D: DbConnType](conn: D, todoItem: TodoItem, + ## pagination = none[PaginationParams]()): PagedRecords[TimeEntry] ## ## `dbType` is expected to be some type that has a defined `withConnection` ## procedure (see `Database Object`_ for details). + ## As with the other generated helpers, use the connection overloads when + ## you are already inside `withConnection` or `inTransaction`. ## ## .. _Database Object: #database-object result = newStmtList() @@ -944,6 +1032,13 @@ macro generateJoinTableProcs*( associate(conn, `joinTableNameNode`, rec1, rec2) template inTransaction*(db, body: untyped) = + ## Execute `body` inside a transaction using a single connection bound to + ## `conn`. + ## + ## When calling generated Fiber ORM helpers inside this block, use the + ## 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. db.withConnection conn: conn.exec(sql"BEGIN TRANSACTION") try: