@@ -21,53 +21,96 @@ export SqlQuery, mapIt
2121type
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-
2924randomize () # 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
3644proc 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)
3846proc ozarkWhereInResult (sql: static [string ], vals: varargs [string ]): NimNode {.compileTime .} = newLit (sql)
3947proc ozarkRawSQLResult (sql: static [string ]): NimNode {.compileTime .} = newLit (sql)
4048proc ozarkInsertResult (sql: static [string ], values: seq [string ]): NimNode {.compileTime .} = newLit (sql)
4149proc 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+
4376macro 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
5588macro 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
66102macro 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
95134proc 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
153179macro 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
207233template 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
320350macro 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