Skip to content

Commit 41b025d

Browse files
zzzeekGerrit Code Review
authored andcommitted
Merge "accommodate subclass mapper in post-loader entity_isa check" into main
2 parents 9ad9069 + 2ac8c1a commit 41b025d

3 files changed

Lines changed: 191 additions & 2 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.. change::
2+
:tags: bug, orm, inheritance
3+
:tickets: 13209
4+
5+
Fixed issue where using chained loader options such as
6+
:func:`_orm.selectinload` after :func:`_orm.joinedload` with
7+
:meth:`_orm.PropComparator.of_type` for a polymorphic relationship would
8+
not properly apply the chained loader option. The loader option is now
9+
correctly applied when using a call such as
10+
``joinedload(A.b.of_type(poly)).selectinload(poly.SubClass.c)`` to eagerly
11+
load related objects.

lib/sqlalchemy/orm/strategies.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,7 +2034,12 @@ def create_row_processor(
20342034
if len(path) == 1:
20352035
if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
20362036
return
2037-
elif not orm_util._entity_isa(path[-1], self.parent):
2037+
elif not orm_util._entity_isa(
2038+
path[-1], self.parent
2039+
) and not self.parent.isa(path[-1].mapper):
2040+
# second check accommodates a polymorphic entity where
2041+
# the path has been normalized to the base mapper but
2042+
# self.parent is a subclass mapper. Fixes #13209.
20382043
return
20392044

20402045
subq = self._setup_query_from_rowproc(
@@ -3110,7 +3115,14 @@ def create_row_processor(
31103115
if len(path) == 1:
31113116
if not orm_util._entity_isa(query_entity.entity_zero, self.parent):
31123117
return
3113-
elif not orm_util._entity_isa(path[-1], self.parent):
3118+
elif not orm_util._entity_isa(
3119+
path[-1], self.parent
3120+
) and not self.parent.isa(path[-1].mapper):
3121+
# second check accommodates a polymorphic entity where
3122+
# the path has been normalized to the base mapper but
3123+
# self.parent is a subclass mapper, e.g.
3124+
# joinedload(A.b.of_type(poly)).selectinload(poly.Sub.rel)
3125+
# Fixes #13209.
31143126
return
31153127

31163128
selectin_path = effective_path

test/orm/test_of_type.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,3 +1371,169 @@ def test_joinedload_of_type_chained_vs_options(
13711371
" AS c_sub_1 ON c_1.id = c_sub_1.id) ON b_1.id = c_1.b_id"
13721372
)
13731373
)
1374+
1375+
1376+
class ChainedLoaderAfterOfTypeTest(
1377+
testing.AssertsCompiledSQL, fixtures.DeclarativeMappedTest
1378+
):
1379+
"""Regression test for issue #13209.
1380+
1381+
Tests that loader options chained after of_type() are properly applied.
1382+
"""
1383+
1384+
run_setup_classes = "once"
1385+
run_setup_mappers = "once"
1386+
run_inserts = "once"
1387+
run_deletes = None
1388+
__dialect__ = "default"
1389+
1390+
@classmethod
1391+
def setup_classes(cls):
1392+
Base = cls.DeclarativeBasic
1393+
1394+
class TopABC(ComparableEntity, Base):
1395+
__tablename__ = "top_abc"
1396+
id = Column(Integer, primary_key=True)
1397+
1398+
class Top(ComparableEntity, Base):
1399+
__tablename__ = "top"
1400+
id = Column(Integer, ForeignKey("top_abc.id"), primary_key=True)
1401+
top_abc_id = Column(Integer, ForeignKey("top_abc.id"))
1402+
type = Column(String(50))
1403+
__mapper_args__ = {"polymorphic_on": type}
1404+
1405+
class Foo(Top):
1406+
__tablename__ = "foo"
1407+
id = Column(Integer, ForeignKey("top.id"), primary_key=True)
1408+
foo_name = Column(String(50))
1409+
__mapper_args__ = {"polymorphic_identity": "FOO"}
1410+
1411+
class Bar(Top):
1412+
__tablename__ = "bar"
1413+
id = Column(Integer, ForeignKey("top.id"), primary_key=True)
1414+
bar_name = Column(String(50))
1415+
foo_id = Column(Integer, ForeignKey("foo.id"))
1416+
__mapper_args__ = {"polymorphic_identity": "BAR"}
1417+
1418+
TopABC.top = relationship(
1419+
Top, foreign_keys=[Top.top_abc_id], uselist=False
1420+
)
1421+
Bar.foo = relationship(Foo, foreign_keys=[Bar.foo_id], uselist=False)
1422+
1423+
@classmethod
1424+
def insert_data(cls, connection):
1425+
with Session(connection) as sess:
1426+
TopABC, Foo, Bar = cls.classes("TopABC", "Foo", "Bar")
1427+
sess.add(
1428+
TopABC(id=1, top=Foo(id=1, top_abc_id=1, foo_name="foo1"))
1429+
)
1430+
sess.add(
1431+
TopABC(
1432+
id=2,
1433+
top=Bar(id=2, top_abc_id=2, bar_name="bar1", foo_id=1),
1434+
)
1435+
)
1436+
sess.commit()
1437+
1438+
@testing.variation("loader", ["joined", "selectin", "subquery"])
1439+
def test_chained_loader_after_of_type(self, loader: testing.Variation):
1440+
"""Test that selectinload/joinedload/subqueryload works when chained
1441+
after joinedload with of_type().
1442+
1443+
Regression test for issue #13209 where chaining a loader option
1444+
after joinedload(...of_type(poly)) would not properly apply the
1445+
chained loader, resulting in lazy loads.
1446+
"""
1447+
TopABC, Top, Foo, Bar = self.classes("TopABC", "Top", "Foo", "Bar")
1448+
1449+
top_poly = with_polymorphic(Top, "*", flat=True)
1450+
1451+
if loader.selectin:
1452+
stmt = select(TopABC).options(
1453+
joinedload(TopABC.top.of_type(top_poly)).selectinload(
1454+
top_poly.Bar.foo
1455+
)
1456+
)
1457+
elif loader.joined:
1458+
stmt = select(TopABC).options(
1459+
joinedload(TopABC.top.of_type(top_poly)).joinedload(
1460+
top_poly.Bar.foo
1461+
)
1462+
)
1463+
elif loader.subquery:
1464+
stmt = select(TopABC).options(
1465+
joinedload(TopABC.top.of_type(top_poly)).subqueryload(
1466+
top_poly.Bar.foo
1467+
)
1468+
)
1469+
else:
1470+
loader.fail()
1471+
1472+
session = fixture_session()
1473+
with self.sql_execution_asserter(testing.db) as asserter_:
1474+
result = session.scalars(stmt).unique().all()
1475+
# Access the chained relationship - should not trigger lazy load
1476+
for obj in result:
1477+
if isinstance(obj.top, Bar):
1478+
_ = obj.top.foo
1479+
1480+
if loader.selectin:
1481+
asserter_.assert_(
1482+
CompiledSQL(
1483+
"SELECT top_abc.id, top_1.id AS id_1, top_1.top_abc_id,"
1484+
" top_1.type, foo_1.id AS id_2, foo_1.foo_name,"
1485+
" bar_1.id AS id_3, bar_1.bar_name, bar_1.foo_id"
1486+
" FROM top_abc LEFT OUTER JOIN (top AS top_1 LEFT"
1487+
" OUTER JOIN foo AS foo_1 ON top_1.id = foo_1.id"
1488+
" LEFT OUTER JOIN bar AS bar_1 ON top_1.id ="
1489+
" bar_1.id) ON top_abc.id = top_1.top_abc_id"
1490+
),
1491+
CompiledSQL(
1492+
"SELECT top.id, foo.id, top.top_abc_id, top.type,"
1493+
" foo.foo_name FROM top JOIN foo ON top.id = foo.id"
1494+
" WHERE top.id IN (__[POSTCOMPILE_primary_keys])"
1495+
),
1496+
)
1497+
elif loader.subquery:
1498+
asserter_.assert_(
1499+
CompiledSQL(
1500+
"SELECT top_abc.id, top_1.id AS id_1, top_1.top_abc_id,"
1501+
" top_1.type, foo_1.id AS id_2, foo_1.foo_name,"
1502+
" bar_1.id AS id_3, bar_1.bar_name, bar_1.foo_id"
1503+
" FROM top_abc LEFT OUTER JOIN (top AS top_1 LEFT"
1504+
" OUTER JOIN foo AS foo_1 ON top_1.id = foo_1.id"
1505+
" LEFT OUTER JOIN bar AS bar_1 ON top_1.id ="
1506+
" bar_1.id) ON top_abc.id = top_1.top_abc_id"
1507+
),
1508+
CompiledSQL(
1509+
"SELECT foo.id AS foo_id, top.id AS top_id,"
1510+
" top.top_abc_id AS top_top_abc_id, top.type AS"
1511+
" top_type, foo.foo_name AS foo_foo_name,"
1512+
" anon_1.bar_foo_id AS anon_1_bar_foo_id FROM"
1513+
" (SELECT top_abc.id AS top_abc_id FROM top_abc)"
1514+
" AS anon_2 JOIN (SELECT top.id AS top_id,"
1515+
" top.top_abc_id AS top_top_abc_id, top.type AS"
1516+
" top_type, bar.id AS bar_id, bar.bar_name AS"
1517+
" bar_bar_name, bar.foo_id AS bar_foo_id FROM top"
1518+
" JOIN bar ON top.id = bar.id) AS anon_1 ON"
1519+
" anon_2.top_abc_id = anon_1.top_top_abc_id JOIN"
1520+
" (top JOIN foo ON top.id = foo.id) ON foo.id ="
1521+
" anon_1.bar_foo_id"
1522+
),
1523+
)
1524+
elif loader.joined:
1525+
asserter_.assert_(
1526+
CompiledSQL(
1527+
"SELECT top_abc.id, top_1.id AS id_1, top_1.top_abc_id,"
1528+
" top_1.type, foo_1.id AS id_2, foo_1.foo_name,"
1529+
" bar_1.id AS id_3, bar_1.bar_name, bar_1.foo_id,"
1530+
" foo_2.id AS id_4, top_2.id AS id_5, top_2.top_abc_id"
1531+
" AS top_abc_id_1, top_2.type AS type_1, foo_2.foo_name"
1532+
" AS foo_name_1 FROM top_abc LEFT OUTER JOIN (top AS"
1533+
" top_1 LEFT OUTER JOIN foo AS foo_1 ON top_1.id ="
1534+
" foo_1.id LEFT OUTER JOIN bar AS bar_1 ON top_1.id ="
1535+
" bar_1.id) ON top_abc.id = top_1.top_abc_id LEFT"
1536+
" OUTER JOIN (top AS top_2 JOIN foo AS foo_2 ON"
1537+
" top_2.id = foo_2.id) ON foo_2.id = bar_1.foo_id"
1538+
)
1539+
)

0 commit comments

Comments
 (0)