为什么 Connection.exec_driver_sql 对 IN 运算符的查询参数解析不一致?
Why does Connection.exec_driver_sql have inconsistent parameter parsing for queries with the IN operator?
这两个查询在语义上是相同的,但其中一个成功,另一个失败。唯一的区别在于 WHERE
子句,其中 OR
运算符的两个操作数已交换。
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://user:pwd@host:port/db',
# Query succeeds (and does what's expected)
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id = %s OR id IN %s',
(3, (1, 2))
)
# Query fails
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id IN %s OR id = %s',
((1, 2), 3)
)
失败查询的输出:
TypeError: 'int' object is not iterable
sqlalchemy 参数解析似乎取决于第一个参数的类型。如果第一个是元组,则查询失败,如果是int/float/str,则成功。
到目前为止我找到的解决方法是使用命名参数:
# Query succeeds
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id IN %(arg1)s OR id = %(arg2)s',
{'arg1': (1, 2), 'arg2': 3}
)
但是它更冗长,我不想在任何地方都使用它。另请注意,PyMySQL 游标的 execute
方法接受了这两个查询。
这种行为有原因吗?
我觉得问题出在DefaultExecutionContext._init_statement classmethod.
...
if not parameters:
...
elif isinstance(parameters[0], dialect.execute_sequence_format):
self.parameters = parameters
elif isinstance(parameters[0], dict):
...
else:
self.parameters = [
dialect.execute_sequence_format(p) for p in parameters
]
self.executemany = len(parameters) > 1
isinstance(parameters[0], dialect.execute_sequence_format)
正在检查 parameters
的第一个元素是否是 tuple
。 似乎 是有效检测 executemany
场景的启发式方法:可能它应该检查所有元素是否都是元组且长度相等*。因为它是值 ((1, 2), 3)
将导致相当于
cursor.executemany(sql, [(1, 2), 3])
和语法无效的语句,如
SELECT * FROM tbl WHERE id IN 1 OR id = 2
-- ^^^^
将参数包装在 list
中可以解决问题,因为 len(parameters)
将不再大于 1。
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id IN %s OR id = %s',
[((1, 2), 3)]
)
显然这是启发式方法之上的解决方法,因此它可能无法在所有可能的情况下工作。可能值得在 GitHub 上打开一个 discussion 来探讨这是否是一个应该修复的错误。
* 并非所有驱动程序都支持通过元组为 ...IN %s
或 ...VALUES %s
创建带括号的值,因此启发式算法还不错。
这两个查询在语义上是相同的,但其中一个成功,另一个失败。唯一的区别在于 WHERE
子句,其中 OR
运算符的两个操作数已交换。
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://user:pwd@host:port/db',
# Query succeeds (and does what's expected)
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id = %s OR id IN %s',
(3, (1, 2))
)
# Query fails
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id IN %s OR id = %s',
((1, 2), 3)
)
失败查询的输出:
TypeError: 'int' object is not iterable
sqlalchemy 参数解析似乎取决于第一个参数的类型。如果第一个是元组,则查询失败,如果是int/float/str,则成功。
到目前为止我找到的解决方法是使用命名参数:
# Query succeeds
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id IN %(arg1)s OR id = %(arg2)s',
{'arg1': (1, 2), 'arg2': 3}
)
但是它更冗长,我不想在任何地方都使用它。另请注意,PyMySQL 游标的 execute
方法接受了这两个查询。
这种行为有原因吗?
我觉得问题出在DefaultExecutionContext._init_statement classmethod.
...
if not parameters:
...
elif isinstance(parameters[0], dialect.execute_sequence_format):
self.parameters = parameters
elif isinstance(parameters[0], dict):
...
else:
self.parameters = [
dialect.execute_sequence_format(p) for p in parameters
]
self.executemany = len(parameters) > 1
isinstance(parameters[0], dialect.execute_sequence_format)
正在检查 parameters
的第一个元素是否是 tuple
。 似乎 是有效检测 executemany
场景的启发式方法:可能它应该检查所有元素是否都是元组且长度相等*。因为它是值 ((1, 2), 3)
将导致相当于
cursor.executemany(sql, [(1, 2), 3])
和语法无效的语句,如
SELECT * FROM tbl WHERE id IN 1 OR id = 2
-- ^^^^
将参数包装在 list
中可以解决问题,因为 len(parameters)
将不再大于 1。
with engine.connect() as cn:
cn.exec_driver_sql(
f'UPDATE table SET column = "value" WHERE id IN %s OR id = %s',
[((1, 2), 3)]
)
显然这是启发式方法之上的解决方法,因此它可能无法在所有可能的情况下工作。可能值得在 GitHub 上打开一个 discussion 来探讨这是否是一个应该修复的错误。
* 并非所有驱动程序都支持通过元组为 ...IN %s
或 ...VALUES %s
创建带括号的值,因此启发式算法还不错。