]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
upgrade deferred loader to regular loader if refresh_state
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 9 Nov 2021 16:31:23 +0000 (11:31 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 9 Nov 2021 18:57:18 +0000 (13:57 -0500)
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

doc/build/changelog/unreleased_14/7304.rst [new file with mode: 0644]
lib/sqlalchemy/orm/strategies.py
test/orm/inheritance/test_poly_loading.py

diff --git a/doc/build/changelog/unreleased_14/7304.rst b/doc/build/changelog/unreleased_14/7304.rst
new file mode 100644 (file)
index 0000000..44d188a
--- /dev/null
@@ -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.
index 2a283caad6e12fcdb1bca47c9fbafb3bc1ffb571..71c4a6976119d1f3a233d0fd8d2e831f46442ce2 100644 (file)
@@ -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
index 35822a29e9fb7773b705419ec3eb207bc24f90be..332f11214d4ee058baa786c7d6621afe526855ea 100644 (file)
@@ -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)