Fixed critical caching issue where combination of :func:`_orm.aliased()`
:func:`_sql.case` and :func:`_hybrid.hybrid_property` expressions would
cause a cache key mismatch, leading to cache keys that held onto the actual
:func:`_orm.aliased` object while also not matching each other, filling up
the cache.
Fixes: #9728
Change-Id: I700645b5629a81a0104cf923db72a7421fa43ff4
(cherry picked from commit
4d69d83530666f9aaf3fb327d8c63110ef5e7ff5)
--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 9728
+ :versions: 2.0.12
+
+ Fixed critical caching issue where combination of :func:`_orm.aliased()`
+ :func:`_sql.case` and :func:`_hybrid.hybrid_property` expressions would
+ cause a cache key mismatch, leading to cache keys that held onto the actual
+ :func:`_orm.aliased` object while also not matching each other, filling up
+ the cache.
from ..sql import roles
from ..sql import traversals
from ..sql import visitors
+from ..sql.traversals import HasCacheKey
+from ..sql.visitors import InternalTraversal
class NoKey(str):
subclass representing a column expression.
"""
+ entity_namespace = self._entity_namespace
+ assert isinstance(entity_namespace, HasCacheKey)
+
if self.key is NO_KEY:
- annotations = {"entity_namespace": self._entity_namespace}
+ annotations = {"entity_namespace": entity_namespace}
else:
annotations = {
"proxy_key": self.key,
"proxy_owner": self._parententity,
- "entity_namespace": self._entity_namespace,
+ "entity_namespace": entity_namespace,
}
ce = self.comparator.__clause_element__()
return self.impl.get(state, dict_)
-HasEntityNamespace = util.namedtuple(
- "HasEntityNamespace", ["entity_namespace"]
-)
-HasEntityNamespace.is_mapper = HasEntityNamespace.is_aliased_class = False
+class HasEntityNamespace(HasCacheKey):
+ __slots__ = ("_entity_namespace",)
+
+ is_mapper = False
+ is_aliased_class = False
+
+ _traverse_internals = [
+ ("_entity_namespace", InternalTraversal.dp_has_cache_key),
+ ]
+
+ def __init__(self, ent):
+ self._entity_namespace = ent
+
+ @property
+ def entity_namespace(self):
+ return self._entity_namespace.entity_namespace
def create_proxied_attribute(descriptor):
else:
# used by hybrid attributes which try to remain
# agnostic of any ORM concepts like mappers
- return HasEntityNamespace(self.class_)
+ return HasEntityNamespace(self._parententity)
@property
def property(self):
import sqlalchemy as sa
from sqlalchemy import Column
+from sqlalchemy import column
from sqlalchemy import func
from sqlalchemy import inspect
from sqlalchemy import Integer
from sqlalchemy import update
from sqlalchemy import util
from sqlalchemy.ext.declarative import ConcreteBase
+from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import aliased
from sqlalchemy.orm import Bundle
from sqlalchemy.orm import defaultload
compare_values=True,
)
+ @testing.variation(
+ "exprtype", ["plain_column", "self_standing_case", "case_w_columns"]
+ )
+ def test_hybrid_w_case_ac(self, decl_base, exprtype):
+ """test #9728"""
+
+ class Employees(decl_base):
+ __tablename__ = "employees"
+ id = Column(String(128), primary_key=True)
+ first_name = Column(String(length=64))
+
+ @hybrid_property
+ def name(self):
+ return self.first_name
+
+ @name.expression
+ def name(
+ cls,
+ ):
+ if exprtype.plain_column:
+ return cls.first_name
+ elif exprtype.self_standing_case:
+ return case(
+ (column("x") == 1, column("q")),
+ else_=column("q"),
+ )
+ elif exprtype.case_w_columns:
+ return case(
+ (column("x") == 1, column("q")),
+ else_=cls.first_name,
+ )
+ else:
+ exprtype.fail()
+
+ def go1():
+ employees_2 = aliased(Employees, name="employees_2")
+ stmt = select(employees_2.name)
+ return stmt
+
+ def go2():
+ employees_2 = aliased(Employees, name="employees_2")
+ stmt = select(employees_2)
+ return stmt
+
+ self._run_cache_key_fixture(
+ lambda: stmt_20(go1(), go2()),
+ compare_values=True,
+ )
+
class RoundTripTest(QueryTest, AssertsCompiledSQL):
__dialect__ = "default"