0

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?

Philipp Doerner
  • 1,090
  • 7
  • 24

1 Answers1

2

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


Philipp Doerner
  • 1,090
  • 7
  • 24