From 4e9fe6e3b7a72fc3b116403ea9b27e847b5bf186 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 10 Jan 2022 14:59:59 -0500 Subject: [PATCH] ensure with_options not switched to a list Fixed regression which appeared in 1.4.23 which could cause loader options to be mis-handled in some cases, in particular when using joined table inheritance in combination with the ``polymorphic_load="selectin"`` option as well as relationship lazy loading, leading to a ``TypeError``. Fixes: #7557 Change-Id: Id38619692f94308fd5f567a02337efef7a3a7544 --- doc/build/changelog/unreleased_14/7557.rst | 9 +++ lib/sqlalchemy/orm/strategies.py | 2 +- test/orm/inheritance/test_poly_loading.py | 85 ++++++++++++++++++++-- 3 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 doc/build/changelog/unreleased_14/7557.rst diff --git a/doc/build/changelog/unreleased_14/7557.rst b/doc/build/changelog/unreleased_14/7557.rst new file mode 100644 index 0000000000..b7ccc87cf2 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7557.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm, regression + :tickets: 7557 + + Fixed regression which appeared in 1.4.23 which could cause loader options + to be mis-handled in some cases, in particular when using joined table + inheritance in combination with the ``polymorphic_load="selectin"`` option + as well as relationship lazy loading, leading to a ``TypeError``. + diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index c29fc7749d..a8a5639528 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -964,7 +964,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): if state.load_options or (loadopt and loadopt._extra_criteria): effective_path = state.load_path[self.parent_property] - opts = list(state.load_options) + opts = tuple(state.load_options) if loadopt and loadopt._extra_criteria: use_get = False diff --git a/test/orm/inheritance/test_poly_loading.py b/test/orm/inheritance/test_poly_loading.py index 4fe13887e1..517431e705 100644 --- a/test/orm/inheritance/test_poly_loading.py +++ b/test/orm/inheritance/test_poly_loading.py @@ -1,18 +1,22 @@ from sqlalchemy import exc from sqlalchemy import ForeignKey +from sqlalchemy import inspect from sqlalchemy import Integer from sqlalchemy import select from sqlalchemy import String from sqlalchemy import testing from sqlalchemy.orm import backref from sqlalchemy.orm import defaultload +from sqlalchemy.orm import immediateload from sqlalchemy.orm import joinedload from sqlalchemy.orm import lazyload from sqlalchemy.orm import relationship from sqlalchemy.orm import selectin_polymorphic from sqlalchemy.orm import selectinload from sqlalchemy.orm import Session +from sqlalchemy.orm import subqueryload from sqlalchemy.orm import with_polymorphic +from sqlalchemy.orm.interfaces import CompileStateOption from sqlalchemy.sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from sqlalchemy.testing import assertsql from sqlalchemy.testing import eq_ @@ -589,17 +593,11 @@ class LoaderOptionsTest( session.add_all([parent, subclass1, other]) session.commit() - def test_options_dont_pollute_baked(self): - self._test_options_dont_pollute(True) - - def test_options_dont_pollute_unbaked(self): - self._test_options_dont_pollute(False) - - def _test_options_dont_pollute(self, enable_baked): + def test_options_dont_pollute(self): Parent, ChildSubclass1, Other = self.classes( "Parent", "ChildSubclass1", "Other" ) - session = fixture_session(enable_baked_queries=enable_baked) + session = fixture_session() def no_opt(): q = session.query(Parent).options( @@ -860,3 +858,74 @@ class IgnoreOptionsOnSubclassAttrLoad(fixtures.DeclarativeMappedTest): ) asserter_.assert_(*expected) + + +class LazyLoaderTransfersOptsTest(fixtures.DeclarativeMappedTest): + """test #7557""" + + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class Address(Base): + __tablename__ = "address" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("user.id")) + address_type = Column(String(50)) + __mapper_args__ = { + "polymorphic_identity": "base_address", + "polymorphic_on": address_type, + } + + class EmailAddress(Address): + __tablename__ = "email_address" + email = Column(String(50)) + address_id = Column( + Integer, + ForeignKey(Address.id), + primary_key=True, + ) + + __mapper_args__ = { + "polymorphic_identity": "email", + "polymorphic_load": "selectin", + } + + class User(Base): + __tablename__ = "user" + + id = Column(Integer, primary_key=True) + name = Column(String(50)) + address = relationship(Address, uselist=False) + + @classmethod + def insert_data(cls, connection): + User, EmailAddress = cls.classes("User", "EmailAddress") + with Session(connection) as sess: + sess.add_all( + [User(name="u1", address=EmailAddress(email="foo", user_id=1))] + ) + + sess.commit() + + @testing.combinations( + None, selectinload, joinedload, lazyload, subqueryload, immediateload + ) + def test_opt_propagates(self, strat): + User, EmailAddress = self.classes("User", "EmailAddress") + sess = fixture_session() + + class AnyOpt(CompileStateOption): + _cache_key_traversal = () + propagate_to_loaders = True + + any_opt = AnyOpt() + if strat is None: + opts = (any_opt,) + else: + opts = (strat(User.address), any_opt) + + u = sess.execute(select(User).options(*opts)).scalars().one() + address = u.address + eq_(inspect(address).load_options, set(opts)) -- 2.47.2