Skip to content

Commit 047539c

Browse files
committed
update
Signed-off-by: George Lemon <georgelemon@protonmail.com>
1 parent 687ab7d commit 047539c

File tree

2 files changed

+139
-32
lines changed

2 files changed

+139
-32
lines changed

src/ozark/query.nim

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,34 @@ template withColumnCheck(model: NimNode, col: string, body) =
6868
expectKind(x[2][0][1], nnkOfInherit)
6969
if x[2][0][1][0] != bindSym"Model":
7070
error("The first argument must be a model type.", x[2][0][1][0])
71-
var withColumnCheckPassed: bool
71+
var checkPassed: bool
7272
for field in x[2][0][2]:
7373
if $(field[0][1]) == col:
74-
withColumnCheckPassed = true
74+
checkPassed = true
7575
body; break
76-
if not withColumnCheckPassed:
76+
if not checkPassed:
7777
error("Column `" & col & "` does not exist in model `" & $model[1] & "`.")
7878

79+
template withColumn(x: NimNode, col: string, body) =
80+
if col == "*":
81+
body # allow all columns, no need to check for existence
82+
elif not col.validIdentifier:
83+
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
84+
else:
85+
expectKind(x, nnkTypeDef) # ensure it's a type definition
86+
expectKind(x[2], nnkRefTy) # ensure it's a ref object
87+
expectKind(x[2][0], nnkObjectTy) # ensure it's an object type
88+
# expectKind(x[2][0][1], nnkOfInherit)
89+
# if x[2][0][1][0] != bindSym"Model":
90+
# error("The first argument must be a model type.", x[2][0][1][0])
91+
var checkPassed: bool
92+
for field in x[2][0][2]:
93+
if $(field[0][1]) == col:
94+
checkPassed = true
95+
body; break
96+
if not checkPassed:
97+
error("Column `" & col & "` does not exist in model `" & $x[0][1] & "`.")
98+
7999
macro prepareTable*(modelName): untyped =
80100
## Compile-time macro to prepare a model's table in the database.
81101
##
@@ -504,6 +524,76 @@ macro get*(sql: untyped): untyped =
504524
let v = sql[1][^1][2]
505525
result = sql.parseSqlQuery("getRow", @[v])
506526

527+
proc validateSqlNodes(nodes: seq[SqlNode], colNames: var seq[string]) {.compileTime.} =
528+
# Walk through the parsed SQL nodes and perform checks to ensure
529+
# that the specified table names and column names exist in the models.
530+
for sqlNode in nodes:
531+
case sqlNode.kind
532+
of nkSelect:
533+
# we must check the columns (if any are specified) and the
534+
# table name in the FROM clause to ensure they exist in the models
535+
let tableName = getTableName(sqlNode.sons[1].sons[0].sons[0].strVal)
536+
if sqlNode.sons[0].sons[0].kind != nkIdent:
537+
for col in sqlNode.sons[0].sons:
538+
let typeDef = StaticSchemas[tableName][0][0]
539+
withColumn(typeDef, col.sons[0].strVal):
540+
colNames.add(col.sons[0].strVal)
541+
else: discard
542+
543+
macro getWith*(sql: untyped, toModelIdent: untyped): untyped =
544+
## Finalize a RAW SQL statement. This macro produces the final SQL
545+
## string and emits runtime code that maps selected columns into
546+
## a new instance of the specified model type.
547+
##
548+
## This is used in conjunction with the `rawSQL` macro. For getting the
549+
## raw results when using `rawSQL`, use the `getRaw` macro instead.
550+
var runtimeCode: NimNode
551+
let calledMacro = sql[1][1][0].strVal
552+
if calledMacro != "ozarkRawSQLResult":
553+
error("The first argument to `getWith` must be the result of a `rawSQL` macro. Got " & calledMacro, sql)
554+
try:
555+
let parsedSql = parseSQL(sql[1][1][1].strVal)
556+
557+
var colNames: seq[string]
558+
validateSqlNodes(parsedSql.sons, colNames)
559+
560+
let tableName = getTableName(toModelIdent.strVal)
561+
let model = StaticSchemas[tableName][0][0]
562+
let args = sql[1][1][^1][1][1]
563+
564+
var idx = 0
565+
var assigns: seq[string]
566+
for colName in colNames:
567+
if colName != "*":
568+
assigns.add("inst." & colName & " = row[" & $idx & "]")
569+
else:
570+
# assign all columns to fields with matching names
571+
for field in model[2][0][2]:
572+
assigns.add("inst." & $(field[0][1]) & " = row[" & $idx & "]")
573+
inc idx # increment the column index for the next assignment
574+
575+
# generate the runtime code that fetches the row and applies
576+
# the generated assignments
577+
let randId = genSym(nskVar, "id")
578+
let runtimeCode =
579+
staticRead("private" / "stubs" / "iteratorGetRow.nim") % [
580+
$parsedSql,
581+
toModelIdent.strVal,
582+
assigns.join("\n "),
583+
"getRow",
584+
(if args.len > 0: "," & args.mapIt(it.repr).join(",") else: ""),
585+
(if args.len > 0: $args.len else: "0"),
586+
randId.repr
587+
]
588+
result = macros.parseStmt(runtimeCode)
589+
except SqlParseError as e:
590+
error("SQL parsing error: " & e.msg, sql)
591+
592+
macro getRaw*(sql: untyped): untyped =
593+
## Finalize a RAW SQL statement. This macro produces the final SQL
594+
## string and emits runtime code that returns the raw results as a sequence of sequences of strings.
595+
discard # TODO
596+
507597
macro exists*(tableName: untyped) =
508598
## Search in the current table for a record matching
509599
## the specified values. This is a placeholder for an `EXISTS` query.
@@ -546,11 +636,20 @@ macro rawSQL*(models: ptr ModelsTable, sql: static string, values: varargs[untyp
546636
let fromNode = sqlNode.sons[0].sons[1]
547637
assert fromNode.kind == nkFrom
548638
for table in fromNode.sons:
549-
withTableCheck ident(table[0].strVal):
550-
discard
639+
if not StaticSchemas.hasKey(getTableName(table[0].strVal)):
640+
raise newException(OzarkModelDefect, "Unknown model `" & $table[0].strVal & "`")
551641
else: discard
552-
result = newCall(
553-
bindSym"ozarkRawSQLResult", newLit(sql)
642+
let blockIdent = genSym(nskLabel, "ozarkBlockRawSQL")
643+
result = nnkBlockStmt.newTree(
644+
blockIdent,
645+
newStmtList(
646+
newCall(bindSym"ozarkHoldModel", nil),
647+
newCall(
648+
bindSym"ozarkRawSQLResult",
649+
newLit(sql),
650+
nnkPrefix.newTree(ident"@", values)
651+
)
652+
)
554653
)
555654
except SqlParseError as e:
556655
raise newException(OzarkModelDefect, "SQL Parsing Error: " & e.msg)

tests/test2.nim

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,10 @@ test "init embedded postgres and create tables":
3838
suite "INSERT and SELECT queries":
3939
test "insert and select data":
4040
withDBPool do:
41-
var name = "John"
4241
let id = Models.table(Users).insert({
43-
name: name,
44-
username: "john1232",
45-
email: "test@example.com",
42+
name: "John Doe",
43+
username: "johndoe",
44+
email: "johndoe@example.com",
4645
}).execGet() # returns the id of the inserted row
4746

4847
let res = Models.table(Users).selectAll()
@@ -51,52 +50,53 @@ suite "INSERT and SELECT queries":
5150

5251
check res.isEmpty == false
5352
check parseInt(res.entries[0].id) == id
54-
check res.entries[0].name == "John"
55-
check res.entries[0].username == "john1232"
56-
check res.entries[0].email == "test@example.com"
53+
check res.entries[0].name == "John Doe"
54+
check res.entries[0].username == "johndoe"
55+
check res.entries[0].email == "johndoe@example.com"
5756

5857
test "select specific columns":
5958
withDBPool do:
6059
let res = Models.table(Users)
6160
.select(["name", "email"])
62-
.where("name", "John").get()
61+
.where("name", "John Doe").get()
6362
check res.isEmpty == false
64-
check res.get(0).name == "John"
65-
check res.get(0).email == "test@example.com"
63+
check res.get(0).name == "John Doe"
64+
check res.get(0).email == "johndoe@example.com"
65+
check res.get(0).username == "" # not selected, should be empty
6666

6767
suite "WHERE queries":
6868
test "where query":
6969
withDBPool do:
7070
let res = Models.table(Users)
7171
.select("name")
72-
.where("name", "John").get()
72+
.where("name", "John Doe").get()
7373
check res.isEmpty == false
74-
# check res.get(0).name == "John"
74+
# check res.get(0).name == "John Doe"
7575

7676
test "whereNot query":
7777
withDBPool do:
7878
let res = Models.table(Users)
7979
.select("name")
80-
.whereNot("name", "John").get()
80+
.whereNot("name", "John Doe").get()
8181
check res.isEmpty
8282

8383
test "orWhere query":
8484
withDBPool do:
8585
let res = Models.table(Users)
8686
.select("name")
8787
.where("name", "Ghost")
88-
.orWhere("name", "John").get()
88+
.orWhere("name", "John Doe").get()
8989
check res.isEmpty == false
90-
check res.get(0).name == "John"
90+
check res.get(0).name == "John Doe"
9191

9292
test "orWhereNot query":
9393
withDBPool do:
9494
let res = Models.table(Users)
9595
.select("name")
96-
.whereNot("name", "John")
96+
.whereNot("name", "John Doe")
9797
.orWhereNot("name", "Ghost").get()
9898
check res.isEmpty == false
99-
check res.get(0).name == "John"
99+
check res.get(0).name == "John Doe"
100100

101101

102102
suite "LIKE queries":
@@ -105,50 +105,58 @@ suite "LIKE queries":
105105
let res = Models.table(Users).select("name")
106106
.whereLike("name", "Jo").get()
107107
check res.isEmpty == false
108-
check res.get(0).name == "John"
108+
check res.get(0).name == "John Doe"
109109

110110
test "whereStartsLike query":
111111
withDBPool do:
112112
let res = Models.table(Users).select("name")
113113
.whereStartsLike("name", "Jo").get()
114114
check res.isEmpty == false
115-
check res.get(0).name == "John"
115+
check res.get(0).name == "John Doe"
116116

117117

118118
test "whereEndsLike query":
119119
withDBPool do:
120120
let res = Models.table(Users).select("name")
121-
.whereEndsLike("name", "hn").get()
121+
.whereEndsLike("name", "oe").get()
122122
check res.isEmpty == false
123-
check res.get(0).name == "John"
123+
check res.get(0).name == "John Doe"
124124

125125
test "whereNotLike query":
126126
withDBPool do:
127127
let res = Models.table(Users).select("name")
128128
.whereNot("name", "Ghost").get()
129129
check res.isEmpty == false
130-
check res.get(0).name == "John"
130+
check res.get(0).name == "John Doe"
131131

132132
test "wereNotStartsLike query":
133133
withDBPool do:
134134
let res = Models.table(Users).select("name")
135135
.whereNotStartsLike("name", "Gh").get()
136136
check res.isEmpty == false
137-
check res.get(0).name == "John"
137+
check res.get(0).name == "John Doe"
138138

139139
test "whereNotEndsLike query":
140140
withDBPool do:
141141
let res = Models.table(Users).select("name")
142142
.whereNotEndsLike("name", "st").get()
143143
check res.isEmpty == false
144-
check res.get(0).name == "John"
144+
check res.get(0).name == "John Doe"
145145

146146
suite "IN queries":
147147
test "whereNotIn query":
148148
withDBPool do:
149149
let res = Models.table(Users).select("name")
150-
.whereNotIn("name", "John").get()
150+
.whereNotIn("name", "John Doe").get()
151151
check res.isEmpty == true
152+
153+
suite "RAW queries":
154+
test "raw where query":
155+
withDBPool do:
156+
let res = Models.rawSQL("SELECT name FROM users WHERE name = $1", "Alice")
157+
.getWith(Users)
158+
assert res.isEmpty
159+
152160
{.pop.}
153161

154162
test "close embedded postgres":

0 commit comments

Comments
 (0)