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
3839proc 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
5758proc 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+
66248macro 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 (
0 commit comments