Initial stab at better documentation.
This commit is contained in:
parent
1d7c955805
commit
9bf3c4f3ec
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
*.sw?
|
*.sw?
|
||||||
nimcache/
|
nimcache/
|
||||||
|
htmdocs/
|
||||||
|
7
Makefile
Normal file
7
Makefile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
SOURCES=$(shell find src -type f)
|
||||||
|
|
||||||
|
doc: $(shell find src -type f)
|
||||||
|
nim doc --project --index:on --git.url:https://github.com/jdbernard/fiber-orm --outdir:htmdocs src/fiber_orm
|
||||||
|
nim rst2html --outdir:htmdocs README.rst
|
||||||
|
|
||||||
|
.PHONY: doc
|
33
README.rst
Normal file
33
README.rst
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
Fiber ORM
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
Lightweight ORM supporting the `Postgres`_ and `SQLite`_ databases in Nim.
|
||||||
|
It supports a simple, opinionated model mapper to generate SQL queries based
|
||||||
|
on Nim objects. It also includes a simple connection pooling implementation.
|
||||||
|
|
||||||
|
.. _Postgres: https://nim-lang.org/docs/db_postgres.html
|
||||||
|
.. _SQLite: https://nim-lang.org/docs/db_sqlite.html
|
||||||
|
|
||||||
|
Basic Usage
|
||||||
|
===========
|
||||||
|
|
||||||
|
Object-Relational Modeling
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Model Class
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Table Name
|
||||||
|
``````````
|
||||||
|
|
||||||
|
Column Names
|
||||||
|
````````````
|
||||||
|
|
||||||
|
ID Field
|
||||||
|
````````
|
||||||
|
|
||||||
|
Supported Data Types
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Database Object
|
||||||
|
===============
|
@ -1,3 +1,38 @@
|
|||||||
|
# Fiber ORM
|
||||||
|
#
|
||||||
|
# Copyright 2019 Jonathan Bernard <jonathan@jdbernard.com>
|
||||||
|
|
||||||
|
## Lightweight ORM supporting the `Postgres`_ and `SQLite`_ databases in Nim.
|
||||||
|
## It supports a simple, opinionated model mapper to generate SQL queries based
|
||||||
|
## on Nim objects. It also includes a simple connection pooling implementation.
|
||||||
|
##
|
||||||
|
## .. _Postgres: https://nim-lang.org/docs/db_postgres.html
|
||||||
|
## .. _SQLite: https://nim-lang.org/docs/db_sqlite.html
|
||||||
|
##
|
||||||
|
## Basic Usage
|
||||||
|
## ===========
|
||||||
|
##
|
||||||
|
## Object-Relational Modeling
|
||||||
|
## ==========================
|
||||||
|
##
|
||||||
|
## Model Class
|
||||||
|
## -----------
|
||||||
|
##
|
||||||
|
## Table Name
|
||||||
|
## ``````````
|
||||||
|
##
|
||||||
|
## Column Names
|
||||||
|
## ````````````
|
||||||
|
##
|
||||||
|
## ID Field
|
||||||
|
## ````````
|
||||||
|
##
|
||||||
|
## Supported Data Types
|
||||||
|
## --------------------
|
||||||
|
##
|
||||||
|
## Database Object
|
||||||
|
## ===============
|
||||||
|
|
||||||
import std/db_postgres, std/macros, std/options, std/sequtils, std/strutils
|
import std/db_postgres, std/macros, std/options, std/sequtils, std/strutils
|
||||||
import namespaced_logging, uuids
|
import namespaced_logging, uuids
|
||||||
|
|
||||||
@ -16,7 +51,8 @@ export
|
|||||||
util.rowToModel,
|
util.rowToModel,
|
||||||
util.tableName
|
util.tableName
|
||||||
|
|
||||||
type NotFoundError* = object of CatchableError
|
type NotFoundError* = object of CatchableError ##\
|
||||||
|
## Error type raised when no record matches a given ID
|
||||||
|
|
||||||
var logNs {.threadvar.}: LoggingNamespace
|
var logNs {.threadvar.}: LoggingNamespace
|
||||||
|
|
||||||
@ -31,6 +67,15 @@ proc newMutateClauses(): MutateClauses =
|
|||||||
values: @[])
|
values: @[])
|
||||||
|
|
||||||
proc createRecord*[T](db: DbConn, rec: T): T =
|
proc createRecord*[T](db: DbConn, rec: T): T =
|
||||||
|
## Create a new record. `rec` is expected to be a `model class`_. The `id
|
||||||
|
## field`_ is only set if it is `non-empty`_
|
||||||
|
##
|
||||||
|
## Returns the newly created record.
|
||||||
|
##
|
||||||
|
## .. _model class: #objectminusrelational-modeling-model-class
|
||||||
|
## .. _id field: #model-class-id-field
|
||||||
|
## .. _non-empty:
|
||||||
|
|
||||||
var mc = newMutateClauses()
|
var mc = newMutateClauses()
|
||||||
populateMutateClauses(rec, true, mc)
|
populateMutateClauses(rec, true, mc)
|
||||||
|
|
||||||
@ -46,6 +91,9 @@ proc createRecord*[T](db: DbConn, rec: T): T =
|
|||||||
result = rowToModel(T, newRow)
|
result = rowToModel(T, newRow)
|
||||||
|
|
||||||
proc updateRecord*[T](db: DbConn, rec: T): bool =
|
proc updateRecord*[T](db: DbConn, rec: T): bool =
|
||||||
|
## Update a record by id. `rec` is expected to be a `model class`_.
|
||||||
|
##
|
||||||
|
## .. _model class: #objectminusrelational-modeling-model-class
|
||||||
var mc = newMutateClauses()
|
var mc = newMutateClauses()
|
||||||
populateMutateClauses(rec, false, mc)
|
populateMutateClauses(rec, false, mc)
|
||||||
|
|
||||||
@ -61,16 +109,21 @@ proc updateRecord*[T](db: DbConn, rec: T): bool =
|
|||||||
return numRowsUpdated > 0;
|
return numRowsUpdated > 0;
|
||||||
|
|
||||||
template deleteRecord*(db: DbConn, modelType: type, id: typed): untyped =
|
template deleteRecord*(db: DbConn, modelType: type, id: typed): untyped =
|
||||||
|
## Delete a record by id.
|
||||||
let sqlStmt = "DELETE FROM " & tableName(modelType) & " WHERE id = ?"
|
let sqlStmt = "DELETE FROM " & tableName(modelType) & " WHERE id = ?"
|
||||||
log().debug "deleteRecord: [" & sqlStmt & "] id: " & $id
|
log().debug "deleteRecord: [" & sqlStmt & "] id: " & $id
|
||||||
db.tryExec(sql(sqlStmt), $id)
|
db.tryExec(sql(sqlStmt), $id)
|
||||||
|
|
||||||
proc deleteRecord*[T](db: DbConn, rec: T): bool =
|
proc deleteRecord*[T](db: DbConn, rec: T): bool =
|
||||||
|
## Delete a record by `id`_.
|
||||||
|
##
|
||||||
|
## .. _id: #model-class-id-field
|
||||||
let sqlStmt = "DELETE FROM " & tableName(rec) & " WHERE id = ?"
|
let sqlStmt = "DELETE FROM " & tableName(rec) & " WHERE id = ?"
|
||||||
log().debug "deleteRecord: [" & sqlStmt & "] id: " & $rec.id
|
log().debug "deleteRecord: [" & sqlStmt & "] id: " & $rec.id
|
||||||
return db.tryExec(sql(sqlStmt), $rec.id)
|
return db.tryExec(sql(sqlStmt), $rec.id)
|
||||||
|
|
||||||
template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
|
template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
|
||||||
|
## Fetch a record by id.
|
||||||
let sqlStmt =
|
let sqlStmt =
|
||||||
"SELECT " & columnNamesForModel(modelType).join(",") &
|
"SELECT " & columnNamesForModel(modelType).join(",") &
|
||||||
" FROM " & tableName(modelType) &
|
" FROM " & tableName(modelType) &
|
||||||
@ -85,6 +138,9 @@ template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
|
|||||||
rowToModel(modelType, row)
|
rowToModel(modelType, row)
|
||||||
|
|
||||||
template findRecordsWhere*(db: DbConn, modelType: type, whereClause: string, values: varargs[string, dbFormat]): untyped =
|
template findRecordsWhere*(db: DbConn, modelType: type, whereClause: string, values: varargs[string, dbFormat]): untyped =
|
||||||
|
## Find all records matching a given `WHERE` clause. The number of elements in
|
||||||
|
## the `values` array must match the number of placeholders (`?`) in the
|
||||||
|
## provided `WHERE` clause.
|
||||||
let sqlStmt =
|
let sqlStmt =
|
||||||
"SELECT " & columnNamesForModel(modelType).join(",") &
|
"SELECT " & columnNamesForModel(modelType).join(",") &
|
||||||
" FROM " & tableName(modelType) &
|
" FROM " & tableName(modelType) &
|
||||||
@ -94,6 +150,7 @@ template findRecordsWhere*(db: DbConn, modelType: type, whereClause: string, val
|
|||||||
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
|
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
|
||||||
|
|
||||||
template getAllRecords*(db: DbConn, modelType: type): untyped =
|
template getAllRecords*(db: DbConn, modelType: type): untyped =
|
||||||
|
## Fetch all records of the given type.
|
||||||
let sqlStmt =
|
let sqlStmt =
|
||||||
"SELECT " & columnNamesForModel(modelType).join(",") &
|
"SELECT " & columnNamesForModel(modelType).join(",") &
|
||||||
" FROM " & tableName(modelType)
|
" FROM " & tableName(modelType)
|
||||||
@ -102,6 +159,7 @@ template getAllRecords*(db: DbConn, modelType: type): untyped =
|
|||||||
db.getAllRows(sql(sqlStmt)).mapIt(rowToModel(modelType, it))
|
db.getAllRows(sql(sqlStmt)).mapIt(rowToModel(modelType, it))
|
||||||
|
|
||||||
template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: string, value: string]]): untyped =
|
template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: string, value: string]]): untyped =
|
||||||
|
## Find all records matching the provided lookup values.
|
||||||
let sqlStmt =
|
let sqlStmt =
|
||||||
"SELECT " & columnNamesForModel(modelType).join(",") &
|
"SELECT " & columnNamesForModel(modelType).join(",") &
|
||||||
" FROM " & tableName(modelType) &
|
" FROM " & tableName(modelType) &
|
||||||
@ -112,6 +170,23 @@ template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: s
|
|||||||
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
|
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
|
||||||
|
|
||||||
macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untyped =
|
macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untyped =
|
||||||
|
## Generate all standard access procedures for the given model types. For a
|
||||||
|
## `model class`_ named `SampleRecord`, this will generate the following
|
||||||
|
## procedures:
|
||||||
|
##
|
||||||
|
## .. code-block:: Nim
|
||||||
|
## proc getSampleRecord*(db: dbType): SampleRecord;
|
||||||
|
## proc getAllSampleRecords*(db: dbType): SampleRecord;
|
||||||
|
## proc createSampleRecord*(db: dbType, rec: SampleRecord): SampleRecord;
|
||||||
|
## proc deleteSampleRecord*(db: dbType, rec: SampleRecord): bool;
|
||||||
|
## proc deleteSampleRecord*(db: dbType, id: idType): bool;
|
||||||
|
## proc updateSampleRecord*(db: dbType, rec: SampleRecord): bool;
|
||||||
|
##
|
||||||
|
## proc findSampleRecordsWhere*(
|
||||||
|
## db: dbType, whereClause: string, values: varargs[string]): SampleRecord;
|
||||||
|
##
|
||||||
|
## `dbType` is expected to be some type that has a defined `withConn`_
|
||||||
|
## procedure.
|
||||||
result = newStmtList()
|
result = newStmtList()
|
||||||
|
|
||||||
for t in modelTypes:
|
for t in modelTypes:
|
||||||
@ -147,6 +222,15 @@ macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untype
|
|||||||
db.withConn: result = deleteRecord(conn, `t`, id)
|
db.withConn: 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,
|
||||||
|
##
|
||||||
|
## .. code-block:: Nim
|
||||||
|
## generateLookup(SampleDB, SampleRecord, ["name", "location"])
|
||||||
|
##
|
||||||
|
## will generate the following procedure:
|
||||||
|
##
|
||||||
|
## .. code-block:: Nim
|
||||||
|
## proc findSampleRecordsByNameAndLocation*(db: SampleDB,
|
||||||
let fieldNames = fields[1].mapIt($it)
|
let fieldNames = fields[1].mapIt($it)
|
||||||
let procName = ident("find" & pluralize($modelType.getType[1]) & "By" & fieldNames.mapIt(it.capitalize).join("And"))
|
let procName = ident("find" & pluralize($modelType.getType[1]) & "By" & fieldNames.mapIt(it.capitalize).join("And"))
|
||||||
|
|
||||||
@ -211,6 +295,20 @@ proc initPool*(
|
|||||||
poolSize = 10,
|
poolSize = 10,
|
||||||
hardCap = false,
|
hardCap = false,
|
||||||
healthCheckQuery = "SELECT 'true' AS alive"): DbConnPool =
|
healthCheckQuery = "SELECT 'true' AS alive"): DbConnPool =
|
||||||
|
## Initialize a new DbConnPool.
|
||||||
|
##
|
||||||
|
## * `connect` must be a factory which creates a new `DbConn`
|
||||||
|
## * `poolSize` sets the desired capacity of the connection pool.
|
||||||
|
## * `hardCap` defaults to `false`.
|
||||||
|
##
|
||||||
|
## 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` should be a simple and fast SQL query that the pool
|
||||||
|
## can use to test the liveliness of pooled connections.
|
||||||
|
|
||||||
initDbConnPool(DbConnPoolConfig(
|
initDbConnPool(DbConnPoolConfig(
|
||||||
connect: connect,
|
connect: connect,
|
||||||
|
@ -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 std/db_postgres, std/sequtils, std/strutils, std/sugar
|
||||||
|
|
||||||
import namespaced_logging
|
import namespaced_logging
|
||||||
@ -5,10 +11,21 @@ import namespaced_logging
|
|||||||
|
|
||||||
type
|
type
|
||||||
DbConnPoolConfig* = object
|
DbConnPoolConfig* = object
|
||||||
connect*: () -> DbConn
|
connect*: () -> DbConn ## Factory procedure to create a new DBConn
|
||||||
poolSize*: int
|
poolSize*: int ## The pool capacity.
|
||||||
hardCap*: bool
|
hardCap*: bool ## Is the pool capacity a hard cap?
|
||||||
healthCheckQuery*: string
|
##
|
||||||
|
## 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
|
PooledDbConn = ref object
|
||||||
conn: DbConn
|
conn: DbConn
|
||||||
@ -16,6 +33,7 @@ type
|
|||||||
free: bool
|
free: bool
|
||||||
|
|
||||||
DbConnPool* = ref object
|
DbConnPool* = ref object
|
||||||
|
## Database connection pool
|
||||||
conns: seq[PooledDbConn]
|
conns: seq[PooledDbConn]
|
||||||
cfg: DbConnPoolConfig
|
cfg: DbConnPoolConfig
|
||||||
lastId: int
|
lastId: int
|
||||||
@ -74,6 +92,13 @@ proc maintain(pool: DbConnPool): void =
|
|||||||
[$toCull.len, $pool.conns.len])
|
[$toCull.len, $pool.conns.len])
|
||||||
|
|
||||||
proc take*(pool: DbConnPool): tuple[id: int, conn: DbConn] =
|
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
|
pool.maintain
|
||||||
let freeConns = pool.conns.filterIt(it.free)
|
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)
|
return (id: reserved.id, conn: reserved.conn)
|
||||||
|
|
||||||
proc release*(pool: DbConnPool, connId: int): void =
|
proc release*(pool: DbConnPool, connId: int): void =
|
||||||
|
## Release a connection back to the pool.
|
||||||
log().debug("Reclaiming released connaction $#" % [$connId])
|
log().debug("Reclaiming released connaction $#" % [$connId])
|
||||||
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*(pool: DbConnPool, stmt: untyped): untyped =
|
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)
|
let (connId, conn {.inject.}) = take(pool)
|
||||||
try: stmt
|
try: stmt
|
||||||
finally: release(pool, connId)
|
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,
|
import json, macros, options, sequtils, strutils, times, timeutils, unicode,
|
||||||
uuids
|
uuids
|
||||||
|
|
||||||
@ -5,6 +10,10 @@ import nre except toSeq
|
|||||||
|
|
||||||
type
|
type
|
||||||
MutateClauses* = object
|
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]
|
columns*: seq[string]
|
||||||
placeholders*: seq[string]
|
placeholders*: seq[string]
|
||||||
values*: seq[string]
|
values*: seq[string]
|
||||||
@ -12,18 +21,23 @@ type
|
|||||||
# TODO: more complete implementation
|
# TODO: more complete implementation
|
||||||
# see https://github.com/blakeembrey/pluralize
|
# see https://github.com/blakeembrey/pluralize
|
||||||
proc pluralize*(name: string): string =
|
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[^2..^1] == "ey": return name[0..^3] & "ies"
|
||||||
if name[^1] == 'y': return name[0..^2] & "ies"
|
if name[^1] == 'y': return name[0..^2] & "ies"
|
||||||
return name & "s"
|
return name & "s"
|
||||||
|
|
||||||
macro modelName*(model: object): string =
|
macro modelName*(model: object): string =
|
||||||
|
## For a given concrete record object, return the name of the `model class`_
|
||||||
return newStrLitNode($model.getTypeInst)
|
return newStrLitNode($model.getTypeInst)
|
||||||
|
|
||||||
macro modelName*(modelType: type): string =
|
macro modelName*(modelType: type): string =
|
||||||
|
## Get the name of a given `model class`_
|
||||||
return newStrLitNode($modelType.getType[1])
|
return newStrLitNode($modelType.getType[1])
|
||||||
|
|
||||||
|
|
||||||
proc identNameToDb*(name: string): string =
|
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]
|
const UNDERSCORE_RUNE = "_".toRunes[0]
|
||||||
let nameInRunes = name.toRunes
|
let nameInRunes = name.toRunes
|
||||||
var prev: Rune
|
var prev: Rune
|
||||||
@ -42,28 +56,40 @@ proc identNameToDb*(name: string): string =
|
|||||||
return $resultRunes
|
return $resultRunes
|
||||||
|
|
||||||
proc dbNameToIdent*(name: string): string =
|
proc dbNameToIdent*(name: string): string =
|
||||||
|
## Map a DB name to a Nim identifier name. See the `rules for name mapping`_
|
||||||
let parts = name.split("_")
|
let parts = name.split("_")
|
||||||
return @[parts[0]].concat(parts[1..^1].mapIt(capitalize(it))).join("")
|
return @[parts[0]].concat(parts[1..^1].mapIt(capitalize(it))).join("")
|
||||||
|
|
||||||
proc tableName*(modelType: type): string =
|
proc tableName*(modelType: type): string =
|
||||||
|
## Get the `table name`_ for a given `model class`_
|
||||||
return pluralize(modelName(modelType).identNameToDb)
|
return pluralize(modelName(modelType).identNameToDb)
|
||||||
|
|
||||||
proc tableName*[T](rec: T): string =
|
proc tableName*[T](rec: T): string =
|
||||||
|
## Get the `table name`_ for a given record.
|
||||||
return pluralize(modelName(rec).identNameToDb)
|
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 =
|
proc dbFormat*[T](list: seq[T]): string =
|
||||||
|
## Format a `seq` for inclusion in a SQL Query.
|
||||||
return "{" & list.mapIt(dbFormat(it)).join(",") & "}"
|
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
|
type DbArrayParseState = enum
|
||||||
expectStart, inQuote, inVal, expectEnd
|
expectStart, inQuote, inVal, expectEnd
|
||||||
|
|
||||||
proc parsePGDatetime*(val: string): DateTime =
|
proc parsePGDatetime*(val: string): DateTime =
|
||||||
|
## Parse a Postgres datetime value into a Nim DateTime object.
|
||||||
|
|
||||||
const PG_TIMESTAMP_FORMATS = [
|
const PG_TIMESTAMP_FORMATS = [
|
||||||
"yyyy-MM-dd HH:mm:ss",
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
@ -103,6 +129,7 @@ proc parsePGDatetime*(val: string): DateTime =
|
|||||||
raise newException(ValueError, "Cannot parse PG date. Tried:" & errStr)
|
raise newException(ValueError, "Cannot parse PG date. Tried:" & errStr)
|
||||||
|
|
||||||
proc parseDbArray*(val: string): seq[string] =
|
proc parseDbArray*(val: string): seq[string] =
|
||||||
|
## Parse a Postgres array column into a Nim seq[string]
|
||||||
result = newSeq[string]()
|
result = newSeq[string]()
|
||||||
|
|
||||||
var parseState = DbArrayParseState.expectStart
|
var parseState = DbArrayParseState.expectStart
|
||||||
@ -161,6 +188,9 @@ proc parseDbArray*(val: string): seq[string] =
|
|||||||
result.add(curStr)
|
result.add(curStr)
|
||||||
|
|
||||||
proc createParseStmt*(t, value: NimNode): NimNode =
|
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
|
#echo "Creating parse statment for ", t.treeRepr
|
||||||
if t.typeKind == ntyObject:
|
if t.typeKind == ntyObject:
|
||||||
@ -219,6 +249,9 @@ proc createParseStmt*(t, value: NimNode): NimNode =
|
|||||||
error "Unknown value type: " & $t.typeKind
|
error "Unknown value type: " & $t.typeKind
|
||||||
|
|
||||||
template walkFieldDefs*(t: NimNode, body: untyped) =
|
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
|
let tTypeImpl = t.getTypeImpl
|
||||||
|
|
||||||
var nodeToItr: NimNode
|
var nodeToItr: NimNode
|
||||||
@ -239,6 +272,8 @@ template walkFieldDefs*(t: NimNode, body: untyped) =
|
|||||||
body
|
body
|
||||||
|
|
||||||
macro columnNamesForModel*(modelType: typed): seq[string] =
|
macro columnNamesForModel*(modelType: typed): seq[string] =
|
||||||
|
## Return the column names corresponding to the the fields of the given
|
||||||
|
## `model class`_
|
||||||
var columnNames = newSeq[string]()
|
var columnNames = newSeq[string]()
|
||||||
|
|
||||||
modelType.walkFieldDefs:
|
modelType.walkFieldDefs:
|
||||||
@ -247,6 +282,8 @@ macro columnNamesForModel*(modelType: typed): seq[string] =
|
|||||||
result = newLit(columnNames)
|
result = newLit(columnNames)
|
||||||
|
|
||||||
macro rowToModel*(modelType: typed, row: seq[string]): untyped =
|
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
|
# Create the object constructor AST node
|
||||||
result = newNimNode(nnkObjConstr).add(modelType)
|
result = newNimNode(nnkObjConstr).add(modelType)
|
||||||
@ -269,6 +306,7 @@ macro listFields*(t: typed): untyped =
|
|||||||
result = newLit(fields)
|
result = newLit(fields)
|
||||||
|
|
||||||
proc typeOfColumn*(modelType: NimNode, colName: string): NimNode =
|
proc typeOfColumn*(modelType: NimNode, colName: string): NimNode =
|
||||||
|
## Given a model type and a column name, return the Nim type for that column.
|
||||||
modelType.walkFieldDefs:
|
modelType.walkFieldDefs:
|
||||||
if $fieldIdent != colName: continue
|
if $fieldIdent != colName: continue
|
||||||
|
|
||||||
@ -289,6 +327,8 @@ proc isEmpty(val: UUID): bool = return val.isZero
|
|||||||
proc isEmpty(val: string): bool = return val.isEmptyOrWhitespace
|
proc isEmpty(val: string): bool = return val.isEmptyOrWhitespace
|
||||||
|
|
||||||
macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses): untyped =
|
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()
|
result = newStmtList()
|
||||||
|
|
||||||
@ -326,3 +366,7 @@ macro populateMutateClauses*(t: typed, newRecord: bool, mc: var MutateClauses):
|
|||||||
`mc`.columns.add(identNameToDb(`fieldName`))
|
`mc`.columns.add(identNameToDb(`fieldName`))
|
||||||
`mc`.placeholders.add("?")
|
`mc`.placeholders.add("?")
|
||||||
`mc`.values.add(dbFormat(`t`.`fieldIdent`))
|
`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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user