From 58afe70742529963af1eb5e7048fa4e8228f01b2 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 21 Apr 2026 02:13:19 +0300 Subject: [PATCH 01/40] add return type support --- config.nims | 2 + ormin/queries.nim | 223 ++++++++++++++++++++++++++++++++++++++++-- tests/tquery_hook.nim | 74 ++++++++++++++ 3 files changed, 293 insertions(+), 6 deletions(-) create mode 100644 tests/tquery_hook.nim diff --git a/config.nims b/config.nims index a9fa5fd..f363714 100644 --- a/config.nims +++ b/config.nims @@ -16,6 +16,7 @@ task test, "Run all test suite": exec "nim c -f -r tests/tfeature" exec "nim c -f -r tests/tcommon" + exec "nim c -f -r tests/tquery_hook" exec "nim c -f -r tests/tsqlite" exec "nim c -f -r tests/tdb_utils" exec "nim c -f -r tests/timportstatic" @@ -34,6 +35,7 @@ task test_postgres, "Run PostgreSQL test suite": exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon" + exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tquery_hook" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre" task buildexamples, "Build examples: chat and forum": diff --git a/ormin/queries.nim b/ormin/queries.nim index 88cdd79..f8543df 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1631,17 +1631,228 @@ proc queryImpl(q: QueryBuilder; body: NimNode; attempt, produceJson: bool): NimN if q.retType.len > 0: result.add res -macro query*(body: untyped): untyped = +proc fromQueryHook*[T](to: typedesc[T], x: T): T = + ## Default conversion hook used by `query(T): ...`. + ## Users can overload this proc to customize field/type conversions. + x + +proc nodeFieldName(n: NimNode): string {.compileTime.} = + case n.kind + of nnkIdent, nnkSym: + result = n.strVal + of nnkPostfix: + if n.len == 2: + result = nodeFieldName(n[1]) + else: + result = "" + else: + result = "" + +proc unwrapTypedescNode(n: NimNode): NimNode {.compileTime.} = + if n.kind == nnkBracketExpr and n.len == 2 and + cmpIgnoreCase(nodeName(n[0]), "typedesc") == 0: + result = n[1] + else: + result = n + +proc resolveTypeImplNode(n: NimNode): NimNode {.compileTime.} = + var it = n + if it.kind notin {nnkTupleTy, nnkObjectTy, nnkRefTy, nnkPtrTy, nnkVarTy, nnkDistinctTy}: + it = unwrapTypedescNode(getTypeInst(it)) + it = getTypeImpl(it) + while it.kind in {nnkRefTy, nnkPtrTy, nnkVarTy, nnkDistinctTy}: + it = getTypeImpl(it[0]) + result = it + +proc collectFieldNames(n: NimNode; names: var seq[string]) {.compileTime.} = + case n.kind + of nnkTupleTy: + for child in n: + if child.kind == nnkIdentDefs: + for i in 0 ..< child.len - 2: + let name = nodeFieldName(child[i]) + if name.len > 0: + names.add name + of nnkObjectTy: + if n.len >= 3: + collectFieldNames(n[2], names) + of nnkRecList: + for child in n: + collectFieldNames(child, names) + of nnkRecCase: + for i in 1 ..< n.len: + collectFieldNames(n[i], names) + of nnkOfBranch, nnkElse: + for child in n: + collectFieldNames(child, names) + of nnkIdentDefs: + for i in 0 ..< n.len - 2: + let name = nodeFieldName(n[i]) + if name.len > 0: + names.add name + else: + discard + +proc findFieldName(fields: openArray[string]; target: string): string {.compileTime.} = + for field in fields: + if cmpIgnoreCase(field, target) == 0: + return field + result = "" + +macro mapQueryRowToType(retType: typedesc; row: typed): untyped = + let baseType = unwrapTypedescNode(getTypeInst(retType)) + let rawImpl = getTypeImpl(baseType) + let isRefType = rawImpl.kind == nnkRefTy + let targetImpl = resolveTypeImplNode(baseType) + + if targetImpl.kind == nnkObjectTy: + var targetFieldNames: seq[string] = @[] + collectFieldNames(targetImpl, targetFieldNames) + + let rowImpl = resolveTypeImplNode(row.getTypeInst) + let mapped = genSym(nskVar, "mapped") + result = newStmtList() + result.add newTree(nnkVarSection, newIdentDefs(mapped, copyNimTree(retType), newEmptyNode())) + if isRefType: + result.add newCall(bindSym"new", mapped) + + if rowImpl.kind notin {nnkTupleTy, nnkObjectTy}: + if targetFieldNames.len != 1: + macros.error("query(T) object mapping requires tuple/object rows unless T has exactly one field, got: " & + repr(row.getTypeInst), row) + let targetField = targetFieldNames[0] + let targetAccess = newTree(nnkDotExpr, mapped, ident(targetField)) + result.add newAssignment( + targetAccess, + newCall(ident"fromQueryHook", + newCall(bindSym"typeof", copyNimTree(targetAccess)), + copyNimTree(row)) + ) + result = newTree(nnkStmtListExpr, result, mapped) + return + + var rowFieldNames: seq[string] = @[] + collectFieldNames(rowImpl, rowFieldNames) + + for targetField in targetFieldNames: + let rowField = findFieldName(rowFieldNames, targetField) + if rowField.len == 0: + macros.error("query(T) cannot map missing field '" & targetField & + "' from row type: " & repr(row.getTypeInst), row) + let targetAccess = newTree(nnkDotExpr, mapped, ident(targetField)) + let rowAccess = newTree(nnkDotExpr, copyNimTree(row), ident(rowField)) + result.add newAssignment( + targetAccess, + newCall(ident"fromQueryHook", + newCall(bindSym"typeof", copyNimTree(targetAccess)), + rowAccess) + ) + result = newTree(nnkStmtListExpr, result, mapped) + return + + result = newCall(ident"fromQueryHook", copyNimTree(retType), copyNimTree(row)) + +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 = queryImpl(q, body, false, false) + let baseQuery = queryImpl(q, body, false, false) + 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) + + if q.singleRow: + let row = genSym(nskLet, "row") + result = newTree(nnkBlockStmt, newEmptyNode(), + newStmtList( + newLetStmt(row, baseQuery), + newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row) + ) + ) + else: + let rows = genSym(nskLet, "rows") + let row = genSym(nskForVar, "row") + let mapped = genSym(nskVar, "mapped") + result = newTree(nnkBlockStmt, newEmptyNode(), + newStmtList( + newLetStmt(rows, baseQuery), + newTree(nnkVarSection, newIdentDefs(mapped, + newTree(nnkBracketExpr, ident"seq", copyNimTree(retType)), + newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))), + newTree(nnkForStmt, row, rows, + newStmtList( + newCall(bindSym"add", mapped, + newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row)) + ) + ), + mapped + ) + ) when defined(debugOrminDsl): - macros.hint("Ormin Query: " & repr(result), body) + 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 -macro tryQuery*(body: untyped): untyped = + 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 = queryImpl(q, body, true, false) + let baseQuery = queryImpl(q, body, true, false) + if q.retType.len == 0: + macros.error("tryQuery(T) requires a query that returns data", body) + if q.retTypeIsJson: + macros.error("tryQuery(T) does not support 'produce json'", body) + + if q.singleRow: + let row = genSym(nskLet, "row") + result = newTree(nnkBlockStmt, newEmptyNode(), + newStmtList( + newLetStmt(row, baseQuery), + newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row) + ) + ) + else: + let rows = genSym(nskLet, "rows") + let row = genSym(nskForVar, "row") + let mapped = genSym(nskVar, "mapped") + result = newTree(nnkBlockStmt, newEmptyNode(), + newStmtList( + newLetStmt(rows, baseQuery), + newTree(nnkVarSection, newIdentDefs(mapped, + newTree(nnkBracketExpr, ident"seq", copyNimTree(retType)), + newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))), + newTree(nnkForStmt, row, rows, + newStmtList( + newCall(bindSym"add", mapped, + newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row)) + ) + ), + mapped + ) + ) when defined(debugOrminDsl): - macros.hint("Ormin Query: " & repr(result), body) + macros.hint("Ormin TryQuery(T): " & repr(result), body) # ------------------------- # Transactions DSL diff --git a/tests/tquery_hook.nim b/tests/tquery_hook.nim new file mode 100644 index 0000000..f208906 --- /dev/null +++ b/tests/tquery_hook.nim @@ -0,0 +1,74 @@ +import unittest, strutils, strformat, os +import ormin +import ormin/db_utils + +when defined(postgre): + when defined(macosx): + {.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".} + const backend = DbBackend.postgre + importModel(backend, "model_postgre") + const sqlFileName = "model_postgre.sql" + let db {.global.} = open("localhost", "test", "test", "test_ormin") +else: + const backend = DbBackend.sqlite + importModel(backend, "model_sqlite") + const sqlFileName = "model_sqlite.sql" + let db {.global.} = open("test.db", "", "", "") + +let + testDir = currentSourcePath.parentDir() + sqlFile = Path(testDir / sqlFileName) + +type + CompositeRow = object + id: int + message: string + + SplitMessageRow = object + parts: seq[string] + +proc fromQueryHook*(to: typedesc[seq[string]], x: string): seq[string] = + x.split(",") + +suite &"query(T) mapping on {backend}": + setup: + db.dropTable(sqlFile, "tb_composite_pk") + db.createTable(sqlFile, "tb_composite_pk") + db.dropTable(sqlFile, "tb_string") + db.createTable(sqlFile, "tb_string") + + query: + insert tb_composite_pk(pk1 = 1, pk2 = 1, message = "hello") + query: + insert tb_composite_pk(pk1 = 2, pk2 = 2, message = "world") + + query: + insert tb_string(typstring = "alice,bob") + query: + insert tb_string(typstring = "carol,dave") + + test "maps selected rows to objects": + let rows = query(CompositeRow): + select tb_composite_pk(pk1 as id, message) + orderby pk1 + + check rows == @[ + CompositeRow(id: 1, message: "hello"), + CompositeRow(id: 2, message: "world") + ] + + test "applies fromQueryHook per destination field type": + let rows = query(SplitMessageRow): + select tb_string(typstring as parts) + orderby typstring + + check rows[0].parts == @["alice", "bob"] + check rows[1].parts == @["carol", "dave"] + + test "single-row query(T) returns a single object": + let row = query(CompositeRow): + select tb_composite_pk(pk1 as id, message) + where pk1 == 1 and pk2 == 1 + limit 1 + + check row == CompositeRow(id: 1, message: "hello") From c9d9007b59574c86ecac735f7981cbe2a37f6de8 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 21 Apr 2026 02:48:28 +0300 Subject: [PATCH 02/40] add return type support --- ormin.nimble | 2 + ormin/ormin_postgre.nim | 24 +++- ormin/ormin_sqlite.nim | 24 +++- ormin/queries.nim | 260 +++++++++++++--------------------------- ormin/query_hooks.nim | 191 +++++++++++++++++++++++++++++ tests/tquery_hook.nim | 157 +++++++++++++++--------- tests/tquery_types.nim | 97 +++++++++++++++ 7 files changed, 512 insertions(+), 243 deletions(-) create mode 100644 ormin/query_hooks.nim create mode 100644 tests/tquery_types.nim diff --git a/ormin.nimble b/ormin.nimble index d1db178..55e6501 100644 --- a/ormin.nimble +++ b/ormin.nimble @@ -20,3 +20,5 @@ feature "examples": import std/os when fileExists("config.nims"): include "config.nims" +requires "msgpack4nim" + diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index bbe75a4..5cc7a80 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -1,6 +1,7 @@ import strutils, db_connector/postgres, json, times import db_connector/db_common +import query_hooks export db_common type @@ -129,9 +130,15 @@ proc fillString(dest: var string; src: cstring; srcLen: int) {.inline.} = template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string; t: typedesc; name: string) = - let src = pqgetvalue(queryResult, queryI, idx.cint) - let srcLen = int(pqgetlength(queryResult, queryI, idx.cint)) - fillString(dest, src, srcLen) + 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) = @@ -174,6 +181,17 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; else: bindToJson(db, s, idx, x, t, name) +template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: string) = + item.name = name + if pqgetisnull(queryResult, queryI, idx.cint) != 0: + item.isNull = true + setLen(item.value, 0) + else: + item.isNull = false + let src = pqgetvalue(queryResult, queryI, idx.cint) + let srcLen = int(pqgetlength(queryResult, queryI, idx.cint)) + fillString(item.value, src, srcLen) + template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; t: typedesc; name: string) = {.error: "invalid type for JSON object".} diff --git a/ormin/ormin_sqlite.nim b/ormin/ormin_sqlite.nim index c343e51..e87459d 100644 --- a/ormin/ormin_sqlite.nim +++ b/ormin/ormin_sqlite.nim @@ -5,6 +5,7 @@ import json, times import db_connector/db_common import db_connector/sqlite3 +import query_hooks export db_common type @@ -149,9 +150,15 @@ proc fillBytes(dest: var seq[byte]; src: pointer; srcLen: int) = template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string; t: typedesc; name: string) = - let srcLen = column_bytes(s, idx.cint) - let src = column_text(s, idx.cint) - fillString(dest, src, srcLen) + 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) = @@ -194,6 +201,17 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; else: bindToJson(db, s, idx, x, t, name) +template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: string) = + item.name = name + if column_type(s, idx.cint) == SQLITE_NULL: + item.isNull = true + setLen(item.value, 0) + else: + item.isNull = false + let srcLen = column_bytes(s, idx.cint) + let src = column_text(s, idx.cint) + fillString(item.value, src, srcLen) + template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; t: typedesc; name: string) = {.error: "invalid type for JSON object".} diff --git a/ormin/queries.nim b/ormin/queries.nim index f8543df..6754190 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -10,6 +10,7 @@ import db_connector/db_common from os import parentDir, `/` import db_types +import query_hooks # SQL dialect specific things: const @@ -1631,126 +1632,93 @@ proc queryImpl(q: QueryBuilder; body: NimNode; attempt, produceJson: bool): NimN if q.retType.len > 0: result.add res -proc fromQueryHook*[T](to: typedesc[T], x: T): T = - ## Default conversion hook used by `query(T): ...`. - ## Users can overload this proc to customize field/type conversions. - x +proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNode): NimNode = + expectKind body, nnkStmtList + expectMinLen body, 1 -proc nodeFieldName(n: NimNode): string {.compileTime.} = - case n.kind - of nnkIdent, nnkSym: - result = n.strVal - of nnkPostfix: - if n.len == 2: - result = nodeFieldName(n[1]) - else: - result = "" - else: - result = "" + 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 -proc unwrapTypedescNode(n: NimNode): NimNode {.compileTime.} = - if n.kind == nnkBracketExpr and n.len == 2 and - cmpIgnoreCase(nodeName(n[0]), "typedesc") == 0: - result = n[1] + 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 = n - -proc resolveTypeImplNode(n: NimNode): NimNode {.compileTime.} = - var it = n - if it.kind notin {nnkTupleTy, nnkObjectTy, nnkRefTy, nnkPtrTy, nnkVarTy, nnkDistinctTy}: - it = unwrapTypedescNode(getTypeInst(it)) - it = getTypeImpl(it) - while it.kind in {nnkRefTy, nnkPtrTy, nnkVarTy, nnkDistinctTy}: - it = getTypeImpl(it[0]) - result = it - -proc collectFieldNames(n: NimNode; names: var seq[string]) {.compileTime.} = - case n.kind - of nnkTupleTy: - for child in n: - if child.kind == nnkIdentDefs: - for i in 0 ..< child.len - 2: - let name = nodeFieldName(child[i]) - if name.len > 0: - names.add name - of nnkObjectTy: - if n.len >= 3: - collectFieldNames(n[2], names) - of nnkRecList: - for child in n: - collectFieldNames(child, names) - of nnkRecCase: - for i in 1 ..< n.len: - collectFieldNames(n[i], names) - of nnkOfBranch, nnkElse: - for child in n: - collectFieldNames(child, names) - of nnkIdentDefs: - for i in 0 ..< n.len - 2: - let name = nodeFieldName(n[i]) - if name.len > 0: - names.add name + 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: + let fn = if p.isJson: bindSym"bindParamJson" else: bindSym"bindParam" + blk.add newCall(fn, ident"db", prepStmt, newLit(i), p.ex, p.typ) + inc i + blk.add newCall(bindSym"startQuery", ident"db", prepStmt) + + let row = genSym(nskVar, "dbRow") + var action = newStmtList() + action.add newTree(nnkVarSection, newIdentDefs(row, ident"DbRow", + newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))) + for idx, name in q.retNames: + let item = genSym(nskVar, "dbItem") + action.add newTree(nnkVarSection, newIdentDefs(item, ident"DbItem", newEmptyNode())) + action.add newCall(bindSym"bindResultRaw", ident"db", prepStmt, newLit(idx), item, newLit(name)) + action.add newCall(bindSym"add", row, item) + let mappedExpr = newCall(ident"fromQueryHook", copyNimTree(retType), row) + if q.singleRow: + action.add newAssignment(res, mappedExpr) else: - discard + action.add newCall(bindSym"add", res, mappedExpr) -proc findFieldName(fields: openArray[string]; target: string): string {.compileTime.} = - for field in fields: - if cmpIgnoreCase(field, target) == 0: - return field - result = "" + template ifStmt2Hook(prepStmt; action) {.dirty.} = + bind stepQuery + bind stopQuery + bind dbError + if stepQuery(db, prepStmt, true): + action + stopQuery(db, prepStmt) + else: + stopQuery(db, prepStmt) + dbError(db) -macro mapQueryRowToType(retType: typedesc; row: typed): untyped = - let baseType = unwrapTypedescNode(getTypeInst(retType)) - let rawImpl = getTypeImpl(baseType) - let isRefType = rawImpl.kind == nnkRefTy - let targetImpl = resolveTypeImplNode(baseType) - - if targetImpl.kind == nnkObjectTy: - var targetFieldNames: seq[string] = @[] - collectFieldNames(targetImpl, targetFieldNames) - - let rowImpl = resolveTypeImplNode(row.getTypeInst) - let mapped = genSym(nskVar, "mapped") - result = newStmtList() - result.add newTree(nnkVarSection, newIdentDefs(mapped, copyNimTree(retType), newEmptyNode())) - if isRefType: - result.add newCall(bindSym"new", mapped) - - if rowImpl.kind notin {nnkTupleTy, nnkObjectTy}: - if targetFieldNames.len != 1: - macros.error("query(T) object mapping requires tuple/object rows unless T has exactly one field, got: " & - repr(row.getTypeInst), row) - let targetField = targetFieldNames[0] - let targetAccess = newTree(nnkDotExpr, mapped, ident(targetField)) - result.add newAssignment( - targetAccess, - newCall(ident"fromQueryHook", - newCall(bindSym"typeof", copyNimTree(targetAccess)), - copyNimTree(row)) - ) - result = newTree(nnkStmtListExpr, result, mapped) - return + template ifStmt1Hook(prepStmt; action) {.dirty.} = + bind stepQuery + bind stopQuery + if stepQuery(db, prepStmt, true): + action + stopQuery(db, prepStmt) - var rowFieldNames: seq[string] = @[] - collectFieldNames(rowImpl, rowFieldNames) - - for targetField in targetFieldNames: - let rowField = findFieldName(rowFieldNames, targetField) - if rowField.len == 0: - macros.error("query(T) cannot map missing field '" & targetField & - "' from row type: " & repr(row.getTypeInst), row) - let targetAccess = newTree(nnkDotExpr, mapped, ident(targetField)) - let rowAccess = newTree(nnkDotExpr, copyNimTree(row), ident(rowField)) - result.add newAssignment( - targetAccess, - newCall(ident"fromQueryHook", - newCall(bindSym"typeof", copyNimTree(targetAccess)), - rowAccess) - ) - result = newTree(nnkStmtListExpr, result, mapped) - return + template whileStmtHook(prepStmt; action) {.dirty.} = + bind stepQuery + bind stopQuery + while stepQuery(db, prepStmt, true): + action + stopQuery(db, prepStmt) - result = newCall(ident"fromQueryHook", copyNimTree(retType), copyNimTree(row)) + if q.singleRow: + if attempt: + blk.add getAst(ifStmt1Hook(prepStmt, action)) + else: + blk.add getAst(ifStmt2Hook(prepStmt, action)) + else: + blk.add getAst(whileStmtHook(prepStmt, action)) + + result.add newTree(nnkBlockStmt, newEmptyNode(), blk) + result.add res macro query*(args: varargs[untyped]): untyped = if args.len == 1: @@ -1767,39 +1735,7 @@ macro query*(args: varargs[untyped]): untyped = let retType = args[0] let body = args[1] var q = newQueryBuilder() - let baseQuery = queryImpl(q, body, false, false) - 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) - - if q.singleRow: - let row = genSym(nskLet, "row") - result = newTree(nnkBlockStmt, newEmptyNode(), - newStmtList( - newLetStmt(row, baseQuery), - newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row) - ) - ) - else: - let rows = genSym(nskLet, "rows") - let row = genSym(nskForVar, "row") - let mapped = genSym(nskVar, "mapped") - result = newTree(nnkBlockStmt, newEmptyNode(), - newStmtList( - newLetStmt(rows, baseQuery), - newTree(nnkVarSection, newIdentDefs(mapped, - newTree(nnkBracketExpr, ident"seq", copyNimTree(retType)), - newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))), - newTree(nnkForStmt, row, rows, - newStmtList( - newCall(bindSym"add", mapped, - newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row)) - ) - ), - mapped - ) - ) + result = queryHookImpl(q, body, false, retType) when defined(debugOrminDsl): macros.hint("Ormin Query(T): " & repr(result), body) @@ -1818,39 +1754,7 @@ macro tryQuery*(args: varargs[untyped]): untyped = let retType = args[0] let body = args[1] var q = newQueryBuilder() - let baseQuery = queryImpl(q, body, true, false) - if q.retType.len == 0: - macros.error("tryQuery(T) requires a query that returns data", body) - if q.retTypeIsJson: - macros.error("tryQuery(T) does not support 'produce json'", body) - - if q.singleRow: - let row = genSym(nskLet, "row") - result = newTree(nnkBlockStmt, newEmptyNode(), - newStmtList( - newLetStmt(row, baseQuery), - newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row) - ) - ) - else: - let rows = genSym(nskLet, "rows") - let row = genSym(nskForVar, "row") - let mapped = genSym(nskVar, "mapped") - result = newTree(nnkBlockStmt, newEmptyNode(), - newStmtList( - newLetStmt(rows, baseQuery), - newTree(nnkVarSection, newIdentDefs(mapped, - newTree(nnkBracketExpr, ident"seq", copyNimTree(retType)), - newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))), - newTree(nnkForStmt, row, rows, - newStmtList( - newCall(bindSym"add", mapped, - newCall(bindSym"mapQueryRowToType", copyNimTree(retType), row)) - ) - ), - mapped - ) - ) + result = queryHookImpl(q, body, true, retType) when defined(debugOrminDsl): macros.hint("Ormin TryQuery(T): " & repr(result), body) diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim new file mode 100644 index 0000000..9a78c35 --- /dev/null +++ b/ormin/query_hooks.nim @@ -0,0 +1,191 @@ +import strutils, options, json, times + +type + DbItem* = object + name*: string + value*: string + isNull*: bool + + DbRow* = seq[DbItem] + +var queryHookTimeFormat* = "yyyy-MM-dd HH:mm:ss" + +template fromQueryHook*[T](to: typedesc[T], x: T): T = + ## Default conversion hook used by `query(T): ...`. + ## Users can overload this proc to customize field/type conversions. + x + +proc dbItemIndex*(val: openArray[DbItem]; name: string): int = + result = -1 + for i, it in val: + if cmpIgnoreCase(it.name, name) == 0: + return i + +proc dbItemByName*(val: var DbRow; name: string): var DbItem = + let idx = dbItemIndex(val, name) + if idx < 0: + raise newException(KeyError, "query(T): missing field in DbRow: " & name) + val[idx] + +proc fromQueryHook*(to: typedesc[string], val: var DbItem): string = + if val.isNull: + when defined(nimNoNilSeqs): + "" + else: + nil + else: + val.value + +proc fromQueryHook*(to: typedesc[int], val: var DbItem): int = + if val.isNull: + raise newException(ValueError, "cannot map NULL to int") + parseInt(val.value) + +proc fromQueryHook*(to: typedesc[int64], val: var DbItem): int64 = + if val.isNull: + raise newException(ValueError, "cannot map NULL to int64") + parseInt(val.value).int64 + +proc fromQueryHook*(to: typedesc[float64], val: var DbItem): float64 = + if val.isNull: + raise newException(ValueError, "cannot map NULL to float64") + parseFloat(val.value) + +proc fromQueryHook*(to: typedesc[bool], val: var DbItem): bool = + if val.isNull: + raise newException(ValueError, "cannot map NULL to bool") + let s = val.value.toLowerAscii() + if s in ["t", "true", "1", "yes", "y"]: + true + elif s in ["f", "false", "0", "no", "n"]: + false + else: + raise newException(ValueError, "invalid boolean DbItem value: " & val.value) + +proc fromQueryHook*(to: typedesc[DateTime], val: var DbItem): DateTime = + if val.isNull: + raise newException(ValueError, "cannot map NULL to DateTime") + let src = val.value + let i = src.find('.') + if i < 0: + if src.len >= 3 and src[src.len - 3] in {'+', '-'}: + parse(src, "yyyy-MM-dd HH:mm:sszz") + else: + parse(src, "yyyy-MM-dd HH:mm:ss", utc()) + else: + if src.len >= 3 and src[src.len - 3] in {'+', '-'}: + let itz = src.len - 3 + let dtstr = src[0.. Date: Tue, 21 Apr 2026 03:00:37 +0300 Subject: [PATCH 03/40] add return type support --- config.nims | 6 ++-- ormin/ormin_postgre.nim | 5 +++ ormin/ormin_sqlite.nim | 5 +++ ormin/queries.nim | 64 +++++++++++++++++------------------ ormin/query_hooks.nim | 75 ++++++++++++++++++++++++++++++----------- 5 files changed, 100 insertions(+), 55 deletions(-) diff --git a/config.nims b/config.nims index f363714..633e107 100644 --- a/config.nims +++ b/config.nims @@ -16,7 +16,8 @@ task test, "Run all test suite": exec "nim c -f -r tests/tfeature" exec "nim c -f -r tests/tcommon" - exec "nim c -f -r tests/tquery_hook" + exec "nim c -f --nimcache:.nimcache/tquery_hook -r tests/tquery_hook" + exec "nim c -f --nimcache:.nimcache/tquery_types -r tests/tquery_types" exec "nim c -f -r tests/tsqlite" exec "nim c -f -r tests/tdb_utils" exec "nim c -f -r tests/timportstatic" @@ -35,7 +36,8 @@ task test_postgres, "Run PostgreSQL test suite": exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon" - exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tquery_hook" + exec "nim c -f -d:nimDebugDlOpen --nimcache:.nimcache/tquery_hook_postgre -r -d:postgre tests/tquery_hook" + exec "nim c -f -d:nimDebugDlOpen --nimcache:.nimcache/tquery_types_postgre -r -d:postgre tests/tquery_types" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre" task buildexamples, "Build examples: chat and forum": diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index 5cc7a80..7b524cb 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -192,6 +192,11 @@ template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: let srcLen = int(pqgetlength(queryResult, queryI, idx.cint)) fillString(item.value, src, srcLen) +proc bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; name: string) = + var item: DbItem + bindResultRaw(db, s, idx, item, name) + row.add item + template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; t: typedesc; name: string) = {.error: "invalid type for JSON object".} diff --git a/ormin/ormin_sqlite.nim b/ormin/ormin_sqlite.nim index e87459d..29d41c6 100644 --- a/ormin/ormin_sqlite.nim +++ b/ormin/ormin_sqlite.nim @@ -212,6 +212,11 @@ template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: let src = column_text(s, idx.cint) fillString(item.value, src, srcLen) +proc bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; name: string) = + var item: DbItem + bindResultRaw(db, s, idx, item, name) + row.add item + template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; t: typedesc; name: string) = {.error: "invalid type for JSON object".} diff --git a/ormin/queries.nim b/ormin/queries.nim index 6754190..9c7208b 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1648,8 +1648,13 @@ proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNo let sql = queryAsString(q, body) let prepStmt = genSym(nskLet) let res = genSym(nskVar) + let mapper = genSym(nskProc, "mapDbRow") + let mapperRow = genSym(nskParam, "row") result = newTree( nnkStmtListExpr, + quote do: + proc `mapper`(`mapperRow`: var DbRow): `retType` = + fromQueryRow(`retType`, `mapperRow`), newLetStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit sql)) ) if q.singleRow: @@ -1674,48 +1679,41 @@ proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNo action.add newTree(nnkVarSection, newIdentDefs(row, ident"DbRow", newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))) for idx, name in q.retNames: - let item = genSym(nskVar, "dbItem") - action.add newTree(nnkVarSection, newIdentDefs(item, ident"DbItem", newEmptyNode())) - action.add newCall(bindSym"bindResultRaw", ident"db", prepStmt, newLit(idx), item, newLit(name)) - action.add newCall(bindSym"add", row, item) - let mappedExpr = newCall(ident"fromQueryHook", copyNimTree(retType), row) + action.add newCall(bindSym"bindResultRawToRow", ident"db", prepStmt, newLit(idx), row, newLit(name)) + let mappedExpr = newCall(mapper, row) if q.singleRow: action.add newAssignment(res, mappedExpr) else: action.add newCall(bindSym"add", res, mappedExpr) - template ifStmt2Hook(prepStmt; action) {.dirty.} = - bind stepQuery - bind stopQuery - bind dbError - if stepQuery(db, prepStmt, true): - action - stopQuery(db, prepStmt) - else: - stopQuery(db, prepStmt) - dbError(db) - - template ifStmt1Hook(prepStmt; action) {.dirty.} = - bind stepQuery - bind stopQuery - if stepQuery(db, prepStmt, true): - action - stopQuery(db, prepStmt) - - template whileStmtHook(prepStmt; action) {.dirty.} = - bind stepQuery - bind stopQuery - while stepQuery(db, prepStmt, true): - action - stopQuery(db, prepStmt) - if q.singleRow: if attempt: - blk.add getAst(ifStmt1Hook(prepStmt, action)) + 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 getAst(ifStmt2Hook(prepStmt, action)) + 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 getAst(whileStmtHook(prepStmt, action)) + 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 diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim index 9a78c35..071e0e1 100644 --- a/ormin/query_hooks.nim +++ b/ormin/query_hooks.nim @@ -1,4 +1,4 @@ -import strutils, options, json, times +import macros, strutils, options, json, times type DbItem* = object @@ -93,7 +93,7 @@ proc fromQueryHook*[T](to: typedesc[Option[T]], val: var DbItem): Option[T] = else: some(fromQueryHook(T, val)) -proc fromQueryHook*[T](to: typedesc[T], val: var DbItem): T = +proc fromQueryItem*[T](to: typedesc[T], val: var DbItem): T = when compiles(fromQueryHook(to, val.value)): if val.isNull: raise newException(ValueError, "cannot map NULL DbItem value") @@ -101,15 +101,16 @@ proc fromQueryHook*[T](to: typedesc[T], val: var DbItem): T = else: {.error: "No fromQueryHook for this destination type. Provide fromQueryHook(typedesc[T], val: var DbItem) or fromQueryHook(typedesc[T], val: string).".} -proc fromQueryHook*[T](to: typedesc[T], val: var DbRow): T = +proc fromQueryRow*[T](to: typedesc[T], val: var DbRow): T = + mixin fromQueryHook when compiles(block: var probe: T for field, value in fieldPairs(probe): discard field discard value): for field, value in fieldPairs(result): - var item = dbItemByName(val, field) - value = fromQueryHook(typeof(value), item) + var sourceItem = dbItemByName(val, field) + bindFromQueryItem(value, sourceItem) elif compiles(block: var probe: T new(probe) @@ -118,13 +119,47 @@ proc fromQueryHook*[T](to: typedesc[T], val: var DbRow): T = discard value): new(result) for field, value in fieldPairs(result[]): - var item = dbItemByName(val, field) - value = fromQueryHook(typeof(value), item) + var sourceItem = dbItemByName(val, field) + bindFromQueryItem(value, sourceItem) else: if val.len != 1: raise newException(ValueError, "query(T): expected exactly one DbItem for scalar mapping") - var item = val[0] - result = fromQueryHook(T, item) + var sourceItem = val[0] + result = fromQueryItem(T, sourceItem) + +proc fromQueryHook*[T](to: typedesc[T], val: var DbItem): T = + fromQueryItem(to, val) + +proc fromQueryHook*[T](to: typedesc[T], val: var DbRow): T = + fromQueryRow(to, val) + +proc bindFromQueryItem*[T](dest: var T, sourceItem: var DbItem) = + mixin fromQueryHook + dest = fromQueryHook(T, sourceItem) + +proc fromQueryRowAst*(retType, rowExpr: NimNode): NimNode {.compileTime.} = + result = quote do: + block: + when `retType` is ref object: + var mapped: `retType` + new(mapped) + for field, value in fieldPairs(mapped[]): + var sourceItem = dbItemByName(`rowExpr`, field) + bindFromQueryItem(value, sourceItem) + mapped + elif `retType` is object: + var mapped: `retType` + for field, value in fieldPairs(mapped): + var sourceItem = dbItemByName(`rowExpr`, field) + bindFromQueryItem(value, sourceItem) + mapped + else: + fromQueryRow(`retType`, `rowExpr`) + +macro fromQueryRowExpr*(retType, val: untyped): untyped = + result = fromQueryRowAst(copyNimTree(retType), copyNimTree(val)) + when defined(debugOrminDsl): + macros.hint("Ormin fromQueryRowExpr: " & repr(result), retType) proc toQueryHook*(val: var string, x: string) = val = x @@ -168,10 +203,10 @@ proc toQueryHook*[T](val: var DbRow, x: T) = discard field discard value): for field, value in fieldPairs(x): - var item: DbItem - item.name = field - toQueryHook(item, value) - val.add item + var destItem: DbItem + destItem.name = field + toQueryHook(destItem, value) + val.add destItem elif compiles(block: if x.isNil: discard @@ -181,11 +216,11 @@ proc toQueryHook*[T](val: var DbRow, x: T) = if x.isNil: raise newException(ValueError, "cannot map nil ref object to DbRow") for field, value in fieldPairs(x[]): - var item: DbItem - item.name = field - toQueryHook(item, value) - val.add item + var destItem: DbItem + destItem.name = field + toQueryHook(destItem, value) + val.add destItem else: - var item: DbItem - toQueryHook(item, x) - val.add item + var destItem: DbItem + toQueryHook(destItem, x) + val.add destItem From 6786b13c15f28e7a964f871161046a9d5f1b2088 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 21 Apr 2026 03:01:25 +0300 Subject: [PATCH 04/40] add return type support --- ormin/ormin_postgre.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index 7b524cb..3e6408d 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -192,7 +192,7 @@ template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: let srcLen = int(pqgetlength(queryResult, queryI, idx.cint)) fillString(item.value, src, srcLen) -proc bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; name: string) = +template bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; name: string) = var item: DbItem bindResultRaw(db, s, idx, item, name) row.add item From c8908e9c98455d4cb521d1769e7ea756f3d5cc91 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 21 Apr 2026 03:06:05 +0300 Subject: [PATCH 05/40] add return type support --- ormin/ormin_postgre.nim | 8 ++++---- ormin/ormin_sqlite.nim | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index 3e6408d..9abb8e6 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -181,8 +181,8 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; else: bindToJson(db, s, idx, x, t, name) -template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: string) = - item.name = name +template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; colName: string) = + item.name = colName if pqgetisnull(queryResult, queryI, idx.cint) != 0: item.isNull = true setLen(item.value, 0) @@ -192,9 +192,9 @@ template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: let srcLen = int(pqgetlength(queryResult, queryI, idx.cint)) fillString(item.value, src, srcLen) -template bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; name: string) = +template bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; colName: string) = var item: DbItem - bindResultRaw(db, s, idx, item, name) + bindResultRaw(db, s, idx, item, colName) row.add item template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; diff --git a/ormin/ormin_sqlite.nim b/ormin/ormin_sqlite.nim index 29d41c6..bb721b1 100644 --- a/ormin/ormin_sqlite.nim +++ b/ormin/ormin_sqlite.nim @@ -201,8 +201,8 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; else: bindToJson(db, s, idx, x, t, name) -template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: string) = - item.name = name +template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; colName: string) = + item.name = colName if column_type(s, idx.cint) == SQLITE_NULL: item.isNull = true setLen(item.value, 0) @@ -212,9 +212,9 @@ template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; name: let src = column_text(s, idx.cint) fillString(item.value, src, srcLen) -proc bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; name: string) = +proc bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; colName: string) = var item: DbItem - bindResultRaw(db, s, idx, item, name) + bindResultRaw(db, s, idx, item, colName) row.add item template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; From eec4ce63b384366ef85ff47e227b50ec2f6b0816 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 21 Apr 2026 14:49:29 +0300 Subject: [PATCH 06/40] cleanup --- ormin.nimble | 1 - 1 file changed, 1 deletion(-) diff --git a/ormin.nimble b/ormin.nimble index 55e6501..a1f9f4f 100644 --- a/ormin.nimble +++ b/ormin.nimble @@ -20,5 +20,4 @@ feature "examples": import std/os when fileExists("config.nims"): include "config.nims" -requires "msgpack4nim" From fd76ba70129a2759004d67a367915daf40d71914 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Tue, 21 Apr 2026 14:55:54 +0300 Subject: [PATCH 07/40] cleanup --- ormin/query_hooks.nim | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim index 071e0e1..b6f6c28 100644 --- a/ormin/query_hooks.nim +++ b/ormin/query_hooks.nim @@ -28,13 +28,7 @@ proc dbItemByName*(val: var DbRow; name: string): var DbItem = val[idx] proc fromQueryHook*(to: typedesc[string], val: var DbItem): string = - if val.isNull: - when defined(nimNoNilSeqs): - "" - else: - nil - else: - val.value + if val.isNull: "" else: val.value proc fromQueryHook*(to: typedesc[int], val: var DbItem): int = if val.isNull: From 3cc5e1a91fb6a44ee6e79ad103fe7a76d7df376f Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Thu, 23 Apr 2026 17:52:21 +0300 Subject: [PATCH 08/40] add benchmark --- tests/tquery_types.nim | 72 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index ddb820e..4315a9a 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -1,4 +1,4 @@ -import unittest, strutils, strformat, os +import unittest, strutils, strformat, os, times import std/options import ormin import ormin/db_utils @@ -25,6 +25,10 @@ type id: int message: string + BenchmarkCompositeRow = object + pk1: int + message: string + SplitMessageRow = object parts: seq[string] @@ -35,6 +39,44 @@ type proc fromQueryHook*(to: typedesc[seq[string]], x: string): seq[string] = x.split(",") +when backend == DbBackend.sqlite: + const + benchmarkRowCount = 256 + benchmarkWarmupIterations = 75 + benchmarkIterations = 250 + benchmarkRounds = 5 + maxTypedQuerySlowdown = 1.10 + + proc loadBenchmarkRows() = + db.dropTable(sqlFile, "tb_composite_pk") + db.createTable(sqlFile, "tb_composite_pk") + for i in 1 .. benchmarkRowCount: + let message = &"message-{i}" + query: + insert tb_composite_pk(pk1 = ?i, pk2 = ?i, message = ?message) + + proc benchmarkCurrentQuery(iterations: int): float = + var checksum = 0 + let started = cpuTime() + for _ in 0 ..< iterations: + let rows = query: + select tb_composite_pk(pk1, message) + orderby pk1 + checksum += rows.len + rows[^1][0] + rows[^1][1].len + doAssert checksum > 0 + result = cpuTime() - started + + proc benchmarkTypedQuery(iterations: int): float = + var checksum = 0 + let started = cpuTime() + for _ in 0 ..< iterations: + let rows = query(BenchmarkCompositeRow): + select tb_composite_pk(pk1, message) + orderby pk1 + checksum += rows.len + rows[^1].pk1 + rows[^1].message.len + doAssert checksum > 0 + result = cpuTime() - started + suite &"query(T) mapping on {backend}": setup: db.dropTable(sqlFile, "tb_composite_pk") @@ -95,3 +137,31 @@ suite &"query(T) mapping on {backend}": check rows[1].id == 2 check rows[1].note.isSome check rows[1].note.get == "hello" + + when backend == DbBackend.sqlite: + test "sqlite benchmark for query and query(T)": + loadBenchmarkRows() + + let untypedRows = query: + select tb_composite_pk(pk1, message) + orderby pk1 + let typedRows = query(BenchmarkCompositeRow): + select tb_composite_pk(pk1, message) + orderby pk1 + check untypedRows.len == typedRows.len + check typedRows[0] == BenchmarkCompositeRow(pk1: untypedRows[0][0], message: untypedRows[0][1]) + check typedRows[^1] == BenchmarkCompositeRow(pk1: untypedRows[^1][0], message: untypedRows[^1][1]) + + discard benchmarkCurrentQuery(benchmarkWarmupIterations) + discard benchmarkTypedQuery(benchmarkWarmupIterations) + + var currentBest = high(float) + var typedBest = high(float) + for _ in 0 ..< benchmarkRounds: + currentBest = min(currentBest, benchmarkCurrentQuery(benchmarkIterations)) + typedBest = min(typedBest, benchmarkTypedQuery(benchmarkIterations)) + + let ratio = typedBest / currentBest + echo &"sqlite benchmark query={currentBest:.6f}s query(T)={typedBest:.6f}s ratio={ratio:.3f}x; 10% budget={(ratio <= maxTypedQuerySlowdown)}" + check currentBest > 0.0 + check typedBest > 0.0 From 1cdaef343166b7d05c44827c9c0b4d7d719a9291 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 24 Apr 2026 15:17:12 +0300 Subject: [PATCH 09/40] move to faster hook types --- ormin/ormin_postgre.nim | 21 +--- ormin/ormin_sqlite.nim | 34 +++--- ormin/queries.nim | 145 +++++++++++++++++++++---- ormin/query_hooks.nim | 234 ++++++---------------------------------- tests/tquery_hook.nim | 120 +++++---------------- tests/tquery_types.nim | 15 +++ 6 files changed, 218 insertions(+), 351 deletions(-) diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index 9abb8e6..ed1ebdd 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -1,7 +1,6 @@ import strutils, db_connector/postgres, json, times import db_connector/db_common -import query_hooks export db_common type @@ -63,6 +62,9 @@ template bindParamUnchecked(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyp 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 @@ -181,21 +183,8 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; else: bindToJson(db, s, idx, x, t, name) -template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; colName: string) = - item.name = colName - if pqgetisnull(queryResult, queryI, idx.cint) != 0: - item.isNull = true - setLen(item.value, 0) - else: - item.isNull = false - let src = pqgetvalue(queryResult, queryI, idx.cint) - let srcLen = int(pqgetlength(queryResult, queryI, idx.cint)) - fillString(item.value, src, srcLen) - -template bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; colName: string) = - var item: DbItem - bindResultRaw(db, s, idx, item, colName) - row.add item +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) = diff --git a/ormin/ormin_sqlite.nim b/ormin/ormin_sqlite.nim index bb721b1..3d4f0cc 100644 --- a/ormin/ormin_sqlite.nim +++ b/ormin/ormin_sqlite.nim @@ -5,7 +5,6 @@ import json, times import db_connector/db_common import db_connector/sqlite3 -import query_hooks export db_common type @@ -46,12 +45,12 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) = cast[pointer](nil) else: cast[pointer](unsafeAddr(xs[0])) - if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_STATIC) != SQLITE_OK: + 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_STATIC) != SQLITE_OK: + 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: @@ -62,15 +61,19 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) = else: x.utc().format("yyyy-MM-dd HH:mm:ss") - if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_STATIC) != SQLITE_OK: + 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_STATIC) != SQLITE_OK: + 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 @@ -87,7 +90,7 @@ 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_STATIC) != SQLITE_OK: + 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; @@ -119,7 +122,7 @@ template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode; dtStr[0 ..< i] else: dtStr - if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_STATIC) != SQLITE_OK: + 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; @@ -201,21 +204,8 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode; else: bindToJson(db, s, idx, x, t, name) -template bindResultRaw*(db: DbConn; s: PStmt; idx: int; item: var DbItem; colName: string) = - item.name = colName - if column_type(s, idx.cint) == SQLITE_NULL: - item.isNull = true - setLen(item.value, 0) - else: - item.isNull = false - let srcLen = column_bytes(s, idx.cint) - let src = column_text(s, idx.cint) - fillString(item.value, src, srcLen) - -proc bindResultRawToRow*(db: DbConn; s: PStmt; idx: int; row: var DbRow; colName: string) = - var item: DbItem - bindResultRaw(db, s, idx, item, colName) - row.add item +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) = diff --git a/ormin/queries.nim b/ormin/queries.nim index 9c7208b..46837ca 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -32,6 +32,8 @@ type 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), @@ -810,10 +812,10 @@ proc generateRoutine(name: NimNode, q: QueryBuilder; for p in q.params: if p.isJson: finalParams.add newIdentDefs(p.ex, ident"JsonNode") - body.add newCall(bindSym"bindParamJson", ident"db", prepStmt, newLit(i), p.ex, p.typ) + body.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, true) else: finalParams.add newIdentDefs(p.ex, p.typ) - body.add newCall(bindSym"bindParam", ident"db", prepStmt, newLit(i), 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() @@ -1503,6 +1505,122 @@ proc makeSeq(retType: NimNode; singleRow: bool): NimNode = 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) + + let converted = genSym(nskVar, "queryParam") + let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(typ)) + let valueExpr = newTree(nnkDotExpr, converted, ident"value") + let isNullExpr = newTree(nnkDotExpr, converted, ident"isNull") + result = newTree(nnkBlockStmt, newEmptyNode(), newStmtList( + newTree(nnkVarSection, newIdentDefs(converted, dbValueType, newEmptyNode())), + newCall(bindSym"toQueryHook", converted, ex), + newTree(nnkIfStmt, + newTree(nnkElifBranch, + isNullExpr, + newStmtList(newCall(bindSym"bindNullParam", ident"db", prepStmt, newLit(idx))) + ), + newTree(nnkElse, + newStmtList(newCall(bindSym"bindParam", ident"db", prepStmt, newLit(idx), valueExpr, copyNimTree(typ))) + ) + ) + )) + +proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = + let rawValue = genSym(nskVar, "queryValue") + let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) + let rawValueExpr = newTree(nnkDotExpr, rawValue, ident"value") + let rawIsNullExpr = newTree(nnkDotExpr, rawValue, ident"isNull") + result = newTree(nnkBlockStmt, newEmptyNode(), newStmtList( + newTree(nnkVarSection, newIdentDefs(rawValue, dbValueType, newEmptyNode())), + newTree(nnkIfStmt, + newTree(nnkElifBranch, + newCall(bindSym"columnIsNull", ident"db", prepStmt, newLit(idx)), + newStmtList(newAssignment(rawIsNullExpr, ident"true")) + ), + newTree(nnkElse, + newStmtList( + newAssignment(rawIsNullExpr, ident"false"), + newCall(bindSym"bindResult", ident"db", prepStmt, newLit(idx), rawValueExpr, copyNimTree(sourceType), newLit(colName)) + ) + ) + ), + newAssignment(destExpr, newCall(bindSym"fromQueryHook", copyNimTree(destType), rawValue)) + )) + +proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): NimNode = + result = newStmtList() + for idx, name in q.retNames: + let fieldExpr = newTree(nnkDotExpr, mapped, ident(name)) + let rawValue = genSym(nskVar, "queryValue") + let sourceType = q.retType[idx][1] + let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) + let rawValueExpr = newTree(nnkDotExpr, rawValue, ident"value") + let rawIsNullExpr = newTree(nnkDotExpr, rawValue, ident"isNull") + result.add quote do: + when compiles(`fieldExpr`): + block: + var `rawValue`: `dbValueType` + if columnIsNull(db, `prepStmt`, `idx`): + `rawIsNullExpr` = true + else: + `rawIsNullExpr` = false + bindResult(db, `prepStmt`, `idx`, `rawValueExpr`, `sourceType`, `name`) + bindFromQueryHook(`fieldExpr`, `rawValue`) + +proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = + let mapped = genSym(nskVar, "mapped") + let scalarMapped = genSym(nskVar, "mapped") + let selectedCount = newLit(q.retType.len) + let mappedObjectStmt = newStmtList( + newTree(nnkVarSection, newIdentDefs(mapped, copyNimTree(retType), newEmptyNode())), + buildQueryHookFieldAssigns(q, prepStmt, mapped), + if singleRow: + newAssignment(res, mapped) + else: + newCall(bindSym"add", res, mapped) + ) + let mappedRefObjectStmt = newStmtList( + newTree(nnkVarSection, newIdentDefs(mapped, copyNimTree(retType), newEmptyNode())), + newCall(bindSym"new", mapped), + buildQueryHookFieldAssigns(q, prepStmt, mapped), + if singleRow: + newAssignment(res, mapped) + else: + newCall(bindSym"add", res, mapped) + ) + let scalarStmt = + if singleRow: + newStmtList(buildHookedResultAssign(prepStmt, res, retType, q.retType[0][1], 0, q.retNames[0])) + else: + newStmtList( + newTree(nnkVarSection, newIdentDefs(scalarMapped, copyNimTree(retType), newEmptyNode())), + buildHookedResultAssign(prepStmt, scalarMapped, retType, q.retType[0][1], 0, q.retNames[0]), + newCall(bindSym"add", res, scalarMapped) + ) + + result = quote do: + block: + when compiles(block: + var probe: `retType` + for field, value in fieldPairs(probe): + discard field + discard value): + `mappedObjectStmt` + elif compiles(block: + var probe: `retType` + new(probe) + for field, value in fieldPairs(probe[]): + discard field + discard value): + `mappedRefObjectStmt` + 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 @@ -1538,8 +1656,7 @@ proc queryImpl(q: QueryBuilder; body: NimNode; attempt, produceJson: bool): NimN if q.params.len > 0: blk.add newCall(bindSym"startBindings", prepStmt, newLit(q.params.len)) for p in q.params: - let fn = if p.isJson: bindSym"bindParamJson" else: bindSym"bindParam" - blk.add newCall(fn, ident"db", prepStmt, newLit(i), p.ex, p.typ) + blk.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, p.isJson) inc i blk.add newCall(bindSym"startQuery", ident"db", prepStmt) var body = newStmtList() @@ -1648,13 +1765,8 @@ proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNo let sql = queryAsString(q, body) let prepStmt = genSym(nskLet) let res = genSym(nskVar) - let mapper = genSym(nskProc, "mapDbRow") - let mapperRow = genSym(nskParam, "row") result = newTree( nnkStmtListExpr, - quote do: - proc `mapper`(`mapperRow`: var DbRow): `retType` = - fromQueryRow(`retType`, `mapperRow`), newLetStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit sql)) ) if q.singleRow: @@ -1669,22 +1781,11 @@ proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNo if q.params.len > 0: blk.add newCall(bindSym"startBindings", prepStmt, newLit(q.params.len)) for p in q.params: - let fn = if p.isJson: bindSym"bindParamJson" else: bindSym"bindParam" - blk.add newCall(fn, ident"db", prepStmt, newLit(i), p.ex, p.typ) + blk.add buildHookedParamBinding(prepStmt, i, p.ex, p.typ, p.isJson) inc i blk.add newCall(bindSym"startQuery", ident"db", prepStmt) - let row = genSym(nskVar, "dbRow") - var action = newStmtList() - action.add newTree(nnkVarSection, newIdentDefs(row, ident"DbRow", - newTree(nnkPrefix, bindSym"@", newTree(nnkBracket)))) - for idx, name in q.retNames: - action.add newCall(bindSym"bindResultRawToRow", ident"db", prepStmt, newLit(idx), row, newLit(name)) - let mappedExpr = newCall(mapper, row) - if q.singleRow: - action.add newAssignment(res, mappedExpr) - else: - action.add newCall(bindSym"add", res, mappedExpr) + let action = buildQueryHookAction(q, prepStmt, res, retType, body, q.singleRow) if q.singleRow: if attempt: diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim index b6f6c28..9d3f232 100644 --- a/ormin/query_hooks.nim +++ b/ormin/query_hooks.nim @@ -1,220 +1,54 @@ -import macros, strutils, options, json, times +import options, json type - DbItem* = object - name*: string - value*: string + DbValue*[T] = object isNull*: bool + value*: T - DbRow* = seq[DbItem] - -var queryHookTimeFormat* = "yyyy-MM-dd HH:mm:ss" - -template fromQueryHook*[T](to: typedesc[T], x: T): T = +template fromQueryHook*[T, S](to: typedesc[T], x: S): T = ## Default conversion hook used by `query(T): ...`. ## Users can overload this proc to customize field/type conversions. - x - -proc dbItemIndex*(val: openArray[DbItem]; name: string): int = - result = -1 - for i, it in val: - if cmpIgnoreCase(it.name, name) == 0: - return i - -proc dbItemByName*(val: var DbRow; name: string): var DbItem = - let idx = dbItemIndex(val, name) - if idx < 0: - raise newException(KeyError, "query(T): missing field in DbRow: " & name) - val[idx] - -proc fromQueryHook*(to: typedesc[string], val: var DbItem): string = - if val.isNull: "" else: val.value + block: + var converted: T = x + converted -proc fromQueryHook*(to: typedesc[int], val: var DbItem): int = - if val.isNull: - raise newException(ValueError, "cannot map NULL to int") - parseInt(val.value) - -proc fromQueryHook*(to: typedesc[int64], val: var DbItem): int64 = - if val.isNull: - raise newException(ValueError, "cannot map NULL to int64") - parseInt(val.value).int64 - -proc fromQueryHook*(to: typedesc[float64], val: var DbItem): float64 = - if val.isNull: - raise newException(ValueError, "cannot map NULL to float64") - parseFloat(val.value) - -proc fromQueryHook*(to: typedesc[bool], val: var DbItem): bool = - if val.isNull: - raise newException(ValueError, "cannot map NULL to bool") - let s = val.value.toLowerAscii() - if s in ["t", "true", "1", "yes", "y"]: - true - elif s in ["f", "false", "0", "no", "n"]: - false - else: - raise newException(ValueError, "invalid boolean DbItem value: " & val.value) - -proc fromQueryHook*(to: typedesc[DateTime], val: var DbItem): DateTime = - if val.isNull: - raise newException(ValueError, "cannot map NULL to DateTime") - let src = val.value - let i = src.find('.') - if i < 0: - if src.len >= 3 and src[src.len - 3] in {'+', '-'}: - parse(src, "yyyy-MM-dd HH:mm:sszz") - else: - parse(src, "yyyy-MM-dd HH:mm:ss", utc()) - else: - if src.len >= 3 and src[src.len - 3] in {'+', '-'}: - let itz = src.len - 3 - let dtstr = src[0.. Date: Fri, 24 Apr 2026 15:19:25 +0300 Subject: [PATCH 10/40] move to faster hook types --- config.nims | 2 -- tests/tquery_hook.nim | 51 ------------------------------------------ tests/tquery_types.nim | 4 ++-- 3 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 tests/tquery_hook.nim diff --git a/config.nims b/config.nims index 633e107..fc01eae 100644 --- a/config.nims +++ b/config.nims @@ -16,7 +16,6 @@ task test, "Run all test suite": exec "nim c -f -r tests/tfeature" exec "nim c -f -r tests/tcommon" - exec "nim c -f --nimcache:.nimcache/tquery_hook -r tests/tquery_hook" exec "nim c -f --nimcache:.nimcache/tquery_types -r tests/tquery_types" exec "nim c -f -r tests/tsqlite" exec "nim c -f -r tests/tdb_utils" @@ -36,7 +35,6 @@ task test_postgres, "Run PostgreSQL test suite": exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon" - exec "nim c -f -d:nimDebugDlOpen --nimcache:.nimcache/tquery_hook_postgre -r -d:postgre tests/tquery_hook" exec "nim c -f -d:nimDebugDlOpen --nimcache:.nimcache/tquery_types_postgre -r -d:postgre tests/tquery_types" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre" diff --git a/tests/tquery_hook.nim b/tests/tquery_hook.nim deleted file mode 100644 index 8af5ceb..0000000 --- a/tests/tquery_hook.nim +++ /dev/null @@ -1,51 +0,0 @@ -import unittest, strutils, json, times -import std/options - -import ormin/query_hooks - -proc fromQueryHook*(to: typedesc[seq[string]], x: string): seq[string] = - x.split(",") - -proc toQueryHook*(val: var string, x: seq[string]) = - val = x.join(",") - -suite "query hook helpers": - test "maps raw DbValue primitives directly": - check fromQueryHook(int, DbValue[int](value: 42)) == 42 - check fromQueryHook(bool, DbValue[bool](value: true)) == true - check fromQueryHook(JsonNode, DbValue[JsonNode](value: %*{"hello": "world"})) == %*{"hello": "world"} - - test "maps null DbValue to Option": - check fromQueryHook(Option[string], DbValue[string](isNull: true)).isNone - check fromQueryHook(Option[string], DbValue[string](value: "hello")) == some("hello") - - test "maps null DbValue to string and JsonNode defaults": - check fromQueryHook(string, DbValue[string](isNull: true)) == "" - check fromQueryHook(JsonNode, DbValue[JsonNode](isNull: true)) == newJNull() - - test "applies custom hooks over raw DB values": - let mapped = fromQueryHook(seq[string], DbValue[string](value: "alice,bob")) - check mapped == @["alice", "bob"] - - test "writes Option.none to a null DbValue": - var value: DbValue[string] - - toQueryHook(value, none(string)) - check value.isNull - check value.value.len == 0 - - test "round-trips custom hook types through DbValue": - var value: DbValue[string] - - toQueryHook(value, @["carol", "dave"]) - check not value.isNull - check value.value == "carol,dave" - check fromQueryHook(seq[string], value) == @["carol", "dave"] - - test "round-trips DateTime through DbValue": - let source = dateTime(2024, mJan, 2, 3, 4, 5, zone = utc()) - var value: DbValue[DateTime] - - toQueryHook(value, source) - check not value.isNull - check fromQueryHook(DateTime, value) == source diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 7387386..2e99a55 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -114,7 +114,7 @@ suite &"query(T) mapping on {backend}": CompositeRow(id: 2, message: "world") ] - test "applies fromQueryHook per destination field type": + test "applies destination hook per field type": let rows = query(SplitMessageRow): select tb_string(typstring as parts) orderby typstring @@ -122,7 +122,7 @@ suite &"query(T) mapping on {backend}": check rows[0].parts == @["alice", "bob"] check rows[1].parts == @["carol", "dave"] - test "applies toQueryHook for custom parameter types": + test "applies parameter hook for custom parameter types": let parts = @["erin", "frank"] query: From ccfc9446f1d41307992a867ff84a6661c95521ff Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 24 Apr 2026 15:21:52 +0300 Subject: [PATCH 11/40] move to faster hook types --- tests/tquery_hook.nim | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/tquery_hook.nim diff --git a/tests/tquery_hook.nim b/tests/tquery_hook.nim new file mode 100644 index 0000000..8af5ceb --- /dev/null +++ b/tests/tquery_hook.nim @@ -0,0 +1,51 @@ +import unittest, strutils, json, times +import std/options + +import ormin/query_hooks + +proc fromQueryHook*(to: typedesc[seq[string]], x: string): seq[string] = + x.split(",") + +proc toQueryHook*(val: var string, x: seq[string]) = + val = x.join(",") + +suite "query hook helpers": + test "maps raw DbValue primitives directly": + check fromQueryHook(int, DbValue[int](value: 42)) == 42 + check fromQueryHook(bool, DbValue[bool](value: true)) == true + check fromQueryHook(JsonNode, DbValue[JsonNode](value: %*{"hello": "world"})) == %*{"hello": "world"} + + test "maps null DbValue to Option": + check fromQueryHook(Option[string], DbValue[string](isNull: true)).isNone + check fromQueryHook(Option[string], DbValue[string](value: "hello")) == some("hello") + + test "maps null DbValue to string and JsonNode defaults": + check fromQueryHook(string, DbValue[string](isNull: true)) == "" + check fromQueryHook(JsonNode, DbValue[JsonNode](isNull: true)) == newJNull() + + test "applies custom hooks over raw DB values": + let mapped = fromQueryHook(seq[string], DbValue[string](value: "alice,bob")) + check mapped == @["alice", "bob"] + + test "writes Option.none to a null DbValue": + var value: DbValue[string] + + toQueryHook(value, none(string)) + check value.isNull + check value.value.len == 0 + + test "round-trips custom hook types through DbValue": + var value: DbValue[string] + + toQueryHook(value, @["carol", "dave"]) + check not value.isNull + check value.value == "carol,dave" + check fromQueryHook(seq[string], value) == @["carol", "dave"] + + test "round-trips DateTime through DbValue": + let source = dateTime(2024, mJan, 2, 3, 4, 5, zone = utc()) + var value: DbValue[DateTime] + + toQueryHook(value, source) + check not value.isNull + check fromQueryHook(DateTime, value) == source From be421e98a7777d27edefecd6922ab3f8aa8afa39 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Fri, 24 Apr 2026 15:27:49 +0300 Subject: [PATCH 12/40] move to faster hook types --- tests/tquery_hook.nim | 51 ------------------------------------------ tests/tquery_types.nim | 38 +------------------------------ 2 files changed, 1 insertion(+), 88 deletions(-) delete mode 100644 tests/tquery_hook.nim diff --git a/tests/tquery_hook.nim b/tests/tquery_hook.nim deleted file mode 100644 index 8af5ceb..0000000 --- a/tests/tquery_hook.nim +++ /dev/null @@ -1,51 +0,0 @@ -import unittest, strutils, json, times -import std/options - -import ormin/query_hooks - -proc fromQueryHook*(to: typedesc[seq[string]], x: string): seq[string] = - x.split(",") - -proc toQueryHook*(val: var string, x: seq[string]) = - val = x.join(",") - -suite "query hook helpers": - test "maps raw DbValue primitives directly": - check fromQueryHook(int, DbValue[int](value: 42)) == 42 - check fromQueryHook(bool, DbValue[bool](value: true)) == true - check fromQueryHook(JsonNode, DbValue[JsonNode](value: %*{"hello": "world"})) == %*{"hello": "world"} - - test "maps null DbValue to Option": - check fromQueryHook(Option[string], DbValue[string](isNull: true)).isNone - check fromQueryHook(Option[string], DbValue[string](value: "hello")) == some("hello") - - test "maps null DbValue to string and JsonNode defaults": - check fromQueryHook(string, DbValue[string](isNull: true)) == "" - check fromQueryHook(JsonNode, DbValue[JsonNode](isNull: true)) == newJNull() - - test "applies custom hooks over raw DB values": - let mapped = fromQueryHook(seq[string], DbValue[string](value: "alice,bob")) - check mapped == @["alice", "bob"] - - test "writes Option.none to a null DbValue": - var value: DbValue[string] - - toQueryHook(value, none(string)) - check value.isNull - check value.value.len == 0 - - test "round-trips custom hook types through DbValue": - var value: DbValue[string] - - toQueryHook(value, @["carol", "dave"]) - check not value.isNull - check value.value == "carol,dave" - check fromQueryHook(seq[string], value) == @["carol", "dave"] - - test "round-trips DateTime through DbValue": - let source = dateTime(2024, mJan, 2, 3, 4, 5, zone = utc()) - var value: DbValue[DateTime] - - toQueryHook(value, source) - check not value.isNull - check fromQueryHook(DateTime, value) == source diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 2e99a55..5f4a1f6 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -1,4 +1,4 @@ -import unittest, strutils, strformat, os, times +import unittest, strformat, os, times import std/options import ormin import ormin/db_utils @@ -29,19 +29,10 @@ type pk1: int message: string - SplitMessageRow = object - parts: seq[string] - NullableNoteOptionRow = object id: int note: Option[string] -proc fromQueryHook*(to: typedesc[seq[string]], x: string): seq[string] = - x.split(",") - -proc toQueryHook*(val: var string, x: seq[string]) = - val = x.join(",") - when backend == DbBackend.sqlite: const benchmarkRowCount = 256 @@ -84,8 +75,6 @@ suite &"query(T) mapping on {backend}": setup: db.dropTable(sqlFile, "tb_composite_pk") db.createTable(sqlFile, "tb_composite_pk") - db.dropTable(sqlFile, "tb_string") - db.createTable(sqlFile, "tb_string") db.dropTable(sqlFile, "tb_nullable") db.createTable(sqlFile, "tb_nullable") @@ -94,11 +83,6 @@ suite &"query(T) mapping on {backend}": query: insert tb_composite_pk(pk1 = 2, pk2 = 2, message = "world") - query: - insert tb_string(typstring = "alice,bob") - query: - insert tb_string(typstring = "carol,dave") - query: insert tb_nullable(id = 1, note = nil) query: @@ -114,26 +98,6 @@ suite &"query(T) mapping on {backend}": CompositeRow(id: 2, message: "world") ] - test "applies destination hook per field type": - let rows = query(SplitMessageRow): - select tb_string(typstring as parts) - orderby typstring - - check rows[0].parts == @["alice", "bob"] - check rows[1].parts == @["carol", "dave"] - - test "applies parameter hook for custom parameter types": - let parts = @["erin", "frank"] - - query: - insert tb_string(typstring = ?parts) - - let rows = query(SplitMessageRow): - select tb_string(typstring as parts) - where typstring == "erin,frank" - - check rows == @[SplitMessageRow(parts: @["erin", "frank"])] - test "single-row query(T) returns a single object": let row = query(CompositeRow): select tb_composite_pk(pk1 as id, message) From e6f67549a79b98c9eda769c10457f1b4db68d314 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 25 Apr 2026 15:28:06 +0300 Subject: [PATCH 13/40] check speed --- config.nims | 4 ++-- tests/tquery_types.nim | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config.nims b/config.nims index fc01eae..9eb46ad 100644 --- a/config.nims +++ b/config.nims @@ -16,7 +16,7 @@ task test, "Run all test suite": exec "nim c -f -r tests/tfeature" exec "nim c -f -r tests/tcommon" - exec "nim c -f --nimcache:.nimcache/tquery_types -r tests/tquery_types" + exec "nim c -f -r -d:release tests/tquery_types" exec "nim c -f -r tests/tsqlite" exec "nim c -f -r tests/tdb_utils" exec "nim c -f -r tests/timportstatic" @@ -35,7 +35,7 @@ task test_postgres, "Run PostgreSQL test suite": exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon" - exec "nim c -f -d:nimDebugDlOpen --nimcache:.nimcache/tquery_types_postgre -r -d:postgre tests/tquery_types" + exec "nim c -f -d:nimDebugDlOpen -r -d:release -d:postgre tests/tquery_types" exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre" task buildexamples, "Build examples: chat and forum": diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 5f4a1f6..e63292e 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -39,7 +39,7 @@ when backend == DbBackend.sqlite: benchmarkWarmupIterations = 75 benchmarkIterations = 250 benchmarkRounds = 5 - maxTypedQuerySlowdown = 1.10 + maxTypedQuerySlowdown = 1.20 proc loadBenchmarkRows() = db.dropTable(sqlFile, "tb_composite_pk") @@ -117,8 +117,7 @@ suite &"query(T) mapping on {backend}": check rows[1].note.isSome check rows[1].note.get == "hello" - when backend == DbBackend.sqlite: - test "sqlite benchmark for query and query(T)": + test "sqlite benchmark for query and query(T)": loadBenchmarkRows() let untypedRows = query: @@ -144,3 +143,6 @@ suite &"query(T) mapping on {backend}": echo &"sqlite benchmark query={currentBest:.6f}s query(T)={typedBest:.6f}s ratio={ratio:.3f}x; 10% budget={(ratio <= maxTypedQuerySlowdown)}" check currentBest > 0.0 check typedBest > 0.0 + when defined(release): + check ratio <= maxTypedQuerySlowdown + From 8936f681a4e16237f02d6fe86255d6880ea6be41 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 25 Apr 2026 15:32:16 +0300 Subject: [PATCH 14/40] cleanup --- ormin.nimble | 2 +- tests/tquery_types.nim | 73 +++++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/ormin.nimble b/ormin.nimble index a1f9f4f..1de2b6e 100644 --- a/ormin.nimble +++ b/ormin.nimble @@ -1,6 +1,6 @@ # Package -version = "0.8.1" +version = "0.9.0" author = "Araq" description = "Prepared SQL statement generator. A lightweight ORM." license = "MIT" diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index e63292e..0c6ad72 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -33,43 +33,42 @@ type id: int note: Option[string] -when backend == DbBackend.sqlite: - const - benchmarkRowCount = 256 - benchmarkWarmupIterations = 75 - benchmarkIterations = 250 - benchmarkRounds = 5 - maxTypedQuerySlowdown = 1.20 - - proc loadBenchmarkRows() = - db.dropTable(sqlFile, "tb_composite_pk") - db.createTable(sqlFile, "tb_composite_pk") - for i in 1 .. benchmarkRowCount: - let message = &"message-{i}" - query: - insert tb_composite_pk(pk1 = ?i, pk2 = ?i, message = ?message) - - proc benchmarkCurrentQuery(iterations: int): float = - var checksum = 0 - let started = cpuTime() - for _ in 0 ..< iterations: - let rows = query: - select tb_composite_pk(pk1, message) - orderby pk1 - checksum += rows.len + rows[^1][0] + rows[^1][1].len - doAssert checksum > 0 - result = cpuTime() - started - - proc benchmarkTypedQuery(iterations: int): float = - var checksum = 0 - let started = cpuTime() - for _ in 0 ..< iterations: - let rows = query(BenchmarkCompositeRow): - select tb_composite_pk(pk1, message) - orderby pk1 - checksum += rows.len + rows[^1].pk1 + rows[^1].message.len - doAssert checksum > 0 - result = cpuTime() - started +const + benchmarkRowCount = 256 + benchmarkWarmupIterations = 75 + benchmarkIterations = 250 + benchmarkRounds = 5 + maxTypedQuerySlowdown = 1.20 + +proc loadBenchmarkRows() = + db.dropTable(sqlFile, "tb_composite_pk") + db.createTable(sqlFile, "tb_composite_pk") + for i in 1 .. benchmarkRowCount: + let message = &"message-{i}" + query: + insert tb_composite_pk(pk1 = ?i, pk2 = ?i, message = ?message) + +proc benchmarkCurrentQuery(iterations: int): float = + var checksum = 0 + let started = cpuTime() + for _ in 0 ..< iterations: + let rows = query: + select tb_composite_pk(pk1, message) + orderby pk1 + checksum += rows.len + rows[^1][0] + rows[^1][1].len + doAssert checksum > 0 + result = cpuTime() - started + +proc benchmarkTypedQuery(iterations: int): float = + var checksum = 0 + let started = cpuTime() + for _ in 0 ..< iterations: + let rows = query(BenchmarkCompositeRow): + select tb_composite_pk(pk1, message) + orderby pk1 + checksum += rows.len + rows[^1].pk1 + rows[^1].message.len + doAssert checksum > 0 + result = cpuTime() - started suite &"query(T) mapping on {backend}": setup: From d354729c95d343d0e930955ef5988b798345ad1a Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 25 Apr 2026 15:35:37 +0300 Subject: [PATCH 15/40] add fromQueryHook types --- tests/tquery_types.nim | 54 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 0c6ad72..7df1e4a 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -2,6 +2,7 @@ import unittest, strformat, os, times import std/options import ormin import ormin/db_utils +import ormin/query_hooks when defined(postgre): when defined(macosx): @@ -33,6 +34,18 @@ type id: int note: Option[string] + MessageSize = distinct int + + HookedMessageRow = object + id: int + message: MessageSize + + NullFallbackNote = distinct string + + HookedNullableNoteRow = object + id: int + note: NullFallbackNote + const benchmarkRowCount = 256 benchmarkWarmupIterations = 75 @@ -40,6 +53,15 @@ const benchmarkRounds = 5 maxTypedQuerySlowdown = 1.20 +proc fromQueryHook*(to: typedesc[MessageSize], value: string): MessageSize = + MessageSize(value.len) + +proc fromQueryHook*(to: typedesc[NullFallbackNote], value: DbValue[string]): NullFallbackNote = + if value.isNull: + NullFallbackNote("") + else: + NullFallbackNote("note:" & value.value) + proc loadBenchmarkRows() = db.dropTable(sqlFile, "tb_composite_pk") db.createTable(sqlFile, "tb_composite_pk") @@ -116,6 +138,37 @@ suite &"query(T) mapping on {backend}": check rows[1].note.isSome check rows[1].note.get == "hello" + test "maps object fields through user fromQueryHook overloads": + let rows = query(HookedMessageRow): + select tb_composite_pk(pk1 as id, message) + orderby pk1 + + check rows.len == 2 + check rows[0].id == 1 + check int(rows[0].message) == "hello".len + check rows[1].id == 2 + check int(rows[1].message) == "world".len + + test "maps scalar query(T) through user fromQueryHook overloads": + let values = query(MessageSize): + select tb_composite_pk(message) + orderby pk1 + + check values.len == 2 + check int(values[0]) == "hello".len + check int(values[1]) == "world".len + + test "allows user fromQueryHook overloads to handle NULL values": + let rows = query(HookedNullableNoteRow): + select tb_nullable(id, note) + orderby id + + check rows.len == 2 + check rows[0].id == 1 + check string(rows[0].note) == "" + check rows[1].id == 2 + check string(rows[1].note) == "note:hello" + test "sqlite benchmark for query and query(T)": loadBenchmarkRows() @@ -144,4 +197,3 @@ suite &"query(T) mapping on {backend}": check typedBest > 0.0 when defined(release): check ratio <= maxTypedQuerySlowdown - From 8c7bf32a91bd5a9084fb39f4c8d87963e1726597 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 25 Apr 2026 15:41:38 +0300 Subject: [PATCH 16/40] add fromQueryHook types --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/README.md b/README.md index fb79293..3763527 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,75 @@ let newId = query: ``` +### Typed Queries + +Use `query(T):` when you want Ormin to deserialize selected columns directly into a named Nim type instead of returning the default tuple shape. This is useful at module boundaries where a named object, ref object, or scalar domain type is clearer than a tuple. + +For object results, selected column names must match fields on the destination type. Use `as` aliases when the database column name differs from the Nim field name: + +```nim +type + ThreadSummary = object + id: int + title: string + +let threads = query(ThreadSummary): + select thread(id, name as title) + orderby id +``` + +Selecting one column can map directly to a scalar type: + +```nim +let names = query(string): + select thread(name) +``` + +Queries that return a single row, such as a `limit 1` query, return one `T` instead of `seq[T]`: + +```nim +let thread = query(ThreadSummary): + select thread(id, name as title) + where id == ?threadId + limit 1 +``` + +#### `fromQueryHook` Column Hooks + +Typed queries deserialize each selected column through `fromQueryHook`. You can overload this hook for your own field or scalar destination types: + +```nim +import ormin/query_hooks + +type + TitleLength = distinct int + + ThreadTitleSize = object + id: int + title: TitleLength + +proc fromQueryHook*(to: typedesc[TitleLength], value: string): TitleLength = + TitleLength(value.len) + +let rows = query(ThreadTitleSize): + select thread(id, name as title) +``` + +If a hook needs to handle SQL `NULL` itself, accept a `DbValue[SourceType]`: + +```nim +type + NullableTitle = distinct string + +proc fromQueryHook*(to: typedesc[NullableTitle], value: DbValue[string]): NullableTitle = + if value.isNull: + NullableTitle("") + else: + NullableTitle(value.value) +``` + +These are column deserialization hooks. In object typed queries, Ormin calls `fromQueryHook` separately for each selected column that maps to a destination field; it does not currently call a hook for the entire row object. For whole-row transformations, query into an intermediate typed result and convert it in regular Nim code. + ### JSON and Raw SQL JSON values can be spliced directly using `%` expressions. The `%` prefix tells Ormin to treat the following Nim expression as a `JsonNode` without conversion: From 4c13acb25581d3e21601a9758410e1ff99b795ff Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 25 Apr 2026 16:03:55 +0300 Subject: [PATCH 17/40] typed queries --- tests/tquery_types.nim | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 7df1e4a..fa23581 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -26,6 +26,10 @@ type id: int message: string + RefCompositeRow = ref object + id: int + message: string + BenchmarkCompositeRow = object pk1: int message: string @@ -119,6 +123,19 @@ suite &"query(T) mapping on {backend}": CompositeRow(id: 2, message: "world") ] + test "maps selected rows to ref objects": + let rows = query(RefCompositeRow): + select tb_composite_pk(pk1 as id, message) + orderby pk1 + + check rows.len == 2 + check rows[0] != nil + check rows[0].id == 1 + check rows[0].message == "hello" + check rows[1] != nil + check rows[1].id == 2 + check rows[1].message == "world" + test "single-row query(T) returns a single object": let row = query(CompositeRow): select tb_composite_pk(pk1 as id, message) From 73699e213905f854e71e6491369a78b0825d657f Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 25 Apr 2026 16:11:51 +0300 Subject: [PATCH 18/40] typed queries --- README.md | 4 ++-- ormin/query_hooks.nim | 6 +++--- tests/tquery_types.nim | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3763527..b601ee0 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ type id: int title: TitleLength -proc fromQueryHook*(to: typedesc[TitleLength], value: string): TitleLength = +proc fromQueryHook*(tp: typedesc[TitleLength], value: string): TitleLength = TitleLength(value.len) let rows = query(ThreadTitleSize): @@ -301,7 +301,7 @@ If a hook needs to handle SQL `NULL` itself, accept a `DbValue[SourceType]`: type NullableTitle = distinct string -proc fromQueryHook*(to: typedesc[NullableTitle], value: DbValue[string]): NullableTitle = +proc fromQueryHook*(tp: typedesc[NullableTitle], value: DbValue[string]): NullableTitle = if value.isNull: NullableTitle("") else: diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim index 9d3f232..dbacf8c 100644 --- a/ormin/query_hooks.nim +++ b/ormin/query_hooks.nim @@ -5,7 +5,7 @@ type isNull*: bool value*: T -template fromQueryHook*[T, S](to: typedesc[T], x: S): T = +template fromQueryHook*[T, S](tp: typedesc[T], x: S): T = ## Default conversion hook used by `query(T): ...`. ## Users can overload this proc to customize field/type conversions. block: @@ -20,13 +20,13 @@ template toQueryHook*[T, S](val: var T, x: S) = proc nullQueryValueError() {.noreturn.} = raise newException(ValueError, "cannot map NULL query result") -proc fromQueryHook*[T, S](to: typedesc[Option[T]], x: DbValue[S]): Option[T] = +proc fromQueryHook*[T, S](tp: typedesc[Option[T]], x: DbValue[S]): Option[T] = if x.isNull: none(T) else: some(fromQueryHook(T, x.value)) -proc fromQueryHook*[T, S](to: typedesc[T], x: DbValue[S]): T = +proc fromQueryHook*[T, S](tp: typedesc[T], x: DbValue[S]): T = if x.isNull: when T is string: "" diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index fa23581..12c83f3 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -57,10 +57,10 @@ const benchmarkRounds = 5 maxTypedQuerySlowdown = 1.20 -proc fromQueryHook*(to: typedesc[MessageSize], value: string): MessageSize = +proc fromQueryHook*(tp: typedesc[MessageSize], value: string): MessageSize = MessageSize(value.len) -proc fromQueryHook*(to: typedesc[NullFallbackNote], value: DbValue[string]): NullFallbackNote = +proc fromQueryHook*(tp: typedesc[NullFallbackNote], value: DbValue[string]): NullFallbackNote = if value.isNull: NullFallbackNote("") else: From 9ea8fdb65bda2723bcddf32e64f0eeeb2e93611b Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:04:46 +0300 Subject: [PATCH 19/40] use monotimes --- tests/tquery_types.nim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 12c83f3..3585023 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -1,4 +1,4 @@ -import unittest, strformat, os, times +import unittest, strformat, os, times, std/monotimes import std/options import ormin import ormin/db_utils @@ -76,25 +76,25 @@ proc loadBenchmarkRows() = proc benchmarkCurrentQuery(iterations: int): float = var checksum = 0 - let started = cpuTime() + let started = getMonoTime() for _ in 0 ..< iterations: let rows = query: select tb_composite_pk(pk1, message) orderby pk1 checksum += rows.len + rows[^1][0] + rows[^1][1].len doAssert checksum > 0 - result = cpuTime() - started + result = (getMonoTime() - started).inNanoseconds.float / 1_000_000_000.0 proc benchmarkTypedQuery(iterations: int): float = var checksum = 0 - let started = cpuTime() + let started = getMonoTime() for _ in 0 ..< iterations: let rows = query(BenchmarkCompositeRow): select tb_composite_pk(pk1, message) orderby pk1 checksum += rows.len + rows[^1].pk1 + rows[^1].message.len doAssert checksum > 0 - result = cpuTime() - started + result = (getMonoTime() - started).inNanoseconds.float / 1_000_000_000.0 suite &"query(T) mapping on {backend}": setup: From a6d4f6105e58f11c1f248467e43db22bef68bf66 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:30:14 +0300 Subject: [PATCH 20/40] cleanup macros --- ormin/queries.nim | 62 ++++++++++++++++++++---------------------- tests/tquery_types.nim | 2 +- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 46837ca..5016446 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1513,41 +1513,29 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(typ)) let valueExpr = newTree(nnkDotExpr, converted, ident"value") let isNullExpr = newTree(nnkDotExpr, converted, ident"isNull") - result = newTree(nnkBlockStmt, newEmptyNode(), newStmtList( - newTree(nnkVarSection, newIdentDefs(converted, dbValueType, newEmptyNode())), - newCall(bindSym"toQueryHook", converted, ex), - newTree(nnkIfStmt, - newTree(nnkElifBranch, - isNullExpr, - newStmtList(newCall(bindSym"bindNullParam", ident"db", prepStmt, newLit(idx))) - ), - newTree(nnkElse, - newStmtList(newCall(bindSym"bindParam", ident"db", prepStmt, newLit(idx), valueExpr, copyNimTree(typ))) - ) - ) - )) + result = quote do: + block: + var `converted`: `dbValueType` + toQueryHook(`converted`, `ex`) + if `isNullExpr`: + bindNullParam(db, `prepStmt`, `idx`) + else: + bindParam(db, `prepStmt`, `idx`, `valueExpr`, `typ`) proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = let rawValue = genSym(nskVar, "queryValue") let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) let rawValueExpr = newTree(nnkDotExpr, rawValue, ident"value") let rawIsNullExpr = newTree(nnkDotExpr, rawValue, ident"isNull") - result = newTree(nnkBlockStmt, newEmptyNode(), newStmtList( - newTree(nnkVarSection, newIdentDefs(rawValue, dbValueType, newEmptyNode())), - newTree(nnkIfStmt, - newTree(nnkElifBranch, - newCall(bindSym"columnIsNull", ident"db", prepStmt, newLit(idx)), - newStmtList(newAssignment(rawIsNullExpr, ident"true")) - ), - newTree(nnkElse, - newStmtList( - newAssignment(rawIsNullExpr, ident"false"), - newCall(bindSym"bindResult", ident"db", prepStmt, newLit(idx), rawValueExpr, copyNimTree(sourceType), newLit(colName)) - ) - ) - ), - newAssignment(destExpr, newCall(bindSym"fromQueryHook", copyNimTree(destType), rawValue)) - )) + result = quote do: + block: + var `rawValue`: `dbValueType` + if columnIsNull(db, `prepStmt`, `idx`): + `rawIsNullExpr` = true + else: + `rawIsNullExpr` = false + bindResult(db, `prepStmt`, `idx`, `rawValueExpr`, `sourceType`, `colName`) + `destExpr` = fromQueryHook(`destType`, `rawValue`) proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): NimNode = result = newStmtList() @@ -1574,7 +1562,9 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode let scalarMapped = genSym(nskVar, "mapped") let selectedCount = newLit(q.retType.len) let mappedObjectStmt = newStmtList( - newTree(nnkVarSection, newIdentDefs(mapped, copyNimTree(retType), newEmptyNode())), + quote do: + var `mapped`: `retType` + , buildQueryHookFieldAssigns(q, prepStmt, mapped), if singleRow: newAssignment(res, mapped) @@ -1582,8 +1572,12 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode newCall(bindSym"add", res, mapped) ) let mappedRefObjectStmt = newStmtList( - newTree(nnkVarSection, newIdentDefs(mapped, copyNimTree(retType), newEmptyNode())), - newCall(bindSym"new", mapped), + quote do: + var `mapped`: `retType` + , + quote do: + new(`mapped`) + , buildQueryHookFieldAssigns(q, prepStmt, mapped), if singleRow: newAssignment(res, mapped) @@ -1595,7 +1589,9 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode newStmtList(buildHookedResultAssign(prepStmt, res, retType, q.retType[0][1], 0, q.retNames[0])) else: newStmtList( - newTree(nnkVarSection, newIdentDefs(scalarMapped, copyNimTree(retType), newEmptyNode())), + quote do: + var `scalarMapped`: `retType` + , buildHookedResultAssign(prepStmt, scalarMapped, retType, q.retType[0][1], 0, q.retNames[0]), newCall(bindSym"add", res, scalarMapped) ) diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 3585023..8ff6709 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -209,7 +209,7 @@ suite &"query(T) mapping on {backend}": typedBest = min(typedBest, benchmarkTypedQuery(benchmarkIterations)) let ratio = typedBest / currentBest - echo &"sqlite benchmark query={currentBest:.6f}s query(T)={typedBest:.6f}s ratio={ratio:.3f}x; 10% budget={(ratio <= maxTypedQuerySlowdown)}" + echo &"sqlite benchmark query={currentBest:.6f}s query(T)={typedBest:.6f}s ratio={ratio:.3f}x; 20% budget={(ratio <= maxTypedQuerySlowdown)}" check currentBest > 0.0 check typedBest > 0.0 when defined(release): From 12432a71dc2c9ce2c785ef94fbdeda3482fe8f4b Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:32:58 +0300 Subject: [PATCH 21/40] cleanup macros --- ormin/queries.nim | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 5016446..d3457cd 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1511,30 +1511,26 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs let converted = genSym(nskVar, "queryParam") let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(typ)) - let valueExpr = newTree(nnkDotExpr, converted, ident"value") - let isNullExpr = newTree(nnkDotExpr, converted, ident"isNull") result = quote do: block: var `converted`: `dbValueType` toQueryHook(`converted`, `ex`) - if `isNullExpr`: + if `converted`.isNull: bindNullParam(db, `prepStmt`, `idx`) else: - bindParam(db, `prepStmt`, `idx`, `valueExpr`, `typ`) + bindParam(db, `prepStmt`, `idx`, `converted`.value, `typ`) proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = let rawValue = genSym(nskVar, "queryValue") let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) - let rawValueExpr = newTree(nnkDotExpr, rawValue, ident"value") - let rawIsNullExpr = newTree(nnkDotExpr, rawValue, ident"isNull") result = quote do: block: var `rawValue`: `dbValueType` if columnIsNull(db, `prepStmt`, `idx`): - `rawIsNullExpr` = true + `rawValue`.isNull = true else: - `rawIsNullExpr` = false - bindResult(db, `prepStmt`, `idx`, `rawValueExpr`, `sourceType`, `colName`) + `rawValue`.isNull = false + bindResult(db, `prepStmt`, `idx`, `rawValue`.value, `sourceType`, `colName`) `destExpr` = fromQueryHook(`destType`, `rawValue`) proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): NimNode = @@ -1544,17 +1540,15 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): Nim let rawValue = genSym(nskVar, "queryValue") let sourceType = q.retType[idx][1] let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) - let rawValueExpr = newTree(nnkDotExpr, rawValue, ident"value") - let rawIsNullExpr = newTree(nnkDotExpr, rawValue, ident"isNull") result.add quote do: when compiles(`fieldExpr`): block: var `rawValue`: `dbValueType` if columnIsNull(db, `prepStmt`, `idx`): - `rawIsNullExpr` = true + `rawValue`.isNull = true else: - `rawIsNullExpr` = false - bindResult(db, `prepStmt`, `idx`, `rawValueExpr`, `sourceType`, `name`) + `rawValue`.isNull = false + bindResult(db, `prepStmt`, `idx`, `rawValue`.value, `sourceType`, `name`) bindFromQueryHook(`fieldExpr`, `rawValue`) proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = From c84c950cf18d5b338faed82b6ac6aa3b80512823 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:38:20 +0300 Subject: [PATCH 22/40] cleanup macros --- ormin/queries.nim | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index d3457cd..6b0dfae 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1493,12 +1493,6 @@ proc renderInlineQuery(n: NimNode; params: var Params; result.sql = queryAsString(subq, n) result.typ = DbType(kind: dbSet) -proc newGlobalVar(name, typ: NimNode, value: NimNode): NimNode = - result = newTree(nnkVarSection, - newTree(nnkIdentDefs, newTree(nnkPragmaExpr, name, - newTree(nnkPragma, ident"global")), typ, value) - ) - proc makeSeq(retType: NimNode; singleRow: bool): NimNode = if not singleRow: result = newTree(nnkBracketExpr, bindSym"seq", retType) @@ -1510,10 +1504,10 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs return newCall(bindSym"bindParamJson", ident"db", prepStmt, newLit(idx), ex, typ) let converted = genSym(nskVar, "queryParam") - let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(typ)) + let dbValueType = copyNimTree(typ) result = quote do: block: - var `converted`: `dbValueType` + var `converted`: DbValue[`typ`] toQueryHook(`converted`, `ex`) if `converted`.isNull: bindNullParam(db, `prepStmt`, `idx`) From 3e4cb6d5988acd57554caff7333f103fffd93028 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:39:44 +0300 Subject: [PATCH 23/40] cleanup macros --- ormin/queries.nim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 6b0dfae..ca25aed 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1516,10 +1516,9 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = let rawValue = genSym(nskVar, "queryValue") - let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) result = quote do: block: - var `rawValue`: `dbValueType` + var `rawValue`: DbValue[`sourceType`] if columnIsNull(db, `prepStmt`, `idx`): `rawValue`.isNull = true else: From d1843d7fb701e84d6bc3e352e033cbcb1f843d97 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:42:03 +0300 Subject: [PATCH 24/40] cleanup macros --- ormin/queries.nim | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index ca25aed..fd778dd 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1529,20 +1529,19 @@ proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): NimNode = result = newStmtList() for idx, name in q.retNames: - let fieldExpr = newTree(nnkDotExpr, mapped, ident(name)) + let fieldName = ident(name) let rawValue = genSym(nskVar, "queryValue") let sourceType = q.retType[idx][1] - let dbValueType = newTree(nnkBracketExpr, bindSym"DbValue", copyNimTree(sourceType)) result.add quote do: - when compiles(`fieldExpr`): + when compiles(`mapped`.`fieldName`): block: - var `rawValue`: `dbValueType` + var `rawValue`: DbValue[`sourceType`] if columnIsNull(db, `prepStmt`, `idx`): `rawValue`.isNull = true else: `rawValue`.isNull = false bindResult(db, `prepStmt`, `idx`, `rawValue`.value, `sourceType`, `name`) - bindFromQueryHook(`fieldExpr`, `rawValue`) + bindFromQueryHook(`mapped`.`fieldName`, `rawValue`) proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = let mapped = genSym(nskVar, "mapped") From a3e75a99b731f4426d917018666812cc097f594e Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:43:07 +0300 Subject: [PATCH 25/40] cleanup macros --- ormin/queries.nim | 1 - 1 file changed, 1 deletion(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index fd778dd..a841654 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1504,7 +1504,6 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs return newCall(bindSym"bindParamJson", ident"db", prepStmt, newLit(idx), ex, typ) let converted = genSym(nskVar, "queryParam") - let dbValueType = copyNimTree(typ) result = quote do: block: var `converted`: DbValue[`typ`] From d700b8a51dbc847715fd639ebb533b1da1ee1abf Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:48:10 +0300 Subject: [PATCH 26/40] cleanup macros --- ormin/queries.nim | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index a841654..abe0b16 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1506,24 +1506,23 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs let converted = genSym(nskVar, "queryParam") result = quote do: block: - var `converted`: DbValue[`typ`] - toQueryHook(`converted`, `ex`) - if `converted`.isNull: + var converted: DbValue[`typ`] + toQueryHook(converted, `ex`) + if converted.isNull: bindNullParam(db, `prepStmt`, `idx`) else: - bindParam(db, `prepStmt`, `idx`, `converted`.value, `typ`) + bindParam(db, `prepStmt`, `idx`, converted.value, `typ`) proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = - let rawValue = genSym(nskVar, "queryValue") result = quote do: block: - var `rawValue`: DbValue[`sourceType`] + var rawValue: DbValue[`sourceType`] if columnIsNull(db, `prepStmt`, `idx`): - `rawValue`.isNull = true + rawValue.isNull = true else: - `rawValue`.isNull = false - bindResult(db, `prepStmt`, `idx`, `rawValue`.value, `sourceType`, `colName`) - `destExpr` = fromQueryHook(`destType`, `rawValue`) + rawValue.isNull = false + bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `colName`) + `destExpr` = fromQueryHook(`destType`, rawValue) proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): NimNode = result = newStmtList() From 787169ae821b0ff495f68b7683146bbe5b4fb447 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:49:08 +0300 Subject: [PATCH 27/40] cleanup macros --- ormin/queries.nim | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index abe0b16..487731b 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1528,18 +1528,17 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): Nim result = newStmtList() for idx, name in q.retNames: let fieldName = ident(name) - let rawValue = genSym(nskVar, "queryValue") let sourceType = q.retType[idx][1] result.add quote do: when compiles(`mapped`.`fieldName`): block: - var `rawValue`: DbValue[`sourceType`] + var rawValue: DbValue[`sourceType`] if columnIsNull(db, `prepStmt`, `idx`): - `rawValue`.isNull = true + rawValue.isNull = true else: - `rawValue`.isNull = false - bindResult(db, `prepStmt`, `idx`, `rawValue`.value, `sourceType`, `name`) - bindFromQueryHook(`mapped`.`fieldName`, `rawValue`) + rawValue.isNull = false + bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `name`) + bindFromQueryHook(`mapped`.`fieldName`, rawValue) proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = let mapped = genSym(nskVar, "mapped") From e41072801fc1386480e254e6c01a0b5dd902a295 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 00:54:31 +0300 Subject: [PATCH 28/40] cleanup macros --- ormin/queries.nim | 2 -- 1 file changed, 2 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 487731b..757486d 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1515,7 +1515,6 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = result = quote do: - block: var rawValue: DbValue[`sourceType`] if columnIsNull(db, `prepStmt`, `idx`): rawValue.isNull = true @@ -1531,7 +1530,6 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): Nim let sourceType = q.retType[idx][1] result.add quote do: when compiles(`mapped`.`fieldName`): - block: var rawValue: DbValue[`sourceType`] if columnIsNull(db, `prepStmt`, `idx`): rawValue.isNull = true From 7d46a8c843b3829e6eaf66a3f7796c95a6f3c055 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 01:00:30 +0300 Subject: [PATCH 29/40] cleanup macros --- ormin/queries.nim | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 757486d..5536463 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1544,20 +1544,7 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode let selectedCount = newLit(q.retType.len) let mappedObjectStmt = newStmtList( quote do: - var `mapped`: `retType` - , - buildQueryHookFieldAssigns(q, prepStmt, mapped), - if singleRow: - newAssignment(res, mapped) - else: - newCall(bindSym"add", res, mapped) - ) - let mappedRefObjectStmt = newStmtList( - quote do: - var `mapped`: `retType` - , - quote do: - new(`mapped`) + var `mapped` = `retType`() , buildQueryHookFieldAssigns(q, prepStmt, mapped), if singleRow: @@ -1565,6 +1552,7 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode else: newCall(bindSym"add", res, mapped) ) + let mappedRefObjectStmt = copyNimTree(mappedObjectStmt) let scalarStmt = if singleRow: newStmtList(buildHookedResultAssign(prepStmt, res, retType, q.retType[0][1], 0, q.retNames[0])) From e2286e7dbb7af51ceeedcc40062786f5a082decf Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 01:17:24 +0300 Subject: [PATCH 30/40] cleanup macros --- ormin/queries.nim | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 5536463..7df219c 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1523,8 +1523,11 @@ proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `colName`) `destExpr` = fromQueryHook(`destType`, rawValue) -proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): NimNode = +proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = result = newStmtList() + let mapped = genSym(nskVar, "mapped") + result.add quote do: + var `mapped` = `retType`() for idx, name in q.retNames: let fieldName = ident(name) let sourceType = q.retType[idx][1] @@ -1537,22 +1540,19 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt, mapped: NimNode): Nim rawValue.isNull = false bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `name`) bindFromQueryHook(`mapped`.`fieldName`, rawValue) + if singleRow: + result.add quote do: + `res` = `mapped` + else: + result.add quote do: + `res`.add(`mapped`) proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = - let mapped = genSym(nskVar, "mapped") let scalarMapped = genSym(nskVar, "mapped") let selectedCount = newLit(q.retType.len) let mappedObjectStmt = newStmtList( - quote do: - var `mapped` = `retType`() - , - buildQueryHookFieldAssigns(q, prepStmt, mapped), - if singleRow: - newAssignment(res, mapped) - else: - newCall(bindSym"add", res, mapped) + buildQueryHookFieldAssigns(q, prepStmt, singleRow, res, retType), ) - let mappedRefObjectStmt = copyNimTree(mappedObjectStmt) let scalarStmt = if singleRow: newStmtList(buildHookedResultAssign(prepStmt, res, retType, q.retType[0][1], 0, q.retNames[0])) @@ -1579,7 +1579,7 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode for field, value in fieldPairs(probe[]): discard field discard value): - `mappedRefObjectStmt` + `mappedObjectStmt` else: when `selectedCount` != 1: {.error: "query(T): scalar mapping expects exactly one selected column".} From 71d87cdb15f2f7ae6c655cd2c053e423a67a133f Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 01:31:28 +0300 Subject: [PATCH 31/40] cleanup macros --- ormin/queries.nim | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 7df219c..73d69ea 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1547,6 +1547,9 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: b result.add quote do: `res`.add(`mapped`) +import std/typetraits +export typetraits + proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = let scalarMapped = genSym(nskVar, "mapped") let selectedCount = newLit(q.retType.len) @@ -1567,18 +1570,10 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode result = quote do: block: - when compiles(block: - var probe: `retType` - for field, value in fieldPairs(probe): - discard field - discard value): - `mappedObjectStmt` - elif compiles(block: - var probe: `retType` - new(probe) - for field, value in fieldPairs(probe[]): - discard field - discard value): + static: + echo "\nCOMPOSITE CHECK: ", arity(`retType`), " is object: ", `retType` is object, " is ref: ", `retType` is ref object + + when `retType` is object or `retType` is ref object: `mappedObjectStmt` else: when `selectedCount` != 1: From 895747586e30cbe752af8564a29a6bc1342ea651 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 01:31:50 +0300 Subject: [PATCH 32/40] cleanup macros --- ormin/queries.nim | 3 --- 1 file changed, 3 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 73d69ea..a7f134e 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1570,9 +1570,6 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode result = quote do: block: - static: - echo "\nCOMPOSITE CHECK: ", arity(`retType`), " is object: ", `retType` is object, " is ref: ", `retType` is ref object - when `retType` is object or `retType` is ref object: `mappedObjectStmt` else: From a880c0a29b94ac7a9acf7b06dd6687501a72558c Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:11:08 +0300 Subject: [PATCH 33/40] cleanup macros --- ormin/queries.nim | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index a7f134e..294e08d 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1521,7 +1521,7 @@ proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; else: rawValue.isNull = false bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `colName`) - `destExpr` = fromQueryHook(`destType`, rawValue) + `destExpr`.fromQueryHook(rawValue) proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = result = newStmtList() @@ -1531,15 +1531,12 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: b 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) result.add quote do: when compiles(`mapped`.`fieldName`): - var rawValue: DbValue[`sourceType`] - if columnIsNull(db, `prepStmt`, `idx`): - rawValue.isNull = true - else: - rawValue.isNull = false - bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `name`) - bindFromQueryHook(`mapped`.`fieldName`, rawValue) + `hooked` if singleRow: result.add quote do: `res` = `mapped` From 48933971b7bb87c13a8e8029166d5a9a3e012417 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:13:03 +0300 Subject: [PATCH 34/40] cleanup macros --- README.md | 10 +++++----- ormin/queries.nim | 1 - ormin/query_hooks.nim | 24 ++++++++++++------------ tests/tquery_types.nim | 10 +++++----- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b601ee0..f8c649f 100644 --- a/README.md +++ b/README.md @@ -288,8 +288,8 @@ type id: int title: TitleLength -proc fromQueryHook*(tp: typedesc[TitleLength], value: string): TitleLength = - TitleLength(value.len) +proc fromQueryHook*(val: var TitleLength, value: string) = + val = TitleLength(value.len) let rows = query(ThreadTitleSize): select thread(id, name as title) @@ -301,11 +301,11 @@ If a hook needs to handle SQL `NULL` itself, accept a `DbValue[SourceType]`: type NullableTitle = distinct string -proc fromQueryHook*(tp: typedesc[NullableTitle], value: DbValue[string]): NullableTitle = +proc fromQueryHook*(val: var NullableTitle, value: DbValue[string]) = if value.isNull: - NullableTitle("") + val = NullableTitle("") else: - NullableTitle(value.value) + val = NullableTitle(value.value) ``` These are column deserialization hooks. In object typed queries, Ormin calls `fromQueryHook` separately for each selected column that maps to a destination field; it does not currently call a hook for the entire row object. For whole-row transformations, query into an intermediate typed result and convert it in regular Nim code. diff --git a/ormin/queries.nim b/ormin/queries.nim index 294e08d..380df71 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1503,7 +1503,6 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs if isJson: return newCall(bindSym"bindParamJson", ident"db", prepStmt, newLit(idx), ex, typ) - let converted = genSym(nskVar, "queryParam") result = quote do: block: var converted: DbValue[`typ`] diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim index dbacf8c..0e6d158 100644 --- a/ormin/query_hooks.nim +++ b/ormin/query_hooks.nim @@ -5,12 +5,10 @@ type isNull*: bool value*: T -template fromQueryHook*[T, S](tp: typedesc[T], x: S): 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. - block: - var converted: T = x - converted + val = x template toQueryHook*[T, S](val: var T, x: S) = ## Default conversion hook used for query parameters. @@ -20,25 +18,27 @@ template toQueryHook*[T, S](val: var T, x: S) = proc nullQueryValueError() {.noreturn.} = raise newException(ValueError, "cannot map NULL query result") -proc fromQueryHook*[T, S](tp: typedesc[Option[T]], x: DbValue[S]): Option[T] = +proc fromQueryHook*[T, S](val: var Option[T], x: DbValue[S]) = if x.isNull: - none(T) + val = none(T) else: - some(fromQueryHook(T, x.value)) + var converted: T + fromQueryHook(converted, x.value) + val = some(converted) -proc fromQueryHook*[T, S](tp: typedesc[T], x: DbValue[S]): T = +proc fromQueryHook*[T, S](val: var T, x: DbValue[S]) = if x.isNull: when T is string: - "" + val = "" elif T is JsonNode: - newJNull() + val = newJNull() else: nullQueryValueError() else: - fromQueryHook(T, x.value) + fromQueryHook(val, x.value) proc bindFromQueryHook*[T, S](dest: var T, x: DbValue[S]) = - dest = fromQueryHook(T, x) + fromQueryHook(dest, x) proc toQueryHook*[S, T](val: var DbValue[S], x: Option[T]) = if x.isSome: diff --git a/tests/tquery_types.nim b/tests/tquery_types.nim index 8ff6709..e782c39 100644 --- a/tests/tquery_types.nim +++ b/tests/tquery_types.nim @@ -57,14 +57,14 @@ const benchmarkRounds = 5 maxTypedQuerySlowdown = 1.20 -proc fromQueryHook*(tp: typedesc[MessageSize], value: string): MessageSize = - MessageSize(value.len) +proc fromQueryHook*(val: var MessageSize, value: string) = + val = MessageSize(value.len) -proc fromQueryHook*(tp: typedesc[NullFallbackNote], value: DbValue[string]): NullFallbackNote = +proc fromQueryHook*(val: var NullFallbackNote, value: DbValue[string]) = if value.isNull: - NullFallbackNote("") + val = NullFallbackNote("") else: - NullFallbackNote("note:" & value.value) + val = NullFallbackNote("note:" & value.value) proc loadBenchmarkRows() = db.dropTable(sqlFile, "tb_composite_pk") From e924c7ec47d8471586693c5cb085d7ad56e570cb Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:16:04 +0300 Subject: [PATCH 35/40] simplify macros --- ormin/queries.nim | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 380df71..73c2140 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1543,26 +1543,23 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: b result.add quote do: `res`.add(`mapped`) -import std/typetraits -export typetraits +proc buildQueryHookScalarAssign(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = + result = newStmtList() + let sourceType = q.retType[0][1] + if singleRow: + result.add buildHookedResultAssign(prepStmt, res, retType, sourceType, 0, q.retNames[0]) + else: + let mapped = genSym(nskVar, "mapped") + result.add quote do: + var `mapped`: `retType` + result.add buildHookedResultAssign(prepStmt, mapped, retType, sourceType, 0, q.retNames[0]) + result.add quote do: + `res`.add(`mapped`) proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = - let scalarMapped = genSym(nskVar, "mapped") let selectedCount = newLit(q.retType.len) - let mappedObjectStmt = newStmtList( - buildQueryHookFieldAssigns(q, prepStmt, singleRow, res, retType), - ) - let scalarStmt = - if singleRow: - newStmtList(buildHookedResultAssign(prepStmt, res, retType, q.retType[0][1], 0, q.retNames[0])) - else: - newStmtList( - quote do: - var `scalarMapped`: `retType` - , - buildHookedResultAssign(prepStmt, scalarMapped, retType, q.retType[0][1], 0, q.retNames[0]), - newCall(bindSym"add", res, scalarMapped) - ) + let mappedObjectStmt = buildQueryHookFieldAssigns(q, prepStmt, singleRow, res, retType) + let scalarStmt = buildQueryHookScalarAssign(q, prepStmt, singleRow, res, retType) result = quote do: block: From 91ae2a2cf7446eef7961ca54082d992480ab3644 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:24:00 +0300 Subject: [PATCH 36/40] simplify macros --- ormin/queries.nim | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 73c2140..fe23be5 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1522,6 +1522,14 @@ proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `colName`) `destExpr`.fromQueryHook(rawValue) +proc addQueryHookResult(stmts: NimNode; singleRow: bool; res, mapped: NimNode) = + if singleRow: + stmts.add quote do: + `res` = `mapped` + else: + stmts.add quote do: + `res`.add(`mapped`) + proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = result = newStmtList() let mapped = genSym(nskVar, "mapped") @@ -1536,27 +1544,21 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: b result.add quote do: when compiles(`mapped`.`fieldName`): `hooked` - if singleRow: - result.add quote do: - `res` = `mapped` - else: - result.add quote do: - `res`.add(`mapped`) + result.addQueryHookResult(singleRow, res, mapped) proc buildQueryHookScalarAssign(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = result = newStmtList() + let mapped = if singleRow: res else: genSym(nskVar, "mapped") let sourceType = q.retType[0][1] - if singleRow: - result.add buildHookedResultAssign(prepStmt, res, retType, sourceType, 0, q.retNames[0]) - else: - let mapped = genSym(nskVar, "mapped") + + if not singleRow: result.add quote do: var `mapped`: `retType` - result.add buildHookedResultAssign(prepStmt, mapped, retType, sourceType, 0, q.retNames[0]) - result.add quote do: - `res`.add(`mapped`) + result.add buildHookedResultAssign(prepStmt, mapped, retType, sourceType, 0, q.retNames[0]) + if not singleRow: + result.addQueryHookResult(singleRow, res, mapped) -proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType, body: NimNode; singleRow: bool): NimNode = +proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; singleRow: bool): NimNode = let selectedCount = newLit(q.retType.len) let mappedObjectStmt = buildQueryHookFieldAssigns(q, prepStmt, singleRow, res, retType) let scalarStmt = buildQueryHookScalarAssign(q, prepStmt, singleRow, res, retType) @@ -1735,7 +1737,7 @@ proc queryHookImpl(q: QueryBuilder; body: NimNode; attempt: bool; retType: NimNo inc i blk.add newCall(bindSym"startQuery", ident"db", prepStmt) - let action = buildQueryHookAction(q, prepStmt, res, retType, body, q.singleRow) + let action = buildQueryHookAction(q, prepStmt, res, retType, q.singleRow) if q.singleRow: if attempt: From b75a67b1707b2d65f4faf1ddd10bf61df8217870 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:27:18 +0300 Subject: [PATCH 37/40] simplify macros --- ormin/queries.nim | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index fe23be5..49de0a8 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1530,10 +1530,12 @@ proc addQueryHookResult(stmts: NimNode; singleRow: bool; res, mapped: NimNode) = stmts.add quote do: `res`.add(`mapped`) -proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = - result = newStmtList() +proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; singleRow: bool): NimNode = + let selectedCount = newLit(q.retType.len) + let mapped = genSym(nskVar, "mapped") - result.add quote do: + let mappedStmt = newStmtList() + mappedStmt.add quote do: var `mapped` = `retType`() for idx, name in q.retNames: let fieldName = ident(name) @@ -1541,32 +1543,25 @@ proc buildQueryHookFieldAssigns(q: QueryBuilder; prepStmt: NimNode, singleRow: b let destExpr = quote do: `mapped`.`fieldName` let hooked = buildHookedResultAssign(prepStmt, destExpr, retType, sourceType, idx, name) - result.add quote do: + mappedStmt.add quote do: when compiles(`mapped`.`fieldName`): `hooked` - result.addQueryHookResult(singleRow, res, mapped) + mappedStmt.addQueryHookResult(singleRow, res, mapped) -proc buildQueryHookScalarAssign(q: QueryBuilder; prepStmt: NimNode, singleRow: bool, res, retType: NimNode): NimNode = - result = newStmtList() - let mapped = if singleRow: res else: genSym(nskVar, "mapped") + let scalarStmt = newStmtList() + let mappedScalar = if singleRow: res else: genSym(nskVar, "mapped") let sourceType = q.retType[0][1] - if not singleRow: - result.add quote do: - var `mapped`: `retType` - result.add buildHookedResultAssign(prepStmt, mapped, retType, sourceType, 0, q.retNames[0]) + scalarStmt.add quote do: + var `mappedScalar`: `retType` + scalarStmt.add buildHookedResultAssign(prepStmt, mappedScalar, retType, sourceType, 0, q.retNames[0]) if not singleRow: - result.addQueryHookResult(singleRow, res, mapped) - -proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; singleRow: bool): NimNode = - let selectedCount = newLit(q.retType.len) - let mappedObjectStmt = buildQueryHookFieldAssigns(q, prepStmt, singleRow, res, retType) - let scalarStmt = buildQueryHookScalarAssign(q, prepStmt, singleRow, res, retType) + scalarStmt.addQueryHookResult(singleRow, res, mappedScalar) result = quote do: block: when `retType` is object or `retType` is ref object: - `mappedObjectStmt` + `mappedStmt` else: when `selectedCount` != 1: {.error: "query(T): scalar mapping expects exactly one selected column".} From 12d09a5a34ad328ac7b4f6fce052aa41745898c6 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:30:14 +0300 Subject: [PATCH 38/40] simplify macros --- ormin/queries.nim | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ormin/queries.nim b/ormin/queries.nim index 49de0a8..292d2e7 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1522,14 +1522,6 @@ proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `colName`) `destExpr`.fromQueryHook(rawValue) -proc addQueryHookResult(stmts: NimNode; singleRow: bool; res, mapped: NimNode) = - if singleRow: - stmts.add quote do: - `res` = `mapped` - else: - stmts.add quote do: - `res`.add(`mapped`) - proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; singleRow: bool): NimNode = let selectedCount = newLit(q.retType.len) @@ -1546,7 +1538,12 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; sing mappedStmt.add quote do: when compiles(`mapped`.`fieldName`): `hooked` - mappedStmt.addQueryHookResult(singleRow, res, mapped) + 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") @@ -1556,7 +1553,8 @@ proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; sing var `mappedScalar`: `retType` scalarStmt.add buildHookedResultAssign(prepStmt, mappedScalar, retType, sourceType, 0, q.retNames[0]) if not singleRow: - scalarStmt.addQueryHookResult(singleRow, res, mappedScalar) + scalarStmt.add quote do: + `res`.add(`mappedScalar`) result = quote do: block: From 50b5a7da9583b52d901559f16983a7f513c86898 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:48:40 +0300 Subject: [PATCH 39/40] reduce overhead --- ormin/ormin_postgre.nim | 14 ++++++++++++++ ormin/ormin_sqlite.nim | 14 ++++++++++++++ ormin/queries.nim | 6 +----- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index ed1ebdd..e3f1beb 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -1,6 +1,7 @@ import strutils, db_connector/postgres, json, times import db_connector/db_common +import query_hooks export db_common type @@ -171,6 +172,19 @@ 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() diff --git a/ormin/ormin_sqlite.nim b/ormin/ormin_sqlite.nim index 3d4f0cc..762f18a 100644 --- a/ormin/ormin_sqlite.nim +++ b/ormin/ormin_sqlite.nim @@ -5,6 +5,7 @@ import json, times import db_connector/db_common import db_connector/sqlite3 +import query_hooks export db_common type @@ -192,6 +193,19 @@ template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode; 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() diff --git a/ormin/queries.nim b/ormin/queries.nim index 292d2e7..8848126 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -1515,11 +1515,7 @@ proc buildHookedParamBinding(prepStmt: NimNode; idx: int; ex, typ: NimNode; isJs proc buildHookedResultAssign(prepStmt, destExpr, destType, sourceType: NimNode; idx: int; colName: string): NimNode = result = quote do: var rawValue: DbValue[`sourceType`] - if columnIsNull(db, `prepStmt`, `idx`): - rawValue.isNull = true - else: - rawValue.isNull = false - bindResult(db, `prepStmt`, `idx`, rawValue.value, `sourceType`, `colName`) + bindResult(db, `prepStmt`, `idx`, rawValue, `sourceType`, `colName`) `destExpr`.fromQueryHook(rawValue) proc buildQueryHookAction(q: QueryBuilder; prepStmt, res, retType: NimNode; singleRow: bool): NimNode = From 43cc2fc4034bf155892cf4680a7bba88ace44716 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 26 Apr 2026 15:54:24 +0300 Subject: [PATCH 40/40] reduce overhead by using moves --- ormin/query_hooks.nim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ormin/query_hooks.nim b/ormin/query_hooks.nim index 0e6d158..6933b26 100644 --- a/ormin/query_hooks.nim +++ b/ormin/query_hooks.nim @@ -18,15 +18,15 @@ template toQueryHook*[T, S](val: var T, x: S) = proc nullQueryValueError() {.noreturn.} = raise newException(ValueError, "cannot map NULL query result") -proc fromQueryHook*[T, S](val: var Option[T], x: DbValue[S]) = +proc fromQueryHook*[T, S](val: var Option[T], x: var DbValue[S]) = if x.isNull: val = none(T) else: var converted: T - fromQueryHook(converted, x.value) + fromQueryHook(converted, move x.value) val = some(converted) -proc fromQueryHook*[T, S](val: var T, x: DbValue[S]) = +proc fromQueryHook*[T, S](val: var T, x: var DbValue[S]) = if x.isNull: when T is string: val = "" @@ -35,9 +35,9 @@ proc fromQueryHook*[T, S](val: var T, x: DbValue[S]) = else: nullQueryValueError() else: - fromQueryHook(val, x.value) + fromQueryHook(val, move x.value) -proc bindFromQueryHook*[T, S](dest: var T, x: DbValue[S]) = +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]) =