Search code examples
genericscompile-timenim-lang

Nim - Norm - How to get related fieldnames at compile time


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?


Solution

  • 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")