diff --git a/README.rst b/README.rst index dcc6304..cb64fbc 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ Models may be defined as: .. code-block:: Nim # models.nim - import std/options, std/times + import std/[options, times] import uuids type @@ -82,6 +82,8 @@ 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 fiber_orm import ./models.nim @@ -102,6 +104,7 @@ This will generate the following procedures: .. code-block:: Nim proc getTodoItem*(db: TodoDB, id: UUID): TodoItem; + proc getTodoItemIfItExists*(db: TodoDB, id: UUID): Option[TodoItem]; proc getAllTodoItems*(db: TodoDB): seq[TodoItem]; proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem; proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool; @@ -112,6 +115,7 @@ This will generate the following procedures: values: varargs[string, dbFormat]): seq[TodoItem]; proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry; + proc getTimeEntryIfItExists*(db: TodoDB, id: UUID): Option[TimeEntry]; proc getAllTimeEntries*(db: TodoDB): seq[TimeEntry]; proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry; proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool; diff --git a/fiber_orm.nimble b/fiber_orm.nimble index 12e1612..0b15f2f 100644 --- a/fiber_orm.nimble +++ b/fiber_orm.nimble @@ -1,6 +1,6 @@ # Package -version = "2.2.0" +version = "3.0.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 70d6955..3e071eb 100644 --- a/src/fiber_orm.nim +++ b/src/fiber_orm.nim @@ -1,6 +1,6 @@ # Fiber ORM # -# Copyright 2019 Jonathan Bernard +# Copyright 2019-2024 Jonathan Bernard ## Lightweight ORM supporting the `Postgres`_ and `SQLite`_ databases in Nim. ## It supports a simple, opinionated model mapper to generate SQL queries based @@ -264,26 +264,28 @@ ## 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 a `conn: DbConn` object ## to the provided statement body. ## ## For example, a valid database object implementation that opens a new ## connection for every request might look like this: ## ## .. code-block:: Nim -## import std/db_postgres +## import db_connector/db_postgres ## ## type TodoDB* = object ## connString: string ## -## template withConn*(db: TodoDB, stmt: untyped): untyped = -## let conn {.inject.} = open("", "", "", db.connString) -## try: stmt -## finally: close(conn) +## template withConnection*(db: TodoDB, stmt: untyped): untyped = +## block: +## let conn = open("", "", "", db.connString) +## try: stmt +## finally: close(conn) ## ## .. _pool.DbConnPool: fiber_orm/pool.html#DbConnPool ## -import std/[db_common, logging, macros, options, sequtils, strutils] +import std/[logging, macros, options, sequtils, strutils] +import db_connector/db_common import namespaced_logging, uuids from std/unicode import capitalize @@ -306,7 +308,7 @@ type PaginationParams* = object pageSize*: int offset*: int - orderBy*: Option[string] + orderBy*: Option[seq[string]] PagedRecords*[T] = object pagination*: Option[PaginationParams] @@ -438,7 +440,8 @@ template findRecordsWhere*[D: DbConnType]( if page.isSome: let p = page.get if p.orderBy.isSome: - fetchStmt &= " ORDER BY " & identNameToDb(p.orderBy.get) + let orderByClause = page.orderBy.get.map(identNameToDb).join(",") + fetchStmt &= " ORDER BY " & orderByClause else: fetchStmt &= " ORDER BY id" @@ -469,7 +472,8 @@ template getAllRecords*[D: DbConnType]( if page.isSome: let p = page.get if p.orderBy.isSome: - fetchStmt &= " ORDER BY " & identNameToDb(p.orderBy.get) + let orderByClause = page.orderBy.get.map(identNameToDb).join(",") + fetchStmt &= " ORDER BY " & orderByClause else: fetchStmt &= " ORDER BY id" @@ -508,7 +512,8 @@ template findRecordsBy*[D: DbConnType]( if page.isSome: let p = page.get if p.orderBy.isSome: - fetchStmt &= " ORDER BY " & identNameToDb(p.orderBy.get) + let orderByClause = page.orderBy.get.map(identNameToDb).join(",") + fetchStmt &= " ORDER BY " & orderByClause else: fetchStmt &= " ORDER BY id" @@ -543,7 +548,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype ## proc findTodoItemsWhere*( ## db: TodoDB, whereClause: string, values: varargs[string]): TodoItem; ## - ## `dbType` is expected to be some type that has a defined `withConn` + ## `dbType` is expected to be some type that has a defined `withConnection` ## procedure (see `Database Object`_ for details). ## ## .. _Database Object: #database-object @@ -556,6 +561,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype let modelName = $(t.getType[1]) let getName = ident("get" & modelName) + let getIfExistsName = ident("get" & modelName & "IfItExists") let getAllName = ident("getAll" & pluralize(modelName)) let findWhereName = ident("find" & pluralize(modelName) & "Where") let createName = ident("create" & modelName) @@ -565,33 +571,38 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype let idType = typeOfColumn(t, "id") result.add quote do: proc `getName`*(db: `dbType`, id: `idType`): `t` = - db.withConn: result = getRecord(conn, `t`, id) + db.withConnection conn: result = getRecord(conn, `t`, id) + + proc `getIfExistsName`*(db: `dbType`, id: `idType`): Option[`t`] = + db.withConnection conn: + try: result = some(getRecord(conn, `t`, id)) + except NotFoundError: result = none[`t`]() proc `getAllName`*(db: `dbType`, pagination = none[PaginationParams]()): PagedRecords[`t`] = - db.withConn: result = getAllRecords(conn, `t`, pagination) + db.withConnection conn: result = getAllRecords(conn, `t`, pagination) proc `findWhereName`*( db: `dbType`, whereClause: string, values: varargs[string, dbFormat], pagination = none[PaginationParams]()): PagedRecords[`t`] = - db.withConn: + db.withConnection conn: result = findRecordsWhere(conn, `t`, whereClause, values, pagination) proc `createName`*(db: `dbType`, rec: `t`): `t` = - db.withConn: result = createRecord(conn, rec) + db.withConnection conn: result = createRecord(conn, rec) proc `updateName`*(db: `dbType`, rec: `t`): bool = - db.withConn: result = updateRecord(conn, rec) + db.withConnection conn: result = updateRecord(conn, rec) proc `createOrUpdateName`*(db: `dbType`, rec: `t`): `t` = db.inTransaction: result = createOrUpdateRecord(conn, rec) proc `deleteName`*(db: `dbType`, rec: `t`): bool = - db.withConn: result = deleteRecord(conn, rec) + db.withConnection conn: result = deleteRecord(conn, rec) proc `deleteName`*(db: `dbType`, id: `idType`): bool = - db.withConn: result = deleteRecord(conn, `t`, id) + db.withConnection conn: result = deleteRecord(conn, `t`, id) macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyped = ## Create a lookup procedure for a given set of field names. For example, @@ -611,7 +622,7 @@ macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyp # Create proc skeleton result = quote do: proc `procName`*(db: `dbType`): PagedRecords[`modelType`] = - db.withConn: result = findRecordsBy(conn, `modelType`) + db.withConnection conn: result = findRecordsBy(conn, `modelType`) var callParams = quote do: @[] @@ -635,11 +646,11 @@ macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyp # Add the call params to the inner procedure call # result[6][0][1][0][1] is - # ProcDef -> [6]: StmtList (body) -> [0]: Call -> - # [1]: StmtList (withConn body) -> [0]: Asgn (result =) -> + # ProcDef -> [6]: StmtList (body) -> [0]: Command -> + # [2]: StmtList (withConnection body) -> [0]: Asgn (result =) -> # [1]: Call (inner findRecords invocation) - result[6][0][1][0][1].add(callParams) - result[6][0][1][0][1].add(quote do: pagination) + result[6][0][2][0][1].add(callParams) + result[6][0][2][0][1].add(quote do: pagination) macro generateProcsForFieldLookups*(dbType: type, modelsAndFields: openarray[tuple[t: type, fields: seq[string]]]): untyped = result = newStmtList() @@ -653,7 +664,7 @@ macro generateProcsForFieldLookups*(dbType: type, modelsAndFields: openarray[tup # Create proc skeleton let procDefAST = quote do: proc `procName`*(db: `dbType`): PagedRecords[`modelType`] = - db.withConn: result = findRecordsBy(conn, `modelType`) + db.withConnection conn: result = findRecordsBy(conn, `modelType`) var callParams = quote do: @[] @@ -705,8 +716,8 @@ proc initPool*[D: DbConnType]( hardCap: hardCap, healthCheckQuery: healthCheckQuery)) -template inTransaction*[D: DbConnType](db: DbConnPool[D], body: untyped) = - pool.withConn(db): +template inTransaction*(db, body: untyped) = + db.withConnection conn: conn.exec(sql"BEGIN TRANSACTION") try: body diff --git a/src/fiber_orm/db_common.nim b/src/fiber_orm/db_common.nim index 3736ea8..bc88a0b 100644 --- a/src/fiber_orm/db_common.nim +++ b/src/fiber_orm/db_common.nim @@ -1,3 +1,3 @@ -import std/[db_postgres, db_sqlite] +import db_connector/[db_postgres, db_sqlite] type DbConnType* = db_postgres.DbConn or db_sqlite.DbConn diff --git a/src/fiber_orm/pool.nim b/src/fiber_orm/pool.nim index 60b59bd..a8b46aa 100644 --- a/src/fiber_orm/pool.nim +++ b/src/fiber_orm/pool.nim @@ -4,10 +4,11 @@ ## Simple database connection pooling implementation compatible with Fiber ORM. -import std/[db_common, logging, sequtils, strutils, sugar] +import std/[logging, sequtils, strutils, sugar] +import db_connector/db_common -from std/db_sqlite import getRow, close -from std/db_postgres import getRow, close +from db_connector/db_sqlite import getRow, close +from db_connector/db_postgres import getRow, close import namespaced_logging import ./db_common as fiber_db_common @@ -58,12 +59,13 @@ proc initDbConnPool*[D: DbConnType](cfg: DbConnPoolConfig[D]): DbConnPool[D] = proc newConn[D: DbConnType](pool: DbConnPool[D]): PooledDbConn[D] = log().debug("Creating a new connection to add to the pool.") pool.lastId += 1 - let conn = pool.cfg.connect() - result = PooledDbConn[D]( - conn: conn, - id: pool.lastId, - free: true) - pool.conns.add(result) + {.gcsafe.}: + let conn = pool.cfg.connect() + result = PooledDbConn[D]( + conn: conn, + id: pool.lastId, + free: true) + pool.conns.add(result) proc maintain[D: DbConnType](pool: DbConnPool[D]): void = log().debug("Maintaining pool. $# connections." % [$pool.conns.len]) @@ -124,12 +126,10 @@ proc release*[D: DbConnType](pool: DbConnPool[D], connId: int): void = let foundConn = pool.conns.filterIt(it.id == connId) if foundConn.len > 0: foundConn[0].free = true -template withConn*[D: DbConnType](pool: DbConnPool[D], stmt: untyped): untyped = +template withConnection*[D: DbConnType](pool: DbConnPool[D], conn, stmt: untyped): untyped = ## Convenience template to provide a connection from the pool for use in a ## statement block, automatically releasing that connnection when done. - ## - ## The provided connection is injected as the variable `conn` in the - ## statement body. - let (connId, conn {.inject.}) = take(pool) - try: stmt - finally: release(pool, connId) + block: + let (connId, conn) = take(pool) + try: stmt + finally: release(pool, connId)