Skip to content

Commit ec11c16

Browse files
committed
update
Signed-off-by: George Lemon <georgelemon@protonmail.com>
1 parent 61ca879 commit ec11c16

File tree

2 files changed

+192
-41
lines changed

2 files changed

+192
-41
lines changed

src/ozark/query.nim

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
import std/[macros, macrocache, strutils, options,
88
sequtils, tables, os, random, strformat]
99

10+
import pkg/parsesql
1011
import pkg/db_connector/postgres {.all.}
1112
import pkg/db_connector/db_postgres {.all.}
1213
import pkg/db_connector/db_common {.all.}
1314

14-
import pkg/parsesql
15-
1615
import ./model, ./collection
1716
import ./private/types
1817

18+
include ./runtime_helpers
19+
1920
export SqlQuery, mapIt
2021

2122
type
@@ -454,6 +455,33 @@ macro whereNotIn*(sql: untyped, col: static string, vals: untyped): untyped =
454455
#
455456
# SQL Query Validator
456457
#
458+
proc countSqlArgs(args: NimNode): int {.compileTime.} =
459+
case args.kind
460+
of nnkEmpty:
461+
0
462+
of nnkBracket:
463+
args.len
464+
of nnkHiddenStdConv:
465+
if args.len > 1 and args[1].kind == nnkBracket: args[1].len else: 1
466+
else:
467+
1
468+
469+
proc appendSqlArgs(callNode: var NimNode, args: NimNode) {.compileTime.} =
470+
case args.kind
471+
of nnkEmpty:
472+
discard
473+
of nnkBracket:
474+
for a in args:
475+
callNode.add(a)
476+
of nnkHiddenStdConv:
477+
if args.len > 1 and args[1].kind == nnkBracket:
478+
for a in args[1]:
479+
callNode.add(a)
480+
else:
481+
callNode.add(args)
482+
else:
483+
callNode.add(args)
484+
457485
proc parseSqlQuery(sql: NimNode, getRowProcName: string,
458486
args: NimNode = newEmptyNode()): NimNode {.compileTime.} =
459487
# Compile-time procedure to validate the SQL query and
@@ -476,46 +504,82 @@ proc parseSqlQuery(sql: NimNode, getRowProcName: string,
476504

477505
# generate code to assign columns to model instance fields
478506
var idx = 0
479-
var assigns: seq[string]
507+
var assigns = newStmtList()
480508
for cn in colNames:
481509
if cn != "*":
482-
assigns.add("inst." & cn & " = row[" & $idx & "]")
510+
# assigns.add("inst." & cn & " = row[" & $idx & "]")
511+
assigns.add(
512+
nnkAsgn.newTree(
513+
nnkDotExpr.newTree(ident("inst"), ident(cn)),
514+
nnkBracketExpr.newTree(ident("row"), newLit(idx))
515+
)
516+
)
483517
else:
484518
# assign all columns to fields with matching names
485519
let modelFields = getTypeImpl(m)[0].getTypeImpl[1]
486520
for field in getImpl(m)[2][0][2]:
487-
assigns.add("inst." & $(field[0][1]) & " = row[" & $idx & "]")
521+
# assigns.add("inst." & $(field[0][1]) & " = row[" & $idx & "]")
522+
assigns.add(
523+
nnkAsgn.newTree(
524+
nnkDotExpr.newTree(ident("inst"), field[0][1]),
525+
nnkBracketExpr.newTree(ident("row"), newLit(idx))
526+
)
527+
)
488528
inc idx
489529

490-
# Create the runtime code that fetches the row and
491-
# applies the generated assignments
492-
var runtimeCode: string
530+
# validate the number of SQL arguments and generate
531+
# the appropriate runtime code to execute the query and
532+
# map results to model instances.
533+
let nParams = countSqlArgs(args)
493534
if getRowProcName == "getRow":
494-
let randId = genSym(nskVar, "id")
495-
runtimeCode =
496-
staticRead("private" / "stubs" / "iteratorGetRow.nim") % [
497-
$parsedSql,
498-
$(m.getImpl[0][1]),
499-
assigns.join("\n "),
500-
getRowProcName,
501-
(if args.len > 0: "," & args.mapIt(it.repr).join(",") else: ""),
502-
(if args.len > 0: $args.len else: "0"),
503-
randId.repr
504-
]
535+
result = newCall(
536+
nnkBracketExpr.newTree(
537+
bindSym"getRowToModel",
538+
ident($(m.getImpl[0][1]))
539+
),
540+
ident"dbcon",
541+
newCall(bindSym"SqlQuery", newLit($parsedSql)),
542+
newLit(nParams),
543+
)
544+
appendSqlArgs(result, args)
545+
result.add(
546+
nnkLambda.newTree(
547+
newEmptyNode(),
548+
newEmptyNode(),
549+
newEmptyNode(),
550+
nnkFormalParams.newTree(
551+
newEmptyNode(),
552+
nnkIdentDefs.newTree(
553+
ident("inst"),
554+
ident($(m.getImpl[0][1])),
555+
newEmptyNode()
556+
),
557+
nnkIdentDefs.newTree(
558+
ident("row"),
559+
nnkBracketExpr.newTree(
560+
ident("seq"),
561+
ident("string")
562+
),
563+
newEmptyNode()
564+
)
565+
),
566+
newEmptyNode(),
567+
newEmptyNode(),
568+
assigns
569+
)
570+
)
505571
else:
506-
let randId = genSym(nskVar, "id")
507-
runtimeCode =
508-
staticRead("private" / "stubs" / "iteratorInstantRows.nim") % [
509-
$parsedSql,
510-
$(m.getImpl[0][1]),
511-
colNames.mapIt("\"" & it & "\"").join(","),
512-
getRowProcName,
513-
(if args.len > 0: "," & args.mapIt(it.repr).join(",") else: ""),
514-
(if args.len > 0: $args.len else: "0"),
515-
randId.repr
516-
]
517-
result = macros.parseStmt(runtimeCode) # parse the generated code into a NimNode
518-
result[0][0] = sql[0] # the original block identifier
572+
result = newCall(
573+
nnkBracketExpr.newTree(
574+
bindSym"instantRowsToModels",
575+
ident($(m.getImpl[0][1]))
576+
),
577+
ident"dbcon",
578+
newCall(bindSym"SqlQuery", newLit($parsedSql)),
579+
newLit(colNames),
580+
newLit(nParams)
581+
)
582+
appendSqlArgs(result, args)
519583
except SqlParseError as e:
520584
error("SQL parsing error: " & e.msg, sql[1][^1][1])
521585

@@ -752,15 +816,6 @@ macro rawSQL*(models: ptr ModelsTable, sql: static string, values: varargs[untyp
752816
except SqlParseError as e:
753817
raise newException(OzarkModelDefect, "SQL Parsing Error: " & e.msg)
754818

755-
type PreparedKey = tuple[conn: pointer, name: string]
756-
var preparedRtCache {.global.}: TableRef[PreparedKey, SqlPrepared] = newTable[PreparedKey, SqlPrepared]()
757-
758-
proc ensurePrepared*(db: DbConn, name: string, sql: SqlQuery, nParams: int): SqlPrepared =
759-
let key: PreparedKey = (cast[pointer](db), name)
760-
if key notin preparedRtCache:
761-
preparedRtCache[key] = prepare(db, name, sql, nParams)
762-
result = preparedRtCache[key]
763-
764819
macro exec*(sql: untyped) =
765820
## Finalize and execute an SQL statement that doesn't
766821
## return results (e.g. INSERT, UPDATE, DELETE).

src/ozark/runtime_helpers.nim

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# A magical ORM for the Nim language
2+
#
3+
# (c) 2026 George Lemon | MIT License
4+
# Made by Humans from OpenPeeps
5+
# https://github.com/openpeeps/ozark
6+
7+
import std/[tables, sequtils, hashes, strutils]
8+
import pkg/db_connector/[postgres, db_postgres, db_common]
9+
10+
import ./collection, ./model
11+
12+
type
13+
PreparedKey* = tuple[conn: pointer, stmtName: string]
14+
15+
var preparedRtCache* {.global.}: TableRef[PreparedKey, SqlPrepared] = newTable[PreparedKey, SqlPrepared]()
16+
## A runtime cache for prepared SQL statements, keyed
17+
## by a combination of the database connection pointer
18+
## and a unique name for the prepared statement. This allows
19+
## us to reuse prepared statements across multiple queries
20+
## without having to prepare them again, improving performance
21+
22+
proc stmtNameFor(sql: SqlQuery, nParams: int): string =
23+
let sig = string(sql) & "|" & $nParams
24+
result = "ozark_stmt_" & toHex(cast[uint64](hash(sig)))
25+
26+
proc ensurePrepared*(db: DbConn, name: string, sql: SqlQuery, nParams: int): SqlPrepared =
27+
## `name` is kept for API compatibility, but we use a
28+
## deterministic name from SQL
29+
let stmtName = stmtNameFor(sql, nParams)
30+
let key: PreparedKey = (cast[pointer](db), stmtName)
31+
if key notin preparedRtCache:
32+
preparedRtCache[key] = prepare(db, stmtName, sql, nParams)
33+
result = preparedRtCache[key]
34+
proc instantRowsToModels*[T](
35+
dbcon: DbConn,
36+
sql: SqlQuery,
37+
colNames: seq[string],
38+
nParams: int,
39+
params: varargs[string, `$`]
40+
): Collection[T] =
41+
## Execute a SQL query that returns multiple rows, and map each row to an
42+
## instance of the specified model type T.
43+
var
44+
isEmpty = true
45+
results: Collection[T]
46+
cols: DBColumns = @[]
47+
colKeys: seq[string]
48+
let sqlPrepared = ensurePrepared(dbcon, "", sql, nParams)
49+
for row in instantRows(dbcon, cols, sqlPrepared, params):
50+
isEmpty = isEmpty and row.len == 0
51+
if isEmpty: continue
52+
if colKeys.len == 0:
53+
colKeys = cols.mapIt(it.name)
54+
var inst = new(T)
55+
var modelFields: seq[string] = @[]
56+
for fName, fValue in inst[].fieldPairs():
57+
modelFields.add(fName)
58+
if colNames[0] != "*" and fName in colNames:
59+
if fName in colKeys:
60+
when fValue.type is string:
61+
fValue = row[colKeys.find(fName)]
62+
else:
63+
raise newException(OzarkModelDefect, "Model field `" & $T & "." & fName & "` does not have a corresponding column in the SQL result")
64+
elif colNames[0] == "*":
65+
if fName in colKeys:
66+
when fValue.type is string:
67+
fValue = row[colKeys.find(fName)]
68+
when compiles(inst.extra):
69+
inst.extra = initTable[string, string]()
70+
for i, k in colKeys:
71+
if k notin modelFields:
72+
inst.extra[k] = row[i]
73+
results.entries.add(inst)
74+
results
75+
76+
proc getRowToModel*[T](
77+
dbcon: DbConn,
78+
sql: SqlQuery,
79+
nParams: int,
80+
params: varargs[string, `$`],
81+
assignProc: proc(inst: T, row: seq[string])
82+
): Collection[T] =
83+
## Execute a SQL query that is expected to return a single row,
84+
## and map that row to an instance of the specified model type T
85+
let sqlPrepared = ensurePrepared(dbcon, "", sql, nParams)
86+
var
87+
row = getRow(dbcon, sqlPrepared, params)
88+
isEmpty = true
89+
results: Collection[T]
90+
for v in row:
91+
isEmpty = isEmpty and v.len == 0
92+
if row.len > 0 and not isEmpty:
93+
var inst = new(T)
94+
assignProc(inst, row)
95+
results.entries.add(inst)
96+
results

0 commit comments

Comments
 (0)