From: Mike Bayer Date: Tue, 9 Nov 2021 16:31:23 +0000 (-0500) Subject: upgrade deferred loader to regular loader if refresh_state X-Git-Tag: rel_2_0_0b1~658^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=52b3d6649525929ee1ec14487a2f007194ed741d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git upgrade deferred loader to regular loader if refresh_state Fixed issue where deferred polymorphic loading of attributes from a joined-table inheritance subclass would fail to populate the attribute correctly if the :func:`_orm.load_only` option were used to originally exclude that attribute, in the case where the load_only were descending from a relationship loader option. The fix allows that other valid options such as ``defer(..., raiseload=True)`` etc. still function as expected. Fixes: #7304 Change-Id: I58b7ce7c450bcc52d2f0c9bfbcb4d747463ee9b2 --- diff --git a/doc/build/changelog/unreleased_14/7304.rst b/doc/build/changelog/unreleased_14/7304.rst new file mode 100644 index 0000000000..44d188a30e --- /dev/null +++ b/doc/build/changelog/unreleased_14/7304.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, orm + :tickets: 7304 + + Fixed issue where deferred polymorphic loading of attributes from a + joined-table inheritance subclass would fail to populate the attribute + correctly if the :func:`_orm.load_only` option were used to originally + exclude that attribute, in the case where the load_only were descending + from a relationship loader option. The fix allows that other valid options + such as ``defer(..., raiseload=True)`` etc. still function as expected. diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 2a283caad6..71c4a69761 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -382,7 +382,26 @@ class DeferredColumnLoader(LoaderStrategy): # dictionary. Normally, the DeferredColumnLoader.setup_query() # sets up that data in the "memoized_populators" dictionary # and "create_row_processor()" here is never invoked. - if not self.is_class_level: + + if ( + context.refresh_state + and context.query._compile_options._only_load_props + and self.key in context.query._compile_options._only_load_props + ): + self.parent_property._get_strategy( + (("deferred", False), ("instrument", True)) + ).create_row_processor( + context, + query_entity, + path, + loadopt, + mapper, + result, + adapter, + populators, + ) + + elif not self.is_class_level: if self.raiseload: set_deferred_for_local_state = ( self.parent_property._raise_column_loader diff --git a/test/orm/inheritance/test_poly_loading.py b/test/orm/inheritance/test_poly_loading.py index 35822a29e9..332f11214d 100644 --- a/test/orm/inheritance/test_poly_loading.py +++ b/test/orm/inheritance/test_poly_loading.py @@ -1,23 +1,29 @@ -from sqlalchemy import Column +from sqlalchemy import exc from sqlalchemy import ForeignKey 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 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 with_polymorphic from sqlalchemy.sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL +from sqlalchemy.testing import assertsql from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertions import expect_raises_message from sqlalchemy.testing.assertsql import AllOf from sqlalchemy.testing.assertsql import CompiledSQL from sqlalchemy.testing.assertsql import EachOf from sqlalchemy.testing.assertsql import Or from sqlalchemy.testing.fixtures import fixture_session +from sqlalchemy.testing.schema import Column from ._poly_fixtures import _Polymorphic from ._poly_fixtures import Company from ._poly_fixtures import Engineer @@ -686,3 +692,161 @@ class LoaderOptionsTest( result = no_opt() with self.assert_statement_count(testing.db, 1): eq_(result, [Parent(children=[ChildSubclass1(others=[Other()])])]) + + +class IgnoreOptionsOnSubclassAttrLoad(fixtures.DeclarativeMappedTest): + """test #7304 and related cases + + in this case we trigger the subclass attribute load, while at the same + time there will be a deferred loader option present in the state's + options that was established by the previous loader. + + test both that the option takes effect (i.e. raiseload) and that a deferred + loader doesn't interfere with the mapper's load of the attribute. + + """ + + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class Parent(Base): + __tablename__ = "parent" + + id = Column( + Integer, primary_key=True, test_needs_autoincrement=True + ) + + entity_id = Column(ForeignKey("entity.id")) + entity = relationship("Entity") + + class Entity(Base): + __tablename__ = "entity" + + id = Column( + Integer, primary_key=True, test_needs_autoincrement=True + ) + type = Column(String(32)) + + __mapper_args__ = { + "polymorphic_on": type, + "polymorphic_identity": "entity", + } + + class SubEntity(Entity): + __tablename__ = "sub_entity" + + id = Column(ForeignKey(Entity.id), primary_key=True) + + name = Column(String(32)) + + __mapper_args__ = {"polymorphic_identity": "entity_two"} + + @classmethod + def insert_data(cls, connection): + Parent, SubEntity = cls.classes("Parent", "SubEntity") + + with Session(connection) as session: + session.add(Parent(entity=SubEntity(name="some name"))) + session.commit() + + @testing.combinations( + defaultload, + joinedload, + selectinload, + lazyload, + argnames="first_option", + ) + @testing.combinations( + ("load_only", "id", True), + ("defer", "name", True), + ("undefer", "name", True), + ("raise", "name", False), + (None, None, True), + # these don't seem possible at the moment as the "type" column + # doesn't load and it can't recognize the polymorphic identity. + # we assume load_only() is smart enough to include this column + # ("defer", '*', True), + # ("undefer", '*', True), + # ("raise", '*', False), + argnames="second_option,second_argument,expect_load", + ) + def test_subclass_loadattr( + self, first_option, second_option, second_argument, expect_load + ): + Parent, Entity, SubEntity = self.classes( + "Parent", "Entity", "SubEntity" + ) + + stmt = select(Parent) + + will_lazyload = first_option in (defaultload, lazyload) + + opt = first_option(Parent.entity) + + if second_argument == "name": + second_argument = SubEntity.name + elif second_argument == "id": + second_argument = Entity.id + + if second_option is None: + sub_opt = opt + elif second_option == "raise": + sub_opt = opt.defer(second_argument, raiseload=True) + else: + sub_opt = getattr(opt, second_option)(second_argument) + + stmt = stmt.options(sub_opt) + + session = fixture_session() + result = session.execute(stmt).scalars() + + parent_obj = result.first() + + entity_id = parent_obj.__dict__["entity_id"] + + with assertsql.assert_engine(testing.db) as asserter_: + if expect_load: + eq_(parent_obj.entity.name, "some name") + else: + with expect_raises_message( + exc.InvalidRequestError, + "'SubEntity.name' is not available due to raiseload=True", + ): + parent_obj.entity.name + + expected = [] + + if will_lazyload: + expected.append( + CompiledSQL( + "SELECT entity.id AS entity_id, " + "entity.type AS entity_type FROM entity " + "WHERE entity.id = :pk_1", + [{"pk_1": entity_id}], + ) + ) + + if second_option in ("undefer", "load_only", None): + # load will be a mapper optimized load for the name alone + expected.append( + CompiledSQL( + "SELECT sub_entity.name AS sub_entity_name " + "FROM sub_entity " + "WHERE :param_1 = sub_entity.id", + [{"param_1": entity_id}], + ) + ) + elif second_option == "defer": + # load will be a deferred load. this is because the explicit + # call to the deferred load put a deferred loader on the attribute + expected.append( + CompiledSQL( + "SELECT sub_entity.name AS sub_entity_name FROM entity " + "JOIN sub_entity ON entity.id = sub_entity.id " + "WHERE entity.id = :pk_1", + [{"pk_1": entity_id}], + ) + ) + + asserter_.assert_(*expected)