From: Mike Bayer Date: Sun, 23 Oct 2022 23:24:54 +0000 (-0400) Subject: skip ad-hoc properties within subclass_load_via_in X-Git-Tag: rel_2_0_0b3~31^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bd1777426255648215328252795dff24dfd08616;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git skip ad-hoc properties within subclass_load_via_in 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 --- diff --git a/doc/build/changelog/unreleased_14/8704.rst b/doc/build/changelog/unreleased_14/8704.rst new file mode 100644 index 0000000000..7327c95313 --- /dev/null +++ b/doc/build/changelog/unreleased_14/8704.rst @@ -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. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 5d784498a0..5d255c9ab1 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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, diff --git a/test/orm/inheritance/test_poly_loading.py b/test/orm/inheritance/test_poly_loading.py index f03f15bd25..9086be3c4a 100644 --- a/test/orm/inheritance/test_poly_loading.py +++ b/test/orm/inheritance/test_poly_loading.py @@ -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)