Initial stab at better documentation.

This commit is contained in:
2022-09-02 23:25:52 -05:00
parent 1d7c955805
commit 9bf3c4f3ec
6 changed files with 223 additions and 9 deletions

View File

@ -1,3 +1,9 @@
# Fiber ORM
#
# Copyright 2019 Jonathan Bernard <jonathan@jdbernard.com>
## Simple database connection pooling implementation compatible with Fiber ORM.
import std/db_postgres, std/sequtils, std/strutils, std/sugar
import namespaced_logging
@ -5,10 +11,21 @@ import namespaced_logging
type
DbConnPoolConfig* = object
connect*: () -> DbConn
poolSize*: int
hardCap*: bool
healthCheckQuery*: string
connect*: () -> DbConn ## Factory procedure to create a new DBConn
poolSize*: int ## The pool capacity.
hardCap*: bool ## Is the pool capacity a hard cap?
##
## When `false`, the pool can grow beyond the configured
## capacity, but will release connections down to the its
## capacity (no less than `poolSize`).
##
## When `true` the pool will not create more than its
## configured capacity. It a connection is requested, none
## are free, and the pool is at capacity, this will result
## in an Error being raised.
healthCheckQuery*: string ## Should be a simple and fast SQL query that the
## pool can use to test the liveliness of pooled
## connections.
PooledDbConn = ref object
conn: DbConn
@ -16,6 +33,7 @@ type
free: bool
DbConnPool* = ref object
## Database connection pool
conns: seq[PooledDbConn]
cfg: DbConnPoolConfig
lastId: int
@ -74,6 +92,13 @@ proc maintain(pool: DbConnPool): void =
[$toCull.len, $pool.conns.len])
proc take*(pool: DbConnPool): tuple[id: int, conn: DbConn] =
## Request a connection from the pool. Returns a DbConn if the pool has free
## connections, or if it has the capacity to create a new connection. If the
## pool is configured with a hard capacity limit and is out of free
## connections, this will raise an Error.
##
## Connections taken must be returned via `release` when the caller is
## finished using them in order for them to be released back to the pool.
pool.maintain
let freeConns = pool.conns.filterIt(it.free)
@ -89,11 +114,17 @@ proc take*(pool: DbConnPool): tuple[id: int, conn: DbConn] =
return (id: reserved.id, conn: reserved.conn)
proc release*(pool: DbConnPool, connId: int): void =
## Release a connection back to the pool.
log().debug("Reclaiming released connaction $#" % [$connId])
let foundConn = pool.conns.filterIt(it.id == connId)
if foundConn.len > 0: foundConn[0].free = true
template withConn*(pool: DbConnPool, 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)

View File

@ -1,3 +1,8 @@
# Fiber ORM
#
# Copyright 2019 Jonathan Bernard <jonathan@jdbernard.com>
## Utility methods used internally by Fiber ORM.
import json, macros, options, sequtils, strutils, times, timeutils, unicode,
uuids
@ -5,6 +10,10 @@ import nre except toSeq
type
MutateClauses* = object
## Data structure to hold information about the clauses that should be
## added to a query. How these clauses are used will depend on the query.
## This common data structure provides the information needed to create
## WHERE clauses, UPDATE clauses, etc.
columns*: seq[string]
placeholders*: seq[string]
values*: seq[string]
@ -12,18 +21,23 @@ type
# TODO: more complete implementation
# see https://github.com/blakeembrey/pluralize
proc pluralize*(name: string): string =
## Return the plural form of the given name.
if name[^2..^1] == "ey": return name[0..^3] & "ies"
if name[^1] == 'y': return name[0..^2] & "ies"
return name & "s"
macro modelName*(model: object): string =
## For a given concrete record object, return the name of the `model class`_
return newStrLitNode($model.getTypeInst)
macro modelName*(modelType: type): string =
## Get the name of a given `model class`_
return newStrLitNode($modelType.getType[1])
proc identNameToDb*(name: string): string =
## Map a Nim identifier name to a DB name. See the `rules for name mapping`_
##
## TODO link above
const UNDERSCORE_RUNE = "_".toRunes[0]
let nameInRunes = name.toRunes
var prev: Rune
@ -42,28 +56,40 @@ proc identNameToDb*(name: string): string =
return $resultRunes
proc dbNameToIdent*(name: string): string =
## Map a DB name to a Nim identifier name. See the `rules for name mapping`_
let parts = name.split("_")
return @[parts[0]].concat(parts[1..^1].mapIt(capitalize(it))).join("")
proc tableName*(modelType: type): string =
## Get the `table name`_ for a given `model class`_
return pluralize(modelName(modelType).identNameToDb)
proc tableName*[T](rec: T): string =
## Get the `table name`_ for a given record.
return pluralize(modelName(rec).identNameToDb)
proc dbFormat*(s: string): string = return s
proc dbFormat*(s: string): string =
## Format a string for inclusion in a SQL Query.
return s
proc dbFormat*(dt: DateTime): string = return dt.formatIso8601
proc dbFormat*(dt: DateTime): string =
## Format a DateTime for inclusion in a SQL Query.
return dt.formatIso8601
proc dbFormat*[T](list: seq[T]): string =
## Format a `seq` for inclusion in a SQL Query.
return "{" & list.mapIt(dbFormat(it)).join(",") & "}"
proc dbFormat*[T](item: T): string = return $item
proc dbFormat*[T](item: T): string =
## For all other types, fall back on a defined `$` function to create a
## string version of the value we can include in an SQL query>
return $item
type DbArrayParseState = enum
expectStart, inQuote, inVal, expectEnd
proc parsePGDatetime*(val: string): DateTime =
## Parse a Postgres datetime value into a Nim DateTime object.
const PG_TIMESTAMP_FORMATS = [
"yyyy-MM-dd HH:mm:ss",
@ -103,6 +129,7 @@ proc parsePGDatetime*(val: string): DateTime =
raise newException(ValueError, "Cannot parse PG date. Tried:" & errStr)
proc parseDbArray*(val: string): seq[string] =
## Parse a Postgres array column into a Nim seq[string]
result = newSeq[string]()
var parseState = DbArrayParseState.expectStart
@ -161,6 +188,9 @@ proc parseDbArray*(val: string): seq[string] =
result.add(curStr)
proc createParseStmt*(t, value: NimNode): NimNode =
## Utility method to create the Nim cod required to parse a value coming from
## the a database query. This is used by functions like `rowToModel` to parse
## the dataabase columns into the Nim object fields.
#echo "Creating parse statment for ", t.treeRepr
if t.typeKind == ntyObject:
@ -219,6 +249,9 @@ proc createParseStmt*(t, value: NimNode): NimNode =
error "Unknown value type: " & $t.typeKind
template walkFieldDefs*(t: NimNode, body: untyped) =
## Iterate over every field of the given Nim object, yielding and defining
## `fieldIdent` and `fieldType`, the name of the field as a Nim Ident node
## and the type of the field as a Nim Type node respectively.
let tTypeImpl = t.getTypeImpl
var nodeToItr: NimNode
@ -239,6 +272,8 @@ template walkFieldDefs*(t: NimNode, body: untyped) =
body
macro columnNamesForModel*(modelType: typed): seq[string] =
## Return the column names corresponding to the the fields of the given
## `model class`_
var columnNames = newSeq[string]()
modelType.walkFieldDefs:
@ -247,6 +282,8 @@ macro columnNamesForModel*(modelType: typed): seq[string] =
result = newLit(columnNames)
macro rowToModel*(modelType: typed, row: seq[string]): untyped =
## Return a new Nim model object of type `modelType` populated with the
## values returned in the given database `row`
# Create the object constructor AST node
result = newNimNode(nnkObjConstr).add(modelType)
@ -269,6 +306,7 @@ macro listFields*(t: typed): untyped =
result = newLit(fields)
proc typeOfColumn*(modelType: NimNode, colName: string): NimNode =
## Given a model type and a column name, return the Nim type for that column.
modelType.walkFieldDefs:
if $fieldIdent != colName: continue
@ -289,6 +327,8 @@ proc isEmpty(val: UUID): bool = return val.isZero
proc isEmpty(val: string): bool = return val.isEmptyOrWhitespace
macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses): untyped =
## Given a record type, create the datastructure used to generate SQL clauses
## for the fields of this record type.
result = newStmtList()
@ -326,3 +366,7 @@ macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses):
`mc`.columns.add(identNameToDb(`fieldName`))
`mc`.placeholders.add("?")
`mc`.values.add(dbFormat(`t`.`fieldIdent`))
## .. _model class: ../fiber_orm.html#objectminusrelational-modeling-model-class
## .. _rules for name mapping: ../fiber_orm.html
## .. _table name: ../fiber_orm.html