Uh oh!
There was an error while loading. Please reload this page.
✨ Add support for SQLAlchemy polymorphic models - #1226
Conversation
mmx86
commented
Dec 2, 2024
@tiangolo |
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: John Pocock <John-P@users.noreply.github.com>
ndeybach
commented
Dec 22, 2024
We are also exploring using SQLModel in our products. This would be quite an ease of life in how we are building our stack. @tiangolo do you have a timeline as to when could this be merged / what needs to be done ? |
Thanks a lot for this PR! We would love to add this feature in our codebase. Unfortunately, we could not use this PR along with a custom type. @PaleNeutron would you mind checking this MRE? (1) the code works fine if you comment (2) however, it fails if both are in the module! Codeimportjsonimporttypingastfromfastapi.encodersimportjsonable_encoderfrompydanticimportBaseModel, TypeAdapter# Warning: we import a deprecated class from the `pydantic` package# See: https://github.com/pydantic/pydantic/issues/6381frompydantic._internal._model_constructionimportModelMetaclass# noqa: PLC2701fromsqlalchemy.engine.interfacesimportDialectfromsqlalchemy.ormimportmapped_columnfromsqlalchemy.sql.type_apiimport_BindProcessorType, _ResultProcessorTypefromsqlmodelimport (
JSON,
Column,
Field,
Session,
SQLModel,
TypeDecorator,
create_engine,
select,
)
defpydantic_column_type( # noqa: C901pydantic_type: type[t.Any],
) ->type[TypeDecorator]:
""" See details here: https://github.com/tiangolo/sqlmodel/issues/63#issuecomment-1081555082 """T=t.TypeVar("T")
classPydanticJSONType(TypeDecorator, t.Generic[T]):
impl=JSON()
cache_ok=Falsedef__init__(
self,
json_encoder: t.Any=json,
):
self.json_encoder=json_encodersuper().__init__()
defbind_processor(self, dialect: Dialect) ->_BindProcessorType[T] |None:
impl_processor=self.impl.bind_processor(dialect)
ifimpl_processor:
defprocess(value: T|None) ->T|None:
ifvalueisnotNone:
ifisinstance(pydantic_type, ModelMetaclass):
value_to_dump=pydantic_type.model_validate(value)
else:
value_to_dump=valuevalue=jsonable_encoder(value_to_dump)
returnimpl_processor(value)
else:
defprocess(value: T|None) ->T|None:
ifisinstance(pydantic_type, ModelMetaclass):
value_to_dump=pydantic_type.model_validate(value)
else:
value_to_dump=valuereturnjsonable_encoder(value_to_dump)
returnprocessdefresult_processor(
self,
dialect: Dialect,
coltype: object,
) ->_ResultProcessorType[T] |None:
impl_processor=self.impl.result_processor(dialect, coltype)
ifimpl_processor:
defprocess(value: T) ->T|None:
value=impl_processor(value)
ifvalueisNone:
returnNoneifisinstance(value, str):
value=json.loads(value)
returnTypeAdapter(pydantic_type).validate_python(value)
else:
defprocess(value: T) ->T|None:
ifvalueisNone:
returnNoneifisinstance(value, str):
value=json.loads(value)
returnTypeAdapter(pydantic_type).validate_python(value)
returnprocessdefcompare_values(self, x: t.Any, y: t.Any) ->bool:
returnx==yreturnPydanticJSONTypeclassMyModel(BaseModel):
name: str|None=NoneclassComplexModel(SQLModel, table=True):
id: t.Annotated[
int|None,
Field(
default=None,
primary_key=True,
),
] =Nonemy_model: t.Annotated[
MyModel|None,
Field(
sa_column=Column(pydantic_column_type(MyModel)),
),
] =NoneclassHero(SQLModel, table=True):
__tablename__="hero"id: int|None=Field(default=None, primary_key=True)
hero_type: str=Field(default="hero")
__mapper_args__= {
"polymorphic_on": "hero_type",
"polymorphic_identity": "hero",
}
classDarkHero(Hero):
dark_power: str=Field(
default="dark",
sa_column=mapped_column(
nullable=False, use_existing_column=True, default="dark"
),
)
__mapper_args__= {
"polymorphic_identity": "dark",
}
engine=create_engine("sqlite:///:memory:", echo=True)
SQLModel.metadata.create_all(engine)
withSession(engine) asdb:
hero=Hero()
db.add(hero)
dark_hero=DarkHero(dark_power="pokey")
db.add(dark_hero)
db.commit()
statement=select(DarkHero)
result=db.exec(statement).all()
assertlen(result) ==1assertisinstance(result[0].dark_power, str)Corresponding error codepython test.py
Traceback (most recent call last):
File "/Users/guhur/src/argile-lib-python/test.py", line 101, in<module>
class DarkHero(Hero):
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/sqlmodel/main.py", line 542, in __new__
new_cls = super().__new__(cls, name, bases, dict_used, **config_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py", line 202, in __new__
complete_model_class(
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py", line 572, in complete_model_class
generate_pydantic_signature(init=cls.__init__, fields=cls.model_fields, config_wrapper=config_wrapper),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/pydantic/_internal/_signature.py", line 159, in generate_pydantic_signature
merged_params = _generate_signature_parameters(init, fields, config_wrapper)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/pydantic/_internal/_signature.py", line 115, in _generate_signature_parameters
kwargs = {} iffield.is_required() else {'default': field.get_default(call_default_factory=False)}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/pydantic/fields.py", line 546, in get_default
return _utils.smart_deepcopy(self.default)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/guhur/Library/Caches/pypoetry/virtualenvs/argile-lib-python-RxGRaJe1-py3.11/lib/python3.11/site-packages/pydantic/_internal/_utils.py", line 318, in smart_deepcopy
return deepcopy(obj) # slowest way when we actually might need a deepcopy
^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 265, in _reconstruct
y = func(*args)
^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 264, in<genexpr>
args = (deepcopy(arg, memo) forargin args)
^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 265, in _reconstruct
y = func(*args)
^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 264, in<genexpr>
args = (deepcopy(arg, memo) forargin args)
^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 206, in _deepcopy_list
append(deepcopy(a, memo))
^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 211, in _deepcopy_tuple
y = [deepcopy(a, memo) forain x]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 211, in<listcomp>
y = [deepcopy(a, memo) forain x]
^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 172, in deepcopy
y = _reconstruct(x, memo, *rv)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 271, in _reconstruct
state = deepcopy(state, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 146, in deepcopy
y = copier(x, memo)
^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 231, in _deepcopy_dict
y[deepcopy(key, memo)] = deepcopy(value, memo)
^^^^^^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.11/3.11.8/Frameworks/Python.framework/Versions/3.11/lib/python3.11/copy.py", line 161, in deepcopy
rv = reductor(4)
^^^^^^^^^^^
TypeError: cannot pickle 'module' object
|
adsharma
commented
Jan 17, 2025
This could help with a different kind of polymorphism. Details here. Specifically: Creates two classes Using polymorphism, we could allow the caller to return |
PaleNeutron
commented
Feb 5, 2025
@guhur , good test, I found a bug through it. |
…aleNeutron/sqlmodel into sqlalchemy_polymorphic_support
…aleNeutron/sqlmodel into sqlalchemy_polymorphic_support
0x003e
commented
Jul 7, 2025
Hello, could someone please approve this merge request ? |
@PaleNeutron, I have found another potential issue. Consider the following linked list: fromtypingimportOptionalfromsqlmodelimportField, Relationship, SQLModelclassBaseNode(SQLModel, table=True):
__tablename__='node_table'id: str=Field(primary_key=True)
node_type: str# Self-referential relationship - this causes the issuenext_id: Optional[str] =Field(default=None, foreign_key='node_table.id')
next: Optional['BaseNode'] =Relationship(
sa_relationship_kwargs={
'remote_side': '[BaseNode.id]',
'uselist': False
}
)
__mapper_args__= {
'polymorphic_on': 'node_type',
'polymorphic_identity': 'base',
}
classEmailNode(BaseNode):
__mapper_args__= {
'polymorphic_identity': 'email',
}
# Create two nodesnode1=EmailNode(id="1", node_type="email")
node2=EmailNode(id="2", node_type="email")
try:
node1.next=node2# This failsexceptAttributeErrorase:
print(e)
# This works because it's just a regular fieldtry:
node1.next_id="2"# This worksexceptExceptionase:
print(e)Thanks again for your contributions! Hopefully they can be merged soon! |
PaleNeutron
commented
Jul 10, 2025
@jensrischbieth Confirmed, working on it. |
Thanks for this PR! I constructed a simple example, in case anybody wants something to quickly evaluate. The initial result looks quite promising to me and may mean we won't have to migrate away from SQLModel after all. In difference to test_polymorphic_model.py I swapped Let's see how merging this PR goes, it would alleviate a lot of pain for a lot of people. # This is a uv script. Run with `uv run` and dependencies will be fetched on the fly.## /// script# requires-python = ">=3.9,<3.14"# dependencies = [# "sqlmodel @ git+https://github.com/PaleNeutron/sqlmodel@sqlalchemy_polymorphic_support"# ]# ///# Adapted from https://github.com/PaleNeutron/sqlmodel/blob/64f774fb3b5b66ef8d55aab0b26a7733146e60a8/tests/test_polymorphic_model.pyfromtypingimportOptionalfromsqlalchemyimportColumn, IntegerfromsqlmodelimportField, Session, SQLModel, create_engine, selectclassAnimal(SQLModel, table=True):
__tablename__="animal"# type: ignoreid: Optional[int] =Field(default=None, primary_key=True)
name: strtype: str=Field(default="animal")
__mapper_args__= {
"polymorphic_on": "type",
"polymorphic_identity": "animal",
}
classCat(Animal):
meow_cuteness: int=Field(sa_column=Column(Integer, nullable=True), default=None)
__mapper_args__= {"polymorphic_identity": "cat"}
classDog(Animal):
bark_loudness: int=Field(sa_column=Column(Integer, nullable=True), default=None)
__mapper_args__= {"polymorphic_identity": "dog"}
if__name__=="__main__":
# Create database and sessionengine=create_engine("sqlite:///:memory:", echo=False)
SQLModel.metadata.create_all(engine)
withSession(engine) asdb:
db.add_all(
[
Animal(name="Generic Animal"),
Cat(name="Whiskers", meow_cuteness=10),
Dog(name="Rocky", bark_loudness=8),
]
)
db.commit()
animals=db.exec(select(Animal)).all()
print("All animals:", animals)
cats=db.exec(select(Cat)).all()
print("All cats:", cats)
dogs=db.exec(select(Dog)).all()
print("All dogs:", dogs)Result: |
ssoimada
commented
Aug 14, 2025
Nice work, please release this! |
piewared
commented
Aug 21, 2025
Can this be merged in soon? Don't want to have to abandon SQLModel, but polymorphism is really important for us |
a0s
commented
Aug 21, 2025
MissingGreenlet Error with Polymorphic Models and Lazy LoadingProblem DescriptionWhen accessing lazy-loaded attributes on polymorphic SQLAlchemy models in async context, getting Minimal ExamplefromsqlalchemyimportColumn, String, ForeignKey, selectfromsqlalchemy.ext.asyncioimportAsyncSessionfromsqlmodelimportSQLModel, Field# Base polymorphic modelclassBaseConnection(SQLModel, table=True):
__tablename__="connections"__mapper_args__= {"polymorphic_on": "type"}
id: int=Field(primary_key=True)
type: str=Field(sa_column=Column(String(50)))
# Derived polymorphic model with FKclassWireGuardConnection(BaseConnection, table=False):
__mapper_args__= {"polymorphic_identity": "wireguard"}
peer_id: int=Field(sa_column=Column(ForeignKey("peers.id"), nullable=True))
asyncdefdelete_connection(session: AsyncSession, connection_id: int):
# Initial query works finestmt=select(BaseConnection).where(BaseConnection.id==connection_id)
result=awaitsession.execute(stmt)
connection=result.scalar_one_or_none()
# This triggers MissingGreenlet error due to lazy loadingifhasattr(connection, "peer_id") andconnection.peer_id: # ❌ Error hereprint(f"Peer ID: {connection.peer_id}")Error DetailsRoot CauseWhen accessing This issue specifically occurs with polymorphic inheritance when trying to access subclass attributes that weren't loaded in the initial query. Solution 1: Use session.refresh()Use asyncdefdelete_connection(session: AsyncSession, connection_id: int):
stmt=select(BaseConnection).where(BaseConnection.id==connection_id)
result=awaitsession.execute(stmt)
connection=result.scalar_one_or_none()
# Refresh to load all attributes properlyawaitsession.refresh(connection) # ✅ Fix# Now safe to access attributesifhasattr(connection, "peer_id") andconnection.peer_id:
print(f"Peer ID: {connection.peer_id}")Solution 2: Use joinedload or selectinloadLoad relationships eagerly in the initial query: fromsqlalchemy.ormimportselectinloadasyncdefdelete_connection(session: AsyncSession, connection_id: int):
stmt= (
select(BaseConnection)
.options(selectinload("*")) # Load all relationships
.where(BaseConnection.id==connection_id)
)
result=awaitsession.execute(stmt)
connection=result.scalar_one_or_none()
# Safe to access attributes - already loadedifhasattr(connection, "peer_id") andconnection.peer_id:
print(f"Peer ID: {connection.peer_id}")Solution 3: Type-safe approachCheck the type before accessing subclass attributes: asyncdefdelete_connection(session: AsyncSession, connection_id: int):
stmt=select(BaseConnection).where(BaseConnection.id==connection_id)
result=awaitsession.execute(stmt)
connection=result.scalar_one_or_none()
# Refresh to be safeawaitsession.refresh(connection)
# Type-safe checkifconnection.type=="wireguard":
wireguard_conn=connection# Type hint: WireGuardConnectionifwireguard_conn.peer_id:
print(f"Peer ID: {wireguard_conn.peer_id}")Environment
Related Issues
Prevention
|
@a0s , I think you will face the same error in pure sqlalchemy, check this test below: full test codeimportasynciofromsqlalchemyimportColumn, ForeignKey, Integer, String, selectfromsqlalchemy.ext.asyncioimportAsyncSession, create_async_enginefromsqlalchemy.ormimportdeclarative_base, joinedload, relationship, sessionmaker# A base for our declarative modelsBase=declarative_base()
# A hypothetical Peer modelclassPeer(Base):
__tablename__="peers"id=Column(Integer, primary_key=True)
name=Column(String)
def__repr__(self):
returnf"Peer(id={self.id}, name='{self.name}')"# Base polymorphic modelclassBaseConnection(Base):
__tablename__="connections"__mapper_args__= {"polymorphic_on": "type"}
id=Column(Integer, primary_key=True)
type=Column(String(50))
def__repr__(self):
returnf"BaseConnection(id={self.id})"# Derived polymorphic model with a foreign keyclassWireGuardConnection(BaseConnection):
__mapper_args__= {"polymorphic_identity": "wireguard"}
peer_id=Column(ForeignKey("peers.id"))
# This is the relationship that would cause lazy loading.# When you access `connection.peer`, SQLAlchemy will query the `peers` table.peer=relationship("Peer")
def__repr__(self):
returnf"WireGuardConnection(id={self.id})"asyncdefsetup_db(engine):
"""Create database tables."""asyncwithengine.begin() asconn:
awaitconn.run_sync(Base.metadata.create_all)
asyncdefseed_data(session: AsyncSession):
"""Insert some example data."""print("--- Inserting example data ---")
peer1=Peer(name="user_A_peer")
connection1=WireGuardConnection()
connection1.peer=peer1# SQLAlchemy will automatically set peer_idsession.add_all([peer1, connection1])
awaitsession.commit()
print("Data insertion complete.")
asyncdefdemonstrate_bug(Session: sessionmaker):
"""Demonstrates the MissingGreenlet error caused by lazy loading."""asyncwithSession() assession:
# Get the connection objectstmt=select(BaseConnection).where(BaseConnection.id==1)
result=awaitsession.execute(stmt)
connection=result.scalar_one_or_none()
# Attempt to access a lazy-loaded attribute after the session is closedprint("\n--- Attempting to access lazy-loaded 'connection.peer_id' ---")
print(f"Current object: {connection}")
try:
# ❌ ERROR: This will attempt to execute a new database query, but the session is inactive# This is the MissingGreenlet error you might encounterprint(f"✅ Access successful: peer id is {connection.peer_id}")
exceptExceptionase:
print(f"❌ Caught error: {type(e).__name__}: {e}")
asyncdefdemonstrate_fix(Session: sessionmaker):
"""Demonstrates how to fix the issue using eager loading."""asyncwithSession() assession:
# Use joinedload to eagerly load the 'peer' relationship# select(WireGuardConnection) ensures we're loading the subclass that has the 'peer' relationshipstmt=select(WireGuardConnection).where(WireGuardConnection.id==1)
result=awaitsession.execute(stmt)
connection=result.scalar_one_or_none()
print("\n--- Attempting to access eagerly-loaded 'connection.peer_id' ---")
print(f"Current object: {connection}")
try:
# ✅ OK: The 'peer' data was loaded in the initial query, so no new database query is neededprint(f"✅ Access successful: peer id is {connection.peer_id}")
print("Since the data was eagerly loaded, no new query is triggered.")
exceptExceptionase:
# No error will occur hereprint(f"❌ Caught error: {type(e).__name__}: {e}")
asyncdefmain():
# Use an in-memory databaseengine=create_async_engine("sqlite+aiosqlite:///:memory:", echo=True)
# Create an async session with a crucial setting: expire_on_commit=FalseSession=sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
awaitsetup_db(engine)
# Use a session to insert dataasyncwithSession() assession:
awaitseed_data(session)
# Run the demonstrationsawaitdemonstrate_bug(Session)
awaitdemonstrate_fix(Session)
# Close the engine connection poolawaitengine.dispose()
if__name__=="__main__":
asyncio.run(main())If you look at the SQL queries being logged to the console, you'll see exactly why this is happening. The Lazy-Loading Issue Initially, your code using SELECTconnections.id, connections.typeFROM connectionsNotice that the When your code later tries to access the SELECTconnections.peer_idAS connections_peer_id FROM connections...The problem is that this lazy-loading operation doesn't work correctly in an async context. It tries to perform I/O without the required BTW, even the code will work in sync sqlalchemy, it will harm performance seriously and should be treated as a bug. It will perform an implicit select query for each item. Recommendation for FastAPI Users Because of complex I/O issues like this, I don't recommend using async database sessions and async api functions for junior developers. FastAPI (via Starlette) is designed to run synchronous functions, like a standard database call, in a separate thread pool. This means a regular, synchronous I/O operation will not block the main application thread. For this reason, it's often much simpler and safer to stick with synchronous database sessions in your app (most internal web app's concurrency is lower than your worker number). If you use sync io function in async api route, you may encounter issues with blocking the event loop which is the worst case scenario for performance in a fastapi application. |
aidenprice
commented
Aug 28, 2025
Is For example; See SQLAlchemy docs https://docs.sqlalchemy.org/en/20/orm/queryguide/inheritance.html#configuring-with-polymorphic-on-mappers |
yurii-franasiuk
commented
Oct 1, 2025
I understand that polymorphic model support has only been implemented based on Single Table Inheritance, and Joined Table Inheritance is not implemented? |
leonardbiofi
commented
Nov 5, 2025
What is the status ? Also can't wait for this to be released :) |
stuaxo
commented
Dec 17, 2025
I'm new to this branch, how is the test coverage? In the django world I use django polymorphic and am really missing this feature. |
budroco
commented
Dec 17, 2025
I'd like to let you know that I discovered problems with objects with relationships. I unfortunately don't have the time to share details or a PoC right now, but hope that I will somewhat soon. |
This pull request has a merge conflict that needs to be resolved. |
heitorslira
commented
Feb 14, 2026
+1 |
Rami-Kassouf-FOO
commented
Feb 27, 2026
can this please be merged? |
HWiese1980
commented
Feb 28, 2026
What's keeping you from merging this, guys? |
alebacca89
commented
Mar 2, 2026
Great feature! Why don't you merge it? It would make projects cleaner and make life easier for many people! Please, let's merge it!!! |
ildivan
commented
Mar 2, 2026
We need this feature please merge it!! |
stuaxo
commented
Mar 6, 2026
Hi all If anybody wants to move it closer to being merged then pull it down, reproduce some of the issues above and then try and submit fixes (maybe try start by resolving the merge conflicts). It's not going to get merged before it works as much as it would be useful to all of us - open source takes time. Have fun out there. S |
@stuaxo Can you give concise feedback on why this isn't ready yet for merging, besides the merge conflicts? I don't think its reasonable to ask the OP to stay interested and start working on this again after 2 years of very little activity by the repo admins, while the OP has responded to and addressed technical issues quite well. I really still want this PR to be merged. I'd be happy to put effort into it, but only if there is consensus on what needs to be addressed and with the commitment that that effort actually leads to this feature being merged into sqlmodel. I would like to push for consensus for it be "good enough" and stay close to the original sqlalchemy syntax and functionality (as that is what many people are used to anyway), not a "perfect" solution. It can evolve from there. But not having polymorphic identities at least with single table inheritance is really annoying in our code base. |
elarchet
commented
Mar 12, 2026
Happy to help too |
stuaxo
commented
Mar 12, 2026
would be the first thing for someone to check, and then the merge conflicts. I'm not asking op to anything, I'm asking everyone to stop asking op "please merge this" while there are still things to fix that can be found by scrolling a up just a tiny bit from this message. |
rdbisme
commented
May 3, 2026
For those interested, we're driving an internal project using Every review is welcome :) |
Introduce support for SQLAlchemy polymorphic models by adjusting field defaults and handling inheritance correctly in the SQLModel metaclass. Add tests to verify functionality with polymorphic joined and single table inheritance. Refer to #36 .