Heyho,
I am using norm, an orm in the nim programming language. I have 2 different models such as this:
import std/options
import norm
type
A {.tableName: "Surprise".} = ref object of Model
name: string
Surprise = ref object of Model
name: string
anotherFieldThatExistsOnTheSQLTableButNotOnA: int
B = ref object of Model
name: string
myA: Option[A]
I want to be able to figure out at compile time the name of a given foreign key-field (here myA
) that points to a given table (here Surprise
) even if the model has a different name than the actual table or is a read-only model (e.g. A). That way I can write SQL queries at compile time that get me the many of a many-to-one relationship.
More importantly, I want this grabbing of the foreign-key relationship to be based upon the tableName
of a model, not the model itself. Thus, if I were to define a proc getRelatedFieldName(startType: typedesc[A], otherType: typedesc[B])
, it would need to give the same result for both getRelatedFieldName(A, B)
AND getRelatedFieldName(A, Surprise)
.
How can I achieve this?
Thanks to some hints of the very helpful folks at the nim discord server, I was able to write the solution.
The answer is: Nim's generics, Nim's getCustomPragmaVal
macro and Norm's table
template.
The below code takes 2 model-types. It dummy-instantiates the sourceType
since that's the type that potentially has a foreignKey-field to your targetType
. It then iterates over the fields of sourceType
and checks whether they are either directly a Model type, are annotated with an fk
aka foreignKey pragma, or are an Option[Model]
type.
If the field has a Model
type, the issue is solved since you can just call Model.table()
and you're done. If the field has an fk
pragma, you can simply call getCustomPragmaVal
to get the Model that this field is a foreign key for. With that you have the type and can just call table()
on that. Lastly, you may have an Option[Model]
type. In that case you need to extract the generic parameters using the genericParams function (see here). That way you can, once again, access the type and call table()
on that.
proc getRelatedFieldName[M: Model, O:Model](targetType: typedesc[O], sourceType: typedesc[M]): Option[string] =
let source = sourceType()
for sourceFieldName, sourceFieldValue in source[].fieldPairs:
#Handles case where field is an int64 with fk pragma
when sourceFieldValue.hasCustomPragma(fk):
when O.table() == sourceFieldValue.getCustomPragmaVal(fk).table():
return some(sourceFieldName)
#Handles case where field is a Model type
when sourceFieldValue is Model:
when O.table() == sourceFieldValue.type().table():
return some(sourceFieldName)
#Handles case where field is a Option[Model] type
when sourceFieldValue is Option:
when sourceFieldValue.get() is Model:
when O.table() == genericParams(sourceFieldValue.type()).get(0).table():
return some(sourceFieldName)
return none(string)
example
type
A = ref object of Model # <-- has implicit tableName "A"
name: string
AC {.tableName: "A".} = ref object of Model
myothername: string
name: string
B = ref object of Model # <-- has implicit tableName "B"
name: string
myA: Option[A]
D = ref object of Model
myothernameid: int
myDA: A
E = ref object of Model
myotherbool: bool
myEA {.fk: A.}: int64
echo A.getRelatedFieldName(B) # some("myA")
echo AC.getRelatedFieldName(B) # some("myA")
echo A.getRelatedFieldName(D) # some("myDA")
echo AC.getRelatedFieldName(D) # some("myDA")
echo A.getRelatedFieldName(E) # some("myEA")
echo AC.getRelatedFieldName(E) # some("myEA")