Skip to content

Commit 3c67f27

Browse files
committed
Model definition - field types | handle pragmas as constraints
Signed-off-by: George Lemon <georgelemon@protonmail.com>
1 parent d44653a commit 3c67f27

File tree

2 files changed

+264
-54
lines changed

2 files changed

+264
-54
lines changed

src/ozark/model.nim

Lines changed: 189 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const
3434
## A cache table that holds all models defined at compile-time.
3535
## This allows us to determine if a model exists at compile-time
3636
## and also to access its schema definition.
37+
var SqlSchemas* {.compileTime.} = newTable[string, newTable[string, SqlNode]()]()
3738

3839
proc initModels() =
3940
# Initialize the Models singleton
@@ -55,6 +56,7 @@ proc getDatatype*(dt: string): (DataType, Option[seq[string]]) =
5556
result[1] = none(seq[string])
5657

5758
proc getTableName*(id: string): string =
59+
## Helper to convert a model name to a table name.
5860
add result, id[0].toLowerAscii
5961
for c in id[1..^1]:
6062
if c.isUpperAscii:
@@ -63,6 +65,186 @@ proc getTableName*(id: string): string =
6365
else:
6466
add result, c
6567

68+
69+
proc toSqlDefaultLiteral(n: NimNode): string =
70+
## Converts Nim literal nodes to SQL literal text.
71+
case n.kind
72+
of nnkIntLit, nnkInt8Lit, nnkInt16Lit, nnkInt32Lit, nnkInt64Lit,
73+
nnkUIntLit, nnkUInt8Lit, nnkUInt16Lit, nnkUInt32Lit, nnkUInt64Lit,
74+
nnkFloatLit, nnkFloat32Lit, nnkFloat64Lit:
75+
n.repr
76+
of nnkStrLit:
77+
"'" & n.strVal.replace("'", "''") & "'"
78+
of nnkIdent:
79+
let v = n.strVal.toLowerAscii
80+
case v
81+
of "true": "TRUE"
82+
of "false": "FALSE"
83+
of "null": "NULL"
84+
else:
85+
error("Unsupported default identifier literal: " & n.repr, n)
86+
of nnkNilLit:
87+
"NULL"
88+
else:
89+
error("Unsupported default literal type: " & n.repr, n)
90+
91+
proc parseFieldDecl(field: NimNode): tuple[fieldCall: NimNode, defaultSql: Option[string]] =
92+
## Accepts:
93+
## fieldHead: TypeExpr
94+
## fieldHead: TypeExpr = defaultExpr
95+
case field.kind
96+
of nnkCall:
97+
result.fieldCall = field
98+
result.defaultSql = none(string)
99+
of nnkAsgn:
100+
if field[0].kind != nnkCall:
101+
raise newException(ValueError, "Invalid field declaration (expected `name: Type` on lhs): " & field.repr)
102+
result.fieldCall = field[0]
103+
result.defaultSql = some(toSqlDefaultLiteral(field[1]))
104+
else:
105+
error("Invalid field declaration: " & field.repr, field)
106+
107+
proc parseFieldHead(head: NimNode): tuple[name: NimNode, pragmas: seq[string]] =
108+
## Parses:
109+
## id
110+
## username {.notnull, unique.}
111+
case head.kind
112+
of nnkIdent:
113+
result.name = head
114+
of nnkPragmaExpr:
115+
case head[0].kind
116+
of nnkIdent:
117+
result.name = head[0]
118+
of nnkAccQuoted:
119+
result.name = head[0][0]
120+
else:
121+
error("Invalid field identifier: " & head.repr, head)
122+
123+
for p in head[1]:
124+
case p.kind
125+
of nnkIdent:
126+
result.pragmas.add(p.strVal.toLowerAscii)
127+
of nnkCall:
128+
# Supports pragma with args, keeps full repr for custom handling later
129+
result.pragmas.add(p.repr.toLowerAscii)
130+
else:
131+
error("Invalid pragma in field declaration: " & p.repr, p)
132+
else:
133+
error("Invalid field declaration head: " & head.repr, head)
134+
135+
proc parseTypeAndDefault(n: NimNode): tuple[typeExpr: NimNode, defaultSql: Option[string]] =
136+
## Accepts:
137+
## TypeExpr
138+
## TypeExpr = defaultExpr
139+
case n.kind
140+
of nnkIdent, nnkCall:
141+
result.typeExpr = n
142+
result.defaultSql = none(string)
143+
of nnkAsgn:
144+
# Example: Asgn(Ident "Boolean", Ident "false")
145+
result.typeExpr = n[0]
146+
result.defaultSql = some(toSqlDefaultLiteral(n[1]))
147+
of nnkDotExpr:
148+
# Handles model references like `Users.id`
149+
if n[0].kind == nnkIdent and n[1].kind == nnkIdent:
150+
let reftableName = getTableName($n[0])
151+
if StaticSchemas.hasKey(reftableName):
152+
result.typeExpr = n
153+
result.defaultSql = none(string)
154+
else:
155+
error("Referenced model '" & n[0].strVal & "' not found for field '" &
156+
$n[1] & "'. Make sure to define the referenced model before using it.", n[0])
157+
else:
158+
raise newException(ValueError, "Invalid type/default expression: " & n.repr)
159+
160+
proc parseDatatypeExpr(typeExpr: NimNode): (DataType, Option[seq[string]]) =
161+
## Parses:
162+
## Serial
163+
## Varchar(50)
164+
case typeExpr.kind
165+
of nnkIdent:
166+
result[0] = parseEnum[DataType](typeExpr.strVal.toLowerAscii)
167+
result[1] = none(seq[string])
168+
of nnkCall:
169+
result = getDatatype(typeExpr[0].strVal)
170+
let params = typeExpr[1..^1].map(proc(it: NimNode): string =
171+
case it.kind
172+
of nnkIntLit:
173+
$it.intVal
174+
of nnkStrLit:
175+
it.strVal
176+
else:
177+
error("Invalid datatype parameter: " & it.repr, it)
178+
)
179+
if result[1].isSome:
180+
result[1] = some(params)
181+
of nnkDotExpr:
182+
let tableName = getTableName(typeExpr[0].strVal)
183+
if StaticSchemas.hasKey(tableName):
184+
let refSchema = SqlSchemas[tableName]
185+
if refSchema.hasKey(typeExpr[1].strVal):
186+
if refSchema.hasKey(typeExpr[1].strVal):
187+
let refFieldNode = refSchema[typeExpr[1].strVal]
188+
result[0] = parseEnum[DataType](refFieldNode[1].strVal.toLowerAscii)
189+
result[1] = none(seq[string])
190+
else:
191+
error("Referenced field '" & typeExpr[1].strVal & "' not found in model '" &
192+
typeExpr[0].strVal & "'. Make sure to define the referenced field before using it.", typeExpr[1])
193+
else:
194+
error("Referenced model '" & typeExpr[0].strVal &
195+
"' not found for field '" &
196+
typeExpr[1].strVal &
197+
"'. Make sure to define the referenced model before using it.",
198+
typeExpr[0])
199+
else:
200+
error("Invalid datatype expression: " & typeExpr.repr, typeExpr)
201+
202+
proc pragmaToConstraint(p: string): string =
203+
## Map Nim pragmas to SQL constraint tokens.
204+
case p
205+
of "notnull": "NOT NULL"
206+
of "unique": "UNIQUE"
207+
of "pk", "primarykey": "PRIMARY KEY"
208+
else: p.toUpperAscii
209+
210+
proc parseObjectField(tableName: string, field, fieldIdent: NimNode) =
211+
## Parses one `newModel` field:
212+
## id: Serial
213+
## username {.notnull.}: Varchar(50)
214+
field.expectKind(nnkCall)
215+
field[1].expectKind(nnkStmtList)
216+
if field[1].len == 0:
217+
raise newException(ValueError, "Missing datatype for field: " & field.repr)
218+
219+
let (fieldName, pragmas) = parseFieldHead(field[0])
220+
let (typeExpr, defaultSql) = parseTypeAndDefault(field[1][0])
221+
let datatype = parseDatatypeExpr(typeExpr)
222+
223+
# Nim object field
224+
fieldIdent.add(nnkPostfix.newTree(ident"*", fieldName))
225+
226+
# SQL schema field
227+
let colDefNode = SqlNode(kind: nkColumnDef)
228+
colDefNode.add(newNode(nkIdent, $fieldName))
229+
230+
if datatype[1].isNone:
231+
colDefNode.add(newNode(nkIdent, $datatype[0]))
232+
else:
233+
let colDefCall = newNode(nkCall)
234+
colDefCall.add(newNode(nkIdent, $datatype[0]))
235+
for param in datatype[1].get:
236+
colDefCall.add(newNode(nkIntegerLit, param))
237+
colDefNode.add(colDefCall)
238+
239+
for p in pragmas:
240+
colDefNode.add(newNode(nkIdent, pragmaToConstraint(p)))
241+
242+
if defaultSql.isSome:
243+
colDefNode.add(newNode(nkIdent, "DEFAULT " & defaultSql.get))
244+
245+
SqlSchemas[tableName][$fieldName] = colDefNode
246+
247+
66248
macro newModel*(id, fields: untyped) =
67249
## Macro for defining a new model at compile time.
68250
## This macro will create a Nim object that represents
@@ -72,55 +254,17 @@ macro newModel*(id, fields: untyped) =
72254
if StaticSchemas.hasKey(tableName):
73255
raise newException(ValueError, "Model with id '" & $id & "' already exists.")
74256
var modelFields = newNimNode(nnkRecList)
75-
var modelSchema = newTable[string, SqlNode]()
257+
# var modelSchema = newTable[string, SqlNode]()
258+
SqlSchemas[tableName] = newTable[string, SqlNode]()
76259
for field in fields:
77260
var fieldIdent = newNimNode(nnkIdentDefs)
78-
case field.kind
79-
of nnkCall:
80-
case field[0].kind
81-
of nnkIdent:
82-
field[1].expectKind(nnkStmtList)
83-
let fieldName = field[0]
84-
var datatype: (DataType, Option[seq[string]])
85-
if field[1][0].kind == nnkIdent:
86-
datatype[0] = parseEnum[DataType](field[1][0].strVal.toLowerAscii())
87-
elif field[1][0].kind == nnkCall:
88-
datatype = getDatatype(field[1][0][0].strVal)
89-
let params = field[1][0][1..^1].map(proc(it: NimNode): string =
90-
case it.kind
91-
of nnkIntLit:
92-
return $it.intVal
93-
of nnkStrLit:
94-
return it.strVal
95-
else:
96-
raise newException(ValueError, "Invalid parameter type for data type '" & datatype[0].repr & "'")
97-
)
98-
if datatype[1].isSome:
99-
datatype[1] = some(params)
100-
else:
101-
raise newException(ValueError, "Invalid data type for field '" & $fieldName & "'")
102-
103-
fieldIdent.add(nnkPostfix.newTree(ident"*", fieldName))
104-
let colDefNode = SqlNode(kind: nkColumnDef)
105-
colDefNode.add(newNode(nkIdent, $fieldName))
106-
colDefNode.add(newNode(nkIdent, $datatype))
107-
modelSchema[$(fieldName)] = colDefNode
108-
of nnkAccQuoted:
109-
field[0][0].expectKind(nnkIdent)
110-
let id = field[0][0]
111-
fieldIdent.add(nnkPostfix.newTree(ident"*", field[0][0]))
112-
of nnkPragmaExpr:
113-
var id: NimNode
114-
if field[0][0].kind == nnkIdent:
115-
id = field[0][0]
116-
elif field[0][0].kind == nnkAccQuoted:
117-
id = field[0][0][0]
118-
fieldIdent.add(nnkPostfix.newTree(ident"*", id))
119-
else: discard
261+
if field.kind == nnkCall:
262+
parseObjectField(tableName, field, fieldIdent)
120263
fieldIdent.add(ident"string")
121264
fieldIdent.add(newEmptyNode())
122-
else: discard
123-
modelFields.add(fieldIdent)
265+
modelFields.add(fieldIdent)
266+
else:
267+
raise newException(ValueError, "Invalid field declaration: " & field.repr)
124268

125269
result = newStmtList(
126270
nnkTypeSection.newTree(

src/ozark/query.nim

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Made by Humans from OpenPeeps
55
# https://github.com/openpeeps/ozark
66

7-
import std/[macros, macrocache, strutils,
7+
import std/[macros, macrocache, strutils, options,
88
sequtils, tables, os, random, strformat]
99

1010
import pkg/db_connector/postgres {.all.}
@@ -43,10 +43,11 @@ template withColumnsCheck(model: NimNode, cols: openArray[string], body) =
4343
proc ozarkSelectResult(sql: static[string]): NimNode {.compileTime.} = newLit(sql)
4444
proc ozarkWhereResult(sql: static[string], val: varargs[string]): NimNode {.compileTime.} = newLit(sql)
4545
proc ozarkWhereInResult(sql: static[string], vals: varargs[string]): NimNode {.compileTime.} = newLit(sql)
46-
proc ozarkRawSQLResult(sql: static[string]): NimNode {.compileTime.} = newLit(sql)
46+
proc ozarkRawSQLResult(sql: static[string], vals: varargs[string]): NimNode {.compileTime.} = newLit(sql)
4747
proc ozarkInsertResult(sql: static[string], values: seq[string]): NimNode {.compileTime.} = newLit(sql)
4848
proc ozarkLimitResult(sql: static[string], count: int): NimNode {.compileTime.} = newLit(sql)
4949
proc ozarkOrderByResult(sql: static[string], col: string, desc: bool): NimNode {.compileTime.} = newLit(sql)
50+
proc ozarkCreateTableResult(sql: static[string]): NimNode {.compileTime.} = newLit(sql)
5051

5152
proc ozarkHoldModel[T](t: T) {.compileTime.} =
5253
var x: T
@@ -72,6 +73,49 @@ template withColumnCheck(model: NimNode, col: string, body) =
7273
if not withColumnCheckPassed:
7374
error("Column `" & col & "` does not exist in model `" & $model[1] & "`.")
7475

76+
macro prepareTable*(modelName): untyped =
77+
## Compile-time macro to prepare a model's table in the database.
78+
##
79+
## This macro generates the SQL string for creating the table based
80+
## on the model definition and executes it at compile time to ensure
81+
## the table exists before any queries are made against it.
82+
withTableCheck(modelName):
83+
let tableName = getTableName($modelName[1])
84+
let schema = SqlSchemas[tableName]
85+
let id = genSym(nskType, "ozarkModel" & tableName)
86+
var types: seq[SqlNode]
87+
for k, v in StaticSchemas[tableName]:
88+
if v.kind == nnkTypeSection:
89+
for f in v[0][2][0][2]:
90+
let fieldName = f[0][1].strVal
91+
types.add(schema[fieldName])
92+
result = newCall(
93+
bindSym"ozarkCreateTableResult",
94+
newLit(
95+
"CREATE TABLE IF NOT EXISTS " & tableName & " (" &
96+
types.map(proc(t: SqlNode): string =
97+
var colDef = t[0].strVal & " "
98+
if t[1].kind == nkIdent:
99+
# handle simple data types without parameters like INTEGER or TEXT
100+
colDef &= t[1].strVal
101+
elif t[1].kind == nkCall:
102+
# handle data types with parameters like VARCHAR(255)
103+
colDef &= t[1][0].strVal & "(" &
104+
t[1].sons[1..^1].mapIt($it.strVal).join(", ") & ")"
105+
colDef
106+
).join(", ") & ")"
107+
)
108+
)
109+
110+
macro dropTable*(modelName): untyped =
111+
## Compile-time macro to drop a model's table from the database.
112+
withTableCheck(modelName):
113+
let tableName = getTableName($modelName[1])
114+
result = newCall(
115+
bindSym"ozarkRawSQLResult",
116+
newLit("DROP TABLE IF EXISTS " & tableName),
117+
)
118+
75119
#
76120
# INSERT and UDATE clause macros
77121
#
@@ -445,15 +489,37 @@ macro rawSQL*(models: ptr ModelsTable, sql: static string, values: varargs[untyp
445489
macro exec*(sql: untyped) =
446490
## Finalize and execute an SQL statement that doesn't
447491
## return results (e.g. INSERT, UPDATE, DELETE).
448-
if sql.kind != nnkCall or sql[0].strVal notin ["ozarkWhereResult", "ozarkRawSQLResult", "ozarkInsertResult"]:
492+
if sql.kind != nnkCall or sql[0].strVal notin ["ozarkWhereResult", "ozarkRawSQLResult", "ozarkInsertResult", "ozarkCreateTableResult"]:
449493
error("The argument to `exec` must be the result of a `where`, `rawSQL`, or `insert` macro.")
450494
try:
451-
let sqlNode = parseSQL(sql[1].strVal)
452-
result = newCall(
453-
ident"exec",
454-
ident"dbcon",
455-
newCall(ident"SqlQuery", newLit($sqlNode))
456-
)
495+
let sqlNode = parseSQL($sql[1])
496+
case sqlNode.sons[0].kind
497+
of nkInsert:
498+
let randId = genSym(nskVar, "id")
499+
let stub = staticRead("private" / "stubs" / "execSql.nim")
500+
result = macros.parseStmt(stub % [
501+
$sql[1],
502+
(
503+
if sql[2][1].len > 0:
504+
", " &
505+
$sql[2][1].mapIt(it.repr).join(",")
506+
else: ""
507+
),
508+
randId.repr,
509+
$(sql[2][1]).len
510+
])
511+
of nkCreateTable, nkCreateTableIfNotExists:
512+
let randId = genSym(nskVar, "id")
513+
let stub = staticRead("private" / "stubs" / "execSql.nim")
514+
result = macros.parseStmt(stub % [
515+
$sql[1],
516+
"",
517+
randId.repr,
518+
"0"
519+
])
520+
of nkDelete:
521+
discard # todo
522+
else: discard
457523
except SqlParseError as e:
458524
raise newException(OzarkModelDefect, "SQL Parsing Error: " & e.msg)
459525

0 commit comments

Comments
 (0)