summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2021-04-28 09:56:15 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2021-04-28 13:39:59 -0400
commit41ac0c7187daed54b0ba44b2287b6679d95d2caa (patch)
treed0a262379c26aadf0dc2a333b4d863e127ae6928
parent2af1b107fce34b15898e6f534097ad34cfd7d503 (diff)
downloadsqlalchemy-41ac0c7187daed54b0ba44b2287b6679d95d2caa.tar.gz
add optional proxy_class to track w/ proxy_key
Fixed regression in ORM where using hybrid property to indicate an expression from a different entity would confuse the column-labeling logic in the ORM and attempt to derive the name of the hybrid from that other class, leading to an attribute error. The owning class of the hybrid attribute is now tracked along with the name. Fixes: #6386 Change-Id: Ica9497ea34fef799d6265de44104c1f3f3b30232
-rw-r--r--doc/build/changelog/unreleased_14/6386.rst9
-rw-r--r--lib/sqlalchemy/orm/attributes.py1
-rw-r--r--lib/sqlalchemy/orm/context.py3
-rw-r--r--test/ext/test_hybrid.py63
-rw-r--r--test/orm/test_utils.py2
5 files changed, 77 insertions, 1 deletions
diff --git a/doc/build/changelog/unreleased_14/6386.rst b/doc/build/changelog/unreleased_14/6386.rst
new file mode 100644
index 000000000..d61a2cc48
--- /dev/null
+++ b/doc/build/changelog/unreleased_14/6386.rst
@@ -0,0 +1,9 @@
+.. change::
+ :tags: ext, bug, regression
+ :tickets: 6386
+
+ Fixed regression in ORM where using hybrid property to indicate an
+ expression from a different entity would confuse the column-labeling logic
+ in the ORM and attempt to derive the name of the hybrid from that other
+ class, leading to an attribute error. The owning class of the hybrid
+ attribute is now tracked along with the name. \ No newline at end of file
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 0c7bc4cf0..b8974196c 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -227,6 +227,7 @@ class QueryableAttribute(
else:
annotations = {
"proxy_key": self.key,
+ "proxy_owner": self.class_,
"entity_namespace": self._entity_namespace,
}
diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py
index 6cdad9f41..042acc9c5 100644
--- a/lib/sqlalchemy/orm/context.py
+++ b/lib/sqlalchemy/orm/context.py
@@ -2691,8 +2691,9 @@ class _ORMColumnEntity(_ColumnEntity):
# within internal loaders.
orm_key = annotations.get("proxy_key", None)
+ proxy_owner = annotations.get("proxy_owner", _entity.entity)
if orm_key:
- self.expr = getattr(_entity.entity, orm_key)
+ self.expr = getattr(proxy_owner, orm_key)
self.translate_raw_column = False
else:
# if orm_key is not present, that means this is an ad-hoc
diff --git a/test/ext/test_hybrid.py b/test/ext/test_hybrid.py
index c9373f9aa..ee991782e 100644
--- a/test/ext/test_hybrid.py
+++ b/test/ext/test_hybrid.py
@@ -492,6 +492,69 @@ class PropertyMirrorTest(fixtures.TestBase, AssertsCompiledSQL):
return A
+ @testing.fixture
+ def _name_mismatch_fixture(self):
+ Base = declarative_base()
+
+ class A(Base):
+ __tablename__ = "a"
+ id = Column(Integer, primary_key=True)
+ addresses = relationship("B")
+
+ @hybrid.hybrid_property
+ def some_email(self):
+ if self.addresses:
+ return self.addresses[0].email_address
+ else:
+ return None
+
+ @some_email.expression
+ def some_email(cls):
+ return B.email_address
+
+ class B(Base):
+ __tablename__ = "b"
+ id = Column(Integer, primary_key=True)
+ aid = Column(ForeignKey("a.id"))
+ email_address = Column(String)
+
+ return A, B
+
+ def test_dont_assume_attr_key_is_present(self, _name_mismatch_fixture):
+ A, B = _name_mismatch_fixture
+ self.assert_compile(
+ select(A, A.some_email).join(A.addresses),
+ "SELECT a.id, b.email_address FROM a JOIN b ON a.id = b.aid",
+ )
+
+ def test_dont_assume_attr_key_is_present_ac(self, _name_mismatch_fixture):
+ A, B = _name_mismatch_fixture
+
+ ac = aliased(A)
+ self.assert_compile(
+ select(ac, ac.some_email).join(ac.addresses),
+ "SELECT a_1.id, b.email_address "
+ "FROM a AS a_1 JOIN b ON a_1.id = b.aid",
+ )
+
+ def test_filter_by_mismatched_col(self, _name_mismatch_fixture):
+ A, B = _name_mismatch_fixture
+ self.assert_compile(
+ select(A).filter_by(some_email="foo").join(A.addresses),
+ "SELECT a.id FROM a JOIN b ON a.id = b.aid "
+ "WHERE b.email_address = :email_address_1",
+ )
+
+ def test_aliased_mismatched_col(self, _name_mismatch_fixture):
+ A, B = _name_mismatch_fixture
+ sess = fixture_session()
+
+ # so what should this do ? it's just a weird hybrid case
+ self.assert_compile(
+ sess.query(aliased(A).some_email),
+ "SELECT b.email_address AS b_email_address FROM b",
+ )
+
def test_property(self):
A = self._fixture()
diff --git a/test/orm/test_utils.py b/test/orm/test_utils.py
index 8b298c6fb..c0829e9b3 100644
--- a/test/orm/test_utils.py
+++ b/test/orm/test_utils.py
@@ -246,6 +246,7 @@ class AliasedClassTest(fixtures.TestBase, AssertsCompiledSQL):
"parententity": point_mapper,
"parentmapper": point_mapper,
"proxy_key": "x_alone",
+ "proxy_owner": Point,
},
)
eq_(
@@ -255,6 +256,7 @@ class AliasedClassTest(fixtures.TestBase, AssertsCompiledSQL):
"parententity": point_mapper,
"parentmapper": point_mapper,
"proxy_key": "x",
+ "proxy_owner": Point,
},
)