Skip to content

Commit 99fcffa

Browse files
committed
Query - Check Model<>Table columns | Simplify API
Signed-off-by: George Lemon <georgelemon@protonmail.com>
1 parent 6b5019f commit 99fcffa

File tree

2 files changed

+191
-160
lines changed

2 files changed

+191
-160
lines changed

src/ozark/query.nim

Lines changed: 163 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -21,53 +21,96 @@ export SqlQuery, mapIt
2121
type
2222
OzarkModelDefect* = object of CatchableError
2323

24-
template checkTableExists(name: string) =
25-
## Check if a model with the given name exists in the Models table.
26-
if not StaticSchemas.hasKey(name):
27-
raise newException(OzarkModelDefect, "Unknown model `" & name & "`")
28-
2924
randomize() # initialize random seed for generating unique statement names in `tryInsertID`
3025

31-
macro table*(models: ptr ModelsTable, name: static string): untyped =
26+
template table*(models: ptr ModelsTable, name): untyped =
3227
## Define SQL statement for a table
33-
checkTableExists(name)
34-
result = newLit(name)
28+
# checkTableExists(name)
29+
bindSym($name)
30+
31+
template withTableCheck*(name: NimNode, body) =
32+
## Check if a model with the given name exists in the Models table.
33+
if not StaticSchemas.hasKey(getTableName($name[1])):
34+
raise newException(OzarkModelDefect,
35+
"Unknown model `" & $name[1] & "`")
36+
body
37+
38+
template withColumnsCheck(model: NimNode, cols: openArray[string], body) =
39+
for col in cols:
40+
withColumnCheck(model, col):
41+
discard
42+
body
3543

3644
proc ozarkSelectResult(sql: static[string]): NimNode {.compileTime.} = newLit(sql)
37-
proc ozarkWhereResult(sql: static[string], val: string): NimNode {.compileTime.} = newLit(sql)
45+
proc ozarkWhereResult(sql: static[string], val: varargs[string]): NimNode {.compileTime.} = newLit(sql)
3846
proc ozarkWhereInResult(sql: static[string], vals: varargs[string]): NimNode {.compileTime.} = newLit(sql)
3947
proc ozarkRawSQLResult(sql: static[string]): NimNode {.compileTime.} = newLit(sql)
4048
proc ozarkInsertResult(sql: static[string], values: seq[string]): NimNode {.compileTime.} = newLit(sql)
4149
proc ozarkLimitResult(sql: static[string], count: int): NimNode {.compileTime.} = newLit(sql)
4250

51+
proc ozarkHoldModel[T](t: T) {.compileTime.} =
52+
var x: T
53+
54+
template withColumnCheck(model: NimNode, col: string, body) =
55+
if col == "*":
56+
body # allow all columns, no need to check for existence
57+
elif not col.validIdentifier:
58+
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
59+
else:
60+
let x = model[1].getImpl
61+
expectKind(x, nnkTypeDef) # ensure it's a type definition
62+
expectKind(x[2], nnkRefTy) # ensure it's a ref object
63+
expectKind(x[2][0], nnkObjectTy) # ensure it's an object type
64+
expectKind(x[2][0][1], nnkOfInherit)
65+
if x[2][0][1][0] != bindSym"Model":
66+
raise newException(OzarkModelDefect, "The first argument must be a model type.")
67+
var withColumnCheckPassed: bool
68+
for field in x[2][0][2]:
69+
if $(field[0][1]) == col:
70+
withColumnCheckPassed = true
71+
body; break
72+
if not withColumnCheckPassed:
73+
raise newException(OzarkModelDefect,
74+
"Column `" & col & "` does not exist in model `" & $model[1] & "`.")
75+
4376
macro select*(tableName: untyped, cols: static openArray[string]): untyped =
44-
## Define SELECT clause
45-
checkTableExists($tableName)
46-
for col in cols:
47-
if col == "*" or col.validIdentifier:
48-
continue # todo check if column exists in model
49-
else:
50-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
51-
result = newCall(bindSym"ozarkSelectResult",
52-
newLit("SELECT " & cols.join() & " FROM " & $tableName)
53-
)
77+
## Define `SELECT` clause
78+
withTableCheck tableName:
79+
withColumnsCheck tableName, cols:
80+
result = nnkBlockStmt.newTree(
81+
newEmptyNode(),
82+
newCall(bindSym"ozarkHoldModel", tableName),
83+
newCall(bindSym"ozarkSelectResult",
84+
newLit("SELECT " & cols.join() & " FROM " & getTableName($tableName[1]))
85+
)
86+
)
5487

5588
macro select*(tableName: untyped, col: static string): untyped =
5689
## Define SELECT clause
57-
checkTableExists($tableName)
58-
if col == "*" or col.validIdentifier:
59-
discard
60-
else:
61-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
62-
result = newCall(bindSym"ozarkSelectResult",
63-
newLit("SELECT " & col & " FROM " & $tableName)
64-
)
90+
withTableCheck tableName:
91+
withColumnCheck tableName, col:
92+
result = nnkBlockStmt.newTree(
93+
newEmptyNode(),
94+
newStmtList(
95+
newCall(bindSym"ozarkHoldModel", tableName),
96+
newCall(bindSym"ozarkSelectResult",
97+
newLit("SELECT " & col & " FROM " & getTableName($tableName[1]))
98+
)
99+
)
100+
)
65101

66102
macro selectAll*(tableName: untyped): untyped =
67103
## Define SELECT * clause
68-
checkTableExists($tableName)
69-
result = newCall(bindSym"ozarkSelectResult", newLit("SELECT * FROM " & $tableName))
70-
104+
withTableCheck tableName:
105+
result = nnkBlockStmt.newTree(
106+
newEmptyNode(),
107+
newStmtList(
108+
newCall(bindSym"ozarkHoldModel", tableName),
109+
newCall(bindSym"ozarkSelectResult",
110+
newLit("SELECT * FROM " & getTableName($tableName[1]))
111+
)
112+
)
113+
)
71114

72115
#
73116
# WHERE clause macros
@@ -79,75 +122,58 @@ proc writeWhereLikeStatements(op: static string, sql: NimNode,
79122
# Writer macro for both `whereLike` and `whereNotLike` to avoid code duplication.
80123
# This macro generates the SQL string for the WHERE LIKE/NOT LIKE clause and
81124
# also constructs the appropriate infix expression for the value with wildcards
82-
if sql.kind != nnkCall or sql[0].strVal != "ozarkSelectResult":
125+
if sql.kind != nnkBlockExpr or sql[1][1][0].strVal != "ozarkSelectResult":
83126
error("The first argument to `where` statement must be the result of a `select` macro.")
84-
if col.validIdentifier:
85-
# todo check if column exists in model
86-
discard
87-
else:
88-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
89-
let selectSql = sql[1].strVal
90-
result = newCall(bindSym"ozarkWhereResult",
91-
newLit(selectSql & " WHERE " & col & " " & op & " $1"),
92-
infix
93-
)
127+
withColumnCheck(sql[1][0][1], col):
128+
let selectSql = sql[1][1][1].strVal
129+
sql[1][1][0] = bindSym"ozarkWhereResult"
130+
sql[1][1][1].strVal = sql[1][1][1].strVal & " WHERE " & col & " " & op & " $1"
131+
sql[1][1].add(infix)
132+
result = sql
94133

95134
proc writeWhereInWhereNotIn(op: static string,
96135
sql: NimNode, col: string, vals: NimNode): NimNode {.compileTime.} =
97136
# Writer macro for both `whereIn` and `whereNotIn` to avoid code duplication.
98137
# This macro generates the SQL string for the WHERE IN/NOT IN clause and
99138
# also adds the values as additional arguments to the macro result for later use in code generation
100-
if sql.kind != nnkCall or sql[0].strVal != "ozarkSelectResult":
139+
if sql.kind != nnkBlockExpr or sql[1][1][0].strVal!= "ozarkSelectResult":
101140
error("The first argument to must be the result of a `select` macro.")
102-
if col.validIdentifier:
103-
# todo check if column exists in model
104-
discard
105-
else:
106-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
107-
let selectSql = sql[1].strVal
108-
var placeholders = newSeq[string](vals.len)
109-
for i in 0..<vals.len:
110-
placeholders[i] = "$" & $(i + 1)
111-
result = newCall(
112-
bindSym"ozarkWhereInResult",
113-
newLit(selectSql & " WHERE " & col & " " & op & " (" & placeholders.join(",") & ")"),
114-
)
115-
for i in 0..<vals.len:
116-
# add the values as additional arguments to the
117-
# macro result for later use in code generation
118-
result.add(vals[i])
119-
120-
proc writeWhereStatement(op: static string, sql: NimNode, col: string, val: NimNode): NimNode {.compileTime.} =
141+
withColumnCheck(sql[1][0][1], col):
142+
var placeholders = newSeq[string](vals.len)
143+
for i in 0..<vals.len:
144+
placeholders[i] = "$" & $(i + 1)
145+
let selectSql = sql[1][1][1].strVal
146+
sql[1][1][0] = bindSym"ozarkWhereInResult"
147+
sql[1][1][1].strVal = sql[1][1][1].strVal & " WHERE " & col & " " & op & " (" & placeholders.join(",") & ")"
148+
for i in 0..<vals.len:
149+
# add the values as additional arguments to the
150+
# macro result for later use in code generation
151+
sql[1][1].add(vals[i])
152+
result = sql
153+
154+
proc writeWhereStatement(op: static string, sql: NimNode,
155+
col: string, val: NimNode): NimNode {.compileTime.} =
121156
# Writer macro for simple WHERE clauses (e.g. `where`, `whereNot`) to avoid code duplication.
122-
if sql.kind != nnkCall or sql[0].strVal != "ozarkSelectResult":
157+
if sql.kind != nnkBlockExpr or sql[1][1][0].strVal != "ozarkSelectResult":
123158
error("The first argument to `where` must be the result of a `select` macro.")
124-
if col.validIdentifier:
125-
# todo check if column exists in model
126-
discard
127-
else:
128-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
129-
let selectSql = sql[1].strVal
130-
result = newCall(bindSym"ozarkWhereResult",
131-
newLit(selectSql & " WHERE " & col & " " & op & " $1"),
132-
val
133-
)
134-
135-
proc writeOrWhereStatement(op: static string, sql: NimNode, col: string, val: NimNode): NimNode {.compileTime.} =
159+
withColumnCheck(sql[1][0][1], col):
160+
sql[1][1][0] = bindSym"ozarkWhereResult"
161+
sql[1][1][1].strVal = sql[1][1][1].strVal & " WHERE " & col & " " & op & " $1"
162+
sql[1][1].add(val)
163+
result = sql
164+
165+
proc writeOrWhereStatement(op: static string,
166+
sql: NimNode, col: string, val: NimNode): NimNode {.compileTime.} =
136167
# Writer macro for `orWhere` to avoid code duplication with `writeWhereStatement`.
137168
# This macro checks that the first argument is a valid `where` result and then
138169
# appends the new condition with an OR to the existing SQL string.
139-
if sql.kind != nnkCall or sql[0].strVal != "ozarkWhereResult":
170+
if sql.kind != nnkBlockExpr or sql[1][1][0].strVal != "ozarkWhereResult":
140171
error("The first argument to `orWhere` must be the result of a `where` macro.")
141-
if col.validIdentifier:
142-
# todo check if column exists in model
143-
discard
144-
else:
145-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
146-
let whereSql = sql[1].strVal
147-
result = newCall(bindSym"ozarkWhereResult",
148-
newLit(whereSql & " OR " & col & " " & op & " $1"),
149-
val
150-
)
172+
withColumnCheck(sql[1][0][1], col):
173+
let len = sql[1][1][2][1].len + 1 # calculate the new param index based on existing params
174+
sql[1][1][1].strVal = sql[1][1][1].strVal & " OR " & col & " " & op & " $" & $(len)
175+
sql[1][1][2][1].add(val)
176+
result = sql
151177

152178
# WHERE clause public macros
153179
macro where*(sql: untyped, col: static string, val: untyped): untyped =
@@ -206,7 +232,7 @@ macro whereNotIn*(sql: untyped, col: static string, vals: openArray[untyped]): u
206232

207233
template parseSqlQuery(getRowProcName: string, args: seq[NimNode] = @[]) {.dirty.} =
208234
try:
209-
let parsedSql = parseSQL(sql[1].strVal)
235+
let parsedSql = parseSQL(sql[1][1][1].strVal)
210236
# extract selected column names from parsedSql AST
211237
var colNames: seq[string]
212238
let top = parsedSql.sons[0]
@@ -229,7 +255,7 @@ template parseSqlQuery(getRowProcName: string, args: seq[NimNode] = @[]) {.dirty
229255
assigns.add("inst." & cn & " = row[" & $idx & "]")
230256
else:
231257
# assign all columns to fields with matching names
232-
let modelFields = getTypeImpl(m)[1]
258+
let modelFields = getTypeImpl(m)[0].getTypeImpl[1]
233259
for field in getImpl(m)[2][0][2]:
234260
assigns.add("inst." & $(field[0][1]) & " = row[" & $idx & "]")
235261
inc idx
@@ -242,7 +268,7 @@ template parseSqlQuery(getRowProcName: string, args: seq[NimNode] = @[]) {.dirty
242268
runtimeCode =
243269
staticRead("private" / "stubs" / "iteratorGetRow.nim") % [
244270
$parsedSql,
245-
$(getTypeImpl(m)[1]),
271+
$(m.getImpl[0][1]),
246272
assigns.join("\n "),
247273
getRowProcName,
248274
(if args.len > 0: "," & args.mapIt(it.repr).join(",") else: ""),
@@ -254,7 +280,7 @@ template parseSqlQuery(getRowProcName: string, args: seq[NimNode] = @[]) {.dirty
254280
runtimeCode =
255281
staticRead("private" / "stubs" / "iteratorInstantRows.nim") % [
256282
$parsedSql,
257-
$(getTypeImpl(m)[1]),
283+
$(m.getImpl[0][1]),
258284
colNames.mapIt("\"" & it & "\"").join(","),
259285
getRowProcName,
260286
(if args.len > 0: "," & args.mapIt(it.repr).join(",") else: ""),
@@ -265,57 +291,61 @@ template parseSqlQuery(getRowProcName: string, args: seq[NimNode] = @[]) {.dirty
265291
except SqlParseError as e:
266292
raise newException(OzarkModelDefect, "SQL Parsing Error: " & e.msg)
267293

268-
macro getAll*(sql: untyped, m: typedesc): untyped =
294+
macro getAll*(sql: untyped): untyped =
269295
## Finalize and get all results of the SQL statement.
270296
## This macro produce the final SQL string and wraps it in a runtime call
271297
## to execute it and return all rows via `instantRows`
272-
if sql.kind != nnkCall or sql[0].strVal notin ["ozarkWhereResult", "ozarkRawSQLResult", "ozarkLimitResult"]:
273-
error("The argument to `get` must be the result of a `where` macro.")
274-
# if sql[0].strVal == "ozarkLimitResult":
275-
# parseSqlQuery("instantRows", @[nnkPrefix.newTree(ident"$", sql[2])])
276-
# else:
277-
parseSqlQuery("instantRows", @[nnkPrefix.newTree(ident"$", sql[2])])
298+
if sql.kind != nnkBlockExpr or sql[1][1][0].strVal notin ["ozarkWhereResult", "ozarkRawSQLResult", "ozarkLimitResult"]:
299+
error("The argument to `getAll` must be the result of a `where` macro.")
300+
let m = sql[1][0][1][1] # extract the model type from the macro arguments for later use in code generation
301+
let v = sql[1][1][2] # extract the additional arguments (e.g. for WHERE IN) from the macro arguments for later use in code generation
302+
parseSqlQuery("instantRows", @[v])
278303

279-
macro get*(sql: untyped, m: typedesc): untyped =
304+
macro get*(sql: untyped): untyped =
280305
## Finalize SQL statement. This macro produces the final SQL
281306
## string and emits runtime code that maps selected columns into a new instance of `m`
282-
if sql.kind != nnkCall or sql[0].strVal notin ["ozarkWhereResult", "ozarkWhereInResult", "ozarkRawSQLResult"]:
283-
error("The argument to `get` must be the result of a `where` or `rawSQL` macro.")
284-
if sql[0].strval == "ozarkWhereInResult":
285-
parseSqlQuery("getRow", @[nnkPrefix.newTree(sql[2][1])])
307+
if sql.kind != nnkBlockExpr or sql[1][1][0].strVal notin ["ozarkWhereResult", "ozarkWhereInResult", "ozarkRawSQLResult", "ozarkLimitResult"]:
308+
error("The argument to `get` must be the result of a `where` macro.")
309+
let m = sql[1][0][1][1] # extract the model type from the macro arguments for later use in code generation
310+
if sql[1][1][0].strval == "ozarkWhereInResult":
311+
var vals: seq[NimNode]
312+
for n in sql[1][1][2][1]:
313+
vals.add(n)
314+
parseSqlQuery("getRow", vals)
286315
else:
287-
parseSqlQuery("getRow", @[nnkPrefix.newTree(newCall(ident"$", sql[2]))])
316+
let v = sql[1][1][2] # extract the additional arguments (e.g. for WHERE IN) from the macro arguments for later use in code generation
317+
parseSqlQuery("getRow", @[v])
288318

289-
macro insert*(tableName: static string, data: untyped): untyped =
319+
macro insert*(tableName, data: untyped): untyped =
290320
## Placeholder for INSERT queries
291-
checkTableExists(tableName)
292-
expectKind(data, nnkTableConstr)
293-
var cols: seq[string]
294-
var values = newNimNode(nnkBracket)
295-
var valuesIds: seq[int]
296-
var idx = 1
297-
for kv in data:
298-
# var idx = genSym(nskVar, "v")
299-
let col = $kv[0]
300-
if col.validIdentifier:
301-
# todo check if column exists in model
302-
# todo check for NOT NULL columns without default values
303-
cols.add(col)
304-
values.add(kv[1])
305-
valuesIds.add(idx)
306-
inc idx
307-
else:
308-
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
309-
result = newCall(
310-
bindSym"ozarkInsertResult",
311-
newLit("insert into " & $tableName & " (" & cols.join(",") & ") VALUES (" & valuesIds.mapIt("$" & $it).join(",") & ")"),
312-
nnkPrefix.newTree(ident"@", values)
313-
)
321+
withTableCheck tableName:
322+
expectKind(data, nnkTableConstr)
323+
var cols: seq[string]
324+
var values = newNimNode(nnkBracket)
325+
var valuesIds: seq[int]
326+
var idx = 1
327+
for kv in data:
328+
# var idx = genSym(nskVar, "v")
329+
let col = $kv[0]
330+
if col.validIdentifier:
331+
# todo check if column exists in model
332+
# todo check for NOT NULL columns without default values
333+
cols.add(col)
334+
values.add(kv[1])
335+
valuesIds.add(idx)
336+
inc idx
337+
else:
338+
raise newException(OzarkModelDefect, "Invalid column name `" & col & "`")
339+
result = newCall(
340+
bindSym"ozarkInsertResult",
341+
newLit("insert into " & getTableName($tableName[1]) & " (" & cols.join(",") & ") VALUES (" & valuesIds.mapIt("$" & $it).join(",") & ")"),
342+
nnkPrefix.newTree(ident"@", values)
343+
)
314344

315-
macro exists*(tableName: static string) =
316-
## Search in the current table for a record matching
317-
## the specified values. This is a placeholder for an `EXISTS` query.
318-
checkTableExists(tableName)
345+
# macro exists*(tableName: static string) =
346+
# ## Search in the current table for a record matching
347+
# ## the specified values. This is a placeholder for an `EXISTS` query.
348+
# checkTableExists(tableName)
319349

320350
macro limit*(sql: untyped, count: untyped): untyped =
321351
## Placeholder for a `LIMIT` clause in SQL queries.
@@ -352,7 +382,8 @@ macro rawSQL*(models: ptr ModelsTable, sql: static string, values: varargs[untyp
352382
let fromNode = sqlNode.sons[0].sons[1]
353383
assert fromNode.kind == nkFrom
354384
for table in fromNode.sons:
355-
checkTableExists(table[0].strVal)
385+
withTableCheck ident(table[0].strVal):
386+
discard
356387
else: discard
357388
result = newCall(
358389
bindSym"ozarkRawSQLResult", newLit(sql)
@@ -411,7 +442,7 @@ macro execGet*(sql: untyped): untyped =
411442
$(sql[2][1]).len
412443
])
413444
of nkDelete:
414-
discard
445+
discard # todo
415446
else: discard
416447
except SqlParseError as e:
417448
raise newException(OzarkModelDefect, "SQL Parsing Error: " & e.msg)

0 commit comments

Comments
 (0)