]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
accommodate for "clone" of ColumnClause
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 21 Dec 2021 23:08:33 +0000 (18:08 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 22 Dec 2021 01:11:35 +0000 (20:11 -0500)
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
(cherry picked from commit 267e9cbf6e3c165a4e953b49d979d7f4ddc533f9)

doc/build/changelog/unreleased_14/7489.rst [new file with mode: 0644]
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/traversals.py
lib/sqlalchemy/sql/visitors.py
test/orm/test_relationship_criteria.py
test/sql/test_external_traversal.py

diff --git a/doc/build/changelog/unreleased_14/7489.rst b/doc/build/changelog/unreleased_14/7489.rst
new file mode 100644 (file)
index 0000000..4af33f4
--- /dev/null
@@ -0,0 +1,13 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 7489
+
+    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.
+
index 08eb37f2ce7b865b0bce7b00a838dd632d9c19fc..48b64545319f9f2c3dd774087775c8059e28a3d1 100644 (file)
@@ -358,6 +358,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:
@@ -373,7 +374,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):
@@ -4880,6 +4883,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
index 27e65652654c9d8a7f276883dab6bfb6286d50b9..9da61ab28cb69d75a15a710e054b4eaee4784ff9 100644 (file)
@@ -271,7 +271,6 @@ class HasCacheKey(object):
                         result += meth(
                             attrname, obj, self, anon_map, bindparams
                         )
-
         return result
 
     def _generate_cache_key(self):
index 7111c5efd70be0babd915a5f0945147ec902825d..3636be4be61511093ba4a5c635e55ce9ec4a591b 100644 (file)
@@ -772,7 +772,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:
index 7e2c6e04f9fe07fa2552ec882e19004711880994..00e84dc8d878be7d6ba2f809cb17635a6c00ddf8 100644 (file)
@@ -1240,6 +1240,66 @@ class RelationshipCriteriaTest(_Fixtures, testing.AssertsCompiledSQL):
                 ),
             )
 
+    def test_selectinload_local_criteria_subquery(self, user_address_fixture):
+        """test #7489"""
+        User, Address = user_address_fixture
+
+        s = Session(testing.db, future=True)
+
+        def go(value):
+            a1 = aliased(Address)
+            subq = select(a1.id).where(a1.email_address != value).subquery()
+            stmt = (
+                select(User)
+                .options(
+                    selectinload(User.addresses.and_(Address.id == subq.c.id)),
+                )
+                .order_by(User.id)
+            )
+            result = s.execute(stmt)
+            return result
+
+        for value in (
+            "ed@wood.com",
+            "ed@lala.com",
+            "ed@wood.com",
+            "ed@lala.com",
+        ):
+            s.close()
+            with self.sql_execution_asserter() as asserter:
+                result = go(value)
+
+                eq_(
+                    result.scalars().unique().all(),
+                    self._user_minus_edwood(*user_address_fixture)
+                    if value == "ed@wood.com"
+                    else self._user_minus_edlala(*user_address_fixture),
+                )
+
+            asserter.assert_(
+                CompiledSQL(
+                    "SELECT users.id, users.name FROM users ORDER BY users.id"
+                ),
+                CompiledSQL(
+                    "SELECT addresses.user_id AS addresses_user_id, "
+                    "addresses.id AS addresses_id, "
+                    "addresses.email_address AS addresses_email_address "
+                    # note the comma-separated FROM clause
+                    "FROM addresses, (SELECT addresses_1.id AS id FROM "
+                    "addresses AS addresses_1 "
+                    "WHERE addresses_1.email_address != :email_address_1) "
+                    "AS anon_1 WHERE addresses.user_id "
+                    "IN (__[POSTCOMPILE_primary_keys]) "
+                    "AND addresses.id = anon_1.id ORDER BY addresses.id",
+                    [
+                        {
+                            "primary_keys": [7, 8, 9, 10],
+                            "email_address_1": value,
+                        }
+                    ],
+                ),
+            )
+
     @testing.combinations((True,), (False,), argnames="use_compiled_cache")
     def test_selectinload_nested_criteria(
         self, user_order_item_fixture, use_compiled_cache
index e01ec0738e9cb76adee8e2873bc53b956296ca9e..c14b8b4c68bd413e76da5d19bab3ae090e5aa524 100644 (file)
@@ -827,6 +827,48 @@ class ClauseTest(fixtures.TestBase, AssertsCompiledSQL):
             sel._generate_cache_key()[1],
         )
 
+    def test_params_on_expr_against_subquery(self):
+        """test #7489"""
+
+        meta = MetaData()
+
+        b = Table("b", meta, Column("id", Integer), Column("data", String))
+
+        subq = select(b.c.id).where(b.c.data == "some data").subquery()
+        criteria = b.c.id == subq.c.id
+
+        stmt = select(b).where(criteria)
+        param_key = stmt._generate_cache_key()[1][0].key
+
+        self.assert_compile(
+            stmt,
+            "SELECT b.id, b.data FROM b, (SELECT b.id AS id "
+            "FROM b WHERE b.data = :data_1) AS anon_1 WHERE b.id = anon_1.id",
+            checkparams={"data_1": "some data"},
+        )
+        eq_(
+            [
+                eq_clause_element(bindparam(param_key, value="some data")),
+            ],
+            stmt._generate_cache_key()[1],
+        )
+
+        stmt = select(b).where(criteria.params({param_key: "some other data"}))
+        self.assert_compile(
+            stmt,
+            "SELECT b.id, b.data FROM b, (SELECT b.id AS id "
+            "FROM b WHERE b.data = :data_1) AS anon_1 WHERE b.id = anon_1.id",
+            checkparams={"data_1": "some other data"},
+        )
+        eq_(
+            [
+                eq_clause_element(
+                    bindparam(param_key, value="some other data")
+                ),
+            ],
+            stmt._generate_cache_key()[1],
+        )
+
     def test_params_subqueries_in_joins_one(self):
         """test #7055"""