]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
skip ad-hoc properties within subclass_load_via_in
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 23 Oct 2022 23:24:54 +0000 (19:24 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 23 Oct 2022 23:24:54 +0000 (19:24 -0400)
Fixed issue where "selectin_polymorphic" loading for inheritance mappers
would not function correctly if the :param:`_orm.Mapper.polymorphic_on`
parameter referred to a SQL expression that was not directly mapped on the
class.

Fixes: #8704
Change-Id: I1b6be2650895fd18d2c804f6ba96de966d11041a

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

diff --git a/doc/build/changelog/unreleased_14/8704.rst b/doc/build/changelog/unreleased_14/8704.rst
new file mode 100644 (file)
index 0000000..7327c95
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 8704
+
+    Fixed issue where "selectin_polymorphic" loading for inheritance mappers
+    would not function correctly if the :param:`_orm.Mapper.polymorphic_on`
+    parameter referred to a SQL expression that was not directly mapped on the
+    class.
index 5d784498a0e2ddf90b2814938a16e890dbb48c7d..5d255c9ab194e066f8138b8109714eae18e89219 100644 (file)
@@ -3432,6 +3432,13 @@ class Mapper(
         enable_opt = strategy_options.Load(entity)
 
         for prop in self.attrs:
+
+            # skip prop keys that are not instrumented on the mapped class.
+            # this is primarily the "_sa_polymorphic_on" property that gets
+            # created for an ad-hoc polymorphic_on SQL expression, issue #8704
+            if prop.key not in self.class_manager:
+                continue
+
             if prop.parent is self or prop in keep_props:
                 # "enable" options, to turn on the properties that we want to
                 # load by default (subject to options from the query)
@@ -3440,7 +3447,8 @@ class Mapper(
 
                 enable_opt = enable_opt._set_generic_strategy(
                     # convert string name to an attribute before passing
-                    # to loader strategy
+                    # to loader strategy.   note this must be in terms
+                    # of given entity, such as AliasedClass, etc.
                     (getattr(entity.entity_namespace, prop.key),),
                     dict(prop.strategy_key),
                     _reconcile_to_other=True,
@@ -3451,7 +3459,8 @@ class Mapper(
                 # the options from the query to override them
                 disable_opt = disable_opt._set_generic_strategy(
                     # convert string name to an attribute before passing
-                    # to loader strategy
+                    # to loader strategy.   note this must be in terms
+                    # of given entity, such as AliasedClass, etc.
                     (getattr(entity.entity_namespace, prop.key),),
                     {"do_nothing": True},
                     _reconcile_to_other=False,
index f03f15bd25110b129ddcc329e23c163802c32a7b..9086be3c4ad7e50ddf594c1bf6b6f08ca64605a1 100644 (file)
@@ -8,6 +8,7 @@ from sqlalchemy import String
 from sqlalchemy import testing
 from sqlalchemy import union
 from sqlalchemy.orm import backref
+from sqlalchemy.orm import column_property
 from sqlalchemy.orm import composite
 from sqlalchemy.orm import defaultload
 from sqlalchemy.orm import immediateload
@@ -1174,3 +1175,111 @@ class CompositeAttributesTest(fixtures.TestBase):
                     B(id=2, thing2="thing2", comp2=XYThing(3, 4)),
                 ],
             )
+
+
+class PolymorphicOnExprTest(
+    testing.AssertsExecutionResults, fixtures.TestBase
+):
+    """test for #8704"""
+
+    @testing.fixture()
+    def poly_fixture(self, connection, decl_base):
+        def fixture(create_prop, use_load):
+            class TypeTable(decl_base):
+                __tablename__ = "type"
+
+                id = Column(Integer, primary_key=True)
+                name = Column(String(30))
+
+            class PolyBase(ComparableEntity, decl_base):
+                __tablename__ = "base"
+
+                id = Column(Integer, primary_key=True)
+                type_id = Column(ForeignKey(TypeTable.id))
+
+                if create_prop == "create_prop":
+                    polymorphic = column_property(
+                        select(TypeTable.name)
+                        .where(TypeTable.id == type_id)
+                        .scalar_subquery()
+                    )
+                    __mapper_args__ = {
+                        "polymorphic_on": polymorphic,
+                    }
+                elif create_prop == "dont_create_prop":
+                    __mapper_args__ = {
+                        "polymorphic_on": select(TypeTable.name)
+                        .where(TypeTable.id == type_id)
+                        .scalar_subquery()
+                    }
+                elif create_prop == "arg_level_prop":
+                    __mapper_args__ = {
+                        "polymorphic_on": column_property(
+                            select(TypeTable.name)
+                            .where(TypeTable.id == type_id)
+                            .scalar_subquery()
+                        )
+                    }
+
+            class Foo(PolyBase):
+                __tablename__ = "foo"
+
+                if use_load == "use_polymorphic_load":
+                    __mapper_args__ = {
+                        "polymorphic_identity": "foo",
+                        "polymorphic_load": "selectin",
+                    }
+                else:
+                    __mapper_args__ = {
+                        "polymorphic_identity": "foo",
+                    }
+
+                id = Column(ForeignKey(PolyBase.id), primary_key=True)
+                foo_attr = Column(String(30))
+
+            decl_base.metadata.create_all(connection)
+
+            with Session(connection) as session:
+                foo_type = TypeTable(name="foo")
+                session.add(foo_type)
+                session.flush()
+
+                foo = Foo(type_id=foo_type.id, foo_attr="foo value")
+                session.add(foo)
+
+                session.commit()
+
+            return PolyBase, Foo, TypeTable
+
+        yield fixture
+
+    @testing.combinations(
+        "create_prop",
+        "dont_create_prop",
+        "arg_level_prop",
+        argnames="create_prop",
+    )
+    @testing.combinations(
+        "use_polymorphic_load",
+        "use_loader_option",
+        "none",
+        argnames="use_load",
+    )
+    def test_load_selectin(
+        self, poly_fixture, connection, create_prop, use_load
+    ):
+        PolyBase, Foo, TypeTable = poly_fixture(create_prop, use_load)
+
+        sess = Session(connection)
+
+        foo_type = sess.scalars(select(TypeTable)).one()
+
+        stmt = select(PolyBase)
+        if use_load == "use_loader_option":
+            stmt = stmt.options(selectin_polymorphic(PolyBase, [Foo]))
+        obj = sess.scalars(stmt).all()
+
+        def go():
+            eq_(obj, [Foo(type_id=foo_type.id, foo_attr="foo value")])
+
+        self.assert_sql_count(testing.db, go, 0 if use_load != "none" else 1)