mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:21:57 +00:00
SQL 差异说明重做(第 3 轮):堵上第 2 轮复验报的不保持语义的规范化,认出复验指出的等价改写,具体说法只说真会影响结果的不同
为什么:第 2 轮复验报了 14 个 blocker(判了等价、结果却不同)和 11 个 major(意思一样,却说了具体的不同;说法 自相矛盾)。等价判定的框架(两边各自规范化、语法树一字不差才算等价)不变,这一轮逐个查清是哪一步不保持语义、 哪一处说法没先确认「真的不同」,按类堵上,并在执行比对语料里收进全部反例。 等价判定(backend/app/data/sql_semantics.py): - 外连接改内连接(_rejects)分「一定是空值」和「一定不成立」两种:NOT 只能套前者,NOT IN 子查询、NOT IN ()、 x op ALL (…)、NOT (x = ANY …)(含解析成函数的 ALL (ARRAY[…]))、PostgreSQL 的 || 都留得下没关联上的行。 - 改列别名、EXISTS 写成 SELECT 1、去掉列名、CTE 的列改名之前,先看本段的 WHERE、ON、相关子查询还按名字引用不引用 这些别名(_alias_refs,SQLite 的 WHERE、ON、相关子查询都认列别名,真实的 SQLite 上试过);qualify 遇到子查询引用 外层列别名时不走认列那一级。 - USING 的列和列别名同名不改写(ORDER BY 先认别名);派生来源的列同名不走认列那一级(SQLite 认第一个);没有 GROUP BY、别处也没有聚合的 HAVING TRUE 留着(PostgreSQL 里只出一行)。 - RANDOM() 这类不确定的函数轻量一级也不走;SQLite 双引号里的名字只看这条语句里看得见的表(_column_in_scope); 子查询的别名遮住外层、又认不准是哪一个的不走规范化(SQLite 里层找不到列就往外层找)。 - MySQL:/*! … */ 会执行、! 比比较结合得紧,不在语法树上判等价;-- 后面不跟空白是两个减号,先拆开再解析; BETWEEN 上下界类型不同或不是常量时不改写。 - 函数节点记下原文写的名字(_mark_surfaces):SQLite 的 MOD() 和 %、PostgreSQL 的 LOG10(x) 和 LOG(10, x) 解析成 同一个节点,记下名字就不会判等价;同义写法按方言文档列在 _SAFE_RESPELLINGS。 - 自己压测找到的老问题:结果看行出来先后的一段(GROUP_CONCAT 等)按 WHERE 把 LEFT JOIN 改成内连接,SQLite 的 扫描先后真的会变(EXPLAIN QUERY PLAN 一个先扫 refunds、一个先扫 orders),这样的一段不再改写。 认出复验指出的等价改写(每一步都写了为什么保持语义,执行比对守门): - EXISTS 里的去重、分组、UNION / UNION ALL,IN 里不影响取值集合的 GROUP BY、DISTINCT、UNION;分组后 COUNT(*) >= 1。 - 不分组、只取可空一侧跳过空值的聚合时 LEFT JOIN 写成内连接(COALESCE(SUM(r.amount), 0) 等);ON 之外没用到、 结果不怕重复的行的 LEFT JOIN 去掉;同一组 AND 里已经保证、或表结构里确定不为空(nullable、主键)的 IS NOT NULL 去掉;给了表结构、类型一样时按关联条件相等的两列换成同一列(分组、排序、选出来的列)。 - 一模一样的分组键、排序键;SQLite 的负数 LIMIT、PostgreSQL 的 LIMIT NULL、LIMIT 的整数算式。 具体说法:被关联条件定成相等的列、IN / EXISTS 里的去重分组、只取可空一侧的聚合、没用到又不怕重复的 LEFT JOIN、 各支自己已经去重的 UNION ALL、只出一行时的排序、自己关联自己的关联条件,都不再说成具体的不同(只给兜底); <>、NOT IN、NOT LIKE 照实说「和 X 为空的行」也筛掉了,NOT 套几个条件的说「只保留 NOT (…) 的行」。 normalize_sql:解析时会丢掉写法的(会执行的注释、!、MOD() 这类重新生成换了名字的函数)改按原文逐词规范化, 会执行的注释里的代码算结构;只改字面量的照旧同一个模板(补了测试)。 测试:fixtures/sql_pairs.py 收进第 3 轮的反例(执行的 _DIFF_R3 / _SAME_R3 / _SAME_TYPED_R3 / _SAME_SCHEMA_R3 / _SAME_HARD_R3,只查结论的 REGRESSIONS_R3 / DIALECT_SAME_R3),schema_cache 带上 nullable 和 primary_key(和探查 出来的一样);专挑写法差异的那组数据加了 'State'。
This commit is contained in:
+1226
-191
File diff suppressed because it is too large
Load Diff
Vendored
+435
-11
@@ -13,8 +13,10 @@ OR 里的标量子查询改成 CROSS JOIN,都是复验时真踩到的),所
|
||||
左右、子查询和 CTE 互换……),编号以 hard: 开头的是要按语义推理才看得出等价的(条件挪进相关子查询、冗余条件),规范化
|
||||
不保证认得出、认不出时只许给兜底说明;diff 是不许判等价的(换时间列、少一个条件、去重与否、外连接与内连接,以及只在
|
||||
某个方言、某种排序规则下才等价的)。第 2 轮复验的反例在 _SAME_HARD、_SAME_R2、_DIFF_R2(执行)和 REGRESSIONS_R2、
|
||||
DIALECT_SAME_R2、KNOWN_UNRECOGNIZED(各方言,只查结论)里。LIMIT 都带能定出唯一顺序的排序,没有 ORDER BY 的 LIMIT 不进语料。过去复验报过的反例都
|
||||
收进来了:第 5 轮回退的「只在 IN 子查询里用到的 CTE」在 PAIRS 里;复验者留在 /private/tmp 的脚本里、在这几组数据上
|
||||
DIALECT_SAME_R2、KNOWN_UNRECOGNIZED(各方言,只查结论)里;第 3 轮的在 _DIFF_R3、_SAME_R3、_SAME_TYPED_R3(要给表结构才认得出,Pair.typed)、_SAME_SCHEMA_R3(要靠
|
||||
表结构里的主键才看得出等价,Pair.needs_schema)、_SAME_HARD_R3(执行)和 REGRESSIONS_R3、DIALECT_SAME_R3(只查
|
||||
结论)里。LIMIT 都带能定出唯一顺序的排序,没有 ORDER BY 的 LIMIT 不进语料(EXISTS 里的 LIMIT 1 OFFSET 1 只看有没有
|
||||
第二行,不看先后)。过去复验报过的反例都收进来了:第 5 轮回退的「只在 IN 子查询里用到的 CTE」在 PAIRS 里;复验者留在 /private/tmp 的脚本里、在这几组数据上
|
||||
结果不同的对是 VALIDATOR_DIFF(也进 PAIRS 执行);旧测试(五轮复核修正按复验报告补的用例,各方言,用到的列这个库里
|
||||
不全有)里报过不同的对是 REGRESSIONS、意思一样的对是 DIALECT_SAME,这两份不执行,只查判没判等价。
|
||||
"""
|
||||
@@ -45,14 +47,18 @@ DDL = {
|
||||
|
||||
|
||||
def schema_cache() -> dict:
|
||||
"""数据源 schema_cache 的形状({"tables": {表: {"columns": [{"name", "type"}]}}}),explain_sql_diff 的 schema。"""
|
||||
"""数据源 schema_cache 的形状({"tables": {表: {"columns": [{"name", "type", "nullable"}], "primary_key": […]}}},
|
||||
和 data/introspect.py 探查出来的一样),explain_sql_diff 的 schema。SQLAlchemy 读 SQLite 的 INTEGER PRIMARY KEY
|
||||
时 nullable 照样是 True(PRAGMA table_info 的 notnull 是 0),主键另记在 primary_key 里。"""
|
||||
tables = {}
|
||||
for name, ddl in DDL.items():
|
||||
cols = []
|
||||
cols, pk = [], []
|
||||
for part in ddl.split(", "):
|
||||
col, typ = part.split(" ")[:2]
|
||||
cols.append({"name": col, "type": typ})
|
||||
tables[name] = {"columns": cols}
|
||||
cols.append({"name": col, "type": typ, "nullable": True})
|
||||
if "PRIMARY KEY" in part:
|
||||
pk.append(col)
|
||||
tables[name] = {"columns": cols, "primary_key": pk}
|
||||
return {"tables": tables}
|
||||
|
||||
|
||||
@@ -117,14 +123,15 @@ def _edge_rows() -> dict[str, list[tuple]]:
|
||||
|
||||
|
||||
def _tricky_rows() -> dict[str, list[tuple]]:
|
||||
"""一组专挑写法差异的数据(第 2 轮复验的反例在这上面现形):金额 0.1、0.2、0.3(浮点数 0.1 + 0.2 ≠ 0.3)、
|
||||
100 * 0.07;状态里有像数字的文本('3.0'、'12')和大小写不同的 'PAID';用户名是像数字的文本('0100' 取两次负是
|
||||
100);城市 'SH'、'sh';id 16 的订单(0x10);nums 表有几行。"""
|
||||
"""一组专挑写法差异的数据(第 2、3 轮复验的反例在这上面现形):金额 0.1、0.2、0.3(浮点数 0.1 + 0.2 ≠ 0.3)、
|
||||
100 * 0.07;状态里有像数字的文本('3.0'、'12')、大小写不同的 'PAID',还有 'State'(SQLite 把找不到这一列的
|
||||
"State" 读成字符串,和 "state" 不同);用户名是像数字的文本('0100' 取两次负是 100);城市 'SH'、'sh';id 16 的
|
||||
订单(0x10);nums 表有几行。"""
|
||||
users = [(1, "0100", "SH", 1, 3, None, "2026-09-01", 0), (2, "5", "sh", 0, 1, 1, "2026-09-02", 0),
|
||||
(3, "alice", "Sh", None, None, 2, None, 1), (4, "Bob", None, 1, 5, None, "2026-08-31 23:59:59", None)]
|
||||
stores = [(1, "s1", "SH", "east", 1), (2, "s2", "sh", None, None), (3, "3.0", "12", "west", 2)]
|
||||
amounts = [0.1, 0.2, 0.3, 0.3, 0.30000000000000004, 7.0, 100 * 0.07, 12.5, None, 100.0, 0, 0.5]
|
||||
statuses = ["paid", "PAID", "3.0", "12", None, "open", "void", "paid"]
|
||||
statuses = ["paid", "PAID", "3.0", "12", None, "open", "void", "paid", "State"]
|
||||
orders = []
|
||||
for i in range(1, 21):
|
||||
orders.append((i, (i % 4) + 1 if i % 7 else None, (i % 3) + 1, i - 1 if i % 5 == 0 else None, (i % 2) + 1,
|
||||
@@ -176,6 +183,11 @@ class Pair:
|
||||
#: same:已知等价的改写;diff:不许判等价——语义不同,或者只在某个方言、某种排序规则、某种列类型下才等价
|
||||
#: (比如 SQLite 里 status > 'b' AND status > 'o' 和 status > 'o' 一样,PostgreSQL 的语言排序规则下不一定)
|
||||
expect: str
|
||||
#: 要靠表结构里的约束(主键不为空)才看得出等价:没给表结构时可以说出具体的不同(那时确实不知道它不会为空)
|
||||
needs_schema: bool = False
|
||||
#: 要给表结构才认得出等价(列的类型、外层表有没有这一列):没给表结构时只许给兜底说明,认出的比例不算进
|
||||
#: 「不给表结构」那一项
|
||||
typed: bool = False
|
||||
|
||||
|
||||
PR = ("WITH paid AS (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id), "
|
||||
@@ -2077,6 +2089,272 @@ _DIFF_R2 = [
|
||||
"SELECT SUM(amount) FROM payments WHERE paid_at >= '2026-09-01'"),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- 第 3 轮复验的反例
|
||||
# 第 2 轮补充之后的复验报的 blocker / major(复验者的表 visits / patients / doctors 改写成这里的 orders / users /
|
||||
# stores:visits.pid → orders.user_id,visits.did → orders.store_id,visits.fee → orders.amount,refunds.vid →
|
||||
# refunds.order_id),以及本轮自己压测时找到的同类写法。
|
||||
|
||||
#: 不许判等价的(在这几组数据上大多结果不同)
|
||||
_DIFF_R3 = [
|
||||
# 外连接改内连接要 WHERE 真把没关联上的行筛掉:NULL NOT IN (空集) 是真,SQLite 里 x NOT IN () 也是真
|
||||
("r3-left-not-in-subquery", "SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.city NOT IN "
|
||||
"(SELECT s.city FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.city NOT IN "
|
||||
"(SELECT s.city FROM stores s WHERE s.region = 'nowhere')"),
|
||||
("r3-left-not-in-empty-list", "SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.city NOT IN ()",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.city NOT IN ()"),
|
||||
("r3-left-not-exists", "SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE NOT EXISTS "
|
||||
"(SELECT 1 FROM stores s WHERE s.owner_id = u.id)",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE NOT EXISTS "
|
||||
"(SELECT 1 FROM stores s WHERE s.owner_id = u.id)"),
|
||||
("r3-left-is-not-1", "SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.vip IS NOT 1",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.vip IS NOT 1"),
|
||||
("r3-left-not-in-subquery-or", "SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.grade > 2 "
|
||||
"OR u.city NOT IN (SELECT s.city FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.grade > 2 "
|
||||
"OR u.city NOT IN (SELECT s.city FROM stores s WHERE s.region = 'nowhere')"),
|
||||
# 列名表改名时 WHERE 里还按原来的列别名引用(SQLite 的 WHERE 认列别名)
|
||||
("r3-cte-column-list-where-alias", "WITH t(a, b) AS (SELECT amount AS b, id AS a FROM orders WHERE b > 50) SELECT a, b FROM t",
|
||||
"WITH t(a, b) AS (SELECT amount AS x, id AS b FROM orders WHERE b > 50) SELECT a, b FROM t"),
|
||||
("r3-cte-column-list-union-where-alias", "WITH t(a, b) AS (SELECT amount AS b, id AS a FROM orders WHERE b > 50 "
|
||||
"UNION ALL SELECT amount, id FROM refunds) SELECT a, b FROM t",
|
||||
"WITH t(a, b) AS (SELECT amount AS x, id AS b FROM orders WHERE b > 50 UNION ALL SELECT amount, id FROM refunds) "
|
||||
"SELECT a, b FROM t"),
|
||||
# EXISTS 写成 SELECT 1 时 WHERE 里还按选出来的列别名引用
|
||||
("r3-exists-alias-where", "SELECT o.id FROM orders o WHERE EXISTS (SELECT r.amount * 2 AS a2 FROM refunds r "
|
||||
"WHERE r.order_id = o.id AND a2 > 100)",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT r.amount AS a2 FROM refunds r WHERE r.order_id = o.id AND a2 > 100)"),
|
||||
("r3-exists-alias-distinct", "SELECT o.id FROM orders o WHERE EXISTS (SELECT DISTINCT r.amount AS a2 FROM refunds r "
|
||||
"WHERE r.order_id = o.id AND a2 > 100)",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT r.state AS a2 FROM refunds r WHERE r.order_id = o.id AND a2 > 100)"),
|
||||
# ORDER BY 里单独一个名字先认列别名:USING 改写 ON 时不能把它写成左表的列
|
||||
("r3-using-order-by-alias", "SELECT -o.id AS id FROM orders o JOIN users USING (id) ORDER BY id LIMIT 2",
|
||||
"SELECT -o.id AS id FROM orders o JOIN users u ON o.id = u.id ORDER BY o.id LIMIT 2"),
|
||||
("r3-using-order-by-alias-left", "SELECT -o.id AS id, u.name FROM orders o LEFT JOIN users u USING (id) "
|
||||
"ORDER BY id LIMIT 3",
|
||||
"SELECT -o.id AS id, u.name FROM orders o LEFT JOIN users u ON o.id = u.id ORDER BY o.id LIMIT 3"),
|
||||
# 子查询选出来的两列同名:SQLite 认第一个
|
||||
("r3-duplicate-output-name", "SELECT * FROM (SELECT user_id AS k, SUM(amount) AS k FROM orders GROUP BY user_id) g "
|
||||
"WHERE g.k > 3",
|
||||
"SELECT * FROM (SELECT user_id AS k, SUM(amount) AS k FROM orders GROUP BY user_id HAVING SUM(amount) > 3) g"),
|
||||
("r3-having-ungrouped-count", "SELECT COUNT(*) FROM orders WHERE status = 'nope' HAVING COUNT(*) > 0",
|
||||
"SELECT COUNT(*) FROM orders WHERE status = 'nope'"),
|
||||
# SQLite 的 % 先把两边转成整数,MOD() 按浮点数算:12.5 % 2 是 0,MOD(12.5, 2) 是 0.5
|
||||
("r3-mod-vs-percent", "SELECT COUNT(*) FROM orders WHERE amount % 2 = 0",
|
||||
"SELECT COUNT(*) FROM orders WHERE MOD(amount, 2) = 0"),
|
||||
# 子查询的别名遮住了外层的别名:refunds 没有 status 列,SQLite 往外层找,o.status 是父单的、c.status 是子单的
|
||||
("r3-shadowed-alias", "SELECT COUNT(*) FROM orders o JOIN orders c ON c.parent_id = o.id WHERE EXISTS "
|
||||
"(SELECT 1 FROM refunds AS o WHERE o.amount > 0 AND o.status = 'paid')",
|
||||
"SELECT COUNT(*) FROM orders o JOIN orders c ON c.parent_id = o.id WHERE EXISTS "
|
||||
"(SELECT 1 FROM refunds AS c WHERE c.amount > 0 AND c.status = 'paid')"),
|
||||
# orders 没有 state 列:SQLite 把双引号里的读成字符串,'State' 和 'state' 不同(refunds 有 state 列不算数)
|
||||
("r3-double-quoted-not-in-scope", 'SELECT COUNT(*) FROM orders WHERE status = "State"',
|
||||
'SELECT COUNT(*) FROM orders WHERE status = "state"'),
|
||||
("r3-double-quoted-not-in-outer-scope", 'SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT 1 FROM orders o '
|
||||
'WHERE o.user_id = u.id AND o.status = "State")',
|
||||
'SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = "state")'),
|
||||
# 自己关联自己,关联的方向不同(复验报的是说法自相矛盾,等价的结论本来就是对的)
|
||||
("r3-self-join-roles", "SELECT a.id, b.id FROM orders a LEFT JOIN orders b ON b.parent_id = a.id",
|
||||
"SELECT a.id, b.id FROM orders a LEFT JOIN orders b ON a.parent_id = b.id"),
|
||||
# 真正的差异只有时间列:分组键、LEFT JOIN 那两处写法不同却不影响结果,不能多说
|
||||
("r3-time-plus-group-key-equal", "SELECT u.id, SUM(o.amount) FROM orders o JOIN users u ON u.id = o.user_id "
|
||||
"WHERE o.paid_at >= '2026-09-01' GROUP BY u.id",
|
||||
"SELECT o.user_id, SUM(o.amount) FROM orders o JOIN users u ON u.id = o.user_id "
|
||||
"WHERE o.created_at >= '2026-09-01' GROUP BY o.user_id"),
|
||||
("r3-time-plus-left-coalesce", "SELECT COALESCE(SUM(r.amount), 0) FROM orders o LEFT JOIN refunds r "
|
||||
"ON r.order_id = o.id WHERE o.paid_at >= '2026-09-01'",
|
||||
"SELECT COALESCE(SUM(r.amount), 0) FROM orders o JOIN refunds r ON r.order_id = o.id "
|
||||
"WHERE o.created_at >= '2026-09-01'"),
|
||||
# 下面几对看起来像本轮新认的等价改写,其实不是(守住新改写的边界)
|
||||
("r3-left-count-star-not-null-blind", "SELECT COUNT(*) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT COUNT(*) FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-left-coalesce-inside-sum", "SELECT SUM(COALESCE(r.amount, 0)) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT SUM(COALESCE(r.amount, 0)) FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-left-grouped-sum", "SELECT o.user_id, SUM(r.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"GROUP BY o.user_id",
|
||||
"SELECT o.user_id, SUM(r.amount) FROM orders o JOIN refunds r ON r.order_id = o.id GROUP BY o.user_id"),
|
||||
("r3-left-sum-other-side", "SELECT SUM(r.amount), SUM(o.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT SUM(r.amount), SUM(o.amount) FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-left-extra-count-star", "SELECT COUNT(*) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT COUNT(*) FROM orders o"),
|
||||
("r3-left-extra-sum", "SELECT SUM(o.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT SUM(o.amount) FROM orders o"),
|
||||
("r3-left-extra-used-in-where", "SELECT DISTINCT o.user_id FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"WHERE r.id IS NULL",
|
||||
"SELECT DISTINCT o.user_id FROM orders o"),
|
||||
("r3-inner-extra-distinct", "SELECT DISTINCT o.user_id FROM orders o JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT DISTINCT o.user_id FROM orders o"),
|
||||
("r3-in-group-by-aggregate", "SELECT COUNT(*) FROM users WHERE id IN (SELECT MAX(user_id) FROM orders GROUP BY store_id)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT MAX(user_id) FROM orders)"),
|
||||
("r3-in-group-by-having", "SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders GROUP BY user_id "
|
||||
"HAVING COUNT(*) > 1)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders)"),
|
||||
("r3-in-union-limit", "SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders UNION "
|
||||
"SELECT user_id FROM refunds ORDER BY 1 LIMIT 3)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders UNION ALL SELECT user_id FROM refunds "
|
||||
"ORDER BY 1 LIMIT 3)"),
|
||||
("r3-exists-group-by-offset", "SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT o.status FROM orders o "
|
||||
"WHERE o.user_id = u.id GROUP BY o.status LIMIT 1 OFFSET 1)",
|
||||
"SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT o.status FROM orders o WHERE o.user_id = u.id LIMIT 1 OFFSET 1)"),
|
||||
("r3-exists-aggregate-ungrouped", "SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT COUNT(*) FROM orders o "
|
||||
"WHERE o.user_id = u.id)",
|
||||
"SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT o.id FROM orders o WHERE o.user_id = u.id)"),
|
||||
("r3-group-key-equal-left-join", "SELECT u.id, COUNT(*) FROM users u LEFT JOIN orders o ON o.user_id = u.id "
|
||||
"GROUP BY u.id",
|
||||
"SELECT o.user_id, COUNT(*) FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY o.user_id"),
|
||||
("r3-not-null-on-nullable-side", "SELECT o.id, r.id FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"WHERE r.id IS NOT NULL",
|
||||
"SELECT o.id, r.id FROM orders o LEFT JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-limit-negative-vs-zero", "SELECT id FROM orders ORDER BY id LIMIT -1", "SELECT id FROM orders ORDER BY id LIMIT 0"),
|
||||
("r3-order-dup-key-direction", "SELECT id, user_id FROM orders ORDER BY user_id, id DESC LIMIT 5",
|
||||
"SELECT id, user_id FROM orders ORDER BY user_id, id LIMIT 5"),
|
||||
]
|
||||
|
||||
#: 意思一样、本轮规范化认得出的改写(一部分要给表结构:要知道两列声明的类型一样,相等才是一样)
|
||||
_SAME_R3 = [
|
||||
# EXISTS、IN 只看有没有、在不在:子查询里去重、分组、UNION 还是 UNION ALL 都不影响
|
||||
("r3-exists-distinct", "SELECT o.id FROM orders o WHERE EXISTS (SELECT DISTINCT r.amount FROM refunds r "
|
||||
"WHERE r.order_id = o.id)",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT r.amount FROM refunds r WHERE r.order_id = o.id)"),
|
||||
("r3-not-exists-distinct", "SELECT o.id FROM orders o WHERE NOT EXISTS (SELECT DISTINCT r.amount FROM refunds r "
|
||||
"WHERE r.order_id = o.id)",
|
||||
"SELECT o.id FROM orders o WHERE NOT EXISTS (SELECT r.amount FROM refunds r WHERE r.order_id = o.id)"),
|
||||
("r3-exists-group-by", "SELECT o.id FROM orders o WHERE EXISTS (SELECT r.state FROM refunds r "
|
||||
"WHERE r.order_id = o.id GROUP BY r.state)",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT r.state FROM refunds r WHERE r.order_id = o.id)"),
|
||||
("r3-exists-union-vs-union-all", "SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT o.id FROM orders o "
|
||||
"WHERE o.user_id = u.id UNION SELECT r.id FROM refunds r WHERE r.user_id = u.id)",
|
||||
"SELECT COUNT(*) FROM users u WHERE EXISTS (SELECT o.id FROM orders o WHERE o.user_id = u.id "
|
||||
"UNION ALL SELECT r.id FROM refunds r WHERE r.user_id = u.id)"),
|
||||
("r3-in-union-vs-union-all", "SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders UNION "
|
||||
"SELECT user_id FROM refunds)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders UNION ALL SELECT user_id FROM refunds)"),
|
||||
("r3-in-group-by", "SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders GROUP BY user_id)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders)"),
|
||||
("r3-in-group-by-two-keys", "SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders "
|
||||
"GROUP BY user_id, status)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders)"),
|
||||
("r3-not-in-group-by", "SELECT COUNT(*) FROM users WHERE id NOT IN (SELECT user_id FROM orders "
|
||||
"WHERE user_id IS NOT NULL GROUP BY user_id)",
|
||||
"SELECT COUNT(*) FROM users WHERE id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL)"),
|
||||
("r3-in-group-by-having-count-noop", "SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders "
|
||||
"GROUP BY user_id HAVING COUNT(*) >= 1)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders)"),
|
||||
# 结果只是可空一侧的聚合(跳过空值、参数在那一侧整行为空时一定是空值):LEFT JOIN 和 INNER JOIN 一样
|
||||
("r3-left-coalesce-sum", "SELECT COALESCE(SUM(r.amount), 0) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"WHERE o.status = 'paid'",
|
||||
"SELECT COALESCE(SUM(r.amount), 0) FROM orders o JOIN refunds r ON r.order_id = o.id WHERE o.status = 'paid'"),
|
||||
("r3-left-sum-div-count", "SELECT SUM(r.amount) / COUNT(r.id) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT SUM(r.amount) / COUNT(r.id) FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-left-round-sum", "SELECT ROUND(SUM(r.amount)) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT ROUND(SUM(r.amount)) FROM orders o INNER JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-left-constant-column", "SELECT 'all', SUM(r.amount), MAX(r.refunded_at) FROM orders o "
|
||||
"LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT 'all', SUM(r.amount), MAX(r.refunded_at) FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
# 没用到的 LEFT JOIN:左表的行只会变多、不会变少,结果再去重(或者只取不怕重复的聚合)就一样
|
||||
("r3-left-extra-distinct", "SELECT DISTINCT o.user_id FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT DISTINCT o.user_id FROM orders o"),
|
||||
("r3-left-extra-max", "SELECT MAX(o.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT MAX(o.amount) FROM orders o"),
|
||||
("r3-left-extra-count-distinct", "SELECT COUNT(DISTINCT o.user_id) FROM orders o LEFT JOIN refunds r "
|
||||
"ON r.order_id = o.id WHERE o.is_deleted = 0",
|
||||
"SELECT COUNT(DISTINCT o.user_id) FROM orders o WHERE o.is_deleted = 0"),
|
||||
("r3-left-extra-group-by-max", "SELECT o.user_id, MAX(o.amount) FROM orders o LEFT JOIN refunds r "
|
||||
"ON r.order_id = o.id GROUP BY o.user_id",
|
||||
"SELECT o.user_id, MAX(o.amount) FROM orders o GROUP BY o.user_id"),
|
||||
("r3-left-extra-in-subquery", "SELECT COUNT(*) FROM users WHERE id IN (SELECT o.user_id FROM orders o "
|
||||
"LEFT JOIN refunds r ON r.order_id = o.id)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT o.user_id FROM orders o)"),
|
||||
# ON 里另一个条件已经保证不为空:多写的 IS NOT NULL 不影响关联上哪些行
|
||||
("r3-left-on-redundant-not-null", "SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"AND r.order_id IS NOT NULL",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-left-on-redundant-not-null-2key", "SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r "
|
||||
"ON r.order_id = o.id AND r.user_id = o.user_id AND r.user_id IS NOT NULL",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id AND r.user_id = o.user_id"),
|
||||
("r3-left-on-redundant-preserved-not-null", "SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r "
|
||||
"ON r.order_id = o.id AND o.id IS NOT NULL",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-where-redundant-not-null-in", "SELECT COUNT(*) FROM orders WHERE user_id IN (SELECT id FROM users) "
|
||||
"AND user_id IS NOT NULL",
|
||||
"SELECT COUNT(*) FROM orders WHERE user_id IN (SELECT id FROM users)"),
|
||||
# 一模一样的分组键、排序键写了两遍;SQLite 的负数 LIMIT 就是不限行数;LIMIT 写成常量算式
|
||||
("r3-group-by-duplicate-key", "SELECT user_id, COUNT(*) FROM orders GROUP BY user_id, user_id",
|
||||
"SELECT user_id, COUNT(*) FROM orders GROUP BY user_id"),
|
||||
("r3-order-by-duplicate-key", "SELECT user_id FROM orders GROUP BY user_id ORDER BY user_id, user_id",
|
||||
"SELECT user_id FROM orders GROUP BY user_id ORDER BY user_id"),
|
||||
("r3-order-by-repeated-key-other-direction", "SELECT id FROM orders ORDER BY id, id DESC LIMIT 5",
|
||||
"SELECT id FROM orders ORDER BY id LIMIT 5"),
|
||||
("r3-limit-minus-one", "SELECT id FROM orders ORDER BY id LIMIT -1", "SELECT id FROM orders ORDER BY id"),
|
||||
("r3-limit-negative", "SELECT id FROM orders ORDER BY id LIMIT -5", "SELECT id FROM orders ORDER BY id"),
|
||||
("r3-limit-negative-offset", "SELECT id FROM orders ORDER BY id LIMIT -1 OFFSET 2",
|
||||
"SELECT id FROM orders ORDER BY id LIMIT -7 OFFSET 2"),
|
||||
("r3-limit-arithmetic", "SELECT id FROM orders ORDER BY id LIMIT 1 + 2", "SELECT id FROM orders ORDER BY id LIMIT 3"),
|
||||
("r3-having-true-aggregate", "SELECT COUNT(*) FROM orders HAVING 1 = 1", "SELECT COUNT(*) FROM orders"),
|
||||
]
|
||||
|
||||
#: 意思一样、要给表结构才认得出的(双引号里的名字是不是外层表的列、关联条件定成相等的两列声明的类型一样不一样,都要
|
||||
#: 看表结构):没给表结构时只许给兜底说明;认出来的比例单算(见 test_most_known_equivalent_rewrites_are_recognized)
|
||||
_SAME_TYPED_R3 = [
|
||||
# 外层的 refunds 有 state 列:双引号里的 "State"、"state" 都是这一列(SQLite 不分大小写),不是字符串
|
||||
("r3-double-quoted-outer-column", 'SELECT COUNT(*) FROM refunds r WHERE EXISTS (SELECT 1 FROM orders o '
|
||||
'WHERE o.id = r.order_id AND o.status = "State")',
|
||||
'SELECT COUNT(*) FROM refunds r WHERE EXISTS (SELECT 1 FROM orders o WHERE o.id = r.order_id AND o.status = "state")'),
|
||||
# 关联条件定成相等的两列:按哪一列分组、排序、选出来都一样(要给表结构:两列声明的类型一样)
|
||||
("r3-group-key-equal-by-join", "SELECT u.id, COUNT(*) AS n FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.id",
|
||||
"SELECT o.user_id, COUNT(*) AS n FROM orders o JOIN users u ON u.id = o.user_id GROUP BY o.user_id"),
|
||||
("r3-group-key-equal-by-join-sum", "SELECT o.user_id, SUM(o.amount) FROM orders o JOIN users u ON u.id = o.user_id "
|
||||
"GROUP BY o.user_id",
|
||||
"SELECT u.id, SUM(o.amount) FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.id"),
|
||||
("r3-group-key-equal-by-using", "SELECT COUNT(*) FROM orders o JOIN refunds r USING (user_id) GROUP BY o.user_id",
|
||||
"SELECT COUNT(*) FROM orders o JOIN refunds r USING (user_id) GROUP BY r.user_id"),
|
||||
("r3-group-key-equal-by-where", "SELECT COUNT(*) FROM orders WHERE user_id = referrer_id GROUP BY user_id",
|
||||
"SELECT COUNT(*) FROM orders WHERE user_id = referrer_id GROUP BY referrer_id"),
|
||||
("r3-order-key-equal-by-join", "SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id ORDER BY o.user_id, o.id",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id ORDER BY u.id, o.id"),
|
||||
# 带 LIMIT 的一段结果看行出来的先后,规范化不换列(换了可能换一种扫描的先后),给了表结构也认不出,只给兜底
|
||||
("r3-order-key-equal-by-join-limit", "SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id "
|
||||
"ORDER BY o.user_id, o.id LIMIT 5",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id ORDER BY u.id, o.id LIMIT 5"),
|
||||
]
|
||||
|
||||
#: 意思一样、要靠约束(主键不为空)才看得出:给了表结构(schema_cache 里有 primary_key)认得出;没给表结构时说得出
|
||||
#: 「A 排除了 id 为空的行」也不算错(不知道它不会为空)
|
||||
_SAME_SCHEMA_R3 = [
|
||||
("r3-left-not-null-primary-key-vs-inner", "SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r "
|
||||
"ON r.order_id = o.id WHERE r.id IS NOT NULL",
|
||||
"SELECT o.id, r.amount FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
("r3-where-not-null-primary-key", "SELECT COUNT(*) FROM orders WHERE id IS NOT NULL AND status = 'paid'",
|
||||
"SELECT COUNT(*) FROM orders WHERE status = 'paid'"),
|
||||
]
|
||||
|
||||
#: 意思一样、要按语义推理才看得出的:认不出时只许给兜底说明(复验专挑这类对查「说出不存在的差异」)
|
||||
_SAME_HARD_R3 = [
|
||||
("r3-disjoint-union-all-vs-union", "SELECT DISTINCT user_id FROM orders WHERE user_id > 5 UNION ALL "
|
||||
"SELECT DISTINCT user_id FROM orders WHERE user_id <= 5",
|
||||
"SELECT user_id FROM orders WHERE user_id > 5 UNION SELECT user_id FROM orders WHERE user_id <= 5"),
|
||||
("r3-order-single-group", "SELECT user_id, COUNT(*) FROM orders WHERE user_id = 3 GROUP BY user_id ORDER BY user_id",
|
||||
"SELECT user_id, COUNT(*) FROM orders WHERE user_id = 3 GROUP BY user_id"),
|
||||
("r3-distinct-join-vs-in", "SELECT DISTINCT o.id FROM orders o JOIN users u ON u.id = o.user_id",
|
||||
"SELECT DISTINCT o.id FROM orders o WHERE o.user_id IN (SELECT id FROM users)"),
|
||||
("r3-count-distinct-join-vs-in", "SELECT COUNT(DISTINCT o.id) FROM orders o JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT COUNT(DISTINCT o.id) FROM orders o WHERE o.id IN (SELECT order_id FROM refunds)"),
|
||||
("r3-join-vs-exists-distinct", "SELECT DISTINCT o.id FROM orders o JOIN refunds r ON r.order_id = o.id "
|
||||
"AND r.state = 'done'",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT 1 FROM refunds r WHERE r.order_id = o.id AND r.state = 'done')"),
|
||||
# 子查询的别名遮住了外层的别名,B 把它写明白了:两条意思一样,A 的 o.status 指外层的 o
|
||||
("r3-shadowed-alias-written-out", "SELECT COUNT(*) FROM orders o JOIN orders c ON c.parent_id = o.id WHERE EXISTS "
|
||||
"(SELECT 1 FROM refunds AS o WHERE o.amount > 0 AND o.status = 'paid')",
|
||||
"SELECT COUNT(*) FROM orders o JOIN orders c ON c.parent_id = o.id WHERE o.status = 'paid' AND EXISTS "
|
||||
"(SELECT 1 FROM refunds AS r WHERE r.amount > 0)"),
|
||||
("r3-group-key-where-vs-having", "SELECT status, COUNT(*) FROM orders WHERE status = 'paid' GROUP BY status",
|
||||
"SELECT status, COUNT(*) FROM orders GROUP BY status HAVING status = 'paid'"),
|
||||
("r3-max-outer-vs-inner", "SELECT COUNT(*) FROM (SELECT user_id, MAX(amount) AS m FROM orders GROUP BY user_id) t "
|
||||
"WHERE m > 50",
|
||||
"SELECT COUNT(*) FROM (SELECT user_id, MAX(amount) AS m FROM orders WHERE amount > 50 GROUP BY user_id) t"),
|
||||
]
|
||||
|
||||
#: 只在 PostgreSQL / MySQL 才有的写法(不执行,只查不判等价):第 2 轮复验在真实的库上跑出结果不同的
|
||||
REGRESSIONS_R2: list[tuple[str, str, str, str]] = [
|
||||
("having_binary_is_not_where", "mysql",
|
||||
@@ -2195,13 +2473,159 @@ KNOWN_UNRECOGNIZED: list[tuple[str, str, str, str]] = [
|
||||
"SELECT DISTINCT city FROM users", "SELECT city FROM users GROUP BY city"),
|
||||
]
|
||||
|
||||
#: 第 3 轮:只在某个方言才有、或者执行起来结果每次都不同的写法(不执行,只查不判等价)。PostgreSQL / MySQL 的几对
|
||||
#: 复验者和本轮都在真实的 PostgreSQL 18、MySQL 8.0 上跑过(或者按官方文档)
|
||||
REGRESSIONS_R3: list[tuple[str, str, str, str]] = [
|
||||
# 外连接改内连接:NOT IN (空集)、x op ALL (空集)、NOT (x = ANY (空集)) 在 x 是空值时也是真
|
||||
("left_not_in_subquery", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.city NOT IN "
|
||||
"(SELECT s.city FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.city NOT IN "
|
||||
"(SELECT s.city FROM stores s WHERE s.region = 'nowhere')"),
|
||||
("left_not_in_subquery", "mysql",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.city NOT IN "
|
||||
"(SELECT s.city FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.city NOT IN "
|
||||
"(SELECT s.city FROM stores s WHERE s.region = 'nowhere')"),
|
||||
("left_gt_all", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.vip > ALL "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.vip > ALL "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere')"),
|
||||
("left_gt_all", "mysql",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.vip > ALL "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.vip > ALL "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere')"),
|
||||
("left_neq_all", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.vip <> ALL "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere')",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.vip <> ALL "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere')"),
|
||||
("left_not_eq_any", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE NOT (u.vip = ANY "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere'))",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE NOT (u.vip = ANY "
|
||||
"(SELECT s.id FROM stores s WHERE s.region = 'nowhere'))"),
|
||||
("left_neq_all_empty_array", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.vip <> ALL (ARRAY[]::int[])",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.vip <> ALL (ARRAY[]::int[])"),
|
||||
# PostgreSQL 里空的数组接上一个数组还是那个数组(不是空值)
|
||||
("left_array_concat", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE u.tags || ARRAY['x'] = ARRAY['x']",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE u.tags || ARRAY['x'] = ARRAY['x']"),
|
||||
("left_concat_function", "postgres",
|
||||
"SELECT o.id FROM orders o LEFT JOIN users u ON u.id = o.user_id WHERE CONCAT(u.city, 'x') = 'x'",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id WHERE CONCAT(u.city, 'x') = 'x'"),
|
||||
# ORDER BY 里单独一个名字先认列别名
|
||||
("using_order_by_alias", "postgres",
|
||||
"SELECT -o.id AS id FROM orders o JOIN users USING (id) ORDER BY id LIMIT 2",
|
||||
"SELECT -o.id AS id FROM orders o JOIN users u ON o.id = u.id ORDER BY o.id LIMIT 2"),
|
||||
# PostgreSQL 里没有 GROUP BY 的 HAVING 让整条语句成了只出一行的聚合
|
||||
("having_true_without_group", "postgres", "SELECT 1 FROM orders HAVING 1 = 1", "SELECT 1 FROM orders"),
|
||||
("having_true_without_group_where", "postgres",
|
||||
"SELECT 1 AS k FROM orders WHERE status = 'paid' HAVING TRUE", "SELECT 1 AS k FROM orders WHERE status = 'paid'"),
|
||||
("having_true_without_group", "mysql", "SELECT 1 FROM orders HAVING 1 = 1", "SELECT 1 FROM orders"),
|
||||
# MySQL:/*! … */ 里的代码会执行;! 比比较运算结合得紧;--1 是减负一(-- 后面要有空白才是注释);
|
||||
# BETWEEN 的上下界类型不同时三个参数一起换算
|
||||
("executable_comment", "mysql", "SELECT COUNT(*) FROM orders /*! WHERE status = 'paid' */",
|
||||
"SELECT COUNT(*) FROM orders"),
|
||||
("executable_comment_versioned", "mysql", "SELECT COUNT(*) FROM orders /*!50001 WHERE status = 'paid' */",
|
||||
"SELECT COUNT(*) FROM orders"),
|
||||
("bang_precedence", "mysql", "SELECT COUNT(*) FROM orders WHERE ! status = 'paid'",
|
||||
"SELECT COUNT(*) FROM orders WHERE NOT status = 'paid'"),
|
||||
("double_dash_without_space", "mysql", "SELECT id, amount --1 FROM orders", "SELECT id, amount FROM orders"),
|
||||
("between_mixed_bound_types", "mysql", "SELECT id FROM users WHERE name BETWEEN 2 AND '3'",
|
||||
"SELECT id FROM users WHERE name >= 2 AND name <= '3'"),
|
||||
("between_column_bounds", "mysql", "SELECT id FROM orders WHERE amount BETWEEN discount AND tax",
|
||||
"SELECT id FROM orders WHERE amount >= discount AND amount <= tax"),
|
||||
# 不确定的函数:每次求值都可能不同,两次 RANDOM() 不是同一个值(执行起来每次结果都不同,只查结论)
|
||||
("random_twice", "sqlite", "SELECT COUNT(*) FROM orders WHERE RANDOM() % 2 = 0 AND RANDOM() % 2 = 0",
|
||||
"SELECT COUNT(*) FROM orders WHERE RANDOM() % 2 = 0"),
|
||||
("random_in_list", "sqlite", "SELECT COUNT(*) FROM orders WHERE 0 IN (abs(RANDOM()) % 3, abs(RANDOM()) % 3)",
|
||||
"SELECT COUNT(*) FROM orders WHERE 0 IN (abs(RANDOM()) % 3)"),
|
||||
("random_or", "sqlite", "SELECT COUNT(*) FROM orders WHERE RANDOM() > 0 OR RANDOM() > 0",
|
||||
"SELECT COUNT(*) FROM orders WHERE RANDOM() > 0"),
|
||||
("random_order_by_twice", "sqlite", "SELECT id FROM orders ORDER BY RANDOM(), RANDOM() LIMIT 3",
|
||||
"SELECT id FROM orders ORDER BY RANDOM() LIMIT 3"),
|
||||
("rand_twice", "mysql", "SELECT COUNT(*) FROM orders WHERE RAND() < 0.5 AND RAND() < 0.5",
|
||||
"SELECT COUNT(*) FROM orders WHERE RAND() < 0.5"),
|
||||
# 解析时换成同一个节点、方言里却不是一回事的写法
|
||||
("log10_vs_log_base", "postgres", "SELECT LOG10(amount) FROM orders", "SELECT LOG(10, amount) FROM orders"),
|
||||
("char_length_is_not_sqlite", "sqlite", "SELECT CHAR_LENGTH(name) FROM users", "SELECT LENGTH(name) FROM users"),
|
||||
# PostgreSQL 的 LIMIT NULL 是不限行数,SQLite 里 LIMIT NULL 报错;MySQL 里 LIMIT 不能写算式
|
||||
("limit_null_is_an_error", "sqlite", "SELECT id FROM orders ORDER BY id LIMIT NULL", "SELECT id FROM orders ORDER BY id"),
|
||||
("limit_arithmetic_is_an_error", "mysql", "SELECT id FROM orders ORDER BY id LIMIT 1 + 2",
|
||||
"SELECT id FROM orders ORDER BY id LIMIT 3"),
|
||||
("limit_negative_is_an_error", "postgres", "SELECT id FROM orders ORDER BY id LIMIT -1", "SELECT id FROM orders ORDER BY id"),
|
||||
]
|
||||
|
||||
#: 第 3 轮:意思一样(PostgreSQL / MySQL 的写法),认得出最好;认不出时只许给兜底说明,不说具体的不同
|
||||
DIALECT_SAME_R3: list[tuple[str, str, str, str]] = [
|
||||
("group_key_equal_by_join", "postgres",
|
||||
"SELECT s.id, COUNT(*) AS n FROM stores s JOIN orders o ON o.store_id = s.id GROUP BY s.id",
|
||||
"SELECT o.store_id, COUNT(*) AS n FROM orders o JOIN stores s ON s.id = o.store_id GROUP BY o.store_id"),
|
||||
("group_key_equal_by_join", "mysql",
|
||||
"SELECT u.id, COUNT(*) FROM orders o JOIN users u ON u.id = o.user_id GROUP BY u.id",
|
||||
"SELECT u.id, COUNT(*) FROM orders o JOIN users u ON u.id = o.user_id GROUP BY o.user_id"),
|
||||
("order_key_equal_by_join_limit", "postgres",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id ORDER BY o.user_id, o.id LIMIT 5",
|
||||
"SELECT o.id FROM orders o JOIN users u ON u.id = o.user_id ORDER BY u.id, o.id LIMIT 5"),
|
||||
("left_round_sum", "postgres",
|
||||
"SELECT ROUND(SUM(r.amount)) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT ROUND(SUM(r.amount)) FROM orders o INNER JOIN refunds r ON r.order_id = o.id"),
|
||||
("left_constant_column", "mysql",
|
||||
"SELECT 'all', SUM(r.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT 'all', SUM(r.amount) FROM orders o JOIN refunds r ON r.order_id = o.id"),
|
||||
("left_extra_max", "postgres", "SELECT MAX(o.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id",
|
||||
"SELECT MAX(o.amount) FROM orders o"),
|
||||
("left_extra_count_distinct", "mysql",
|
||||
"SELECT COUNT(DISTINCT o.user_id) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id WHERE o.is_deleted = 0",
|
||||
"SELECT COUNT(DISTINCT o.user_id) FROM orders o WHERE o.is_deleted = 0"),
|
||||
("left_on_redundant_not_null", "postgres",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id AND r.user_id = o.user_id "
|
||||
"AND r.user_id IS NOT NULL",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id AND r.user_id = o.user_id"),
|
||||
("left_on_redundant_preserved_not_null", "mysql",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id AND o.id IS NOT NULL",
|
||||
"SELECT o.id, r.amount FROM orders o LEFT JOIN refunds r ON r.order_id = o.id"),
|
||||
("in_union_vs_union_all", "postgres",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders UNION SELECT user_id FROM refunds)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM orders UNION ALL SELECT user_id FROM refunds)"),
|
||||
("in_group_by", "postgres",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM refunds WHERE state = 'done' GROUP BY user_id)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM refunds WHERE state = 'done')"),
|
||||
("in_group_by", "mysql",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM refunds GROUP BY user_id)",
|
||||
"SELECT COUNT(*) FROM users WHERE id IN (SELECT user_id FROM refunds)"),
|
||||
("exists_distinct", "postgres",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT DISTINCT r.amount FROM refunds r WHERE r.order_id = o.id)",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT r.amount FROM refunds r WHERE r.order_id = o.id)"),
|
||||
("limit_null_is_no_limit", "postgres", "SELECT id FROM orders ORDER BY id LIMIT NULL", "SELECT id FROM orders ORDER BY id"),
|
||||
("limit_all_is_no_limit", "postgres", "SELECT id FROM orders ORDER BY id LIMIT ALL", "SELECT id FROM orders ORDER BY id"),
|
||||
("limit_arithmetic", "postgres", "SELECT id FROM orders ORDER BY id LIMIT 1 + 2", "SELECT id FROM orders ORDER BY id LIMIT 3"),
|
||||
("mod_function_is_percent", "postgres", "SELECT COUNT(*) FROM orders WHERE MOD(store_id, 2) = 0",
|
||||
"SELECT COUNT(*) FROM orders WHERE store_id % 2 = 0"),
|
||||
("mod_function_is_percent", "mysql", "SELECT COUNT(*) FROM orders WHERE MOD(store_id, 2) = 0",
|
||||
"SELECT COUNT(*) FROM orders WHERE store_id % 2 = 0"),
|
||||
("ifnull_is_coalesce", "mysql", "SELECT SUM(IFNULL(discount, 0)) FROM orders",
|
||||
"SELECT SUM(COALESCE(discount, 0)) FROM orders"),
|
||||
("self_join_roles_swapped_names", "mysql", "SELECT a.id, b.amount FROM orders a JOIN orders b ON a.parent_id = b.id",
|
||||
"SELECT b.id, a.amount FROM orders b JOIN orders a ON b.parent_id = a.id"),
|
||||
]
|
||||
|
||||
|
||||
PAIRS: list[Pair] = [Pair(f"same:{i}", a, b, "same") for i, a, b in _SAME] + \
|
||||
[Pair(f"diff:{i}", a, b, "diff") for i, a, b in _DIFF] + \
|
||||
[Pair(f"validator:{n}:{k}", a, b, "diff") for k, (n, a, b) in enumerate(VALIDATOR_DIFF)] + \
|
||||
[Pair(f"same:{i}", a, b, "same") for i, a, b in _SAME_R2] + \
|
||||
[Pair(f"hard:{i}", a, b, "same") for i, a, b in _SAME_HARD] + \
|
||||
[Pair(f"diff:{i}", a, b, "diff") for i, a, b in _DIFF_R2]
|
||||
[Pair(f"diff:{i}", a, b, "diff") for i, a, b in _DIFF_R2] + \
|
||||
[Pair(f"diff:{i}", a, b, "diff") for i, a, b in _DIFF_R3] + \
|
||||
[Pair(f"same:{i}", a, b, "same") for i, a, b in _SAME_R3] + \
|
||||
[Pair(f"same:{i}", a, b, "same", typed=True) for i, a, b in _SAME_TYPED_R3] + \
|
||||
[Pair(f"same:{i}", a, b, "same", needs_schema=True) for i, a, b in _SAME_SCHEMA_R3] + \
|
||||
[Pair(f"hard:{i}", a, b, "same") for i, a, b in _SAME_HARD_R3]
|
||||
|
||||
#: 按编号找语料里的一对(测试里引用第 2 轮的反例用)
|
||||
PAIRS_BY_ID: dict[str, Pair] = {p.id: p for p in PAIRS}
|
||||
|
||||
@@ -61,9 +61,11 @@ def test_equivalence_is_guarded_by_execution(databases, pair):
|
||||
assert not diff.equivalent, f"{pair.id}:第 {differs} 组数据上结果不同,却判了等价(schema={bool(schema)})"
|
||||
if pair.expect == "diff":
|
||||
assert not diff.equivalent, f"{pair.id} 不该判等价(schema={bool(schema)})"
|
||||
if pair.expect == "same" and not diff.equivalent:
|
||||
if pair.expect == "same" and not diff.equivalent and not (pair.needs_schema and schema is None):
|
||||
specific = [d.text for d in diff.items if d.aspect != "other"]
|
||||
assert not specific, f"{pair.id} 意思一样,却说了具体的不同(schema={bool(schema)}):{specific}"
|
||||
if pair.needs_schema and schema is not None:
|
||||
assert diff.equivalent, f"{pair.id} 给了表结构(主键不为空)应认得出等价:{[d.text for d in diff.items]}"
|
||||
# 换个先后问,结论一样
|
||||
assert explain_sql_diff(pair.b, pair.a, "sqlite", schema=schema).equivalent == diff.equivalent, pair.id
|
||||
|
||||
@@ -73,9 +75,12 @@ def test_most_known_equivalent_rewrites_are_recognized():
|
||||
认不出的给的是兜底说明,不是错误的结论。要按语义推理才看得出的(hard:)另记,不算进比例。"""
|
||||
same = [p for p in sql_pairs.PAIRS if p.expect == "same" and not p.id.startswith("hard:")]
|
||||
hits = [p.id for p in same if explain_sql_diff(p.a, p.b, "sqlite", schema=SCHEMA).equivalent]
|
||||
bare = [p.id for p in same if explain_sql_diff(p.a, p.b, "sqlite").equivalent]
|
||||
ratio, bare_ratio = len(hits) / len(same), len(bare) / len(same)
|
||||
print(f"\n已知等价的改写 {len(same)} 对里认出 {len(hits)} 对({ratio:.0%}),不给表结构时 {len(bare)} 对({bare_ratio:.0%})")
|
||||
# 不给表结构那一项只算不靠表结构就该认得出的(列的类型、主键不为空这些得看表结构)
|
||||
plain = [p for p in same if not p.typed and not p.needs_schema]
|
||||
bare = [p.id for p in plain if explain_sql_diff(p.a, p.b, "sqlite").equivalent]
|
||||
ratio, bare_ratio = len(hits) / len(same), len(bare) / len(plain)
|
||||
print(f"\n已知等价的改写 {len(same)} 对里认出 {len(hits)} 对({ratio:.0%});不靠表结构的 {len(plain)} 对,"
|
||||
f"不给表结构时认出 {len(bare)} 对({bare_ratio:.0%})")
|
||||
print("没认出来的:", sorted(set(p.id for p in same) - set(hits)))
|
||||
hard = [p for p in sql_pairs.PAIRS if p.id.startswith("hard:")]
|
||||
hard_hits = [p.id for p in hard if explain_sql_diff(p.a, p.b, "sqlite", schema=SCHEMA).equivalent]
|
||||
@@ -92,6 +97,19 @@ def test_the_corpus_is_large_and_varied_enough():
|
||||
assert construct in text, construct
|
||||
|
||||
|
||||
def test_the_datasets_expose_the_round_three_counterexamples(databases):
|
||||
"""第 3 轮复验的反例在这几组数据上确实结果不同:外连接改内连接(NOT IN 空集)、列名表和 EXISTS 里的列别名、USING
|
||||
之后 ORDER BY 的别名、同名的两列、SQLite 的 % 和 MOD()、遮住外层的别名、找不到列的双引号名字。"""
|
||||
pairs = {p.id: p for p in sql_pairs.PAIRS}
|
||||
for pid in ("diff:r3-left-not-in-subquery", "diff:r3-left-not-in-empty-list", "diff:r3-cte-column-list-where-alias",
|
||||
"diff:r3-cte-column-list-union-where-alias", "diff:r3-exists-alias-where",
|
||||
"diff:r3-using-order-by-alias", "diff:r3-using-order-by-alias-left", "diff:r3-duplicate-output-name",
|
||||
"diff:r3-mod-vs-percent", "diff:r3-shadowed-alias", "diff:r3-double-quoted-not-in-scope",
|
||||
"diff:r3-double-quoted-not-in-outer-scope", "diff:r3-left-coalesce-inside-sum"):
|
||||
pair = pairs[pid]
|
||||
assert any(sql_pairs.run(c, pair.a) != sql_pairs.run(c, pair.b) for c in databases), pid
|
||||
|
||||
|
||||
def test_the_tricky_dataset_exposes_the_round_two_counterexamples(databases):
|
||||
"""第 2 轮复验的反例在专挑写法差异的那组数据上确实结果不同(不然执行比对守不住它们)。"""
|
||||
tricky = databases[-1]
|
||||
|
||||
@@ -579,7 +579,7 @@ def test_time_column_differs_with_catalog_names():
|
||||
[("time", "A 按 paid_at 筛时间,B 按 created_at")]),
|
||||
# 筛选
|
||||
("SELECT SUM(amount) FROM orders WHERE is_deleted <> 1", "SELECT SUM(amount) FROM orders",
|
||||
[("filter", "A 排除了 is_deleted = 1 的行,B 没有")]),
|
||||
[("filter", "A 排除了 is_deleted = 1 的行和 is_deleted 为空的行,B 没有")]),
|
||||
("SELECT SUM(amount) FROM orders", "SELECT SUM(amount) FROM orders WHERE refund_id IS NOT NULL",
|
||||
[("filter", "B 排除了 refund_id 为空的行,A 没有")]),
|
||||
("SELECT SUM(amount) FROM orders", "SELECT SUM(amount) FROM orders WHERE status = 'paid'",
|
||||
@@ -686,8 +686,10 @@ def test_a_difference_inside_an_aggregating_cte_names_the_cte_on_each_side():
|
||||
def test_an_outer_join_turned_inner_by_where_says_what_was_written():
|
||||
"""原文写的是 LEFT / RIGHT JOIN、WHERE 里的条件把没关联上的行筛掉了:按内连接比,说法照实写「写的是 LEFT JOIN,
|
||||
实际是 INNER JOIN」,不说成「用 INNER JOIN」。从 RIGHT JOIN 改写来的外连接说以哪一侧为主表,不说 LEFT JOIN。"""
|
||||
a = "SELECT SUM(o.amount) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id WHERE o.status = 'paid'"
|
||||
b = "SELECT SUM(o.amount) FROM refunds r LEFT JOIN orders o ON o.id = r.order_id WHERE o.status = 'paid'"
|
||||
# 用 COUNT:PostgreSQL 里对不知道是不是精确数的值求和,结果看行出来的先后(见 _Sensitivity),这样的一段不按 WHERE
|
||||
# 把外连接改成内连接(SQLite 上降成内连接之后扫描的先后真的变了),说不出这一句
|
||||
a = "SELECT COUNT(o.id) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id WHERE o.status = 'paid'"
|
||||
b = "SELECT COUNT(o.id) FROM refunds r LEFT JOIN orders o ON o.id = r.order_id WHERE o.status = 'paid'"
|
||||
assert texts(a, b, "postgres") == [
|
||||
"A 用 LEFT JOIN 关联 refunds 表,B 写的是 LEFT JOIN,但 WHERE 里的条件把关联不上 orders 的行筛掉了,实际是 INNER JOIN"]
|
||||
a = "SELECT COUNT(*) FROM orders o RIGHT JOIN refunds r ON r.order_id = o.id"
|
||||
@@ -707,7 +709,9 @@ def test_each_dialect_gives_the_same_three_findings(dialect):
|
||||
a = (f"SELECT COUNT(DISTINCT o.order_id) AS {q}订单数{q} FROM orders o "
|
||||
"WHERE o.paid_at >= '2026-09-01' AND o.is_deleted <> 1")
|
||||
b = f"SELECT COUNT(o.order_id) AS {q}订单数{q} FROM orders o WHERE o.created_at >= '2026-09-01'"
|
||||
assert texts(a, b, dialect) == ["A 按 paid_at 筛时间,B 按 created_at", "A 排除了 is_deleted = 1 的行,B 没有",
|
||||
# <> 1 也把 is_deleted 为空的行筛掉了(第 3 轮复验:以前只说「排除了 is_deleted = 1 的行」)
|
||||
assert texts(a, b, dialect) == ["A 按 paid_at 筛时间,B 按 created_at",
|
||||
"A 排除了 is_deleted = 1 的行和 is_deleted 为空的行,B 没有",
|
||||
"A 对 order_id 去重计数,B 没去重"]
|
||||
|
||||
|
||||
@@ -1099,3 +1103,166 @@ def test_only_literals_changed_stays_the_same_template_after_the_rewrite(dialect
|
||||
def test_a_structural_change_is_still_a_different_template(dialect, a, b):
|
||||
"""结构变了的(列名表的先后、IN 和 NOT IN、常量写成一个数、连接类型)照旧不是同一个模板。"""
|
||||
assert not normalize_sql(a, dialect).same_shape(normalize_sql(b, dialect))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 第 3 轮复验的反例:等价的结论、具体说法的可靠性
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,dialect,a,b", sql_pairs.REGRESSIONS_R3,
|
||||
ids=[f"{n}-{d}" for n, d, _, _ in sql_pairs.REGRESSIONS_R3])
|
||||
def test_round_three_counterexamples_are_never_equivalent(name, dialect, a, b):
|
||||
"""第 3 轮复验报的 blocker(PostgreSQL / MySQL 的写法、执行起来每次都不同的 RANDOM()):外连接改内连接时 NOT IN、
|
||||
ALL、NOT … ANY、数组拼接其实留得下没关联上的行;ORDER BY 里单独一个名字先认列别名;没有 GROUP BY 的 HAVING 让
|
||||
语句只出一行;MySQL 的 /*! … */、!、--1、类型不同的 BETWEEN;不确定的函数;解析时换成同一个节点的写法。不管
|
||||
给不给表结构、谁先谁后,一律不判等价。"""
|
||||
for schema in (None, sql_pairs.schema_cache()):
|
||||
for x, y in ((a, b), (b, a)):
|
||||
diff = explain_sql_diff(x, y, dialect, schema=schema)
|
||||
check_shape(diff)
|
||||
assert not diff.equivalent, name
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,dialect,a,b", sql_pairs.DIALECT_SAME_R3,
|
||||
ids=[f"{n}-{d}" for n, d, _, _ in sql_pairs.DIALECT_SAME_R3])
|
||||
def test_round_three_equivalent_rewrites_get_at_most_the_fallback(name, dialect, a, b):
|
||||
"""第 3 轮复验报的 major(意思一样,却说了具体的不同)在 PostgreSQL / MySQL 上的写法:认得出就判等价,认不出只给
|
||||
兜底说明,不说「A 按 X 分组,B 按 Y 分组」「A 用 LEFT JOIN,B 用 INNER JOIN」这类其实不影响结果的话。"""
|
||||
for schema in (None, sql_pairs.schema_cache()):
|
||||
for x, y in ((a, b), (b, a)):
|
||||
diff = explain_sql_diff(x, y, dialect, schema=schema)
|
||||
check_shape(diff)
|
||||
assert diff.equivalent or all(d.aspect == "other" for d in diff.items), (name, diff.items)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dialect", DIALECTS)
|
||||
def test_irrelevant_differences_inside_exists_and_in_are_never_described(dialect):
|
||||
"""EXISTS 只看有没有行、IN 只看在不在:子查询里去重、分组、UNION 还是 UNION ALL 都不影响。以前说「A 在 EXISTS
|
||||
子查询里对 amount 去重,B 没有去重」「A 在 id IN 的子查询里用 UNION 合并」,还标成说全了。"""
|
||||
ids = ["same:r3-exists-distinct", "same:r3-not-exists-distinct", "same:r3-exists-group-by",
|
||||
"same:r3-exists-union-vs-union-all", "same:r3-in-union-vs-union-all", "same:r3-in-group-by",
|
||||
"same:r3-in-group-by-two-keys", "same:r3-in-group-by-having-count-noop"]
|
||||
for pid in ids:
|
||||
pair = sql_pairs.PAIRS_BY_ID[pid]
|
||||
for schema in (None, sql_pairs.schema_cache()):
|
||||
diff = explain_sql_diff(pair.a, pair.b, dialect, schema=schema)
|
||||
check_shape(diff)
|
||||
assert diff.equivalent or all(d.aspect == "other" for d in diff.items), (pid, diff.items)
|
||||
|
||||
|
||||
def test_only_the_time_column_is_reported_when_the_group_key_is_equal_by_join():
|
||||
"""真正的差异只有时间列;分组键一边写 users.id、一边写 orders.user_id,被关联条件定成相等。给了表结构(两列都声明
|
||||
INTEGER,相等就是一样)只说时间列、说全了(只差一个方面,选择才能写进数据目录);没给表结构时类型不知道(SQLite 里
|
||||
1 和 '1' 也相等),只说时间列,另附兜底,不说「A 按 users.id 分组,B 按 orders.user_id 分组」。"""
|
||||
pair = sql_pairs.PAIRS_BY_ID["diff:r3-time-plus-group-key-equal"]
|
||||
expected = ("time", "A 按 paid_at 筛时间,B 按 created_at")
|
||||
diff = explain_sql_diff(pair.a, pair.b, "sqlite", schema=sql_pairs.schema_cache())
|
||||
check_shape(diff)
|
||||
assert [(d.aspect, d.text) for d in diff.items] == [expected] and not diff.fallback
|
||||
diff = explain_sql_diff(pair.a, pair.b, "sqlite")
|
||||
check_shape(diff)
|
||||
assert (diff.items[0].aspect, diff.items[0].text) == expected
|
||||
assert all(d.aspect in ("time", "other") for d in diff.items)
|
||||
|
||||
|
||||
def test_only_the_time_column_is_reported_when_the_left_join_cannot_matter():
|
||||
"""结果只是可空一侧的聚合(COALESCE(SUM(r.amount), 0)):LEFT JOIN 和 INNER JOIN 结果一样,真正的差异只有时间列。
|
||||
以前多说一句「A 用 LEFT JOIN 关联 refunds 表,B 用 INNER JOIN」,还标成说全了。"""
|
||||
pair = sql_pairs.PAIRS_BY_ID["diff:r3-time-plus-left-coalesce"]
|
||||
for schema in (None, sql_pairs.schema_cache()):
|
||||
diff = explain_sql_diff(pair.a, pair.b, "sqlite", schema=schema)
|
||||
check_shape(diff)
|
||||
assert [(d.aspect, d.text) for d in diff.items] == [("time", "A 按 paid_at 筛时间,B 按 created_at")]
|
||||
for dialect in ("postgres", "mysql"):
|
||||
diff = explain_sql_diff(pair.a, pair.b, dialect)
|
||||
check_shape(diff)
|
||||
assert not any(d.aspect == "join" for d in diff.items), (dialect, diff.items)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dialect", DIALECTS)
|
||||
def test_a_self_join_direction_gets_only_the_fallback(dialect):
|
||||
"""自己关联自己,关联的方向不同:两个别名都是 orders,以前说成「A 关联 orders 表的条件是 orders.parent_id =
|
||||
orders.id,B 是 orders.id = orders.parent_id」,读起来是同一个等式、自相矛盾。分不清哪个是哪个,只给兜底说明。"""
|
||||
pair = sql_pairs.PAIRS_BY_ID["diff:r3-self-join-roles"]
|
||||
for schema in (None, sql_pairs.schema_cache()):
|
||||
diff = explain_sql_diff(pair.a, pair.b, dialect, schema=schema)
|
||||
check_shape(diff)
|
||||
assert not diff.equivalent and all(d.aspect == "other" for d in diff.items), diff.items
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dialect", DIALECTS)
|
||||
def test_excluding_by_not_equal_also_mentions_the_null_rows(dialect):
|
||||
"""<>、NOT IN 也把这一列为空的行筛掉了:以前只说「A 排除了 is_deleted = 1 的行」,读的人会以为空值的行还在。
|
||||
NOT 套着几个条件的,照原样说「只保留 NOT (…) 的行」。"""
|
||||
diff = explain_sql_diff("SELECT SUM(amount) FROM orders WHERE is_deleted <> 1", "SELECT SUM(amount) FROM orders",
|
||||
dialect)
|
||||
check_shape(diff)
|
||||
assert [d.text for d in diff.items] == ["A 排除了 is_deleted = 1 的行和 is_deleted 为空的行,B 没有"]
|
||||
diff = explain_sql_diff("SELECT COUNT(*) FROM orders WHERE status NOT IN ('void', 'open')",
|
||||
"SELECT COUNT(*) FROM orders", dialect)
|
||||
check_shape(diff)
|
||||
assert [d.text for d in diff.items] == ["A 排除了 status IN ('open', 'void') 的行和 status 为空的行,B 没有"]
|
||||
diff = explain_sql_diff("SELECT COUNT(*) FROM orders WHERE NOT (status = 'void' AND is_deleted = 1)",
|
||||
"SELECT COUNT(*) FROM orders", dialect)
|
||||
check_shape(diff)
|
||||
assert len(diff.items) == 1 and diff.items[0].text.startswith("A 只保留 NOT ") and "B 没有这个条件" in diff.items[0].text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pid", ["hard:r3-disjoint-union-all-vs-union", "hard:r3-order-single-group",
|
||||
"hard:r3-distinct-join-vs-in", "hard:r3-count-distinct-join-vs-in",
|
||||
"hard:r3-join-vs-exists-distinct", "hard:r3-shadowed-alias-written-out"])
|
||||
@pytest.mark.parametrize("dialect", DIALECTS)
|
||||
def test_equivalent_rewrites_needing_reasoning_get_only_the_fallback(pid, dialect):
|
||||
"""意思一样、要按语义推理才看得出的(各支本身去重、互不相交的 UNION ALL 和 UNION;只有一组时的排序;关联一张表和
|
||||
IN / EXISTS 子查询;被遮住的别名写明白):以前说「A 用 UNION ALL(重复的行都留着)」「A 按 user_id 升序排,B 没有
|
||||
排序」「A 关联了 users 表,B 没有关联它」「A 在 EXISTS 子查询里只保留 status = 'paid' 的行」。认不出只给兜底。"""
|
||||
pair = sql_pairs.PAIRS_BY_ID[pid]
|
||||
for schema in (None, sql_pairs.schema_cache()):
|
||||
diff = explain_sql_diff(pair.a, pair.b, dialect, schema=schema)
|
||||
check_shape(diff)
|
||||
assert diff.equivalent or all(d.aspect == "other" for d in diff.items), diff.items
|
||||
|
||||
|
||||
def test_identical_text_is_equivalent_even_when_the_parser_would_lose_something():
|
||||
"""一字不差的两条就是同一条语句:带 MySQL 的 /*! … */、RANDOM() 这些不在语法树上判等价的写法,原文一样也算等价。"""
|
||||
for sql, dialect in (("SELECT COUNT(*) FROM orders /*! WHERE status = 'paid' */", "mysql"),
|
||||
("SELECT COUNT(*) FROM orders WHERE RANDOM() % 2 = 0", "sqlite"),
|
||||
("SELECT COUNT(*) FROM orders WHERE MOD(amount, 2) = 0", "sqlite")):
|
||||
diff = explain_sql_diff(sql, " " + sql + "\n", dialect)
|
||||
check_shape(diff)
|
||||
assert diff.equivalent, sql
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dialect,a,b", [
|
||||
("mysql", "SELECT COUNT(*) FROM orders /*! WHERE status = 'paid' */", "SELECT COUNT(*) FROM orders"),
|
||||
("mysql", "SELECT COUNT(*) FROM orders WHERE ! status = 'paid'", "SELECT COUNT(*) FROM orders WHERE NOT status = 'paid'"),
|
||||
("mysql", "SELECT id, amount --1 FROM orders", "SELECT id, amount FROM orders"),
|
||||
("sqlite", "SELECT COUNT(*) FROM orders WHERE MOD(amount, 2) = 0", "SELECT COUNT(*) FROM orders WHERE amount % 2 = 0"),
|
||||
("sqlite", "SELECT CHAR_LENGTH(name) FROM users", "SELECT LENGTH(name) FROM users"),
|
||||
], ids=["executable-comment", "bang", "double-dash", "mod-function", "char-length"])
|
||||
def test_a_structure_the_parser_loses_is_never_the_same_template(dialect, a, b):
|
||||
"""解析时丢了的结构(MySQL 会执行的 /*! … */ 注释、比比较结合得紧的 !、--1 是减负一,SQLite 的 MOD() 和 %、
|
||||
SQLite 没有的 CHAR_LENGTH)不能被 normalize_sql 当成「只改了参数」。"""
|
||||
assert not normalize_sql(a, dialect).same_shape(normalize_sql(b, dialect))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dialect,a,b", [
|
||||
("mysql", "SELECT COUNT(*) FROM orders /*! WHERE status = 'paid' */", "SELECT COUNT(*) FROM orders /*! WHERE status = 'void' */"),
|
||||
("mysql", "SELECT COUNT(*) FROM orders WHERE ! status = 'paid'", "SELECT COUNT(*) FROM orders WHERE ! status = 'void'"),
|
||||
("sqlite", "SELECT COUNT(*) FROM orders WHERE MOD(amount, 2) = 0", "SELECT COUNT(*) FROM orders WHERE MOD(amount, 3) = 1"),
|
||||
("sqlite", "SELECT o.id FROM orders o WHERE EXISTS (SELECT DISTINCT r.amount FROM refunds r WHERE r.order_id = o.id "
|
||||
"AND r.refunded_at >= '2026-09-01')",
|
||||
"SELECT o.id FROM orders o WHERE EXISTS (SELECT DISTINCT r.amount FROM refunds r WHERE r.order_id = o.id "
|
||||
"AND r.refunded_at >= '2026-10-01')"),
|
||||
("sqlite", "SELECT id FROM orders ORDER BY id LIMIT -1", "SELECT id FROM orders ORDER BY id LIMIT -20"),
|
||||
("postgres", "SELECT COALESCE(SUM(r.amount), 0) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"WHERE o.paid_at >= '2026-09-01'",
|
||||
"SELECT COALESCE(SUM(r.amount), 1) FROM orders o LEFT JOIN refunds r ON r.order_id = o.id "
|
||||
"WHERE o.paid_at >= '2026-10-01'"),
|
||||
], ids=["executable-comment", "bang", "mod-function", "exists-distinct", "limit", "left-coalesce"])
|
||||
def test_only_literals_changed_is_still_the_same_template_in_round_three(dialect, a, b):
|
||||
"""这一轮动过的写法(会执行的注释、!、MOD()、EXISTS 里去重、负数 LIMIT、LEFT JOIN 的聚合):只改字面量的照旧是
|
||||
同一个模板,字面量各是各的。"""
|
||||
na, nb = normalize_sql(a, dialect), normalize_sql(b, dialect)
|
||||
assert na.same_shape(nb) and na.literals != nb.literals
|
||||
|
||||
Reference in New Issue
Block a user