summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2022-10-23 19:24:54 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2022-10-23 19:24:54 -0400
commitbd1777426255648215328252795dff24dfd08616 (patch)
tree3d539521f799315150bdc9e80a38063c050a5bbd
parentbbf68345f4993245689b96cc6c6a50013afa3caa (diff)
downloadsqlalchemy-bd1777426255648215328252795dff24dfd08616.tar.gz
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
-rw-r--r--doc/build/changelog/unreleased_14/8704.rst8
-rw-r--r--lib/sqlalchemy/orm/mapper.py13
-rw-r--r--test/orm/inheritance/test_poly_loading.py109
3 files changed, 128 insertions, 2 deletions
diff --git a/doc/build/changelog/unreleased_14/8704.rst b/doc/build/changelog/unreleased_14/8704.rst
new file mode 100644
index 000000000..7327c9531
--- /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 5d784498a..5d255c9ab 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 f03f15bd2..9086be3c4 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)