4 Commits
0.3.1 ... 0.3.5

Author SHA1 Message Date
f7791b6f60 Add logging statments (behind namespaced logger). 2022-01-13 16:22:15 -06:00
279d9aa7fd Expose a number of useful utility methods and macros. 2021-08-02 05:54:56 -05:00
d90372127b Further fix for ISO8601 date parsing.
Recognize versions of timestamps with 'T' as the date/time separator.
For example, compare:

    '2021-08-01 23:14:00-05:00'
    '2021-08-01T23:14:00-05:00'

This commit adds support for the second flavor (and it's variations).
2021-08-01 23:14:18 -05:00
2b78727356 Fix for PostgreSQL timestamp with timezone fields.
The previous fix for PostgreSQL timestamp fields matched fields with and
without timezones, but did not properly parse values from fields that
included the timezone. Now we check for the presence of the timezone in
the date string and choose a format string to parse it correctly.
2021-07-05 11:24:06 -05:00
3 changed files with 69 additions and 34 deletions

View File

@ -1,6 +1,6 @@
# Package
version = "0.3.1"
version = "0.3.5"
author = "Jonathan Bernard"
description = "Lightweight Postgres ORM for Nim."
license = "GPL-3.0"
@ -11,3 +11,4 @@ srcDir = "src"
# Dependencies
requires "nim >= 1.4.0"
requires "https://git.jdb-software.com/jdb/nim-namespaced-logging.git"

View File

@ -1,11 +1,22 @@
import db_postgres, macros, options, sequtils, strutils, uuids
import namespaced_logging
from unicode import capitalize
import ./fiber_orm/util
export
util.columnNamesForModel,
util.dbFormat,
util.dbNameToIdent,
util.identNameToDb,
util.modelName,
util.rowToModel,
util.tableName
type NotFoundError* = object of CatchableError
let logNs = initLoggingNamespace(name = "fiber_orm", level = lvlNotice)
proc newMutateClauses(): MutateClauses =
return MutateClauses(
columns: @[],
@ -18,11 +29,14 @@ proc createRecord*[T](db: DbConn, rec: T): T =
# Confusingly, getRow allows inserts and updates. We use it to get back the ID
# we want from the row.
let newRow = db.getRow(sql(
let sqlStmt =
"INSERT INTO " & tableName(rec) &
" (" & mc.columns.join(",") & ") " &
" VALUES (" & mc.placeholders.join(",") & ") " &
" RETURNING *"), mc.values)
" RETURNING *"
logNs.debug "createRecord: [" & sqlStmt & "]"
let newRow = db.getRow(sql(sqlStmt), mc.values)
result = rowToModel(T, newRow)
@ -31,24 +45,34 @@ proc updateRecord*[T](db: DbConn, rec: T): bool =
populateMutateClauses(rec, false, mc)
let setClause = zip(mc.columns, mc.placeholders).mapIt(it[0] & " = " & it[1]).join(",")
let numRowsUpdated = db.execAffectedRows(sql(
let sqlStmt =
"UPDATE " & tableName(rec) &
" SET " & setClause &
" WHERE id = ? "), mc.values.concat(@[$rec.id]))
" WHERE id = ? "
logNs.debug "updateRecord: [" & sqlStmt & "] id: " & $rec.id
let numRowsUpdated = db.execAffectedRows(sql(sqlStmt), mc.values.concat(@[$rec.id]))
return numRowsUpdated > 0;
template deleteRecord*(db: DbConn, modelType: type, id: typed): untyped =
db.tryExec(sql("DELETE FROM " & tableName(modelType) & " WHERE id = ?"), $id)
let sqlStmt = "DELETE FROM " & tableName(modelType) & " WHERE id = ?"
logNs.debug "deleteRecord: [" & sqlStmt & "] id: " & $id
db.tryExec(sql(sqlStmt), $id)
proc deleteRecord*[T](db: DbConn, rec: T): bool =
return db.tryExec(sql("DELETE FROM " & tableName(rec) & " WHERE id = ?"), $rec.id)
let sqlStmt = "DELETE FROM " & tableName(rec) & " WHERE id = ?"
logNs.debug "deleteRecord: [" & sqlStmt & "] id: " & $rec.id
return db.tryExec(sql(sqlStmt), $rec.id)
template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
let row = db.getRow(sql(
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
" WHERE id = ?"), @[$id])
" WHERE id = ?"
logNs.debug "getRecord: [" & sqlStmt & "] id: " & $id
let row = db.getRow(sql(sqlStmt), @[$id])
if allIt(row, it.len == 0):
raise newException(NotFoundError, "no " & modelName(modelType) & " record for id " & $id)
@ -56,25 +80,31 @@ template getRecord*(db: DbConn, modelType: type, id: typed): untyped =
rowToModel(modelType, row)
template findRecordsWhere*(db: DbConn, modelType: type, whereClause: string, values: varargs[string, dbFormat]): untyped =
db.getAllRows(sql(
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
" WHERE " & whereClause), values)
.mapIt(rowToModel(modelType, it))
" WHERE " & whereClause
logNs.debug "findRecordsWhere: [" & sqlStmt & "] values: (" & values.join(", ") & ")"
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
template getAllRecords*(db: DbConn, modelType: type): untyped =
db.getAllRows(sql(
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType)))
.mapIt(rowToModel(modelType, it))
" FROM " & tableName(modelType)
logNs.debug "getAllRecords: [" & sqlStmt & "]"
db.getAllRows(sql(sqlStmt)).mapIt(rowToModel(modelType, it))
template findRecordsBy*(db: DbConn, modelType: type, lookups: seq[tuple[field: string, value: string]]): untyped =
db.getAllRows(sql(
let sqlStmt =
"SELECT " & columnNamesForModel(modelType).join(",") &
" FROM " & tableName(modelType) &
" WHERE " & lookups.mapIt(it.field & " = ?").join(" AND ")),
lookups.mapIt(it.value))
.mapIt(rowToModel(modelType, it))
" WHERE " & lookups.mapIt(it.field & " = ?").join(" AND ")
let values = lookups.mapIt(it.value)
logNs.debug "findRecordsBy: [" & sqlStmt & "] values (" & values.join(", ") & ")"
db.getAllRows(sql(sqlStmt), values).mapIt(rowToModel(modelType, it))
macro generateProcsForModels*(dbType: type, modelTypes: openarray[type]): untyped =
result = newStmtList()

View File

@ -67,34 +67,38 @@ proc parsePGDatetime*(val: string): DateTime =
const PG_TIMESTAMP_FORMATS = [
"yyyy-MM-dd HH:mm:ss",
"yyyy-MM-dd'T'HH:mm:ss",
"yyyy-MM-dd HH:mm:sszz",
"yyyy-MM-dd'T'HH:mm:sszz",
"yyyy-MM-dd HH:mm:ss'.'fff",
"yyyy-MM-dd HH:mm:ss'.'fffzz"
"yyyy-MM-dd'T'HH:mm:ss'.'fff",
"yyyy-MM-dd HH:mm:ss'.'fffzz",
"yyyy-MM-dd'T'HH:mm:ss'.'fffzz",
"yyyy-MM-dd HH:mm:ss'.'fffzzz",
"yyyy-MM-dd'T'HH:mm:ss'.'fffzzz",
]
let PG_PARTIAL_FORMAT_REGEX = re"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.)(\d{1,3})(\S+)?"
var errStr = ""
# Try to parse directly using known format strings.
for df in PG_TIMESTAMP_FORMATS:
try: return val.parse(df)
except: errStr &= "\n\t" & getCurrentExceptionMsg()
var correctedVal = val;
# PostgreSQL will truncate any trailing 0's in the millisecond value leading
# to values like `2020-01-01 16:42.3+00`. This cannot currently be parsed by
# the standard times format as it expects exactly three digits for
# millisecond values. So we have to detect this and pad out the millisecond
# value to 3 digits.
let PG_PARTIAL_FORMAT_REGEX = re"(\d{4}-\d{2}-\d{2}( |'T')\d{2}:\d{2}:\d{2}\.)(\d{1,2})(\S+)?"
let match = val.match(PG_PARTIAL_FORMAT_REGEX)
if match.isSome:
let c = match.get.captures
try:
let corrected = c[0] & alignLeft(c[1], 3, '0') & c[2]
return corrected.parse(PG_TIMESTAMP_FORMATS[1])
except:
errStr &= "\n\t" & PG_TIMESTAMP_FORMATS[1] &
" after padding out milliseconds to full 3-digits"
if c.toSeq.len == 2: correctedVal = c[0] & alignLeft(c[2], 3, '0')
else: correctedVal = c[0] & alignLeft(c[2], 3, '0') & c[3]
var errStr = ""
# Try to parse directly using known format strings.
for df in PG_TIMESTAMP_FORMATS:
try: return correctedVal.parse(df)
except: errStr &= "\n\t" & getCurrentExceptionMsg()
raise newException(ValueError, "Cannot parse PG date. Tried:" & errStr)