sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

Relationship type annotations disappear after class definition is evaluated

Open phdowling opened this issue 2 years ago • 1 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the SQLModel documentation, with the integrated search.
  • [X] I already searched in Google "How to X in SQLModel" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to SQLModel but to Pydantic.
  • [X] I already checked if it is not related to SQLModel but to SQLAlchemy.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

from sqlmodel import Relationship, SQLModel, Field
from pydantic.main import BaseModel

class SpecialFieldValidator(BaseModel):
    id: int = Field(primary_key=True, index=True)

    def __init_subclass__(cls) -> None:
        # Try to figure out what type the special_field relation has
        print("special_field" in cls.__annotations__)  # False
        print("special_field" in cls.__fields__)  # False
        print("special_field" in cls.__sqlmodel_relationships__)  # True, but no type info via this dict, just a plain RelationshipInfo() instance:
        print(dict(cls.__sqlmodel_relationships__["special_field"].__dict__))  # {'back_populates': None, 'link_model': None, 'sa_relationship': None, 'sa_relationship_args': None, 'sa_relationship_kwargs': None}

        return super().__init_subclass__()

class MyModelA(SQLModel, SpecialFieldValidator, table=True):
    id: int = Field(primary_key=True)
    special_field_id: int = Field(foreign_key="mymodelb.id")
    special_field: "MyModelB" = Relationship()

class MyModelB(SQLModel, SpecialFieldValidator, table=True):
    id: int = Field(primary_key=True)
    special_field_id: int = Field(foreign_key="mymodela.id")
    special_field: MyModelA = Relationship()

Description

I'm trying to write a class (SpecialFieldValidator) that is supposed to check whether a specific field, which is always a Relationship, was annotated with a particular type.

However, the actual type annotation seems to be getting erased - looking in fields, annotations yields nothing, and the data in sqlmodel_relationships does not feature a type.

Operating System

Linux

Operating System Details

No response

SQLModel Version

0.0.8

Python Version

3.11.0

Additional Context

SQLAlchemy version is 1.4.41 I found this issue: https://github.com/tiangolo/sqlmodel/issues/255 that seems related, saying SQLAlchemy 1.4.36 breaks relationship, but SQLModels dependencies were officially updated to >=1.4.41 more recently than that so I figured this is a new issue.

phdowling avatar Jan 15 '23 23:01 phdowling

I'm also facing this issue. From what I could see, one potential solution could be the following (if support for relationship information as part of the pydantic model is not included as a feature in SQLModel fure releases, so consider this a workaround):

  • Override the __new__ dunder method ( as __init__ is not used by SQLModel).
  • Populate the pydantic _model_fields property with missing info from relationships.
  • Rebuild pydantic model schema to reflect such changes.

In your particular use-case it would be something like the following (behaviour could potentially be abstracted at SQLModel level, or at least at a base class level, so it is not performed in a per-class/per-attribute level each time):


from sqlmodel import Relationship, SQLModel, Field
from pydantic.main import BaseModel


class SpecialFieldValidator(BaseModel):
    id: int = Field(primary_key=True, index=True)

    def __init_subclass__(cls) -> None:
        # Try to figure out what type the special_field relation has
        print("special_field" in cls.__annotations__)  # False
        print("special_field" in cls.__fields__)  # False
        print("special_field" in cls.__sqlmodel_relationships__)  # True, but no type info via this dict, just a plain RelationshipInfo() instance:
        print(dict(cls.__sqlmodel_relationships__["special_field"].__dict__))  # {'back_populates': None, 'link_model': None, 'sa_relationship': None, 'sa_relationship_args': None, 'sa_relationship_kwargs': None}

        return super().__init_subclass__()

class MyModelA(SQLModel, SpecialFieldValidator, table=True):
    id: int = Field(primary_key=True)
    special_field_id: int = Field(foreign_key="mymodelb.id")
    special_field: "MyModelB" = Relationship()

    def __new__(cls, *args: Any, **kwargs: Any) -> Any:
        new_object = super().__new__(cls, *args, **kwargs)
        new_object.model_fields.update({"special_field": Field(..., description="description of your model property coming from the relationship")})
        new_object.model_rebuild(force=True)
        return new_object

class MyModelB(SQLModel, SpecialFieldValidator, table=True):
    id: int = Field(primary_key=True)
    special_field_id: int = Field(foreign_key="mymodela.id")
    special_field: MyModelA = Relationship()
   
    def __new__(cls, *args: Any, **kwargs: Any) -> Any:
        new_object = super().__new__(cls, *args, **kwargs)
        new_object.model_fields.update({"special_field": Field(..., description="description of your model property coming from the relationship")})
        new_object.model_rebuild(force=True)
        return new_object

Kind regards.

jarey avatar Dec 02 '24 14:12 jarey