Add Ormin ORM support for BaraDB (Nim client)
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import std/strutils
|
||||
import db_connector/db_common
|
||||
|
||||
proc dbTypFromName*(name: string): DbTypeKind =
|
||||
var k = dbUnknown
|
||||
|
||||
case name.toLowerAscii
|
||||
of "int", "integer", "int8", "smallint", "int16",
|
||||
"longint", "int32", "int64", "tinyint", "hugeint": k = dbInt
|
||||
of "uint", "uint8", "uint16", "uint32", "uint64": k = dbUInt
|
||||
of "serial": k = dbSerial
|
||||
of "bit": k = dbBit
|
||||
of "bool", "boolean": k = dbBool
|
||||
of "blob": k = dbBlob
|
||||
of "fixedchar": k = dbFixedChar
|
||||
of "varchar", "text", "string": k = dbVarchar
|
||||
of "json": k = dbJson
|
||||
of "xml": k = dbXml
|
||||
of "decimal": k = dbDecimal
|
||||
of "float", "double", "longdouble", "real": k = dbFloat
|
||||
of "date", "day": k = dbDate
|
||||
of "time": k = dbTime
|
||||
of "datetime": k = dbDateTime
|
||||
of "timestamp", "timestamptz": k = dbTimestamp
|
||||
of "timeinterval": k = dbTimeInterval
|
||||
of "set": k = dbSet
|
||||
of "array": k = dbArray
|
||||
of "composite": k = dbComposite
|
||||
of "url", "uri": k = dbUrl
|
||||
of "uuid": k = dbUuid
|
||||
of "inet", "ip", "tcpip": k = dbInet
|
||||
of "mac", "macaddress": k = dbMacAddress
|
||||
of "geometry": k = dbGeometry
|
||||
of "point": k = dbPoint
|
||||
of "line": k = dbLine
|
||||
of "lseg": k = dbLseg
|
||||
of "box": k = dbBox
|
||||
of "path": k = dbPath
|
||||
of "polygon": k = dbPolygon
|
||||
of "circle": k = dbCircle
|
||||
else: discard
|
||||
|
||||
return k
|
||||
@@ -0,0 +1,179 @@
|
||||
import std/[os, paths]
|
||||
import db_connector/db_common, strutils, strformat
|
||||
import db_connector/db_postgres as db_postgres
|
||||
import db_connector/db_sqlite as db_sqlite
|
||||
|
||||
import parsesql_tmp # import std/parsesql
|
||||
|
||||
export paths
|
||||
|
||||
type
|
||||
DbConn = db_postgres.DbConn | db_sqlite.DbConn
|
||||
DbId* = distinct string
|
||||
DbSql* = distinct string
|
||||
|
||||
proc `$`*(dbId: DbId): string {.borrow.}
|
||||
proc `$`*(dbSql: DbSql): string {.borrow.}
|
||||
|
||||
template staticLoad*(filename: string): DbSql =
|
||||
bind isAbsolute, parentDir, `/`, instantiationInfo
|
||||
block:
|
||||
const schemaPath {.gensym.} = static:
|
||||
if isAbsolute(filename):
|
||||
filename
|
||||
else:
|
||||
parentDir(instantiationInfo(-1, true)[0]) / filename
|
||||
const schemaSql {.gensym.} = staticRead(schemaPath)
|
||||
static:
|
||||
try:
|
||||
discard parseSql(schemaSql)
|
||||
except SqlParseError as e:
|
||||
doAssert false, "Invalid SQL in " & schemaPath & ": " & e.msg
|
||||
DbSql(schemaSql)
|
||||
|
||||
proc parseQualifiedIdentifier(name: string): seq[(string, bool)] =
|
||||
var current = ""
|
||||
var quoteChar = '\0'
|
||||
var partWasQuoted = false
|
||||
var i = 0
|
||||
while i < name.len:
|
||||
let c = name[i]
|
||||
if quoteChar == '\0':
|
||||
case c
|
||||
of '.':
|
||||
result.add((current, partWasQuoted))
|
||||
current = ""
|
||||
partWasQuoted = false
|
||||
of '"', '\'', '`':
|
||||
if current.len == 0:
|
||||
quoteChar = c
|
||||
partWasQuoted = true
|
||||
else:
|
||||
current.add(c)
|
||||
else:
|
||||
current.add(c)
|
||||
else:
|
||||
if c == quoteChar:
|
||||
if i + 1 < name.len and name[i + 1] == quoteChar:
|
||||
current.add(c)
|
||||
inc(i)
|
||||
else:
|
||||
quoteChar = '\0'
|
||||
else:
|
||||
current.add(c)
|
||||
inc(i)
|
||||
|
||||
result.add((current, partWasQuoted))
|
||||
|
||||
proc canUseBareIdentifier(part: string): bool =
|
||||
if part.len == 0:
|
||||
return false
|
||||
if part[0] notin {'a'..'z', 'A'..'Z', '_'}:
|
||||
return false
|
||||
for c in part[1..^1]:
|
||||
if c notin {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
|
||||
return false
|
||||
true
|
||||
|
||||
proc quoteDbIdentifier*(name: string): DbId =
|
||||
## Quote a database identifier for direct interpolation into SQL text.
|
||||
## Dot-separated names are treated as qualified identifiers.
|
||||
var partsSql: seq[string] = @[]
|
||||
for (part, wasQuoted) in parseQualifiedIdentifier(name):
|
||||
let normalizedPart = if wasQuoted: part else: part.toLowerAscii()
|
||||
if wasQuoted or not canUseBareIdentifier(normalizedPart):
|
||||
partsSql.add("\"" & normalizedPart.replace("\"", "\"\"") & "\"")
|
||||
else:
|
||||
partsSql.add(normalizedPart)
|
||||
DbId(partsSql.join("."))
|
||||
|
||||
proc dropTableName(db: DbConn; tableName, lookupName: string) =
|
||||
# SQL parameters bind values, not identifiers, so DROP TABLE needs the
|
||||
# identifier rendered into the statement text with correct quoting.
|
||||
when defined(sqlite) and defined(orminLegacySqliteDropNames):
|
||||
db.exec(sql("drop table if exists " & lookupName))
|
||||
else:
|
||||
let tableIdentSql = quoteDbIdentifier(tableName)
|
||||
when defined(postgre):
|
||||
db.exec(sql("drop table if exists " & $tableIdentSql & " cascade"))
|
||||
else:
|
||||
db.exec(sql("drop table if exists " & $tableIdentSql))
|
||||
|
||||
iterator tableDefs(sql: DbSql): tuple[name, tableName, model: string] =
|
||||
# Parse the entire SQL file and iterate statements via the SQL parser
|
||||
var ast: SqlNode
|
||||
try:
|
||||
ast = parseSql($sql)
|
||||
except SqlParseError as e:
|
||||
echo "SQL Parse Error:\n", sql
|
||||
raise e
|
||||
|
||||
if ast.len > 0:
|
||||
# ast is a statement list; iterate each statement node
|
||||
for i in 0 ..< ast.len:
|
||||
let node = ast[i]
|
||||
if node.kind in {nkCreateTable, nkCreateTableIfNotExists}:
|
||||
yield (node[0].strVal.toLowerAscii(), $node[0], $node)
|
||||
else:
|
||||
# Fallback: ast might be a single statement (not a list)
|
||||
let node = ast
|
||||
if node.kind in {nkCreateTable, nkCreateTableIfNotExists}:
|
||||
yield (node[0].strVal.toLowerAscii(), $node[0], $node)
|
||||
|
||||
iterator tablePairs*(sql: string): tuple[name, model: string] =
|
||||
for name, _, model in tableDefs(DbSql(sql)):
|
||||
yield (name, model)
|
||||
|
||||
iterator tablePairs*(sql: static[DbSql]): tuple[name, model: string] =
|
||||
for name, _, model in tableDefs(sql):
|
||||
yield (name, model)
|
||||
|
||||
|
||||
iterator tablePairs*(sql: Path): tuple[name, model: string] =
|
||||
let f = readFile($sql)
|
||||
for n, m in tablePairs(f):
|
||||
yield (n, m)
|
||||
|
||||
proc createTable*(db: DbConn; sqlFile: Path) =
|
||||
for _, m in tablePairs(sqlFile):
|
||||
db.exec(sql(m))
|
||||
|
||||
proc createTable*(db: DbConn; sqlFile: Path, name: string) =
|
||||
for n, m in tablePairs(sqlFile):
|
||||
if n == name:
|
||||
db.exec(sql(m))
|
||||
return
|
||||
raiseAssert &"table: {name} not found in: {sqlFile}"
|
||||
|
||||
proc createTable*(db: DbConn; schemaSql: static[DbSql]) =
|
||||
for _, m in tablePairs(schemaSql):
|
||||
db.exec(sql(m))
|
||||
|
||||
proc createTable*(db: DbConn; schemaSql: static[DbSql], name: string) =
|
||||
for n, m in tablePairs(schemaSql):
|
||||
if n == name:
|
||||
db.exec(sql(m))
|
||||
return
|
||||
raiseAssert &"table: {name} not found in static schema"
|
||||
|
||||
proc dropTable*(db: DbConn; sqlFile: Path) =
|
||||
for lookupName, tableName, _ in tableDefs(DbSql(readFile($sqlFile))):
|
||||
db.dropTableName(tableName, lookupName)
|
||||
|
||||
proc dropTable*(db: DbConn; sqlFile: Path, name: string) =
|
||||
for lookupName, tableName, _ in tableDefs(DbSql(readFile($sqlFile))):
|
||||
if lookupName == name:
|
||||
db.dropTableName(tableName, lookupName)
|
||||
return
|
||||
raiseAssert &"table: {name} not found in: {sqlFile}"
|
||||
|
||||
proc dropTable*(db: DbConn; schemaSql: static[DbSql]) =
|
||||
for lookupName, tableName, _ in tableDefs(schemaSql):
|
||||
db.dropTableName(tableName, lookupName)
|
||||
|
||||
proc dropTable*(db: DbConn; schemaSql: static[DbSql], name: string) =
|
||||
for lookupName, tableName, _ in tableDefs(schemaSql):
|
||||
if lookupName == name:
|
||||
db.dropTableName(tableName, lookupName)
|
||||
return
|
||||
raiseAssert &"table: {name} not found in static schema"
|
||||
@@ -0,0 +1,71 @@
|
||||
## This module implements a little helper macro to ease
|
||||
## writing the dispatching logic for JSON based servers.
|
||||
|
||||
import macros
|
||||
|
||||
proc tbody(n: NimNode): NimNode =
|
||||
# transforms::
|
||||
# f fields(a, "b")
|
||||
# into::
|
||||
# f a = %args["a"], b = %args["b"]
|
||||
case n.kind
|
||||
of nnkCallKinds:
|
||||
result = copyNimNode(n)
|
||||
result.add tbody(n[0])
|
||||
for i in 1..<n.len:
|
||||
let it = n[i]
|
||||
if it.kind == nnkCall and $it[0] == "fields":
|
||||
for j in 1..<it.len:
|
||||
let field = it[j]
|
||||
let s = if field.kind in {nnkStrLit..nnkTripleStrLit}: field.strVal
|
||||
else: $field
|
||||
let arg = newTree(nnkPrefix, ident"%",
|
||||
newTree(nnkBracketExpr, ident"arg", newLit(s)))
|
||||
result.add newTree(nnkExprEqExpr, ident(s), arg)
|
||||
else:
|
||||
result.add tbody(it)
|
||||
else:
|
||||
result = copyNimNode(n)
|
||||
for x in n: result.add tbody(x)
|
||||
|
||||
macro createDispatcher*(name, n: untyped): untyped =
|
||||
expectKind n, nnkStmtList
|
||||
result = newStmtList()
|
||||
let disp = newTree(nnkCaseStmt, ident"cmd")
|
||||
template cmdProc(name, body) {.dirty.} =
|
||||
proc name(arg: JsonNode): JsonNode = body
|
||||
template callCmd(name) {.dirty.} =
|
||||
result = name(arg)
|
||||
|
||||
for x in n:
|
||||
if x.kind == nnkCall and x.len == 2 and
|
||||
x[0].kind == nnkIdent and x[1].kind == nnkStmtList:
|
||||
result.add getAst(cmdProc(x[0], tbody x[1]))
|
||||
disp.add newTree(nnkOfBranch, newLit($x[0]),
|
||||
newStmtList(getAst(callCmd(x[0]))))
|
||||
else:
|
||||
# do not touch:
|
||||
result.add n
|
||||
disp.add newTree(nnkElse, newStmtList(newTree(nnkDiscardStmt, newEmptyNode())))
|
||||
|
||||
template dispatchProc(name, body) {.dirty.} =
|
||||
proc dispatch(inp: JsonNode): JsonNode =
|
||||
let arg = inp["arg"]
|
||||
let cmd = inp["cmd"].getStr("")
|
||||
body
|
||||
result.add getAst(dispatchProc(name, disp))
|
||||
when defined(debugDispatcherDsl):
|
||||
echo repr result
|
||||
|
||||
when isMainModule:
|
||||
import json, db_sqlite
|
||||
|
||||
createDispatcher(dispatch):
|
||||
insertCustomer:
|
||||
echo "insert customer", fields(a, b, c)
|
||||
result = arg
|
||||
selectCustomers:
|
||||
echo "select customer"
|
||||
result = arg
|
||||
|
||||
echo dispatch(nil, %*{"cmd": "insertCustomer", "arg": [1, 2, 3]})
|
||||
@@ -0,0 +1,178 @@
|
||||
import std/[macros, strutils, tables]
|
||||
import db_connector/db_common
|
||||
|
||||
import ./db_types
|
||||
import ./parsesql_tmp
|
||||
|
||||
const
|
||||
FileHeader* = """
|
||||
# Generated by ormin_importer. DO NOT EDIT.
|
||||
|
||||
type
|
||||
Attr = object
|
||||
name: string
|
||||
tabIndex: int
|
||||
typ: DbTypekind
|
||||
key: int # 0 nothing special,
|
||||
# +1 -- primary key
|
||||
# -N -- references attribute N
|
||||
"""
|
||||
|
||||
type
|
||||
DbColumn* = object
|
||||
name*: string
|
||||
tableName*: string
|
||||
typ*: DbType
|
||||
primaryKey*: bool
|
||||
refs*: (string, string)
|
||||
DbColumns* = seq[DbColumn]
|
||||
|
||||
KnownTables* = OrderedTable[string, DbColumns]
|
||||
ImportTarget* = enum
|
||||
postgre, sqlite, mysql, baradb
|
||||
|
||||
proc hasAttribute(colDesc: SqlNode; k: set[SqlNodeKind]): bool =
|
||||
for i in 2 ..< colDesc.len:
|
||||
if colDesc[i].kind in k:
|
||||
return true
|
||||
|
||||
proc hasRefs(colDesc: SqlNode): (string, string) =
|
||||
for i in 2 ..< colDesc.len:
|
||||
let c = colDesc[i]
|
||||
if c.kind == nkReferences:
|
||||
if c[0].kind == nkCall:
|
||||
return (c[0][0].strVal, c[0][1].strVal)
|
||||
elif c[0].kind == nkIdent:
|
||||
return ($c[0], "id")
|
||||
("", "")
|
||||
|
||||
proc getType(n: SqlNode): DbType =
|
||||
var it = n
|
||||
if it.kind == nkCall:
|
||||
it = it[0]
|
||||
if it.kind == nkEnumDef:
|
||||
result.kind = dbEnum
|
||||
result.validValues = @[]
|
||||
for i in 0 ..< it.len:
|
||||
assert it[i].kind == nkStringLit
|
||||
result.validValues.add it[i].strVal
|
||||
elif it.kind in {nkIdent, nkStringLit}:
|
||||
result.kind = dbTypFromName(it.strVal)
|
||||
result.name = it.strVal
|
||||
|
||||
proc collectTables*(n: SqlNode; t: var KnownTables) =
|
||||
if n.isNil:
|
||||
return
|
||||
case n.kind
|
||||
of nkCreateTable, nkCreateTableIfNotExists:
|
||||
let tableName = n[0].strVal
|
||||
var cols: DbColumns = @[]
|
||||
for i in 1 ..< n.len:
|
||||
let it = n[i]
|
||||
if it.kind == nkColumnDef:
|
||||
var typ = getType(it[1])
|
||||
if hasAttribute(it, {nkNotNull}):
|
||||
typ.notNull = true
|
||||
cols.add DbColumn(
|
||||
name: it[0].strVal,
|
||||
tableName: tableName,
|
||||
typ: typ,
|
||||
primaryKey: hasAttribute(it, {nkPrimaryKey}),
|
||||
refs: hasRefs(it)
|
||||
)
|
||||
for i in 1 ..< n.len:
|
||||
let it = n[i]
|
||||
if it.kind == nkForeignKey:
|
||||
var refNode: SqlNode = nil
|
||||
var localCols: seq[string] = @[]
|
||||
for j in 0 ..< it.len:
|
||||
let child = it[j]
|
||||
if child.kind == nkReferences:
|
||||
refNode = child
|
||||
elif child.kind == nkIdent:
|
||||
localCols.add(child.strVal)
|
||||
if refNode.isNil:
|
||||
continue
|
||||
var r = refNode[0]
|
||||
var refTable = ""
|
||||
var refCols: seq[string] = @[]
|
||||
if r.kind == nkColumnReference or r.kind == nkCall:
|
||||
refTable = r[0].strVal
|
||||
for k in 1 ..< r.len:
|
||||
refCols.add(r[k].strVal)
|
||||
let pairCount = min(localCols.len, refCols.len)
|
||||
for k in 0 ..< pairCount:
|
||||
let localName = localCols[k]
|
||||
let targetCol = refCols[k]
|
||||
for c in mitems(cols):
|
||||
if cmpIgnoreCase(c.name, localName) == 0:
|
||||
c.refs = (refTable, targetCol)
|
||||
break
|
||||
t[tableName] = cols
|
||||
else:
|
||||
for i in 0 ..< n.len:
|
||||
collectTables(n[i], t)
|
||||
|
||||
proc attrToKey(a: DbColumn; t: KnownTables): int =
|
||||
if a.primaryKey:
|
||||
return 1
|
||||
if a.refs[0].len > 0:
|
||||
var i = 0
|
||||
for k, v in pairs(t):
|
||||
for b in v:
|
||||
if cmpIgnoreCase(k, a.refs[0]) == 0 and cmpIgnoreCase(b.name, a.refs[1]) == 0:
|
||||
return -i - 1
|
||||
inc i
|
||||
0
|
||||
|
||||
proc renderModelCode(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
|
||||
discard target
|
||||
let sql = parseSql(schemaSql, schemaPath)
|
||||
var knownTables = initOrderedTable[string, DbColumns]()
|
||||
collectTables(sql, knownTables)
|
||||
|
||||
result.add FileHeader
|
||||
result.add "const tableNames = ["
|
||||
var i = 0
|
||||
for k in keys(knownTables):
|
||||
if i > 0:
|
||||
result.add ",\n "
|
||||
else:
|
||||
result.add "\n "
|
||||
result.add escape(k.toLowerAscii)
|
||||
inc i
|
||||
result.add "\n]\n"
|
||||
|
||||
i = 0
|
||||
var j = 0
|
||||
result.add "\nconst attributes = ["
|
||||
for _, v in mpairs(knownTables):
|
||||
for a in v:
|
||||
if j > 0:
|
||||
result.add ",\n "
|
||||
else:
|
||||
result.add "\n "
|
||||
result.add "Attr(name: "
|
||||
result.add escape(a.name.toLowerAscii)
|
||||
result.add ", tabIndex: "
|
||||
result.add $i
|
||||
result.add ", typ: "
|
||||
result.add $a.typ.kind
|
||||
result.add ", key: "
|
||||
result.add $attrToKey(a, knownTables)
|
||||
result.add ")"
|
||||
inc j
|
||||
inc i
|
||||
result.add "\n]\n"
|
||||
|
||||
if includeStatic:
|
||||
result.add "\nconst sqlSchema* = staticLoad("
|
||||
result.add escape(schemaPath)
|
||||
result.add ")\n"
|
||||
|
||||
proc generateModelCode*(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
|
||||
renderModelCode(schemaSql, schemaPath, target, includeStatic)
|
||||
|
||||
macro generateModelCode*(schemaSql: static[string], schemaPath: static[string],
|
||||
target: static[ImportTarget], includeStatic: static[bool] = false): untyped =
|
||||
parseStmt(renderModelCode(schemaSql, schemaPath, target, includeStatic))
|
||||
@@ -0,0 +1,231 @@
|
||||
import std/[strutils, json, times, parseutils]
|
||||
import db_connector/db_common
|
||||
import query_hooks
|
||||
export db_common
|
||||
|
||||
import baradb/client
|
||||
export client.WireValue, client.FieldKind
|
||||
|
||||
type
|
||||
DbConn* = SyncClient
|
||||
PStmt = object
|
||||
sql: string
|
||||
|
||||
varcharType* = string
|
||||
intType* = int
|
||||
floatType* = float
|
||||
boolType* = bool
|
||||
timestampType* = DateTime
|
||||
serialType* = int
|
||||
jsonType* = JsonNode
|
||||
|
||||
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
proc dbError*(db: DbConn) {.noreturn.} =
|
||||
var e: ref DbError
|
||||
new(e)
|
||||
e.msg = "BaraDB query failed"
|
||||
raise e
|
||||
|
||||
proc prepareStmt*(db: DbConn; q: string): PStmt =
|
||||
when defined(debugOrminTrace):
|
||||
echo "[[Ormin Executing]]: ", q
|
||||
result.sql = q
|
||||
|
||||
template startBindings*(s: PStmt; n: int) {.dirty.} =
|
||||
var pparams: seq[WireValue] = newSeq[WireValue](n)
|
||||
|
||||
template bindParam*(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
|
||||
when t is DateTime:
|
||||
let xx = x.format("yyyy-MM-dd HH:mm:ss")
|
||||
pparams[idx-1] = WireValue(kind: fkString, strVal: $xx)
|
||||
elif t is int or t is int64:
|
||||
pparams[idx-1] = WireValue(kind: fkInt64, int64Val: int64(x))
|
||||
elif t is float or t is float64:
|
||||
pparams[idx-1] = WireValue(kind: fkFloat64, float64Val: float64(x))
|
||||
elif t is bool:
|
||||
pparams[idx-1] = WireValue(kind: fkBool, boolVal: bool(x))
|
||||
elif t is string:
|
||||
pparams[idx-1] = WireValue(kind: fkString, strVal: string(x))
|
||||
elif t is JsonNode:
|
||||
pparams[idx-1] = WireValue(kind: fkJson, jsonVal: $x)
|
||||
else:
|
||||
pparams[idx-1] = WireValue(kind: fkString, strVal: $x)
|
||||
|
||||
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
|
||||
pparams[idx-1] = WireValue(kind: fkNull)
|
||||
|
||||
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
|
||||
t: typedesc) =
|
||||
let x = xx
|
||||
if x.kind == JNull:
|
||||
pparams[idx-1] = WireValue(kind: fkNull)
|
||||
else:
|
||||
bindFromJson(db, s, idx, x, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc) =
|
||||
{.error: "invalid type for JSON object".}
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[string]) =
|
||||
doAssert x.kind == JString
|
||||
pparams[idx-1] = WireValue(kind: fkString, strVal: x.str)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[int|int64]) =
|
||||
doAssert x.kind == JInt
|
||||
pparams[idx-1] = WireValue(kind: fkInt64, int64Val: x.num)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[float64]) =
|
||||
doAssert x.kind == JFloat
|
||||
pparams[idx-1] = WireValue(kind: fkFloat64, float64Val: x.fnum)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[bool]) =
|
||||
doAssert x.kind == JBool
|
||||
pparams[idx-1] = WireValue(kind: fkBool, boolVal: x.bval)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[DateTime]) =
|
||||
doAssert x.kind == JString
|
||||
pparams[idx-1] = WireValue(kind: fkString, strVal: x.str)
|
||||
|
||||
template startQuery*(db: DbConn; s: PStmt) =
|
||||
when declared(pparams):
|
||||
var queryResult {.inject.} = db.query(s.sql, pparams)
|
||||
else:
|
||||
var queryResult {.inject.} = db.query(s.sql)
|
||||
var queryI {.inject.} = -1
|
||||
var queryLen {.inject.} = queryResult.rowCount
|
||||
|
||||
template stopQuery*(db: DbConn; s: PStmt) =
|
||||
discard
|
||||
|
||||
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
|
||||
inc queryI
|
||||
queryI < queryLen
|
||||
|
||||
template getLastId*(db: DbConn; s: PStmt): int =
|
||||
0
|
||||
|
||||
template getAffectedRows*(db: DbConn; s: PStmt): int =
|
||||
queryResult.affectedRows
|
||||
|
||||
proc close*(db: DbConn) =
|
||||
client.close(db)
|
||||
|
||||
proc open*(connection, user, password, database: string): DbConn =
|
||||
let colonPos = connection.find(':')
|
||||
let host = if colonPos < 0: connection else: connection[0..<colonPos]
|
||||
let portStr = if colonPos < 0: "9472" else: connection[colonPos+1..^1]
|
||||
let port = parseInt(portStr)
|
||||
let cfg = ClientConfig(
|
||||
host: host,
|
||||
port: port,
|
||||
database: database,
|
||||
username: user,
|
||||
password: password
|
||||
)
|
||||
result = newSyncClient(cfg)
|
||||
result.connect()
|
||||
|
||||
# --- Result binding helpers ---
|
||||
|
||||
template currentValue(s: PStmt; idx: int): string =
|
||||
queryResult.rows[queryI][idx-1]
|
||||
|
||||
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
|
||||
currentValue(s, idx).len == 0
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
|
||||
t: typedesc; name: string) =
|
||||
dest = int(parseBiggestInt(currentValue(s, idx)))
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
|
||||
t: typedesc; name: string) =
|
||||
dest = parseBiggestInt(currentValue(s, idx))
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
|
||||
t: typedesc; name: string) =
|
||||
let v = currentValue(s, idx)
|
||||
dest = v == "true" or v == "1" or v == "t"
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
|
||||
t: typedesc; name: string) =
|
||||
dest = currentValue(s, idx)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
|
||||
t: typedesc; name: string) =
|
||||
dest = parseFloat(currentValue(s, idx))
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var DateTime;
|
||||
t: typedesc; name: string) =
|
||||
let src = currentValue(s, idx)
|
||||
if src.len > 0:
|
||||
dest = parse(src, "yyyy-MM-dd HH:mm:ss")
|
||||
else:
|
||||
dest = initDateTime(1, 1, 1, 0, 0, 0, utc())
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
dest = parseJson(currentValue(s, idx))
|
||||
|
||||
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
|
||||
t: typedesc; name: string) =
|
||||
let v = currentValue(s, idx)
|
||||
if v.len == 0:
|
||||
dest.isNull = true
|
||||
else:
|
||||
dest.isNull = false
|
||||
when T is string:
|
||||
dest.value = v
|
||||
else:
|
||||
bindResult(db, s, idx, dest.value, t, name)
|
||||
|
||||
template createJObject*(): untyped = newJObject()
|
||||
template createJArray*(): untyped = newJArray()
|
||||
|
||||
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
let x = obj
|
||||
doAssert x.kind == JObject
|
||||
let v = currentValue(s, idx)
|
||||
if v.len == 0:
|
||||
x[name] = newJNull()
|
||||
else:
|
||||
bindToJson(db, s, idx, x, t, name)
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
{.error: "invalid type for JSON object".}
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[string]; name: string) =
|
||||
obj[name] = newJString(currentValue(s, idx))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[int|int64]; name: string) =
|
||||
var v: int64
|
||||
discard parseBiggestInt(currentValue(s, idx), v)
|
||||
obj[name] = newJInt(v)
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[float64]; name: string) =
|
||||
obj[name] = newJFloat(parseFloat(currentValue(s, idx)))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[bool]; name: string) =
|
||||
let v = currentValue(s, idx)
|
||||
obj[name] = newJBool(v == "true" or v == "1" or v == "t")
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[DateTime]; name: string) =
|
||||
var dt: DateTime
|
||||
bindResult(db, s, idx, dt, t, name)
|
||||
obj[name] = newJString(format(dt, jsonTimeFormat))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[JsonNode]; name: string) =
|
||||
obj[name] = parseJson(currentValue(s, idx))
|
||||
@@ -0,0 +1,274 @@
|
||||
import strutils, db_connector/postgres, json, times
|
||||
|
||||
import db_connector/db_common
|
||||
import query_hooks
|
||||
export db_common
|
||||
|
||||
type
|
||||
DbConn* = PPGconn ## encapsulates a database connection
|
||||
PStmt = string ## a identifier for the prepared queries
|
||||
|
||||
varcharType* = string
|
||||
intType* = int
|
||||
floatType* = float
|
||||
boolType* = bool
|
||||
timestampType* = Datetime
|
||||
serialType* = int
|
||||
jsonType* = JsonNode
|
||||
|
||||
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
proc dbError*(db: DbConn) {.noreturn.} =
|
||||
## raises a DbError exception.
|
||||
var e: ref DbError
|
||||
new(e)
|
||||
e.msg = $pqErrorMessage(db)
|
||||
raise e
|
||||
|
||||
proc c_strtod(buf: cstring, endptr: ptr cstring = nil): float64 {.
|
||||
importc: "strtod", header: "<stdlib.h>", noSideEffect.}
|
||||
|
||||
proc c_strtol(buf: cstring, endptr: ptr cstring = nil, base: cint = 10): int {.
|
||||
importc: "strtol", header: "<stdlib.h>", noSideEffect.}
|
||||
|
||||
var sid {.compileTime.}: int
|
||||
|
||||
proc prepareStmt*(db: DbConn; q: string): PStmt =
|
||||
when defined(debugOrminTrace):
|
||||
echo "[[Ormin Executing]]: ", q
|
||||
inc sid
|
||||
result = "ormin" & $sid
|
||||
var res = pqprepare(db, result, q, 0, nil)
|
||||
if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db)
|
||||
|
||||
template startBindings*(s: PStmt; n: int) {.dirty.} =
|
||||
# pparams is a duplicated array to keep the Nim string alive
|
||||
# for the duration of the query. This is safer than relying
|
||||
# on the conservative stack marking:
|
||||
var pparams: array[n, string]
|
||||
var parr: array[n, cstring]
|
||||
|
||||
template bindParam*(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
|
||||
# when not (x is t):
|
||||
# {.error: "type mismatch for query argument at position " & $idx.}
|
||||
when t is DateTime:
|
||||
let xx = x.format("yyyy-MM-dd HH:mm:ss\'.\'ffffffzzzz")
|
||||
pparams[idx-1] = $xx
|
||||
else:
|
||||
pparams[idx-1] = $x
|
||||
|
||||
parr[idx-1] = cstring(pparams[idx-1])
|
||||
|
||||
template bindParamUnchecked(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
|
||||
pparams[idx-1] = $x
|
||||
parr[idx-1] = cstring(pparams[idx-1])
|
||||
|
||||
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
|
||||
parr[idx-1] = cstring(nil)
|
||||
|
||||
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
|
||||
t: typedesc) =
|
||||
let x = xx
|
||||
if x.kind == JNull:
|
||||
# a NULL entry is not reflected in the 'pparams' array:
|
||||
parr[idx-1] = cstring(nil)
|
||||
else:
|
||||
bindFromJson(db, s, idx, x, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc) =
|
||||
{.error: "invalid type for JSON object".}
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[string]) =
|
||||
doAssert x.kind == JString
|
||||
let xs = x.str
|
||||
bindParamUnchecked(db, s, idx, xs, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[int|int64]) =
|
||||
doAssert x.kind == JInt
|
||||
let xi = x.num
|
||||
bindParamUnchecked(db, s, idx, xi, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[float64]) =
|
||||
doAssert x.kind == JFloat
|
||||
let xf = x.fnum
|
||||
bindParamUnchecked(db, s, idx, xf, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[bool]) =
|
||||
doAssert x.kind == JBool
|
||||
let xb = x.bval
|
||||
bindParamUnchecked(db, s, idx, xb, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[DateTime]) =
|
||||
doAssert x.kind == JString
|
||||
let dt = x.str
|
||||
bindParamUnchecked(db, s, idx, dt, t)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
|
||||
t: typedesc; name: string) =
|
||||
dest = c_strtol(pqgetvalue(queryResult, queryI, idx.cint))
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
|
||||
t: typedesc; name: string) =
|
||||
dest = c_strtol(pqgetvalue(queryResult, queryI, idx.cint))
|
||||
|
||||
# XXX This needs testing:
|
||||
template isTrue(x): untyped = x[0] == 't'
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
|
||||
t: typedesc; name: string) =
|
||||
dest = isTrue(pqgetvalue(queryResult, queryI, idx.cint))
|
||||
|
||||
proc fillString(dest: var string; src: cstring; srcLen: int) {.inline.} =
|
||||
dest = ""
|
||||
var i = 0
|
||||
while src[i] != '\0':
|
||||
dest.add src[i]
|
||||
inc i
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
|
||||
t: typedesc; name: string) =
|
||||
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
|
||||
when defined(nimNoNilSeqs):
|
||||
setLen(dest, 0)
|
||||
else:
|
||||
dest = nil
|
||||
else:
|
||||
let src = pqgetvalue(queryResult, queryI, idx.cint)
|
||||
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
|
||||
fillString(dest, src, srcLen)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
|
||||
t: typedesc; name: string) =
|
||||
dest = c_strtod(pqgetvalue(queryResult, queryI, idx.cint))
|
||||
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var DateTime;
|
||||
t: typedesc; name: string) =
|
||||
let
|
||||
src = $pqgetvalue(queryResult, queryI, idx.cint)
|
||||
i = src.find('.')
|
||||
tzflag = src[src.len-3]
|
||||
if i < 0:
|
||||
if tzflag in {'+', '-'}:
|
||||
dest = parse(src, "yyyy-MM-dd HH:mm:sszz")
|
||||
else:
|
||||
dest = parse(src, "yyyy-MM-dd HH:mm:ss")
|
||||
else:
|
||||
if tzflag in {'+', '-'}:
|
||||
let itz = src.len - 3
|
||||
let dtstr = src[0..<itz] & '0'.repeat(10-src.len+i) & src[src.len-3 .. ^1]
|
||||
dest = parse(dtstr, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
|
||||
else:
|
||||
let dtstr = src & '0'.repeat(7-src.len+i)
|
||||
dest = parse(dtstr, "yyyy-MM-dd HH:mm:ss\'.\'ffffff")
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
dest = parseJson($pqgetvalue(queryResult, queryI, idx.cint))
|
||||
|
||||
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
|
||||
t: typedesc; name: string) =
|
||||
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
|
||||
dest.isNull = true
|
||||
else:
|
||||
dest.isNull = false
|
||||
when T is string:
|
||||
let src = pqgetvalue(queryResult, queryI, idx.cint)
|
||||
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
|
||||
fillString(dest.value, src, srcLen)
|
||||
else:
|
||||
bindResult(db, s, idx, dest.value, t, name)
|
||||
|
||||
template createJObject*(): untyped = newJObject()
|
||||
template createJArray*(): untyped = newJArray()
|
||||
|
||||
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
let x = obj
|
||||
doAssert x.kind == JObject
|
||||
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
|
||||
x[name] = newJNull()
|
||||
else:
|
||||
bindToJson(db, s, idx, x, t, name)
|
||||
|
||||
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
|
||||
pqgetisnull(queryResult, queryI, idx.cint) != 0
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
{.error: "invalid type for JSON object".}
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[string]; name: string) =
|
||||
let src = pqgetvalue(queryResult, queryI, idx.cint)
|
||||
obj[name] = newJString($src)
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[int|int64]; name: string) =
|
||||
obj[name] = newJInt(c_strtol(pqgetvalue(queryResult, queryI, idx.cint)))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[float64]; name: string) =
|
||||
obj[name] = newJFloat(c_strtod(pqgetvalue(queryResult, queryI, idx.cint)))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[bool]; name: string) =
|
||||
obj[name] = newJBool(isTrue(pqgetvalue(queryResult, queryI, idx.cint)))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[DateTime]; name: string) =
|
||||
var dt: DateTime
|
||||
bindResult(db, s, idx, dt, t, name)
|
||||
obj[name] = newJString(format(dt, jsonTimeFormat))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[JsonNode]; name: string) =
|
||||
let src = pqgetvalue(queryResult, queryI, idx.cint)
|
||||
obj[name] = parseJson($src)
|
||||
|
||||
template startQuery*(db: DbConn; s: PStmt) =
|
||||
when declared(pparams):
|
||||
var queryResult {.inject.} = pqexecPrepared(db, s, int32(parr.len),
|
||||
cast[cstringArray](addr parr), nil, nil, 0)
|
||||
else:
|
||||
var queryResult {.inject.} = pqexecPrepared(db, s, int32(0),
|
||||
nil, nil, nil, 0)
|
||||
if pqResultStatus(queryResult) == PGRES_COMMAND_OK:
|
||||
discard # insert does not returns data in pg
|
||||
elif pqResultStatus(queryResult) != PGRES_TUPLES_OK: ormin_postgre.dbError(db)
|
||||
var queryI {.inject.} = cint(-1)
|
||||
var queryLen {.inject.} = pqntuples(queryResult)
|
||||
|
||||
template stopQuery*(db: DbConn; s: PStmt) =
|
||||
pqclear(queryResult)
|
||||
|
||||
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
|
||||
inc queryI
|
||||
queryI < queryLen
|
||||
|
||||
template getLastId*(db: DbConn; s: PStmt): int = 0 # XXX to implement
|
||||
|
||||
template getAffectedRows*(db: DbConn; s: PStmt): int =
|
||||
c_strtol(pqcmdTuples(queryResult))
|
||||
|
||||
proc close*(db: DbConn) =
|
||||
## closes the database connection.
|
||||
if db != nil: pqfinish(db)
|
||||
|
||||
proc open*(connection, user, password, database: string): DbConn =
|
||||
## opens a database connection. Raises `DbError` if the connection could not
|
||||
## be established.
|
||||
let
|
||||
colonPos = connection.find(':')
|
||||
host = if colonPos < 0: connection
|
||||
else: substr(connection, 0, colonPos-1)
|
||||
port = if colonPos < 0: ""
|
||||
else: substr(connection, colonPos+1)
|
||||
result = pqsetdbLogin(host, port, nil, nil, database, user, password)
|
||||
if pqStatus(result) != CONNECTION_OK: dbError(result) # result = nil
|
||||
@@ -0,0 +1,295 @@
|
||||
|
||||
{.deadCodeElim: on.}
|
||||
|
||||
import json, times
|
||||
|
||||
import db_connector/db_common
|
||||
import db_connector/sqlite3
|
||||
import query_hooks
|
||||
export db_common
|
||||
|
||||
type
|
||||
DbConn* = PSqlite3 ## encapsulates a database connection
|
||||
|
||||
varcharType* = string
|
||||
blobType* = seq[byte]
|
||||
intType* = int
|
||||
floatType* = float
|
||||
boolType* = bool
|
||||
timestampType* = DateTime
|
||||
jsonType* = JsonNode
|
||||
|
||||
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
proc dbError*(db: DbConn) {.noreturn.} =
|
||||
## raises a DbError exception.
|
||||
var e: ref DbError
|
||||
new(e)
|
||||
e.msg = $sqlite3.errmsg(db)
|
||||
raise e
|
||||
|
||||
proc prepareStmt*(db: DbConn; q: string): PStmt =
|
||||
when defined(debugOrminTrace):
|
||||
echo "[[Ormin Executing]]: ", q
|
||||
if prepare_v2(db, q, q.len.cint, result, nil) != SQLITE_OK:
|
||||
dbError(db)
|
||||
|
||||
template startBindings*(s: PStmt; n: int) =
|
||||
if clear_bindings(s) != SQLITE_OK: dbError(db)
|
||||
|
||||
template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
|
||||
#when not (x is t):
|
||||
# {.error: "type mismatch for query argument at position " & $idx.}
|
||||
when t is blobType:
|
||||
let xs = x
|
||||
let blobPtr = if xs.len == 0:
|
||||
cast[pointer](nil)
|
||||
else:
|
||||
cast[pointer](unsafeAddr(xs[0]))
|
||||
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
|
||||
dbError(db)
|
||||
elif t is int or t is int64 or t is bool:
|
||||
if bind_int64(s, idx.cint, x.int64) != SQLITE_OK: dbError(db)
|
||||
elif t is string:
|
||||
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
|
||||
dbError(db)
|
||||
elif t is float64:
|
||||
if bind_double(s, idx.cint, x) != SQLITE_OK:
|
||||
dbError(db)
|
||||
elif t is DateTime:
|
||||
let xx = if x.nanosecond > 0:
|
||||
x.utc().format("yyyy-MM-dd HH:mm:ss\'.\'fff")
|
||||
else:
|
||||
x.utc().format("yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
|
||||
dbError(db)
|
||||
elif t is JsonNode:
|
||||
let xx = $x
|
||||
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
|
||||
dbError(db)
|
||||
else:
|
||||
{.error: "type mismatch for query argument at position " & $idx.}
|
||||
|
||||
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
|
||||
if bind_null(s, idx.cint) != SQLITE_OK:
|
||||
dbError(db)
|
||||
|
||||
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
|
||||
t: typedesc) =
|
||||
let x = xx
|
||||
if x.kind == JNull:
|
||||
if bind_null(s, idx.cint) != SQLITE_OK: dbError(db)
|
||||
else:
|
||||
bindFromJson(db, s, idx, x, t)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc) =
|
||||
{.error: "invalid type for JSON object".}
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[string]) =
|
||||
doAssert x.kind == JString
|
||||
let xs = x.str
|
||||
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
|
||||
dbError(db)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[int|int64]) =
|
||||
doAssert x.kind == JInt
|
||||
let xi = x.num
|
||||
if bind_int64(s, idx.cint, xi.int64) != SQLITE_OK: dbError(db)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[float64]) =
|
||||
doAssert x.kind == JFloat
|
||||
let xf = x.fnum
|
||||
if bind_double(s, idx.cint, xf) != SQLITE_OK:
|
||||
dbError(db)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[bool]) =
|
||||
doAssert x.kind == JBool
|
||||
let xb = x.bval
|
||||
if bind_int64(s, idx.cint, xb.int64) != SQLITE_OK: dbError(db)
|
||||
|
||||
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
|
||||
t: typedesc[DateTime]) =
|
||||
doAssert x.kind == JString
|
||||
let
|
||||
dtStr = x.str
|
||||
i = dtStr.find('.')
|
||||
dt = if i > 0 and dtStr[i+1 .. ^1] == "000":
|
||||
dtStr[0 ..< i]
|
||||
else:
|
||||
dtStr
|
||||
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
|
||||
dbError(db)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
|
||||
t: typedesc; name: string) =
|
||||
dest = int column_int64(s, idx.cint)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
|
||||
t: typedesc; name: string) =
|
||||
dest = column_int64(s, idx.cint)
|
||||
|
||||
proc fillString(dest: var string; src: cstring; srcLen: int) =
|
||||
when defined(nimNoNilSeqs):
|
||||
setLen(dest, srcLen)
|
||||
else:
|
||||
if dest.isNil: dest = newString(srcLen)
|
||||
else: setLen(dest, srcLen)
|
||||
if srcLen > 0:
|
||||
copyMem(unsafeAddr(dest[0]), src, srcLen)
|
||||
|
||||
proc fillBytes(dest: var seq[byte]; src: pointer; srcLen: int) =
|
||||
when defined(nimNoNilSeqs):
|
||||
setLen(dest, srcLen)
|
||||
else:
|
||||
if dest.isNil: dest = newSeq[byte](srcLen)
|
||||
else: setLen(dest, srcLen)
|
||||
if srcLen > 0:
|
||||
copyMem(unsafeAddr(dest[0]), src, srcLen)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
|
||||
t: typedesc; name: string) =
|
||||
if column_type(s, idx.cint) == SQLITE_NULL:
|
||||
when defined(nimNoNilSeqs):
|
||||
setLen(dest, 0)
|
||||
else:
|
||||
dest = nil
|
||||
else:
|
||||
let srcLen = column_bytes(s, idx.cint)
|
||||
let src = column_text(s, idx.cint)
|
||||
fillString(dest, src, srcLen)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var blobType;
|
||||
t: typedesc; name: string) =
|
||||
let srcLen = column_bytes(s, idx.cint)
|
||||
let src = column_blob(s, idx.cint)
|
||||
fillBytes(dest, src, srcLen)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
|
||||
t: typedesc; name: string) =
|
||||
dest = column_double(s, idx.cint)
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
|
||||
t: typedesc; name: string) =
|
||||
dest = column_int64(s, idx.cint) != 0
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: DateTime;
|
||||
t: typedesc; name: string) =
|
||||
let
|
||||
src = $column_text(s, idx.cint)
|
||||
i = src.find('.')
|
||||
if i < 0:
|
||||
dest = parse(src, "yyyy-MM-dd HH:mm:ss", utc())
|
||||
else:
|
||||
dest = parse(src, "yyyy-MM-dd HH:mm:ss\'.\'fff", utc())
|
||||
|
||||
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
let src = column_text(s, idx.cint)
|
||||
dest = parseJson($src)
|
||||
|
||||
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
|
||||
t: typedesc; name: string) =
|
||||
if column_type(s, idx.cint) == SQLITE_NULL:
|
||||
dest.isNull = true
|
||||
else:
|
||||
dest.isNull = false
|
||||
when T is string:
|
||||
let srcLen = column_bytes(s, idx.cint)
|
||||
let src = column_text(s, idx.cint)
|
||||
fillString(dest.value, src, srcLen)
|
||||
else:
|
||||
bindResult(db, s, idx, dest.value, t, name)
|
||||
|
||||
template createJObject*(): untyped = newJObject()
|
||||
template createJArray*(): untyped = newJArray()
|
||||
|
||||
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
let x = obj
|
||||
doAssert x.kind == JObject
|
||||
if column_type(s, idx.cint) == SQLITE_NULL:
|
||||
x[name] = newJNull()
|
||||
else:
|
||||
bindToJson(db, s, idx, x, t, name)
|
||||
|
||||
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
|
||||
column_type(s, idx.cint) == SQLITE_NULL
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc; name: string) =
|
||||
{.error: "invalid type for JSON object".}
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[string]; name: string) =
|
||||
let dest = newJString("")
|
||||
let srcLen = column_bytes(s, idx.cint)
|
||||
let src = column_text(s, idx.cint)
|
||||
fillString(dest.str, src, srcLen)
|
||||
obj[name] = dest
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[int|int64]; name: string) =
|
||||
obj[name] = newJInt(column_int64(s, idx.cint))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[blobType]; name: string) =
|
||||
let srcLen = column_bytes(s, idx.cint)
|
||||
let src = column_blob(s, idx.cint)
|
||||
var data: blobType
|
||||
fillBytes(data, src, srcLen)
|
||||
let arr = newJArray()
|
||||
for b in data:
|
||||
arr.add newJInt(int(b))
|
||||
obj[name] = arr
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[float64]; name: string) =
|
||||
obj[name] = newJFloat(column_double(s, idx.cint))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[bool]; name: string) =
|
||||
obj[name] = newJBool(column_int64(s, idx.cint) != 0)
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[DateTime]; name: string) =
|
||||
var dt: DateTime
|
||||
bindResult(db, s, idx, dt, t, name)
|
||||
obj[name] = newJString(format(dt, jsonTimeFormat))
|
||||
|
||||
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
|
||||
t: typedesc[JsonNode]; name: string) =
|
||||
let src = column_text(s, idx.cint)
|
||||
obj[name] = parseJson($src)
|
||||
|
||||
template startQuery*(db: DbConn; s: PStmt) = discard "nothing to do"
|
||||
|
||||
template stopQuery*(db: DbConn; s: PStmt) =
|
||||
if sqlite3.reset(s) != SQLITE_OK: dbError(db)
|
||||
|
||||
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
|
||||
when returnsData:
|
||||
step(s) == SQLITE_ROW
|
||||
else:
|
||||
step(s) == SQLITE_DONE
|
||||
|
||||
template getLastId*(db: DbConn; s: PStmt): int =
|
||||
int(last_insert_rowid(db))
|
||||
|
||||
template getAffectedRows*(db: DbConn; s: PStmt): int =
|
||||
int(changes(db))
|
||||
|
||||
proc close*(db: DbConn) =
|
||||
## closes the database connection.
|
||||
if sqlite3.close(db) != SQLITE_OK: dbError(db)
|
||||
|
||||
proc open*(connection, user, password, database: string): DbConn =
|
||||
## opens a database connection. Raises `EDb` if the connection could not
|
||||
## be established. Only the ``connection`` parameter is used for ``sqlite``.
|
||||
if sqlite3.open(connection, result) != SQLITE_OK:
|
||||
dbError(result)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
import options, json
|
||||
|
||||
type
|
||||
DbValue*[T] = object
|
||||
isNull*: bool
|
||||
value*: T
|
||||
|
||||
template fromQueryHook*[T, S](val: var T, x: S) =
|
||||
## Default conversion hook used by `query(T): ...`.
|
||||
## Users can overload this proc to customize field/type conversions.
|
||||
val = x
|
||||
|
||||
template toQueryHook*[T, S](val: var T, x: S) =
|
||||
## Default conversion hook used for query parameters.
|
||||
## Users can overload this proc to customize parameter conversions.
|
||||
val = x
|
||||
|
||||
proc nullQueryValueError() {.noreturn.} =
|
||||
raise newException(ValueError, "cannot map NULL query result")
|
||||
|
||||
proc fromQueryHook*[T, S](val: var Option[T], x: var DbValue[S]) =
|
||||
if x.isNull:
|
||||
val = none(T)
|
||||
else:
|
||||
var converted: T
|
||||
fromQueryHook(converted, move x.value)
|
||||
val = some(converted)
|
||||
|
||||
proc fromQueryHook*[T, S](val: var T, x: var DbValue[S]) =
|
||||
if x.isNull:
|
||||
when T is string:
|
||||
val = ""
|
||||
elif T is JsonNode:
|
||||
val = newJNull()
|
||||
else:
|
||||
nullQueryValueError()
|
||||
else:
|
||||
fromQueryHook(val, move x.value)
|
||||
|
||||
proc bindFromQueryHook*[T, S](dest: var T, x: var DbValue[S]) =
|
||||
fromQueryHook(dest, x)
|
||||
|
||||
proc toQueryHook*[S, T](val: var DbValue[S], x: Option[T]) =
|
||||
if x.isSome:
|
||||
val.isNull = false
|
||||
toQueryHook(val.value, x.get)
|
||||
else:
|
||||
val.isNull = true
|
||||
when compiles(val.value = default(S)):
|
||||
val.value = default(S)
|
||||
|
||||
proc toQueryHook*[S, T](val: var DbValue[S], x: T) =
|
||||
val.isNull = false
|
||||
toQueryHook(val.value, x)
|
||||
@@ -0,0 +1,139 @@
|
||||
|
||||
import asynchttpserver, asyncdispatch, asyncnet, json,
|
||||
strutils, times
|
||||
|
||||
import websocket
|
||||
|
||||
type
|
||||
Receivers* {.pure} = enum
|
||||
sender, allExceptSender, all
|
||||
ReqHandler* = proc (msg: JsonNode; receivers: var Receivers): JsonNode {.
|
||||
closure, gcsafe.}
|
||||
|
||||
proc error(msg: string) = echo "[Error] ", msg
|
||||
proc warn(msg: string) = echo "[Warning] ", msg
|
||||
proc hint(msg: string) = echo "[Hint] ", msg
|
||||
|
||||
type
|
||||
Client = ref object
|
||||
socket: AsyncWebSocket
|
||||
connected: bool
|
||||
hostname: string
|
||||
id: int
|
||||
lastMessage: float
|
||||
rapidMessageCount: int
|
||||
|
||||
Server = ref object
|
||||
clients: seq[Client]
|
||||
needsUpdate: bool
|
||||
receivers: Receivers
|
||||
msgs: seq[(string, int)]
|
||||
gid: int
|
||||
handler: ReqHandler
|
||||
|
||||
proc newClient(s: Server; socket: AsyncWebSocket, hostname: string): Client =
|
||||
inc s.gid
|
||||
result = Client(socket: socket, connected: true, hostname: hostname, id: s.gid)
|
||||
|
||||
proc `$`(client: Client): string =
|
||||
"Client(ip: $1)" % [client.hostname]
|
||||
|
||||
proc updateClients(server: Server) {.async.} =
|
||||
while true:
|
||||
var needsUpdate = false
|
||||
for client in server.clients:
|
||||
if not client.connected:
|
||||
needsUpdate = true
|
||||
break
|
||||
|
||||
server.needsUpdate = server.needsUpdate or needsUpdate
|
||||
if server.needsUpdate and server.msgs.len != 0:
|
||||
var someDead = false
|
||||
# perform a copy to prevent the race condition:
|
||||
var msgs = server.msgs
|
||||
setLen(server.msgs, 0)
|
||||
for m in msgs:
|
||||
for c in server.clients:
|
||||
if c.connected:
|
||||
# do not send a broadcast message to the one which sent it:
|
||||
if server.receivers != Receivers.allExceptSender or c.id != m[1]:
|
||||
await c.socket.sendText(m[0]) #, false)
|
||||
else:
|
||||
someDead = true
|
||||
if someDead:
|
||||
var i = 0
|
||||
while i < server.clients.len:
|
||||
if not server.clients[i].connected: del(server.clients, i)
|
||||
else: inc i
|
||||
server.needsUpdate = false
|
||||
# let other stuff in the main loop run:
|
||||
await sleepAsync(10)
|
||||
|
||||
proc processMessage(server: Server, client: Client, data: string) {.async.} =
|
||||
# Check if last message was relatively recent. If so, kick the user.
|
||||
echo "processMessage ", data
|
||||
let now = epochTime()
|
||||
if now - client.lastMessage < 0.1: # 100ms
|
||||
client.rapidMessageCount.inc
|
||||
else:
|
||||
client.rapidMessageCount = 0
|
||||
|
||||
client.lastMessage = now
|
||||
if client.rapidMessageCount > 50:
|
||||
warn("Client ($1) is firing messages too rapidly. Killing." % $client)
|
||||
client.connected = false
|
||||
let msgj = parseJson(data)
|
||||
server.receivers = Receivers.sender
|
||||
if msgj.hasKey("msg") and msgj["msg"].getStr("") == "disconnect":
|
||||
client.connected = false
|
||||
server.needsUpdate = true
|
||||
else:
|
||||
let resp = server.handler(msgj, server.receivers)
|
||||
if not resp.isNil:
|
||||
if server.receivers == sender:
|
||||
await client.socket.sendText($resp) #, false)
|
||||
else:
|
||||
server.msgs.add(($resp, client.id))
|
||||
server.needsUpdate = true
|
||||
|
||||
proc processClient(server: Server, client: Client) {.async.} =
|
||||
while client.connected:
|
||||
var frameFut = client.socket.readData()
|
||||
yield frameFut
|
||||
if frameFut.failed:
|
||||
error("Error occurred handling client messages.\n" &
|
||||
frameFut.error.msg)
|
||||
client.connected = false
|
||||
break
|
||||
|
||||
let frame = frameFut.read()
|
||||
if frame.opcode == Opcode.Text:
|
||||
let processFut = processMessage(server, client, frame.data)
|
||||
if processFut.failed:
|
||||
echo processFut.error.getStackTrace()
|
||||
error("Client ($1) attempted to send bad JSON? " % $client & "\n" &
|
||||
processFut.error.msg)
|
||||
#client.connected = false
|
||||
await client.socket.close()
|
||||
|
||||
proc onRequest(server: Server, req: Request; key: string) {.async.} =
|
||||
let (ws, error) = await verifyWebsocketRequest(req, key)
|
||||
if ws != nil:
|
||||
hint("Client connected from " & req.hostname)
|
||||
ws.protocol = key
|
||||
server.clients.add(newClient(server, ws, req.hostname))
|
||||
asyncCheck processClient(server, server.clients[^1])
|
||||
else:
|
||||
warn("WS negotiation failed: " & $error)
|
||||
await req.respond(Http400, "WebSocket negotiation failed: " & $error)
|
||||
req.client.close()
|
||||
|
||||
proc serve*(key: string; handler: ReqHandler) =
|
||||
let httpServer = newAsyncHttpServer()
|
||||
let server = Server(clients: @[], msgs: @[], handler: handler, gid: 0)
|
||||
|
||||
proc cb(req: Request): Future[void] {.async, gcsafe.} =
|
||||
await onRequest(server, req, key)
|
||||
|
||||
asyncCheck updateClients(server)
|
||||
waitFor httpServer.serve(Port(8080), cb)
|
||||
Reference in New Issue
Block a user