Support Nim 2.x, compatibility with waterpark.
- Nim 2.x has moved the DB connectors outside the standard library to the `db_connector` package. - Refactor the pooling implementation and macro expectations to use the `withConnection` name instead of `withConn`. This change allows a caller to use a [waterpark](https://github.com/guzba/waterpark) pool instance instead of the builtin pool instance. Waterpark provides better support for multi-threaded environments. The builtin pooling mechanism may be deprecated in favor of waterpark in the future. - Add the `getModelIfItExists` generated proc to the list of standard procs we generate. This is a flavour of `getModel` that returns an `Option` instead of raising an exception when there is no model for the given id. - Change `PaginationParams#orderBy` to accept a `seq[string]` to allow for sorting on multiple fields.
This commit is contained in:
parent
fb74d84cb7
commit
8f90f91736
@ -57,7 +57,7 @@ Models may be defined as:
|
|||||||
|
|
||||||
.. code-block:: Nim
|
.. code-block:: Nim
|
||||||
# models.nim
|
# models.nim
|
||||||
import std/options, std/times
|
import std/[options, times]
|
||||||
import uuids
|
import uuids
|
||||||
|
|
||||||
type
|
type
|
||||||
@ -82,6 +82,8 @@ Using Fiber ORM we can generate a data access layer with:
|
|||||||
|
|
||||||
.. code-block:: Nim
|
.. code-block:: Nim
|
||||||
# db.nim
|
# db.nim
|
||||||
|
import std/[options]
|
||||||
|
import db_connectors/db_postgres
|
||||||
import fiber_orm
|
import fiber_orm
|
||||||
import ./models.nim
|
import ./models.nim
|
||||||
|
|
||||||
@ -102,6 +104,7 @@ This will generate the following procedures:
|
|||||||
|
|
||||||
.. code-block:: Nim
|
.. code-block:: Nim
|
||||||
proc getTodoItem*(db: TodoDB, id: UUID): TodoItem;
|
proc getTodoItem*(db: TodoDB, id: UUID): TodoItem;
|
||||||
|
proc getTodoItemIfItExists*(db: TodoDB, id: UUID): Option[TodoItem];
|
||||||
proc getAllTodoItems*(db: TodoDB): seq[TodoItem];
|
proc getAllTodoItems*(db: TodoDB): seq[TodoItem];
|
||||||
proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem;
|
proc createTodoItem*(db: TodoDB, rec: TodoItem): TodoItem;
|
||||||
proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool;
|
proc updateTodoItem*(db: TodoDB, rec: TodoItem): bool;
|
||||||
@ -112,6 +115,7 @@ This will generate the following procedures:
|
|||||||
values: varargs[string, dbFormat]): seq[TodoItem];
|
values: varargs[string, dbFormat]): seq[TodoItem];
|
||||||
|
|
||||||
proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry;
|
proc getTimeEntry*(db: TodoDB, id: UUID): TimeEntry;
|
||||||
|
proc getTimeEntryIfItExists*(db: TodoDB, id: UUID): Option[TimeEntry];
|
||||||
proc getAllTimeEntries*(db: TodoDB): seq[TimeEntry];
|
proc getAllTimeEntries*(db: TodoDB): seq[TimeEntry];
|
||||||
proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry;
|
proc createTimeEntry*(db: TodoDB, rec: TimeEntry): TimeEntry;
|
||||||
proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool;
|
proc updateTimeEntry*(db: TodoDB, rec: TimeEntry): bool;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "2.2.0"
|
version = "3.0.0"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Lightweight Postgres ORM for Nim."
|
description = "Lightweight Postgres ORM for Nim."
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Fiber ORM
|
# Fiber ORM
|
||||||
#
|
#
|
||||||
# Copyright 2019 Jonathan Bernard <jonathan@jdbernard.com>
|
# Copyright 2019-2024 Jonathan Bernard <jonathan@jdbernard.com>
|
||||||
|
|
||||||
## Lightweight ORM supporting the `Postgres`_ and `SQLite`_ databases in Nim.
|
## Lightweight ORM supporting the `Postgres`_ and `SQLite`_ databases in Nim.
|
||||||
## It supports a simple, opinionated model mapper to generate SQL queries based
|
## 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
|
## In the example above the `pool.DbConnPool`_ object is used as database
|
||||||
## object type (aliased as `TodoDB`). This is the intended usage pattern, but
|
## 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
|
## 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.
|
## to the provided statement body.
|
||||||
##
|
##
|
||||||
## For example, a valid database object implementation that opens a new
|
## For example, a valid database object implementation that opens a new
|
||||||
## connection for every request might look like this:
|
## connection for every request might look like this:
|
||||||
##
|
##
|
||||||
## .. code-block:: Nim
|
## .. code-block:: Nim
|
||||||
## import std/db_postgres
|
## import db_connector/db_postgres
|
||||||
##
|
##
|
||||||
## type TodoDB* = object
|
## type TodoDB* = object
|
||||||
## connString: string
|
## connString: string
|
||||||
##
|
##
|
||||||
## template withConn*(db: TodoDB, stmt: untyped): untyped =
|
## template withConnection*(db: TodoDB, stmt: untyped): untyped =
|
||||||
## let conn {.inject.} = open("", "", "", db.connString)
|
## block:
|
||||||
## try: stmt
|
## let conn = open("", "", "", db.connString)
|
||||||
## finally: close(conn)
|
## try: stmt
|
||||||
|
## finally: close(conn)
|
||||||
##
|
##
|
||||||
## .. _pool.DbConnPool: fiber_orm/pool.html#DbConnPool
|
## .. _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
|
import namespaced_logging, uuids
|
||||||
|
|
||||||
from std/unicode import capitalize
|
from std/unicode import capitalize
|
||||||
@ -306,7 +308,7 @@ type
|
|||||||
PaginationParams* = object
|
PaginationParams* = object
|
||||||
pageSize*: int
|
pageSize*: int
|
||||||
offset*: int
|
offset*: int
|
||||||
orderBy*: Option[string]
|
orderBy*: Option[seq[string]]
|
||||||
|
|
||||||
PagedRecords*[T] = object
|
PagedRecords*[T] = object
|
||||||
pagination*: Option[PaginationParams]
|
pagination*: Option[PaginationParams]
|
||||||
@ -438,7 +440,8 @@ template findRecordsWhere*[D: DbConnType](
|
|||||||
if page.isSome:
|
if page.isSome:
|
||||||
let p = page.get
|
let p = page.get
|
||||||
if p.orderBy.isSome:
|
if p.orderBy.isSome:
|
||||||
fetchStmt &= " ORDER BY " & identNameToDb(p.orderBy.get)
|
let orderByClause = page.orderBy.get.map(identNameToDb).join(",")
|
||||||
|
fetchStmt &= " ORDER BY " & orderByClause
|
||||||
else:
|
else:
|
||||||
fetchStmt &= " ORDER BY id"
|
fetchStmt &= " ORDER BY id"
|
||||||
|
|
||||||
@ -469,7 +472,8 @@ template getAllRecords*[D: DbConnType](
|
|||||||
if page.isSome:
|
if page.isSome:
|
||||||
let p = page.get
|
let p = page.get
|
||||||
if p.orderBy.isSome:
|
if p.orderBy.isSome:
|
||||||
fetchStmt &= " ORDER BY " & identNameToDb(p.orderBy.get)
|
let orderByClause = page.orderBy.get.map(identNameToDb).join(",")
|
||||||
|
fetchStmt &= " ORDER BY " & orderByClause
|
||||||
else:
|
else:
|
||||||
fetchStmt &= " ORDER BY id"
|
fetchStmt &= " ORDER BY id"
|
||||||
|
|
||||||
@ -508,7 +512,8 @@ template findRecordsBy*[D: DbConnType](
|
|||||||
if page.isSome:
|
if page.isSome:
|
||||||
let p = page.get
|
let p = page.get
|
||||||
if p.orderBy.isSome:
|
if p.orderBy.isSome:
|
||||||
fetchStmt &= " ORDER BY " & identNameToDb(p.orderBy.get)
|
let orderByClause = page.orderBy.get.map(identNameToDb).join(",")
|
||||||
|
fetchStmt &= " ORDER BY " & orderByClause
|
||||||
else:
|
else:
|
||||||
fetchStmt &= " ORDER BY id"
|
fetchStmt &= " ORDER BY id"
|
||||||
|
|
||||||
@ -543,7 +548,7 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
|
|||||||
## proc findTodoItemsWhere*(
|
## proc findTodoItemsWhere*(
|
||||||
## db: TodoDB, whereClause: string, values: varargs[string]): TodoItem;
|
## 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).
|
## procedure (see `Database Object`_ for details).
|
||||||
##
|
##
|
||||||
## .. _Database Object: #database-object
|
## .. _Database Object: #database-object
|
||||||
@ -556,6 +561,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 getIfExistsName = ident("get" & modelName & "IfItExists")
|
||||||
let getAllName = ident("getAll" & pluralize(modelName))
|
let getAllName = ident("getAll" & pluralize(modelName))
|
||||||
let findWhereName = ident("find" & pluralize(modelName) & "Where")
|
let findWhereName = ident("find" & pluralize(modelName) & "Where")
|
||||||
let createName = ident("create" & modelName)
|
let createName = ident("create" & modelName)
|
||||||
@ -565,33 +571,38 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
|
|||||||
let idType = typeOfColumn(t, "id")
|
let idType = typeOfColumn(t, "id")
|
||||||
result.add quote do:
|
result.add quote do:
|
||||||
proc `getName`*(db: `dbType`, id: `idType`): `t` =
|
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`] =
|
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`*(
|
proc `findWhereName`*(
|
||||||
db: `dbType`,
|
db: `dbType`,
|
||||||
whereClause: string,
|
whereClause: string,
|
||||||
values: varargs[string, dbFormat],
|
values: varargs[string, dbFormat],
|
||||||
pagination = none[PaginationParams]()): PagedRecords[`t`] =
|
pagination = none[PaginationParams]()): PagedRecords[`t`] =
|
||||||
db.withConn:
|
db.withConnection conn:
|
||||||
result = findRecordsWhere(conn, `t`, whereClause, values, pagination)
|
result = findRecordsWhere(conn, `t`, whereClause, values, pagination)
|
||||||
|
|
||||||
proc `createName`*(db: `dbType`, rec: `t`): `t` =
|
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 =
|
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` =
|
proc `createOrUpdateName`*(db: `dbType`, rec: `t`): `t` =
|
||||||
db.inTransaction: result = createOrUpdateRecord(conn, rec)
|
db.inTransaction: result = createOrUpdateRecord(conn, rec)
|
||||||
|
|
||||||
proc `deleteName`*(db: `dbType`, rec: `t`): bool =
|
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 =
|
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 =
|
macro generateLookup*(dbType: type, modelType: type, fields: seq[string]): untyped =
|
||||||
## Create a lookup procedure for a given set of field names. For example,
|
## 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
|
# Create proc skeleton
|
||||||
result = quote do:
|
result = quote do:
|
||||||
proc `procName`*(db: `dbType`): PagedRecords[`modelType`] =
|
proc `procName`*(db: `dbType`): PagedRecords[`modelType`] =
|
||||||
db.withConn: result = findRecordsBy(conn, `modelType`)
|
db.withConnection conn: result = findRecordsBy(conn, `modelType`)
|
||||||
|
|
||||||
var callParams = quote do: @[]
|
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
|
# Add the call params to the inner procedure call
|
||||||
# result[6][0][1][0][1] is
|
# result[6][0][1][0][1] is
|
||||||
# ProcDef -> [6]: StmtList (body) -> [0]: Call ->
|
# ProcDef -> [6]: StmtList (body) -> [0]: Command ->
|
||||||
# [1]: StmtList (withConn body) -> [0]: Asgn (result =) ->
|
# [2]: StmtList (withConnection body) -> [0]: Asgn (result =) ->
|
||||||
# [1]: Call (inner findRecords invocation)
|
# [1]: Call (inner findRecords invocation)
|
||||||
result[6][0][1][0][1].add(callParams)
|
result[6][0][2][0][1].add(callParams)
|
||||||
result[6][0][1][0][1].add(quote do: pagination)
|
result[6][0][2][0][1].add(quote do: pagination)
|
||||||
|
|
||||||
macro generateProcsForFieldLookups*(dbType: type, modelsAndFields: openarray[tuple[t: type, fields: seq[string]]]): untyped =
|
macro generateProcsForFieldLookups*(dbType: type, modelsAndFields: openarray[tuple[t: type, fields: seq[string]]]): untyped =
|
||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
@ -653,7 +664,7 @@ macro generateProcsForFieldLookups*(dbType: type, modelsAndFields: openarray[tup
|
|||||||
# Create proc skeleton
|
# Create proc skeleton
|
||||||
let procDefAST = quote do:
|
let procDefAST = quote do:
|
||||||
proc `procName`*(db: `dbType`): PagedRecords[`modelType`] =
|
proc `procName`*(db: `dbType`): PagedRecords[`modelType`] =
|
||||||
db.withConn: result = findRecordsBy(conn, `modelType`)
|
db.withConnection conn: result = findRecordsBy(conn, `modelType`)
|
||||||
|
|
||||||
var callParams = quote do: @[]
|
var callParams = quote do: @[]
|
||||||
|
|
||||||
@ -705,8 +716,8 @@ proc initPool*[D: DbConnType](
|
|||||||
hardCap: hardCap,
|
hardCap: hardCap,
|
||||||
healthCheckQuery: healthCheckQuery))
|
healthCheckQuery: healthCheckQuery))
|
||||||
|
|
||||||
template inTransaction*[D: DbConnType](db: DbConnPool[D], body: untyped) =
|
template inTransaction*(db, body: untyped) =
|
||||||
pool.withConn(db):
|
db.withConnection conn:
|
||||||
conn.exec(sql"BEGIN TRANSACTION")
|
conn.exec(sql"BEGIN TRANSACTION")
|
||||||
try:
|
try:
|
||||||
body
|
body
|
||||||
|
@ -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
|
type DbConnType* = db_postgres.DbConn or db_sqlite.DbConn
|
||||||
|
@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
## Simple database connection pooling implementation compatible with Fiber ORM.
|
## 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 db_connector/db_sqlite import getRow, close
|
||||||
from std/db_postgres import getRow, close
|
from db_connector/db_postgres import getRow, close
|
||||||
|
|
||||||
import namespaced_logging
|
import namespaced_logging
|
||||||
import ./db_common as fiber_db_common
|
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] =
|
proc newConn[D: DbConnType](pool: DbConnPool[D]): PooledDbConn[D] =
|
||||||
log().debug("Creating a new connection to add to the pool.")
|
log().debug("Creating a new connection to add to the pool.")
|
||||||
pool.lastId += 1
|
pool.lastId += 1
|
||||||
let conn = pool.cfg.connect()
|
{.gcsafe.}:
|
||||||
result = PooledDbConn[D](
|
let conn = pool.cfg.connect()
|
||||||
conn: conn,
|
result = PooledDbConn[D](
|
||||||
id: pool.lastId,
|
conn: conn,
|
||||||
free: true)
|
id: pool.lastId,
|
||||||
pool.conns.add(result)
|
free: true)
|
||||||
|
pool.conns.add(result)
|
||||||
|
|
||||||
proc maintain[D: DbConnType](pool: DbConnPool[D]): void =
|
proc maintain[D: DbConnType](pool: DbConnPool[D]): void =
|
||||||
log().debug("Maintaining pool. $# connections." % [$pool.conns.len])
|
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)
|
let foundConn = pool.conns.filterIt(it.id == connId)
|
||||||
if foundConn.len > 0: foundConn[0].free = true
|
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
|
## Convenience template to provide a connection from the pool for use in a
|
||||||
## statement block, automatically releasing that connnection when done.
|
## statement block, automatically releasing that connnection when done.
|
||||||
##
|
block:
|
||||||
## The provided connection is injected as the variable `conn` in the
|
let (connId, conn) = take(pool)
|
||||||
## statement body.
|
try: stmt
|
||||||
let (connId, conn {.inject.}) = take(pool)
|
finally: release(pool, connId)
|
||||||
try: stmt
|
|
||||||
finally: release(pool, connId)
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user