Files
Baradb/clients/nim/ormin/ormin/queries.nim
T
dimgigov 4e54075078
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
Add Ormin ORM support for BaraDB (Nim client)
2026-05-15 21:49:08 +03:00

2094 lines
69 KiB
Nim

when not declared(tableNames):
{.macros.error: "The query DSL requires a tableNames const.".}
when not declared(attributes):
{.macros.error: "The query DSL requires a attributes const.".}
import macros, strutils
import db_connector/db_common
from os import parentDir, `/`
import db_types
import query_hooks
# SQL dialect specific things:
const
equals = "="
nequals = "<>"
type
Function* = object
name: string
arity: int # -1 for 'varargs'
typ: DbTypeKind # if dbUnknown, use type of the last argument
SourceColumn = object
name: string
typ: DbType
CteDef = object
name: string
sql: string
cols: seq[SourceColumn]
proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJson: bool): NimNode
var
functions {.compileTime.} = @[
Function(name: "count", arity: 1, typ: dbInt),
Function(name: "coalesce", arity: -1, typ: dbUnknown),
Function(name: "min", arity: 1, typ: dbUnknown),
Function(name: "max", arity: 1, typ: dbUnknown),
Function(name: "avg", arity: 1, typ: dbFloat),
Function(name: "sum", arity: 1, typ: dbUnknown),
Function(name: "row_number", arity: 0, typ: dbInt),
Function(name: "rank", arity: 0, typ: dbInt),
Function(name: "dense_rank", arity: 0, typ: dbInt),
Function(name: "percent_rank", arity: 0, typ: dbFloat),
Function(name: "cume_dist", arity: 0, typ: dbFloat),
Function(name: "ntile", arity: 1, typ: dbInt),
Function(name: "isnull", arity: 3, typ: dbUnknown),
Function(name: "concat", arity: -1, typ: dbVarchar),
Function(name: "abs", arity: 1, typ: dbUnknown),
Function(name: "length", arity: 1, typ: dbInt),
Function(name: "lower", arity: 1, typ: dbVarchar),
Function(name: "upper", arity: 1, typ: dbVarchar),
Function(name: "replace", arity: 3, typ: dbVarchar)
]
proc isVarargsType(n: NimNode): bool {.compileTime.} =
## Checks if the provided type node encodes a varargs parameter.
case n.kind
of nnkBracketExpr:
if n.len > 0:
let head = n[0]
if head.kind in {nnkIdent, nnkSym} and head.strVal == "varargs":
return true
else:
discard
result = false
proc typeNodeToDbKind(n: NimNode): DbTypeKind {.compileTime.} =
## Maps the return type of an imported SQL function to DbTypeKind.
if n.kind == nnkEmpty:
return dbUnknown
if isVarargsType(n):
if n.len > 1:
return typeNodeToDbKind(n[1])
return dbUnknown
let name =
case n.kind
of nnkSym, nnkIdent:
n.strVal
else:
$n
result = dbTypFromName(name)
if result == dbUnknown and name.len > 4 and name.endsWith("Type"):
result = dbTypFromName(name[0..^5])
proc registerImportSqlFunction(name: string; arity: int; typ: DbTypeKind) {.compileTime.} =
## Adds or updates a Function descriptor for vendor specific SQL routines.
let lname = name.toLowerAscii()
for f in mitems(functions):
if f.name.toLowerAscii() == lname and f.arity == arity:
f.typ = typ
return
let f = Function(name: name, arity: arity, typ: typ)
when defined(debugOrminDsl):
echo "registerImportSqlFunction: ", f
functions.add f
macro importSql*(n: typed): untyped =
## Registers a Nim proc as callable SQL function within the query DSL.
if n.kind notin {nnkProcDef, nnkFuncDef}:
macros.error("{.importSql.} can only be applied to proc or func definitions", n)
let params = n[3]
expectKind(params, nnkFormalParams)
var paramCount = 0
var hasVarargs = false
for i in 1..<params.len:
let identDefs = params[i]
expectKind(identDefs, nnkIdentDefs)
if identDefs.len < 3:
macros.error("unexpected parameter definition in {.importSql.}", identDefs)
let defaultValue = identDefs[^1]
if defaultValue.kind != nnkEmpty:
macros.error("{.importSql.} procs cannot declare default values", defaultValue)
let typeNode = identDefs[^2]
if isVarargsType(typeNode):
if hasVarargs:
macros.error("{.importSql.} supports only a single varargs parameter", identDefs)
if params.len > 2:
macros.error("varargs parameters cannot be combined with fixed parameters", identDefs)
if identDefs.len - 2 != 1:
macros.error("varargs parameter must declare exactly one identifier", identDefs)
hasVarargs = true
else:
paramCount += identDefs.len - 2
let arity = if hasVarargs: -1 else: paramCount
let fnName = n[0].strVal
let retKind = typeNodeToDbKind(params[0])
registerImportSqlFunction(fnName, arity, retKind)
result = newStmtList()
type
Env = seq[(int, string)]
Params = seq[tuple[ex, typ: NimNode; isJson: bool]]
QueryKind = enum
qkNone,
qkSelect,
qkJoin,
qkInsert,
qkReplace,
qkUpdate,
qkDelete,
qkInsertReturning
QueryBuilder = ref object
head, fromm, join, values, where, groupby, having, orderby: string
limit, offset, returning, onConflict, onConflictWhere: string
env: Env
ctes: seq[CteDef]
cteBase: int
kind: QueryKind
retType: NimNode
singleRow, retTypeIsJson: bool
retNames: seq[string]
params: Params
coln, qmark, aliasGen: int
colAliases: seq[(string, DbType)]
# Track inserted column values for potential SQLite RETURNING handling
insertedValues: seq[(string, NimNode)]
# For SQLite: expression to return instead of last_insert_rowid()
retExpr: NimNode
onConflictTargetSet, onConflictActionSet, onConflictIsDoUpdate, onConflictWhereSet: bool
# Execute a non-row SQL statement strictly (errors on failure)
template execNoRowsStrict*(sqlStmt: string) =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
let s {.gensym.} = prepareStmt(db, sqlStmt)
startQuery(db, s)
if stepQuery(db, s, false):
stopQuery(db, s)
else:
stopQuery(db, s)
dbError(db)
# Execute a non-row SQL statement, relying on startQuery to raise on failure
template execNoRowsLoose(sqlStmt: string) =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", sqlStmt
let s {.gensym.} = prepareStmt(db, sqlStmt)
startQuery(db, s)
discard stepQuery(db, s, false)
stopQuery(db, s)
proc newQueryBuilder(): QueryBuilder {.compileTime.} =
QueryBuilder(head: "", fromm: "", join: "", values: "", where: "",
groupby: "", having: "", orderby: "", limit: "", offset: "",
returning: "", onConflict: "", onConflictWhere: "",
env: @[], ctes: @[], cteBase: 0, kind: qkNone, params: @[],
retType: newNimNode(nnkTupleTy), singleRow: false,
retTypeIsJson: false, retNames: @[],
coln: 0, qmark: 0, aliasGen: 1, colAliases: @[],
insertedValues: @[], retExpr: newEmptyNode(),
onConflictTargetSet: false, onConflictActionSet: false,
onConflictIsDoUpdate: false, onConflictWhereSet: false)
proc getAlias(q: QueryBuilder; tabIndex: int): string =
result = tableNames[tabIndex][0] & $q.aliasGen
inc q.aliasGen
proc placeholder(q: QueryBuilder): string =
when dbBackend == DbBackend.postgre:
inc q.qmark
result = "$" & $q.qmark
else:
result = "?"
proc cteEnvIndex(i: int): int {.compileTime.} = tableNames.len + i
proc isCteEnvIndex(i: int): bool {.compileTime.} = i >= tableNames.len
proc fromCteEnvIndex(i: int): int {.compileTime.} = i - tableNames.len
proc lookupCte(ctes: openArray[CteDef]; name: string): int {.compileTime.} =
result = -1
for i, cte in ctes:
if cmpIgnoreCase(cte.name, name) == 0:
return i
proc sourceName(q: QueryBuilder; source: int): string {.compileTime.} =
if isCteEnvIndex(source):
result = q.ctes[fromCteEnvIndex(source)].name
else:
result = tableNames[source]
proc sourceColumns(q: QueryBuilder; source: int): seq[SourceColumn] {.compileTime.} =
if isCteEnvIndex(source):
result = q.ctes[fromCteEnvIndex(source)].cols
else:
for a in attributes:
if a.tabIndex == source:
result.add SourceColumn(name: a.name, typ: DbType(kind: a.typ))
proc sourceLookup(q: QueryBuilder; table: string): int {.compileTime.} =
for i, t in tableNames:
if cmpIgnoreCase(t, table) == 0:
return i
let cteIdx = lookupCte(q.ctes, table)
if cteIdx >= 0:
return cteEnvIndex(cteIdx)
result = -1
proc sourceAlias(q: QueryBuilder; source: int; sourceName: string): string {.compileTime.} =
if q.kind == qkJoin and q.env.len > 0 and q.env[^1][0] == source:
result = q.env[^1][1]
elif isCteEnvIndex(source):
result = sourceName.toLowerAscii() & $q.aliasGen
inc q.aliasGen
else:
result = q.getAlias(source)
proc joinKeyword(kind: string): string {.compileTime.} =
case kind.toLowerAscii()
of "join", "innerjoin":
"inner join "
of "outerjoin":
"outer join "
of "leftjoin", "leftouterjoin":
"left outer join "
of "rightjoin", "rightouterjoin":
"right outer join "
of "fulljoin", "fullouterjoin":
"full outer join "
of "crossjoin":
"cross join "
else:
""
proc lookup(table, attr: string; qb: QueryBuilder; alias: var string): DbType =
var found = false
var foundSource = -1
for e in qb.env:
if table.len == 0 or cmpIgnoreCase(sourceName(qb, e[0]), table) == 0:
for col in sourceColumns(qb, e[0]):
if cmpIgnoreCase(col.name, attr) == 0:
if found:
if foundSource != e[0] or alias != e[1]:
return DbType(kind: dbUnknown)
found = true
foundSource = e[0]
alias = e[1]
result = col.typ
proc lookup(table, attr: string; qb: QueryBuilder): DbType =
var alias: string
result = lookup(table, attr, qb, alias)
proc lookup(table: openArray[string], name: string): int =
result = -1
for i, t in table:
if cmpIgnoreCase(t, name) == 0:
return i
proc autoJoin(join: var string, src: (int, string), dest: int;
destAlias: string): bool =
var srcCol = -1
var destCol = -1
for i, a in attributes:
if a.tabIndex == src[0] and a.key < 0 and attributes[-a.key - 1].tabIndex == dest:
if srcCol < 0:
srcCol = i
destCol = -a.key - 1
else:
return false
if srcCol >= 0:
join.add " on "
join.add destAlias
join.add "."
join.add attributes[destCol].name
join.add equals
join.add src[1]
join.add "."
join.add attributes[srcCol].name
result = true
proc `$`(a: DbType): string = $a.kind
proc checkBool(a: DbType; n: NimNode) =
if a.kind != dbBool:
macros.error "expected type 'bool', but got: " & $a, n
proc checkInt(a: DbType; n: NimNode) =
if a.kind notin {dbInt, dbSerial}:
macros.error "expected type 'int', but got: " & $a, n
proc checkCompatible(a, b: DbType; n: NimNode) =
# Treat serial and int as compatible
if not (a.kind == b.kind or (a.kind == dbSerial and b.kind == dbInt) or (a.kind == dbInt and b.kind == dbSerial)):
macros.error "incompatible types: " & $a & " and " & $b, n
proc checkCompatibleSet(a, b: DbType; n: NimNode) =
discard "too implement; might require a richer type system"
proc toNimType(t: DbTypeKind): NimNode {.compileTime.} =
let name = ($t).substr(2).toLowerAscii & "Type"
result = ident(name)
proc toNimType(t: DbType): NimNode {.compileTime.} = toNimType(t.kind)
proc escIdent(dest: var string; src: string) =
if allCharsInSet(src, {'\33'..'\127'}):
dest.add(src)
else:
dest.add("\"" & replace(src, "\"", "\"\"") & "\"")
proc fmtTableList(tableNames: openArray[string]): string =
result = ""
for i, t in tableNames:
if i > 10: break
if i > 0: result.add ", "
result.add t
proc nodeName(n: NimNode): string {.compileTime.} =
case n.kind
of nnkIdent, nnkSym:
result = n.strVal
of nnkAccQuoted:
if n.len == 1:
result = nodeName(n[0])
else:
result = ""
else:
result = ""
proc isQueryClause(name: string): bool {.compileTime.} =
case name.toLowerAscii()
of "with", "select", "distinct", "insert", "update", "replace", "delete",
"where", "join", "innerjoin", "outerjoin", "leftjoin", "leftouterjoin",
"rightjoin", "rightouterjoin", "fulljoin", "fullouterjoin", "crossjoin",
"groupby", "orderby", "having", "limit", "offset", "returning", "produce",
"onconflict", "donothing", "doupdate":
result = true
else:
result = false
proc isSetOpName(name: string): bool {.compileTime.} =
case name.toLowerAscii()
of "union", "intersect", "except":
result = true
else:
result = false
proc isSetOpCall(n: NimNode): bool {.compileTime.} =
n.kind == nnkCall and isSetOpName(nodeName(n[0]))
proc isNullLiteral(n: NimNode): bool {.compileTime.} =
case n.kind
of nnkNilLit:
result = true
of nnkIdent, nnkSym:
result = cmpIgnoreCase(n.strVal, "null") == 0
else:
result = false
proc peelTrailingCommand(n: NimNode): tuple[core, tail: NimNode] {.compileTime.} =
if n.kind == nnkCommand and n.len == 2 and n[1].kind == nnkCommand and
nodeName(n[1][0]) == "on":
return (copyNimTree(n), newEmptyNode())
if n.kind == nnkCommand and n.len == 2 and n[1].kind == nnkCommand and
nodeName(n[1][0]).toLowerAscii() in ["like", "ilike"]:
return (copyNimTree(n), newEmptyNode())
if n.kind == nnkCommand and n.len == 2 and n[1].kind == nnkCommand and
not isQueryClause(nodeName(n[0])) and not isSetOpName(nodeName(n[0])):
return (copyNimTree(n[0]), copyNimTree(n[1]))
if (n.kind == nnkCommand and (isQueryClause(nodeName(n[0])) or isSetOpName(nodeName(n[0])))) or
isSetOpCall(n):
return (copyNimTree(n), newEmptyNode())
if n.len > 0:
let idx = n.len - 1
let peeled = peelTrailingCommand(n[idx])
if peeled.tail.kind != nnkEmpty:
result.core = copyNimTree(n)
result.core[idx] = peeled.core
result.tail = peeled.tail
return result
result = (copyNimTree(n), newEmptyNode())
proc flattenQueryCommands(n: NimNode; parts: var seq[NimNode]) {.compileTime.} =
case n.kind
of nnkStmtList:
for it in n:
flattenQueryCommands(it, parts)
of nnkCall:
let name = nodeName(n[0])
if isQueryClause(name):
var cmd = newNimNode(nnkCommand)
for it in n:
cmd.add copyNimTree(it)
parts.add cmd
else:
parts.add copyNimTree(n)
of nnkCommand:
var cmd = copyNimTree(n)
if cmd.len >= 2:
let idx = cmd.len - 1
let peeled = peelTrailingCommand(cmd[idx])
cmd[idx] = peeled.core
parts.add cmd
if peeled.tail.kind != nnkEmpty:
flattenQueryCommands(peeled.tail, parts)
else:
parts.add cmd
else:
parts.add copyNimTree(n)
proc queryh(n: NimNode; q: QueryBuilder)
proc queryAsString(q: QueryBuilder, n: NimNode): string
proc applyQueryNode(n: NimNode; q: QueryBuilder)
proc renderInlineQuery(n: NimNode; params: var Params;
qb: QueryBuilder): tuple[sql: string, typ: DbType]
proc cond(n: NimNode; q: var string; params: var Params;
expected: DbType, qb: QueryBuilder): DbType
proc renderWindowClause(n: NimNode; q: var string; params: var Params;
qb: QueryBuilder) {.compileTime.} =
let op = nodeName(n[0]).toLowerAscii()
case op
of "partitionby":
q.add "partition by "
for i in 1..<n.len:
discard cond(n[i], q, params, DbType(kind: dbUnknown), qb)
if i < n.len - 1: q.add ", "
of "orderby":
q.add "order by "
for i in 1..<n.len:
discard cond(n[i], q, params, DbType(kind: dbUnknown), qb)
if i < n.len - 1: q.add ", "
else:
macros.error "unsupported window clause: " & op, n
proc lookupColumnInEnv(n: NimNode; q: var string; params: var Params;
expected: DbType, qb: QueryBuilder): DbType =
expectKind(n, nnkIdent)
let name = $n
block checkAliases:
for a in qb.colAliases:
if cmpIgnoreCase(a[0], name) == 0:
result = a[1]
break checkAliases
var alias: string
result = lookup("", name, qb, alias)
if result.kind == dbUnknown:
macros.error "unknown column name: " & name, n
elif qb.kind in {qkSelect, qkJoin}:
doAssert alias.len >= 0
q.add alias
q.add '.'
escIdent(q, name)
proc cond(n: NimNode; q: var string; params: var Params;
expected: DbType, qb: QueryBuilder): DbType =
case n.kind
of nnkIdent:
let name = $n
if name == "_":
q.add "*"
result = DbType(kind: dbUnknown)
elif cmpIgnoreCase(name, "null") == 0:
q.add "NULL"
result = expected
else:
result = lookupColumnInEnv(n, q, params, expected, qb)
of nnkDotExpr:
let t = $n[0]
let a = $n[1]
escIdent(q, t)
q.add '.'
escIdent(q, a)
result = lookup(t, a, qb)
of nnkPar, nnkStmtListExpr:
if n.len == 1:
q.add "("
result = cond(n[0], q, params, expected, qb)
q.add ")"
else:
macros.error "tuple construction not allowed here", n
of nnkCurly:
q.add "("
let a = cond(n[0], q, params, expected, qb)
for i in 1..<n.len:
q.add ", "
let b = cond(n[i], q, params, a, qb)
checkCompatible(a, b, n[i])
q.add ")"
result = DbType(kind: dbSet)
of nnkNilLit:
q.add "NULL"
result = expected
of nnkDistinctTy:
q.add "distinct "
result = cond(n[0], q, params, expected, qb)
of nnkStrLit, nnkRStrLit, nnkTripleStrLit:
result = expected
if result.kind == dbUnknown:
# macros.error "cannot infer the type of the literal", n
result.kind = dbVarchar
if result.kind == dbBlob:
# For SQL string literals, single quotes must be doubled.
# Using strutils.escape would introduce backslashes which are
# not valid escapes in standard SQL/SQLite. So replace `'` with `''`.
q.add("b'" & n.strVal.replace("'", "''") & "'")
else:
# Standard SQL quoting for text values
q.add("'" & n.strVal.replace("'", "''") & "'")
of nnkIntLit..nnkInt64Lit:
result = expected
if result.kind == dbUnknown:
result.kind = dbInt
q.add $n.intVal
of nnkFloatLit:
result = expected
if result.kind == dbUnknown:
result.kind = dbFloat
q.add $n.floatVal
of nnkInfix:
let op = $n[0]
case op
of "and", "or":
result = DbType(kind: dbBool)
let a = cond(n[1], q, params, result, qb)
checkBool a, n[1]
q.add ' '
q.add op
q.add ' '
let b = cond(n[2], q, params, result, qb)
checkBool b, n[2]
of "<=", "<", ">=", ">", "==", "!=", "=~":
if isNullLiteral(n[1]) or isNullLiteral(n[2]):
if op != "==" and op != "!=":
macros.error "NULL comparisons only support == and !=", n
if isNullLiteral(n[1]) and isNullLiteral(n[2]):
macros.error "NULL cannot be compared against NULL", n
let target = if isNullLiteral(n[1]): n[2] else: n[1]
discard cond(target, q, params, DbType(kind: dbUnknown), qb)
if op == "==":
q.add " is NULL"
else:
q.add " is not NULL"
else:
let env = qb.env
if env.len == 2:
qb.env = @[env[0]]
let a = cond(n[1], q, params, DbType(kind: dbUnknown), qb)
q.add ' '
if op == "==": q.add equals
elif op == "!=": q.add nequals
elif op == "=~": q.add "like"
else: q.add op
q.add ' '
if env.len == 2:
qb.env = @[env[1]]
let b = cond(n[2], q, params, a, qb)
checkCompatible a, b, n
result = DbType(kind: dbBool)
of "in", "notin":
let a = cond(n[1], q, params, DbType(kind: dbUnknown), qb)
if n[2].kind == nnkInfix and $n[2][0] == "..":
if op == "in": q.add " between "
else: q.add " not between "
let r = n[2]
let b = cond(r[1], q, params, a, qb)
checkCompatible a, b, n
q.add " and "
let c = cond(r[2], q, params, a, qb)
checkCompatible a, c, n
else:
if op == "in": q.add " in "
else: q.add " not in "
let b = cond(n[2], q, params, a, qb)
checkCompatibleSet a, b, n
result = DbType(kind: dbBool)
of "as":
result = cond(n[1], q, params, expected, qb)
q.add " as "
expectKind n[2], nnkIdent
let alias = $n[2]
escIdent(q, alias)
qb.colAliases.add((alias, result))
if expected.kind != dbUnknown:
checkCompatible result, expected, n
of "&":
let a = cond(n[1], q, params, DbType(kind: dbVarchar), qb)
q.add " || "
let b = cond(n[2], q, params, a, qb)
checkCompatible a, b, n
result = DbType(kind: dbVarchar)
else:
# treat as arithmetic operator:
result = cond(n[1], q, params, DbType(kind: dbUnknown), qb)
q.add ' '
q.add op
q.add ' '
let b = cond(n[2], q, params, result, qb)
checkCompatible result, b, n
of nnkPrefix:
let op = $n[0]
case op
of "?", "%":
q.add placeHolder(qb)
result = expected
if result.kind == dbUnknown:
macros.error "cannot infer the type of the placeholder", n
else:
params.add((ex: n[1], typ: toNimType(result), isJson: op == "%"))
of "not":
result = DbType(kind: dbBool)
q.add "not "
let a = cond(n[1], q, params, result, qb)
checkBool a, n[1]
of "!!":
result = expected
if result.kind == dbUnknown:
macros.error "cannot infer the type of the literal", n
let arg = n[1]
if arg.kind in {nnkStrLit..nnkTripleStrLit}: q.add arg.strVal
else: q.add repr(n[1])
else:
# treat as arithmetic operator:
q.add ' '
q.add op
q.add ' '
result = cond(n[1], q, params, DbType(kind: dbUnknown), qb)
of nnkCall:
let op = nodeName(n[0])
if isSetOpCall(n):
let subq = renderInlineQuery(n, params, qb)
q.add subq.sql
result = subq.typ
return
if op == "over":
if n.len < 2:
macros.error "over requires at least one expression", n
result = cond(n[1], q, params, expected, qb)
q.add " over ("
for i in 2..<n.len:
if i > 2: q.add " "
if n[i].kind notin nnkCallKinds:
macros.error "window clauses must be calls like partitionby(...) or orderby(...)", n[i]
renderWindowClause(n[i], q, params, qb)
q.add ")"
return
if op == "exists":
expectLen n, 2
let subq = renderInlineQuery(n[1], params, qb)
q.add "exists ("
q.add subq.sql
q.add ")"
result = DbType(kind: dbBool)
return
if op == "asc" or op == "desc":
expectLen n, 2
result = cond(n[1], q, params, DbType(kind: dbUnknown), qb)
q.add ' '
q.add op
return
for f in functions:
if f.name.toLowerAscii() == op.toLowerAscii():
if f.arity == n.len-1 or (f.arity == -1 and n.len > 1):
q.add op
q.add "("
for i in 1..<n.len:
result = cond(n[i], q, params, DbType(kind: f.typ), qb)
if i < n.len-1: q.add ", "
if f.typ != dbUnknown: result.kind = f.typ
q.add ")"
return
else:
macros.error "function " & op & " takes " & $f.arity & " arguments", n
macros.error "unknown function " & op
of nnkIfStmt, nnkIfExpr:
q.add "\Lcase "
result = DbType(kind: dbUnknown)
for x in n:
case x.kind
of nnkElifBranch, nnkElifExpr:
q.add "\L when "
checkBool(cond(x[0], q, params, DbType(kind: dbBool), qb), x[0])
q.add " then"
of nnkElse, nnkElseExpr:
q.add "\L else"
else: macros.error "illformed if expression", n
q.add "\L "
let t = cond(x[^1], q, params, result, qb)
if result.kind == dbUnknown: result = t
else: checkCompatible(result, t, x)
q.add "\Lend"
of nnkCommand:
let head = nodeName(n[0])
if head == "select" or head == "distinct":
let subq = renderInlineQuery(n, params, qb)
q.add subq.sql
result = subq.typ
elif n.len == 2 and n[1].kind == nnkCommand and n[1].len == 2:
let op = nodeName(n[1][0]).toLowerAscii()
if op in ["like", "ilike"]:
let env = qb.env
if env.len == 2:
qb.env = @[env[0]]
let a =
if op == "ilike" and dbBackend == DbBackend.sqlite:
q.add "lower("
let t = cond(n[0], q, params, DbType(kind: dbUnknown), qb)
q.add ")"
t
else:
cond(n[0], q, params, DbType(kind: dbUnknown), qb)
q.add " "
if op == "ilike" and dbBackend != DbBackend.sqlite:
q.add "ilike"
else:
q.add "like"
q.add " "
if env.len == 2:
qb.env = @[env[1]]
let b =
if op == "ilike" and dbBackend == DbBackend.sqlite:
q.add "lower("
let t = cond(n[1][1], q, params, a, qb)
q.add ")"
t
else:
cond(n[1][1], q, params, a, qb)
checkCompatible a, b, n
result = DbType(kind: dbBool)
else:
macros.error "construct not supported in condition: " & treeRepr n, n
else:
macros.error "construct not supported in condition: " & treeRepr n, n
else:
macros.error "construct not supported in condition: " & treeRepr n, n
proc generateRoutine(name: NimNode, q: QueryBuilder;
sql: string; k: NimNodeKind): NimNode =
let prepStmt = ident($name & "PrepStmt")
let prepare = newVarStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit(sql)))
let body = newStmtList()
# Ensure the prepared statement is created before binding/starting the query
body.add prepare
var finalParams = newNimNode(nnkFormalParams)
if q.retTypeIsJson:
if k == nnkIteratorDef:
body.add newVarStmt(ident"res", newCall(bindSym"createJObject"))
else:
body.add newAssignment(ident"result", newCall(bindSym"createJArray"))
finalParams.add ident"JsonNode"
else:
var rtyp = if q.retType.len > 1:
q.retType
else:
q.retType[0][1]
body.add newTree(nnkVarSection, newIdentDefs(ident"res", rtyp))
if k != nnkIteratorDef:
rtyp = nnkBracketExpr.newTree(ident"seq", rtyp)
finalParams.add rtyp
when dbBackend == DbBackend.postgre:
finalParams.add newIdentDefs(ident"db", newTree(nnkDotExpr, ident"ormin_postgre", ident"DbConn"))
elif dbBackend == DbBackend.sqlite:
finalParams.add newIdentDefs(ident"db", newTree(nnkDotExpr, ident"ormin_sqlite", ident"DbConn"))
else:
finalParams.add newIdentDefs(ident"db", ident("DbConn"))
var i = 1
if q.params.len > 0:
body.add newCall(bindSym"startBindings", prepStmt, newLit(q.params.len))
for p in q.params:
if p.isJson:
finalParams.add newIdentDefs(p.ex, ident"JsonNode")
body.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, true)
else:
finalParams.add newIdentDefs(p.ex, p.typ)
body.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, false)
inc i
body.add newCall(bindSym"startQuery", ident"db", prepStmt)
let yld = newStmtList()
if k != nnkIteratorDef:
if q.retTypeIsJson:
yld.add newVarStmt(ident"res", newCall(bindSym"createJObject"))
let fn = if q.retTypeIsJson: bindSym"bindResultJson" else: bindSym"bindResult"
if q.retType.len > 1:
var i = 0
for r in q.retType:
template resAt(i) {.dirty.} = res[i]
let resx = if q.retTypeIsJson: ident"res" else: getAst(resAt(newLit(i)))
yld.add newCall(fn, ident"db", prepStmt, newLit(i),
resx, copyNimTree r[1], newLit q.retNames[i])
inc i
else:
yld.add newCall(fn, ident"db", prepStmt, newLit(0), ident"res",
copyNimTree q.retType[0][1], newLit q.retNames[0])
if k == nnkIteratorDef:
yld.add newTree(nnkYieldStmt, ident"res")
else:
yld.add newCall("add", ident"result", ident"res")
let whileStmt = newTree(nnkWhileStmt,
newCall(bindSym"stepQuery", ident"db", prepStmt, newLit true), yld)
body.add whileStmt
body.add newCall(bindSym"stopQuery", ident"db", prepStmt)
let iter = newTree(k,
name,
newEmptyNode(),
newEmptyNode(),
finalParams,
newEmptyNode(), # pragmas
newEmptyNode(),
body)
result = newStmtList(prepare, iter)
proc getColumnName(n: NimNode): string =
case n.kind
of nnkPar:
if n.len == 1: result = getColumnName(n[0])
of nnkInfix:
if $n[0] == "as": result = getColumnName(n[2])
of nnkIdent, nnkSym:
result = $n
else:
# this is a little hacky, if the column name starts with
# a space, it means "could not extract the column name"
# but we need to emit this macros.error lazily:
result = " " & repr(n)
proc retName(q: QueryBuilder; i: int; n: NimNode): string =
result = q.retNames[i]
if q.retTypeIsJson and result.startsWith(" "):
macros.error "cannot extract column name of:" & result, n
result = ""
proc selectAll(q: QueryBuilder; tabIndex: int; arg, lineInfo: NimNode) =
proc fieldImpl(q: QueryBuilder; arg: NimNode; name: string): NimNode =
if q.retTypeIsJson:
result = newTree(nnkBracketExpr, arg, newLit(name))
else:
result = newTree(nnkDotExpr, arg, ident(name))
template field(): untyped = fieldImpl(q, arg, a.name)
case q.kind
of qkSelect, qkJoin:
for a in sourceColumns(q, tabIndex):
if q.coln > 0: q.head.add ", "
inc q.coln
q.retType.add nnkIdentDefs.newTree(newIdentNode(a.name), toNimType(a.typ), newEmptyNode())
q.retNames.add a.name
doAssert q.env.len > 0
q.head.add q.env[^1][1]
q.head.add '.'
escIdent(q.head, a.name)
of qkInsert, qkInsertReturning, qkReplace:
if isCteEnvIndex(tabIndex):
macros.error "cannot insert into a CTE", lineInfo
for a in attributes:
# we do not set the primary key:
if a.tabIndex == tabIndex and a.key != 1:
if q.coln > 0: q.head.add ", "
escIdent(q.head, a.name)
inc q.coln
q.params.add((ex: field(), typ: toNimType(a.typ), isJson: q.retTypeIsJson))
if q.values.len > 0: q.values.add ", "
q.values.add placeholder(q)
of qkUpdate:
if isCteEnvIndex(tabIndex):
macros.error "cannot update a CTE", lineInfo
for a in attributes:
if a.tabIndex == tabIndex and a.key != 1:
if q.coln > 0: q.head.add ", "
escIdent(q.head, a.name)
inc q.coln
q.params.add((ex: field(), typ: toNimType(a.typ), isJson: q.retTypeIsJson))
q.head.add " = "
q.head.add placeholder(q)
else:
macros.error "select '_' not supported for this construct", lineInfo
proc tableSel(n: NimNode; q: QueryBuilder) =
if n.kind == nnkCall and q.kind != qkDelete:
let call = n
let tab = $call[0]
let tabIndex = sourceLookup(q, tab)
if tabIndex < 0:
macros.error "unknown table name: " & tab & " from: " & fmtTableList(tableNames), n
return
let alias = sourceAlias(q, tabIndex, tab)
if q.kind == qkSelect:
escIdent(q.fromm, tab)
q.fromm.add " as " & alias
elif q.kind != qkJoin:
escIdent(q.head, tab)
if q.kind == qkUpdate: q.head.add " set "
elif q.kind notin {qkSelect, qkJoin}: q.head.add "("
if q.env.len == 0 or q.env[^1] != (tabIndex, alias):
q.env.add((tabindex, alias))
for i in 1..<call.len:
let col = call[i]
if col.kind == nnkExprEqExpr and q.kind in {qkInsert, qkInsertReturning, qkUpdate, qkReplace}:
let colname = $col[0]
if colname == "_":
selectAll(q, tabIndex, col[1], col)
else:
let coltype = lookup("", colname, q)
if coltype.kind == dbUnknown:
macros.error "unkown column name: " & colname, col
else:
if q.coln > 0: q.head.add ", "
escIdent(q.head, colname)
#q.params.add newIdentDefs(col[1], toNimType(coltype))
inc q.coln
if q.kind == qkUpdate:
q.head.add " = "
discard cond(col[1], q.head, q.params, coltype, q)
else:
if q.values.len > 0: q.values.add ", "
discard cond(col[1], q.values, q.params, coltype, q)
# Track inserted values for potential SQLite RETURNING support
if q.kind in {qkInsert, qkInsertReturning, qkReplace}:
var valNode = col[1]
if valNode.kind == nnkPrefix and (let opv = $valNode[0]; opv == "?" or opv == "%"):
q.insertedValues.add((colname, valNode[1]))
elif valNode.kind in {nnkStrLit, nnkRStrLit, nnkTripleStrLit, nnkIntLit..nnkInt64Lit, nnkFloatLit}:
q.insertedValues.add((colname, valNode))
elif col.kind == nnkPrefix and (let op = $col[0]; op == "?" or op == "%"):
let colname = $col[1]
let coltype = lookup("", colname, q)
if coltype.kind == dbUnknown:
macros.error "unkown column name: " & colname, col
else:
if q.coln > 0: q.head.add ", "
escIdent(q.head, colname)
inc q.coln
q.params.add((ex: col[1], typ: toNimType(coltype), isJson: op == "%"))
if q.kind == qkUpdate:
q.head.add " = "
q.head.add placeholder(q)
else:
if q.values.len > 0: q.values.add ", "
q.values.add placeholder(q)
elif col.kind == nnkIdent and $col == "_":
selectAll(q, tabIndex, ident"arg", col)
elif q.kind in {qkSelect, qkJoin}:
if q.coln > 0: q.head.add ", "
inc q.coln
let t =
if col.kind in {nnkIdent, nnkSym}:
let colname = $col
var typ = DbType(kind: dbUnknown)
for srcCol in sourceColumns(q, tabIndex):
if cmpIgnoreCase(srcCol.name, colname) == 0:
typ = srcCol.typ
break
if typ.kind == dbUnknown:
macros.error "unknown column name: " & colname, col
q.head.add q.env[^1][1]
q.head.add '.'
escIdent(q.head, colname)
typ
else:
cond(col, q.head, q.params, DbType(kind: dbUnknown), q)
q.retType.add nnkIdentDefs.newTree(newIdentNode(getColumnName(col)), toNimType(t), newEmptyNode())
q.retNames.add getColumnName(col)
else:
macros.error "unknown selector: " & repr(n), n
if q.kind notin {qkUpdate, qkSelect, qkJoin}: q.head.add ")"
elif n.kind in {nnkIdent, nnkAccQuoted, nnkSym} and q.kind == qkDelete:
let tab = $n
let tabIndex = sourceLookup(q, tab)
if tabIndex < 0:
macros.error "unknown table name: " & tab & " from: " & fmtTableList(tableNames), n
return
if isCteEnvIndex(tabIndex):
macros.error "cannot delete from a CTE", n
escIdent(q.head, tab)
q.env.add((tabindex, q.getAlias(tabIndex)))
elif n.kind == nnkRStrLit:
q.head.add n.strVal
else:
macros.error "unknown selector: " & repr(n), n
proc queryh(n: NimNode; q: QueryBuilder) =
var n = n
if n.kind == nnkCall:
let c = newNimNode(nnkCommand)
for i in 0..<n.len:
c.add n[i]
n = c
expectKind n, nnkCommand
let kind = nodeName(n[0]).toLowerAscii()
case kind
of "with":
expectLen n, 2
expectKind n[1], nnkCall
if n[1].len != 2:
macros.error "with expects syntax like with cteName(select ...)", n[1]
let cteName = nodeName(n[1][0])
if cteName.len == 0:
macros.error "with requires a CTE name", n[1][0]
if lookupCte(q.ctes, cteName) >= 0:
macros.error "duplicate CTE name: " & cteName, n[1][0]
var subq = newQueryBuilder()
subq.qmark = q.qmark
subq.aliasGen = q.aliasGen
subq.ctes = q.ctes
subq.cteBase = q.ctes.len
applyQueryNode(n[1][1], subq)
if subq.kind notin {qkSelect, qkJoin}:
macros.error "CTEs require a select-style query", n[1][1]
q.qmark = subq.qmark
q.aliasGen = subq.aliasGen
for p in subq.params:
q.params.add p
var cols: seq[SourceColumn] = @[]
for i, name in subq.retNames:
let typNode =
if i < subq.retType.len and subq.retType[i].kind == nnkIdentDefs and subq.retType[i].len > 1:
subq.retType[i][1]
else:
newEmptyNode()
cols.add SourceColumn(name: name, typ: DbType(kind: typeNodeToDbKind(typNode)))
q.ctes.add CteDef(name: cteName, sql: queryAsString(subq, n[1][1]), cols: cols)
of "select":
q.kind = qkSelect
q.head = "select "
expectLen n, 2
if n[1].kind == nnkCommand and nodeName(n[1][0]) == "distinct":
expectLen n[1], 2
q.head = "select distinct "
tableSel(n[1][1], q)
else:
tableSel(n[1], q)
of "distinct":
q.kind = qkSelect
q.head = "select distinct "
expectLen n, 2
tableSel(n[1], q)
of "insert":
q.kind = qkInsert
q.head = "insert into "
expectLen n, 2
tableSel(n[1], q)
of "update":
q.kind = qkUpdate
q.head = "update "
expectLen n, 2
tableSel(n[1], q)
of "replace":
q.kind = qkReplace
q.head = "replace into "
expectLen n, 2
tableSel(n[1], q)
of "delete":
q.kind = qkDelete
q.head = "delete from "
expectLen n, 2
tableSel(n[1], q)
of "where":
expectLen n, 2
if q.kind in {qkInsert, qkInsertReturning}:
if not q.onConflictTargetSet or not q.onConflictActionSet or not q.onConflictIsDoUpdate:
macros.error "'where' for insert is only supported after 'onconflict(...)' and 'doupdate(...)'", n
if q.onConflictWhereSet:
macros.error "conflict update 'where' can only be specified once", n
var conflictWhere = ""
# In PostgreSQL upsert WHERE, bare column names are ambiguous between
# target table and EXCLUDED. Resolve bare identifiers against target table.
let oldKind = q.kind
let oldEnv = q.env
if q.env.len > 0:
let source = q.env[^1][0]
q.kind = qkSelect
q.env = @[(source, sourceName(q, source))]
let t = cond(n[1], conflictWhere, q.params, DbType(kind: dbBool), q)
q.kind = oldKind
q.env = oldEnv
checkBool(t, n)
q.onConflictWhere = " where " & conflictWhere
q.onConflictWhereSet = true
else:
let t = cond(n[1], q.where, q.params, DbType(kind: dbBool), q)
checkBool(t, n)
of "join", "innerjoin", "outerjoin", "leftjoin", "leftouterjoin",
"rightjoin", "rightouterjoin", "fulljoin", "fullouterjoin", "crossjoin":
q.join.add "\L" & joinKeyword(kind)
expectLen n, 2
let joinClause = n[1]
if kind == "crossjoin" and joinClause.kind == nnkCommand and joinClause.len == 2 and
joinClause[1].kind == nnkCommand and joinClause[1].len == 2 and $joinClause[1][0] == "on":
macros.error "crossjoin does not support an on clause", n
if joinClause.kind == nnkCommand and joinClause.len == 2 and
joinClause[1].kind == nnkCommand and joinClause[1].len == 2 and $joinClause[1][0] == "on" and
joinClause[0].kind == nnkCall:
let tab = $joinClause[0][0]
let tabIndex = sourceLookup(q, tab)
if tabIndex < 0:
macros.error "unknown table name: " & tab & " from: " & fmtTableList(tableNames), n
else:
escIdent(q.join, tab)
let alias = sourceAlias(q, tabIndex, tab)
q.join.add " as " & alias
var oldEnv = q.env
q.env = @[(tabIndex, alias)]
q.kind = qkJoin
tableSel(joinClause[0], q)
swap q.env, oldEnv
let onn = joinClause[1][1]
q.join.add " on "
oldEnv = q.env
q.env.add((tabIndex, alias))
let t = cond(onn, q.join, q.params, DbType(kind: dbBool), q)
swap q.env, oldEnv
checkBool(t, onn)
elif joinClause.kind == nnkCall:
let tab = $joinClause[0]
let tabIndex = sourceLookup(q, tab)
if tabIndex < 0:
macros.error "unknown table name: " & tab & " from: " & fmtTableList(tableNames), n[1][0]
else:
if kind == "crossjoin":
let alias = sourceAlias(q, tabIndex, tab)
escIdent(q.join, tab)
q.join.add " as " & alias
var oldEnv = q.env
q.env = @[(tabIndex, alias)]
q.kind = qkJoin
tableSel(n[1], q)
swap q.env, oldEnv
else:
# auto join:
if isCteEnvIndex(tabIndex) or isCteEnvIndex(q.env[^1][0]):
macros.error "automatic joins are only supported for base tables", n
let alias = q.getAlias(tabIndex)
escIdent(q.join, tab)
q.join.add " as " & alias
if not autoJoin(q.join, q.env[^1], tabIndex, alias):
macros.error "cannot compute auto join from: " & tableNames[q.env[^1][0]] & " to: " & tab, n
var oldEnv = q.env
q.env = @[(tabIndex, alias)]
q.kind = qkJoin
tableSel(n[1], q)
swap q.env, oldEnv
else:
macros.error "unknown query component " & repr(n), n
of "groupby":
for i in 1..<n.len:
discard cond(n[i], q.groupby, q.params, DbType(kind: dbUnknown), q)
of "orderby":
for i in 1..<n.len:
discard cond(n[i], q.orderby, q.params, DbType(kind: dbUnknown), q)
if i != n.len - 1: q.orderby &= ", "
of "having":
expectLen n, 2
let t = cond(n[1], q.having, q.params, DbType(kind: dbBool), q)
checkBool(t, n[1])
of "limit":
expectLen n, 2
if n[1].kind == nnkIntLit and n[1].intVal == 1:
q.singleRow = true
let t = cond(n[1], q.limit, q.params, DbType(kind: dbInt), q)
checkInt(t, n[1])
of "offset":
expectLen n, 2
let t = cond(n[1], q.offset, q.params, DbType(kind: dbInt), q)
checkInt(t, n[1])
of "onconflict":
if q.kind notin {qkInsert, qkInsertReturning}:
macros.error "'onconflict' only possible within 'insert'", n
if q.onConflictTargetSet:
macros.error "'onconflict' can only be specified once", n
if n.len < 2:
macros.error "'onconflict' expects one or more columns", n
q.onConflict = "\Lon conflict ("
for i in 1..<n.len:
let col = n[i]
let colname = nodeName(col)
if colname.len == 0:
macros.error "'onconflict' columns must be identifiers", col
if lookup("", colname, q).kind == dbUnknown:
macros.error "unknown column name: " & colname, col
if i > 1:
q.onConflict.add ", "
escIdent(q.onConflict, colname)
q.onConflict.add ")"
q.onConflictTargetSet = true
of "donothing":
if q.kind notin {qkInsert, qkInsertReturning}:
macros.error "'donothing' only possible within 'insert'", n
if not q.onConflictTargetSet:
macros.error "'donothing' requires a preceding 'onconflict' clause", n
if q.onConflictActionSet:
macros.error "conflict action already set; choose only one of 'donothing' or 'doupdate'", n
expectLen n, 1
q.onConflict.add " do nothing"
q.onConflictActionSet = true
q.onConflictIsDoUpdate = false
of "doupdate":
if q.kind notin {qkInsert, qkInsertReturning}:
macros.error "'doupdate' only possible within 'insert'", n
if not q.onConflictTargetSet:
macros.error "'doupdate' requires a preceding 'onconflict' clause", n
if q.onConflictActionSet:
macros.error "conflict action already set; choose only one of 'donothing' or 'doupdate'", n
if n.len < 2:
macros.error "'doupdate' expects assignments like doupdate(col = value)", n
q.onConflict.add " do update set "
for i in 1..<n.len:
let assignment = n[i]
if assignment.kind != nnkExprEqExpr:
macros.error "'doupdate' expects assignments like doupdate(col = value)", assignment
let colname = nodeName(assignment[0])
if colname.len == 0:
macros.error "'doupdate' assignments must target a column identifier", assignment[0]
let coltype = lookup("", colname, q)
if coltype.kind == dbUnknown:
macros.error "unknown column name: " & colname, assignment[0]
if i > 1:
q.onConflict.add ", "
escIdent(q.onConflict, colname)
q.onConflict.add " = "
discard cond(assignment[1], q.onConflict, q.params, coltype, q)
q.onConflictActionSet = true
q.onConflictIsDoUpdate = true
of "returning":
if q.kind != qkInsert:
macros.error "'returning' only possible within 'insert'"
q.kind = qkInsertReturning
expectLen n, 2
when dbBackend == DbBackend.sqlite:
q.returning = ";\nselect last_insert_rowid()"
elif dbBackend == DbBackend.mysql:
q.returning = ";\nselect LAST_INSERT_ID()"
else:
q.returning = " returning "
var colname = ""
let nimType = toNimType lookupColumnInEnv(n[1], colname, q.params, DbType(kind: dbUnknown), q)
q.singleRow = true
when dbBackend != DbBackend.sqlite:
q.retType.add newIdentDefs(ident(colname), nimType)
q.retNames.add colname
else:
discard nimType
# check if the column is inserted, if so, use the inserted expression to return the value
var found = false
for p in q.insertedValues:
if cmpIgnoreCase(p[0], colname) == 0:
q.retExpr = p[1]
found = true
break
when dbBackend == DbBackend.postgre:
q.returning.add colname
of "produce":
expectLen n, 2
if eqIdent(n[1], "json"):
q.retTypeIsJson = true
elif eqIdent(n[1], "nim") or eqIdent(n[1], "tuple"):
q.retTypeIsJson = false
else:
macros.error "produce expects 'json' or 'nim', but got: " & repr(n[1]), n
else:
macros.error "unknown query component " & repr(n), n
proc queryAsString(q: QueryBuilder, n: NimNode): string =
if q.onConflictTargetSet and not q.onConflictActionSet:
macros.error "'onconflict' requires either 'donothing' or 'doupdate'", n
if q.onConflictWhereSet and not q.onConflictIsDoUpdate:
macros.error "conflict update 'where' requires 'doupdate(...)'", n
if q.cteBase < q.ctes.len:
result.add "with "
for i in q.cteBase..<q.ctes.len:
if i > q.cteBase:
result.add ",\L"
escIdent(result, q.ctes[i].name)
result.add " as (\L"
result.add q.ctes[i].sql
result.add "\L)"
result.add "\L"
result.add q.head
if q.fromm.len > 0:
result.add "\Lfrom "
result.add q.fromm
if q.join.len > 0:
result.add q.join
if q.values.len > 0:
result.add "\Lvalues ("
result.add q.values
result.add ")"
if q.onConflict.len > 0:
result.add q.onConflict
if q.onConflictWhere.len > 0:
result.add q.onConflictWhere
if q.where.len > 0:
if q.kind in {qkSelect, qkJoin, qkUpdate, qkDelete}:
result.add "\Lwhere "
result.add q.where
else:
macros.error "'where' is not supported for this query kind", n
if q.groupby.len > 0:
result.add "\Lgroup by "
result.add q.groupby
if q.having.len > 0:
result.add "\Lhaving "
result.add q.having
if q.orderby.len > 0:
result.add "\Lorder by "
result.add q.orderby
if q.limit.len > 0:
result.add "\Llimit "
result.add q.limit
if q.offset.len > 0:
result.add "\Loffset "
result.add q.offset
when dbBackend != DbBackend.sqlite:
if q.returning.len > 0:
result.add q.returning
when defined(debugOrminSql):
macros.hint("Ormin SQL:\n" & $result, n)
proc sameReturnShape(a, b: NimNode): bool {.compileTime.} =
if a.len != b.len:
return false
for i in 0..<a.len:
let at = if a[i].kind == nnkIdentDefs and a[i].len > 1: a[i][1] else: a[i]
let bt = if b[i].kind == nnkIdentDefs and b[i].len > 1: b[i][1] else: b[i]
if repr(at) != repr(bt):
return false
result = true
proc buildSetOpQueryParts(op: string; branches: openArray[NimNode];
q: QueryBuilder; lineInfo: NimNode) {.compileTime.} =
if q.kind != qkNone or q.head.len > 0 or q.params.len > 0:
macros.error "set operations must form the whole query", lineInfo
if branches.len < 2:
macros.error "set operations require at least two queries", lineInfo
q.kind = qkSelect
q.singleRow = false
for i, branchNode in branches:
var branch = newQueryBuilder()
branch.qmark = q.qmark
branch.aliasGen = q.aliasGen
branch.ctes = q.ctes
branch.cteBase = q.ctes.len
applyQueryNode(branchNode, branch)
if branch.kind notin {qkSelect, qkJoin}:
macros.error "set operations only support select-style queries", branchNode
if i > 0:
q.head.add "\L" & op & "\L"
if isSetOpCall(branchNode):
q.head.add "("
q.head.add queryAsString(branch, branchNode)
q.head.add ")"
else:
q.head.add queryAsString(branch, branchNode)
q.qmark = branch.qmark
q.aliasGen = branch.aliasGen
for p in branch.params:
q.params.add p
if i == 0:
q.retType = branch.retType
q.retNames = branch.retNames
q.retTypeIsJson = branch.retTypeIsJson
elif branch.retTypeIsJson != q.retTypeIsJson or
not sameReturnShape(branch.retType, q.retType):
macros.error "all set operation branches must return the same types", branchNode
proc buildSetOpQuery(n: NimNode; q: QueryBuilder) {.compileTime.} =
var branches: seq[NimNode] = @[]
for i in 1..<n.len:
branches.add n[i]
buildSetOpQueryParts(nodeName(n[0]).toLowerAscii(), branches, q, n)
proc isSetOpToken(n: NimNode): bool {.compileTime.} =
n.kind in {nnkIdent, nnkSym, nnkAccQuoted} and isSetOpName(nodeName(n))
proc applyQueryNode(n: NimNode; q: QueryBuilder) =
if isSetOpCall(n):
buildSetOpQuery(n, q)
return
var flattened: seq[NimNode]
flattenQueryCommands(n, flattened)
var hasInfixSetOp = false
for part in flattened:
if isSetOpToken(part):
hasInfixSetOp = true
break
if hasInfixSetOp:
var op = ""
var branches: seq[NimNode] = @[]
var currentBranch = newStmtList()
proc flushBranch(lineInfo: NimNode) {.compileTime.} =
if currentBranch.len == 0:
macros.error "expected query before set operation", lineInfo
branches.add currentBranch
currentBranch = newStmtList()
for part in flattened:
if isSetOpToken(part):
let currentOp = nodeName(part).toLowerAscii()
if op.len == 0:
op = currentOp
elif op != currentOp:
macros.error "mixed infix set operations are not supported; use nesting for precedence", part
flushBranch(part)
else:
if part.kind in {nnkCommand, nnkCall} or isSetOpCall(part):
currentBranch.add part
else:
macros.error "illformed query", part
if op.len == 0:
macros.error "expected set operation between queries", n
if currentBranch.len == 0:
macros.error "set operation requires a query after " & op, n
branches.add currentBranch
buildSetOpQueryParts(op, branches, q, n)
return
for part in flattened:
if isSetOpCall(part):
buildSetOpQuery(part, q)
elif part.kind in {nnkCommand, nnkCall}:
queryh(part, q)
else:
macros.error "illformed query", part
proc renderInlineQuery(n: NimNode; params: var Params;
qb: QueryBuilder): tuple[sql: string, typ: DbType] =
var subq = newQueryBuilder()
subq.qmark = qb.qmark
subq.aliasGen = qb.aliasGen
subq.ctes = qb.ctes
subq.cteBase = qb.ctes.len
applyQueryNode(n, subq)
if subq.kind notin {qkSelect, qkJoin}:
macros.error "subqueries require a select-style query", n
qb.qmark = subq.qmark
qb.aliasGen = subq.aliasGen
for p in subq.params:
params.add p
result.sql = queryAsString(subq, n)
result.typ = DbType(kind: dbSet)
proc makeSeq(retType: NimNode; singleRow: bool): NimNode =
if not singleRow:
result = newTree(nnkBracketExpr, bindSym"seq", retType)
else:
result = retType
proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJson: bool): NimNode =
if isJson:
return newCall(bindSym"bindParamJson", ident"db", prepStmt, newLit(idx), ex, typ)
result = quote do:
block:
var converted: DbValue[`typ`]
toQueryHook(converted, `ex`)
if converted.isNull:
bindNullParam(db, `prepStmt`, `idx`)
else:
bindParam(db, `prepStmt`, `idx`, converted.value, `typ`)
proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode =
result = quote do:
var rawValue: DbValue[`sourceType`]
bindResult(db, `prepStmt`, `idx`, rawValue, `sourceType`, `colName`)
`destExpr`.fromQueryHook(rawValue)
proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; singleRow: bool): NimNode =
let selectedCount = newLit(q.retType.len)
let mapped = genSym(nskVar, "mapped")
let mappedStmt = newStmtList()
mappedStmt.add quote do:
var `mapped` = `retType`()
for idx, name in q.retNames:
let fieldName = ident(name)
let sourceType = q.retType[idx][1]
let destExpr = quote do:
`mapped`.`fieldName`
let hooked = buildHookedResultAssign(prepStmt, destExpr, retType, sourceType, idx, name)
mappedStmt.add quote do:
when compiles(`mapped`.`fieldName`):
`hooked`
if singleRow:
mappedStmt.add quote do:
`res` = `mapped`
else:
mappedStmt.add quote do:
`res`.add(`mapped`)
let scalarStmt = newStmtList()
let mappedScalar = if singleRow: res else: genSym(nskVar, "mapped")
let sourceType = q.retType[0][1]
if not singleRow:
scalarStmt.add quote do:
var `mappedScalar`: `retType`
scalarStmt.add buildHookedResultAssign(prepStmt, mappedScalar, retType, sourceType, 0, q.retNames[0])
if not singleRow:
scalarStmt.add quote do:
`res`.add(`mappedScalar`)
result = quote do:
block:
when `retType` is object or `retType` is ref object:
`mappedStmt`
else:
when `selectedCount` != 1:
{.error: "query(T): scalar mapping expects exactly one selected column".}
else:
`scalarStmt`
proc queryImpl(q: QueryBuilder; body: NimNode; attempt, produceJson: bool): NimNode =
expectKind body, nnkStmtList
expectMinLen body, 1
q.retTypeIsJson = produceJson
applyQueryNode(body, q)
let sql = queryAsString(q, body)
let prepStmt = genSym(nskLet)
let res = genSym(nskVar)
result = newTree(
if q.retType.len > 0: nnkStmtListExpr else: nnkStmtList,
newLetStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit sql))
)
let rtyp = if q.retType.len > 1 or q.retType.len == 0:
q.retType
else:
q.retType[0][1]
if q.retType.len > 0:
if q.singleRow:
if q.retTypeIsJson:
result.add newVarStmt(res, newCall(bindSym"createJObject"))
else:
result.add newTree(nnkVarSection, newIdentDefs(res, rtyp))
else:
if q.retTypeIsJson:
result.add newVarStmt(res, newCall(bindSym"createJArray"))
else:
result.add newTree(nnkVarSection, newIdentDefs(res,
newTree(nnkBracketExpr, bindSym"seq", rtyp),
newTree(nnkPrefix, bindSym"@", newTree(nnkBracket))))
let blk = newStmtList()
var i = 1
if q.params.len > 0:
blk.add newCall(bindSym"startBindings", prepStmt, newLit(q.params.len))
for p in q.params:
blk.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, p.isJson)
inc i
blk.add newCall(bindSym"startQuery", ident"db", prepStmt)
var body = newStmtList()
let it = if q.singleRow: res else: genSym(nskVar)
if not q.singleRow and q.retType.len > 0:
if q.retTypeIsJson:
body.add newVarStmt(it, newCall(bindSym"createJObject"))
else:
body.add newTree(nnkVarSection, newIdentDefs(it, rtyp))
let fn = if q.retTypeIsJson: bindSym"bindResultJson" else: bindSym"bindResult"
if q.retType.len > 1:
var i = 0
for r in q.retType:
template resAt(x, i) {.dirty.} = x[i]
let resx = if q.retTypeIsJson: it else: getAst(resAt(it, newLit(i)))
body.add newCall(fn, ident"db", prepStmt, newLit(i),
resx, (if r.len > 0: r[1] else: r), newLit retName(q, i, body))
inc i
elif q.retType.len > 0:
body.add newCall(fn, ident"db", prepStmt, newLit(0),
it, rtyp, newLit retName(q, 0, body))
else:
body.add newTree(nnkDiscardStmt, newEmptyNode())
template ifStmt2(prepStmt, returnsData: bool; action) {.dirty.} =
bind stepQuery
bind stopQuery
bind dbError
if stepQuery(db, prepStmt, returnsData):
action
stopQuery(db, prepStmt)
else:
stopQuery(db, prepStmt)
dbError(db)
template ifStmt1(prepStmt, returnsData: bool; action) {.dirty.} =
bind stepQuery
bind stopQuery
if stepQuery(db, prepStmt, returnsData):
action
stopQuery(db, prepStmt)
template whileStmt(prepStmt, res, it, action) {.dirty.} =
bind stepQuery
bind stopQuery
while stepQuery(db, prepStmt, true):
action
add res, it
stopQuery(db, prepStmt)
template insertQueryReturningId(prepStmt) {.dirty.} =
bind stepQuery
bind stopQuery
bind getLastId
bind dbError
var insertedId = -1
if stepQuery(db, prepStmt, false):
insertedId = getLastId(db, prepStmt)
stopQuery(db, prepStmt)
else:
stopQuery(db, prepStmt)
dbError(db)
insertedId
let returnsData = q.kind in {qkSelect, qkJoin, qkInsertReturning}
if not q.singleRow and q.retType.len > 0:
blk.add getAst(whileStmt(prepStmt, res, it, body))
elif dbBackend == DbBackend.sqlite and q.kind == qkInsertReturning and q.retExpr.kind != nnkEmpty:
# For SQLite, emulate RETURNING by returning the inserted expression value.
# Execute the insert as a non-row statement, then yield the expression.
if attempt:
blk.add getAst(ifStmt1(prepStmt, false, newStmtList()))
else:
blk.add getAst(ifStmt2(prepStmt, false, newStmtList()))
blk.add q.retExpr
elif q.returning.len > 0 and dbBackend == DbBackend.sqlite:
blk.add getAst(insertQueryReturningId(prepStmt))
# fix #14 delete not return value
elif (q.kind == qkInsert or q.kind == qkUpdate or q.kind == qkDelete) and dbBackend == DbBackend.postgre:
blk.add getAst(ifStmt1(prepStmt, returnsData, body))
else:
if attempt:
blk.add getAst(ifStmt1(prepStmt, returnsData, body))
else:
blk.add getAst(ifStmt2(prepStmt, returnsData, body))
result.add newTree(nnkBlockStmt, newEmptyNode(), blk)
if q.retType.len > 0:
result.add res
proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNode): NimNode =
expectKind body, nnkStmtList
expectMinLen body, 1
q.retTypeIsJson = false
applyQueryNode(body, q)
if q.kind notin {qkSelect, qkJoin}:
macros.error "query(T) currently supports select/join queries only", body
if q.retType.len == 0:
macros.error "query(T) requires a query that returns data", body
if q.retTypeIsJson:
macros.error "query(T) does not support 'produce json'", body
let sql = queryAsString(q, body)
let prepStmt = genSym(nskLet)
let res = genSym(nskVar)
result = newTree(
nnkStmtListExpr,
newLetStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit sql))
)
if q.singleRow:
result.add newTree(nnkVarSection, newIdentDefs(res, copyNimTree(retType), newEmptyNode()))
else:
result.add newTree(nnkVarSection, newIdentDefs(res,
newTree(nnkBracketExpr, bindSym"seq", copyNimTree(retType)),
newTree(nnkPrefix, bindSym"@", newTree(nnkBracket))))
let blk = newStmtList()
var i = 1
if q.params.len > 0:
blk.add newCall(bindSym"startBindings", prepStmt, newLit(q.params.len))
for p in q.params:
blk.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, p.isJson)
inc i
blk.add newCall(bindSym"startQuery", ident"db", prepStmt)
let action = buildQueryHookAction(q, prepStmt, res, retType, q.singleRow)
if q.singleRow:
if attempt:
blk.add newTree(nnkIfStmt,
newTree(nnkElifBranch,
newCall(bindSym"stepQuery", ident"db", prepStmt, newLit true),
action
)
)
blk.add newCall(bindSym"stopQuery", ident"db", prepStmt)
else:
blk.add newTree(nnkIfStmt,
newTree(nnkElifBranch,
newCall(bindSym"stepQuery", ident"db", prepStmt, newLit true),
newStmtList(action, newCall(bindSym"stopQuery", ident"db", prepStmt))
),
newTree(nnkElse,
newStmtList(
newCall(bindSym"stopQuery", ident"db", prepStmt),
newCall(bindSym"dbError", ident"db")
)
)
)
else:
blk.add newTree(nnkWhileStmt,
newCall(bindSym"stepQuery", ident"db", prepStmt, newLit true),
action
)
blk.add newCall(bindSym"stopQuery", ident"db", prepStmt)
result.add newTree(nnkBlockStmt, newEmptyNode(), blk)
result.add res
macro query*(args: varargs[untyped]): untyped =
if args.len == 1:
let body = args[0]
var q = newQueryBuilder()
result = queryImpl(q, body, false, false)
when defined(debugOrminDsl):
macros.hint("Ormin Query: " & repr(result), body)
return
if args.len != 2:
macros.error("query expects either `query: ...` or `query(T): ...`", args)
let retType = args[0]
let body = args[1]
var q = newQueryBuilder()
result = queryHookImpl(q, body, false, retType)
when defined(debugOrminDsl):
macros.hint("Ormin Query(T): " & repr(result), body)
macro tryQuery*(args: varargs[untyped]): untyped =
if args.len == 1:
let body = args[0]
var q = newQueryBuilder()
result = queryImpl(q, body, true, false)
when defined(debugOrminDsl):
macros.hint("Ormin TryQuery: " & repr(result), body)
return
if args.len != 2:
macros.error("tryQuery expects either `tryQuery: ...` or `tryQuery(T): ...`", args)
let retType = args[0]
let body = args[1]
var q = newQueryBuilder()
result = queryHookImpl(q, body, true, retType)
when defined(debugOrminDsl):
macros.hint("Ormin TryQuery(T): " & repr(result), body)
# -------------------------
# Transactions DSL
# -------------------------
# Transaction state for nested transactions
var txDepth {.threadvar.}: int
proc getTxDepth*(): int =
result = txDepth
proc isTopTx*(): bool =
result = txDepth == 1
proc incTxDepth*() =
inc txDepth
proc decTxDepth*() =
dec txDepth
template txBegin*(sp: untyped) =
if isTopTx():
execNoRowsLoose("begin transaction")
else:
execNoRowsLoose("savepoint " & sp)
template txCommit*(sp: untyped) =
if isTopTx():
execNoRowsLoose("commit")
else:
execNoRowsLoose("release savepoint " & sp)
template txRollback*(sp: untyped) =
if isTopTx():
execNoRowsLoose("rollback")
else:
execNoRowsLoose("rollback to savepoint " & sp)
template transaction*(body: untyped) =
## Runs the body inside a database transaction. Commits on success,
## rolls back on any exception and rethrows. Supports nesting via savepoints.
block:
incTxDepth()
let sp = "ormin_tx_" & $txDepth
try:
txBegin(sp)
`body`
txCommit(sp)
except DbError:
txRollback(sp)
raise
except CatchableError, Defect:
txRollback(sp)
raise
finally:
decTxDepth()
macro getBlock(blk: untyped): untyped =
result = blk[0]
template transaction*(body, other: untyped) =
## Runs the body inside a database transaction. Commits on success,
## rolls back on any exception and rethrows. Supports nesting via savepoints.
block:
incTxDepth()
let sp = "ormin_tx_" & $txDepth
try:
txBegin(sp)
`body`
txCommit(sp)
except DbError:
txRollback(sp)
getBlock(`other`)
except CatchableError, Defect:
txRollback(sp)
raise
finally:
decTxDepth()
proc createRoutine(name, query: NimNode; k: NimNodeKind): NimNode =
expectKind query, nnkStmtList
expectMinLen query, 1
var q = newQueryBuilder()
applyQueryNode(query, q)
if q.kind notin {qkSelect, qkJoin}:
macros.error "query must be a 'select' or 'join'", query
let sql = queryAsString(q, query)
result = generateRoutine(name, q, sql, k)
when defined(debugOrminDsl):
macros.hint("Ormin Query: " & repr(result), query)
macro createIter*(name, query: untyped): untyped =
## Creates an iterator of the given 'name' that iterates
## over the result set as described in 'query'.
result = createRoutine(name, query, nnkIteratorDef)
macro createProc*(name, query: untyped): untyped =
## Creates an iterator of the given 'name' that iterates
## over the result set as described in 'query'.
result = createRoutine(name, query, nnkProcDef)
type
ProtoBuilder = ref object
msgId: int
dispClient, types, server, procs, retType: NimNode
retNames: seq[string]
foundObj, singleRow: bool
sectionName: string
proc getTypename(n: NimNode): NimNode =
result = n[0][0]
if result.kind == nnkPostfix:
result = result[1]
proc addFields(n: NimNode; b: ProtoBuilder): NimNode =
if n.kind == nnkObjectTy and not b.foundObj:
b.foundObj = true
expectLen n, 3
var x = n[2]
if x.kind == nnkEmpty:
x = newTree(nnkRecList)
n[2] = x
expectKind x, nnkRecList
doAssert b.retType.len == b.retNames.len, "ormin: types and column names do not match"
for i in 0 ..< b.retType.len:
x.add newTree(nnkIdentDefs, newTree(nnkPostfix, ident"*", ident(b.retNames[i])),
b.retType[i][1], newEmptyNode())
return n
result = copyNimNode(n)
for i in 0 ..< n.len:
result.add addFields(n[i], b)
proc transformClient(n: NimNode; b: ProtoBuilder): NimNode =
template sendReqImpl(x, msgkind): untyped {.dirty.} =
let req = newJObject()
req["cmd"] = %msgkind
req["arg"] = cast[JsonNode](x)
send(req)
template sendReqImplNoArg(msgkind): untyped {.dirty.} =
let req = newJObject()
req["cmd"] = %msgkind
req["arg"] = newJNull()
send(req)
if n.kind in nnkCallKinds and n[0].kind == nnkIdent and $n[0] == "recv":
var castDest: NimNode
if n.len == 2:
castDest = n[1]
else:
expectLen n, 1
# this can happen for the new 'returning' support:
let retType = if b.retType.len != 0 or b.retType.kind != nnkTupleTy: b.retType else: ident"int"
castDest = makeSeq(retType, b.singleRow)
return newTree(nnkCast, castDest, ident"data")
elif n.kind == nnkTypeSection:
b.foundObj = false
let t = addFields(n, b)
b.retType = getTypename(n)
b.types.add t
return newTree(nnkNone)
elif n.kind == nnkProcDef:
let p = n.params
if p.len == 1:
n.body = getAst(sendReqImplNoArg(b.msgId))
else:
expectLen p, 2
expectKind p[1], nnkIdentDefs
if p[1].len != 3:
macros.error "proc must have one or zero parameters", p
n.body = getAst(sendReqImpl(p[1][0], b.msgId))
b.procs.add n
return newTree(nnkNone)
elif n.kind in {nnkLetSection, nnkVarSection}:
b.procs.add n
return newTree(nnkNone)
result = copyNimNode(n)
for i in 0 ..< n.len:
let x = transformClient(n[i], b)
if x.kind != nnkNone: result.add x
proc transformServer(n: NimNode; b: ProtoBuilder): NimNode =
template sendImpl(x, msgkind): untyped {.dirty.} =
result = newJObject()
result["cmd"] = %msgkind
result["data"] = x
template broadCastImpl(x, msgkind): untyped {.dirty.} =
receivers = Receivers.all
result = newJObject()
result["cmd"] = %msgkind
result["data"] = x
if n.kind in nnkCallKinds and n[0].kind == nnkIdent:
case $n[0]
of "send":
expectLen n, 2
return getAst(sendImpl(n[1], b.msgId+1))
of "broadcast":
expectLen n, 2
return getAst(broadCastImpl(n[1], b.msgId+1))
of "query":
expectLen n, 2
var qb = newQueryBuilder()
let q = queryImpl(qb, n[1], false, true)
b.retType = qb.retType
b.singleRow = qb.singleRow
b.retNames = qb.retNames
return q
of "tryQuery":
expectLen n, 2
var qb = newQueryBuilder()
let q = queryImpl(qb, n[1], true, true)
b.retType = qb.retType
b.singleRow = qb.singleRow
b.retNames = qb.retNames
return q
result = copyNimNode(n)
for i in 0 ..< n.len:
result.add transformServer(n[i], b)
proc protoImpl(n: NimNode; b: ProtoBuilder): NimNode =
case n.kind
of nnkCallKinds:
if n[0].kind == nnkIdent:
let op = $n[0]
case op
of "server":
expectLen n, 2, 3
if n.len == 3:
expectKind(n[1], nnkStrLit)
b.sectionName = n[1].strVal
return newTree(nnkOfBranch, newLit(b.msgId), transformServer(n[^1], b))
of "client":
expectLen n, 2, 3
if n.len == 3:
expectKind(n[1], nnkStrLit)
if b.sectionName != n[1].strVal:
macros.error "section names of client/server pair do not match", n[1]
var clientPart = transformClient(n[^1], b)
if clientPart.kind == nnkNone or (clientPart.kind == nnkStmtList and clientPart.len == 0):
clientPart = newStmtList(newTree(nnkDiscardStmt, newEmptyNode()))
b.dispClient.add newTree(nnkOfBranch, newLit(b.msgId+1), clientPart)
inc b.msgId, 2
return newTree(nnkNone)
of "common":
expectLen n, 2
expectKind n[1], nnkStmtList
for s in n[1]:
b.types.add copyNimTree(s)
b.server.add s
return newTree(nnkNone)
else: discard
result = copyNimNode(n)
for i in 0 ..< n.len:
let x = protoImpl(n[i], b)
if x.kind != nnkNone: result.add x
macro protocol*(name: static[string]; body: untyped): untyped =
template serverProc(body) {.dirty.} =
proc dispatch(inp: JsonNode; receivers: var Receivers): JsonNode =
let arg = inp["arg"]
let cmd = inp["cmd"].getInt()
body
template clientProc(body) {.dirty.} =
proc recvMsg*(inp: JsonNode) =
let data = inp["data"]
let cmd = inp["cmd"].getInt()
body
var b = ProtoBuilder(msgId: 0, dispClient: newTree(nnkCaseStmt, ident"cmd"),
types: newStmtList(), procs: newStmtList(), server: newStmtList())
let branches = protoImpl(body, b)
b.dispClient.add newTree(nnkElse, newStmtList(newTree(nnkDiscardStmt, newEmptyNode())))
let disp = newTree(nnkCaseStmt, ident"cmd")
for branch in branches: disp.add branch
disp.add newTree(nnkElse, newStmtList(newTree(nnkDiscardStmt, newEmptyNode())))
let client = getAst(clientProc(b.dispClient))
var clientBody = newStmtList(newCommentStmtNode"Generated by Ormin. DO NOT EDIT!")
for typ in b.types: clientBody.add typ
for prc in b.procs: clientBody.add prc
clientBody.add client
for xx in clientBody:
assert xx.kind != nnkStmtList
writeFile(parentDir(instantiationInfo(-1, true)[0]) / name, repr clientBody)
b.server.add getAst(serverProc(disp))
result = b.server
when defined(debugOrminDsl):
macros.hint("Ormin Query: " & repr(result), body)