为什么 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 创建带括号的值,因此启发式算法还不错。