Initial stab at better documentation.
This commit is contained in:
@ -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)
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user