File 0002-maintain-compiled_params-replacement_expressions.patch of Package python-SQLAlchemy.16963
From e4ec7d2b22546587d7a692041c6c70d23f6de3ac Mon Sep 17 00:00:00 2001
From: Mike Bayer <mike_mp@zzzcomputing.com>
Date: Fri, 21 Dec 2018 17:35:12 -0500
Subject: [PATCH] Maintain compiled_params / replacement_expressions within
expanding IN
URL: https://github.com/sqlalchemy/sqlalchemy/issues/4394
Fixed issue in "expanding IN" feature where using the same bound parameter
name more than once in a query would lead to a KeyError within the process
of rewriting the parameters in the query.
Fixes: #4394
Change-Id: Ibcadce9fefbcb060266d9447c2044ee6efeccf5a
(cherry picked from commit c495769751e8b19d54fb92388ced587b5d13b85d)
---
doc/build/changelog/unreleased_12/4394.rst | 7 ++
lib/sqlalchemy/engine/default.py | 75 +++++++++++++---------
test/sql/test_query.py | 37 ++++++++++-
3 files changed, 87 insertions(+), 32 deletions(-)
create mode 100644 doc/build/changelog/unreleased_12/4394.rst
diff --git a/doc/build/changelog/unreleased_12/4394.rst b/doc/build/changelog/unreleased_12/4394.rst
new file mode 100644
index 0000000000..faa3547ad0
--- /dev/null
+++ b/doc/build/changelog/unreleased_12/4394.rst
@@ -0,0 +1,7 @@
+.. change::
+ :tag: bug, sql
+ :tickets: 4394
+
+ Fixed issue in "expanding IN" feature where using the same bound parameter
+ name more than once in a query would lead to a KeyError within the process
+ of rewriting the parameters in the query.
diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py
index 915812a4f2..e63c5eafca 100644
--- a/lib/sqlalchemy/engine/default.py
+++ b/lib/sqlalchemy/engine/default.py
@@ -726,45 +726,57 @@ class DefaultExecutionContext(interfaces
positiontup = None
replacement_expressions = {}
+ to_update_sets = {}
+
for name in (
self.compiled.positiontup if compiled.positional
else self.compiled.binds
):
parameter = self.compiled.binds[name]
if parameter.expanding:
- values = compiled_params.pop(name)
- if not values:
- raise exc.InvalidRequestError(
- "'expanding' parameters can't be used with an "
- "empty list"
- )
- elif isinstance(values[0], (tuple, list)):
- to_update = [
- ("%s_%s_%s" % (name, i, j), value)
- for i, tuple_element in enumerate(values, 1)
- for j, value in enumerate(tuple_element, 1)
- ]
- replacement_expressions[name] = ", ".join(
- "(%s)" % ", ".join(
- self.compiled.bindtemplate % {
- "name":
- to_update[i * len(tuple_element) + j][0]
- }
- for j, value in enumerate(tuple_element)
+ if name in replacement_expressions:
+ to_update = to_update_sets[name]
+ else:
+ # we are removing the parameter from compiled_params
+ # because it is a list value, which is not expected by
+ # TypeEngine objects that would otherwise be asked to
+ # process it. the single name is being replaced with
+ # individual numbered parameters for each value in the
+ # param.
+ values = compiled_params.pop(name)
+
+ if not values:
+ raise exc.InvalidRequestError(
+ "'expanding' parameters with an empty list not "
+ "supported until SQLAlchemy 1.3."
)
- for i, tuple_element in enumerate(values)
+ elif isinstance(values[0], (tuple, list)):
+ to_update = to_update_sets[name] = [
+ ("%s_%s_%s" % (name, i, j), value)
+ for i, tuple_element in enumerate(values, 1)
+ for j, value in enumerate(tuple_element, 1)
+ ]
+ replacement_expressions[name] = ", ".join(
+ "(%s)" % ", ".join(
+ self.compiled.bindtemplate % {
+ "name":
+ to_update[i * len(tuple_element) + j][0]
+ }
+ for j, value in enumerate(tuple_element)
+ )
+ for i, tuple_element in enumerate(values)
- )
- else:
- to_update = [
- ("%s_%s" % (name, i), value)
- for i, value in enumerate(values, 1)
- ]
- replacement_expressions[name] = ", ".join(
- self.compiled.bindtemplate % {
- "name": key}
- for key, value in to_update
- )
+ )
+ else:
+ to_update = to_update_sets[name] = [
+ ("%s_%s" % (name, i), value)
+ for i, value in enumerate(values, 1)
+ ]
+ replacement_expressions[name] = ", ".join(
+ self.compiled.bindtemplate % {
+ "name": key}
+ for key, value in to_update
+ )
compiled_params.update(to_update)
processors.update(
(key, processors[name])
@@ -778,7 +790,7 @@ class DefaultExecutionContext(interfaces
positiontup.append(name)
def process_expanding(m):
- return replacement_expressions.pop(m.group(1))
+ return replacement_expressions[m.group(1)]
self.statement = re.sub(
r"\[EXPANDING_(\S+)\]",
diff --git a/test/sql/test_query.py b/test/sql/test_query.py
index 3e629fb261..d649da202a 100644
--- a/test/sql/test_query.py
+++ b/test/sql/test_query.py
@@ -457,7 +457,7 @@ def test_expanding_in(self):
assert_raises_message(
exc.StatementError,
- "'expanding' parameters can't be used with an empty list",
+ "'expanding' parameters with an empty list not supported",
conn.execute,
stmt, {"uname": []}
)
@@ -531,6 +531,41 @@ def test_expanding_in_multiple(self):
[(8, 'fred'), (9, 'ed')]
)
+ def test_expanding_in_repeated(self):
+ testing.db.execute(
+ users.insert(),
+ [
+ dict(user_id=7, user_name='jack'),
+ dict(user_id=8, user_name='fred'),
+ dict(user_id=9, user_name='ed')
+ ]
+ )
+
+ with testing.db.connect() as conn:
+ stmt = select([users]).where(
+ users.c.user_name.in_(
+ bindparam('uname', expanding=True)
+ ) | users.c.user_name.in_(bindparam('uname2', expanding=True))
+ ).where(users.c.user_id == 8)
+ stmt = stmt.union(
+ select([users]).where(
+ users.c.user_name.in_(
+ bindparam('uname', expanding=True)
+ ) | users.c.user_name.in_(
+ bindparam('uname2', expanding=True))
+ ).where(users.c.user_id == 9)
+ ).order_by(stmt.c.user_id)
+
+ eq_(
+ conn.execute(
+ stmt,
+ {
+ "uname": ['jack', 'fred'],
+ "uname2": ['ed'], "userid": [8, 9]}
+ ).fetchall(),
+ [(8, 'fred'), (9, 'ed')]
+ )
+
@testing.requires.tuple_in
def test_expanding_in_composite(self):
testing.db.execute(