From 267e9cbf6e3c165a4e953b49d979d7f4ddc533f9 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 21 Dec 2021 18:08:33 -0500 Subject: accommodate for "clone" of ColumnClause for use with the ClauseElement.params() method, altered ColumnClause._clone() so that while the element stays immutable, if the column is associated with a subquery, it returns a new version of itself as corresponding to a clone of the subquery. this allows processing functions to access the parameters in the subquery and produce a copy of it. The use case here is the expanded use of .params() within loader strategies that use HasCacheKey._apply_params_to_element(). Fixed issue in new "loader criteria" method :meth:`_orm.PropComparator.and_` where usage with a loader strategy like :func:`_orm.selectinload` against a column that was a member of the ``.c.`` collection of a subquery object, where the subquery would be dynamically added to the FROM clause of the statement, would be subject to stale parameter values within the subquery in the SQL statement cache, as the process used by the loader strategy to replace the parameters at execution time would fail to accommodate the subquery when received in this form. Fixes: #7489 Change-Id: Ibb3b6af140b8a62a2c8d05b2ac92e86ca3013c46 --- lib/sqlalchemy/sql/elements.py | 18 +++++++++++++++++- lib/sqlalchemy/sql/traversals.py | 1 - lib/sqlalchemy/sql/visitors.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 00270c9b5..75798502a 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -439,6 +439,7 @@ class ClauseElement( return self._replace_params(False, optionaldict, kwargs) def _replace_params(self, unique, optionaldict, kwargs): + if len(optionaldict) == 1: kwargs.update(optionaldict[0]) elif len(optionaldict) > 1: @@ -454,7 +455,9 @@ class ClauseElement( bind._convert_to_unique() return cloned_traverse( - self, {"maintain_key": True}, {"bindparam": visit_bindparam} + self, + {"maintain_key": True, "detect_subquery_cols": True}, + {"bindparam": visit_bindparam}, ) def compare(self, other, **kw): @@ -4869,6 +4872,19 @@ class ColumnClause( else: return super(ColumnClause, self).entity_namespace + def _clone(self, detect_subquery_cols=False, **kw): + if ( + detect_subquery_cols + and self.table is not None + and self.table._is_subquery + ): + clone = kw.pop("clone") + table = clone(self.table, **kw) + new = table.c.corresponding_column(self) + return new + + return super(ColumnClause, self)._clone(**kw) + @HasMemoized.memoized_attribute def _from_objects(self): t = self.table diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index d58b5c2bb..12a5486d2 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -271,7 +271,6 @@ class HasCacheKey: result += meth( attrname, obj, self, anon_map, bindparams ) - return result def _generate_cache_key(self): diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py index b08080753..30103bc8e 100644 --- a/lib/sqlalchemy/sql/visitors.py +++ b/lib/sqlalchemy/sql/visitors.py @@ -774,7 +774,7 @@ def cloned_traverse(obj, opts, visitors): cloned[id(elem)] = newelem return newelem - cloned[id(elem)] = newelem = elem._clone(**kw) + cloned[id(elem)] = newelem = elem._clone(clone=clone, **kw) newelem._copy_internals(clone=clone, **kw) meth = visitors.get(newelem.__visit_name__, None) if meth: -- cgit v1.2.1