From 546391e5a80f647e7ad78ef93f832f10278a8867 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 9 Dec 2021 12:51:43 -0500 Subject: [PATCH] implement attributes.Proxy._clone() Fixed issue where the internal cloning used by the :meth:`_orm.PropComparator.any` method on a :func:`_orm.relationship` in the case where the related class also makes use of ORM polymorphic loading, would fail if a hybrid property on the related, polymorphic class were used within the criteria for the ``any()`` operation. Fixes: #7425 Change-Id: I5f4f4ec5fab17df228bc6e3de412d24114b20600 --- doc/build/changelog/unreleased_14/7425.rst | 9 ++++ lib/sqlalchemy/orm/attributes.py | 10 ++++ test/ext/test_hybrid.py | 61 ++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 doc/build/changelog/unreleased_14/7425.rst diff --git a/doc/build/changelog/unreleased_14/7425.rst b/doc/build/changelog/unreleased_14/7425.rst new file mode 100644 index 0000000000..24b48ef444 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7425.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm, ext + :tickets: 7425 + + Fixed issue where the internal cloning used by the + :meth:`_orm.PropComparator.any` method on a :func:`_orm.relationship` in + the case where the related class also makes use of ORM polymorphic loading, + would fail if a hybrid property on the related, polymorphic class were used + within the criteria for the ``any()`` operation. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index b66d552507..bf8d69b4e2 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -571,6 +571,16 @@ def create_proxied_attribute(descriptor): adapt_to_entity, ) + def _clone(self, **kw): + return self.__class__( + self.class_, + self.key, + self.descriptor, + self._comparator, + adapt_to_entity=self._adapt_to_entity, + original_property=self.original_property, + ) + def __get__(self, instance, owner): retval = self.descriptor.__get__(instance, owner) # detect if this is a plain Python @property, which just returns diff --git a/test/ext/test_hybrid.py b/test/ext/test_hybrid.py index f7524808fc..f3185909a9 100644 --- a/test/ext/test_hybrid.py +++ b/test/ext/test_hybrid.py @@ -245,6 +245,67 @@ class PropertyExpressionTest(fixtures.TestBase, AssertsCompiledSQL): return A, B + @testing.fixture + def _related_polymorphic_attr_fixture(self): + """test for #7425""" + + Base = declarative_base() + + class A(Base): + __tablename__ = "a" + id = Column(Integer, primary_key=True) + + bs = relationship("B", back_populates="a", lazy="joined") + + class B(Base): + __tablename__ = "poly" + __mapper_args__ = { + "polymorphic_on": "type", + # if with_polymorphic is removed, issue does not occur + "with_polymorphic": "*", + } + name = Column(String, primary_key=True) + type = Column(String) + a_id = Column(ForeignKey(A.id)) + + a = relationship(A, back_populates="bs") + + @hybrid.hybrid_property + def is_foo(self): + return self.name == "foo" + + return A, B + + def test_cloning_in_polymorphic_any( + self, _related_polymorphic_attr_fixture + ): + A, B = _related_polymorphic_attr_fixture + + session = fixture_session() + + # in the polymorphic case, A.bs.any() does a traverse() / clone() + # on the expression. so the proxedattribute coming from the hybrid + # has to support this. + + self.assert_compile( + session.query(A).filter(A.bs.any(B.name == "foo")), + "SELECT a.id AS a_id, poly_1.name AS poly_1_name, poly_1.type " + "AS poly_1_type, poly_1.a_id AS poly_1_a_id FROM a " + "LEFT OUTER JOIN poly AS poly_1 ON a.id = poly_1.a_id " + "WHERE EXISTS (SELECT 1 FROM poly WHERE a.id = poly.a_id " + "AND poly.name = :name_1)", + ) + + # SQL should be identical + self.assert_compile( + session.query(A).filter(A.bs.any(B.is_foo)), + "SELECT a.id AS a_id, poly_1.name AS poly_1_name, poly_1.type " + "AS poly_1_type, poly_1.a_id AS poly_1_a_id FROM a " + "LEFT OUTER JOIN poly AS poly_1 ON a.id = poly_1.a_id " + "WHERE EXISTS (SELECT 1 FROM poly WHERE a.id = poly.a_id " + "AND poly.name = :name_1)", + ) + @testing.fixture def _unnamed_expr_fixture(self): Base = declarative_base() -- 2.47.2