SQL 差异说明第 3 轮补充:按唯一键关联又没用到的 LEFT JOIN 去掉,解析时换了写法又没留位置的函数名不在语法树上判等价,说法照原文写列

- 按主键 / 唯一约束关联、关联进来的表在 ON 之外没用到的 LEFT JOIN(左边每一行正好关联上一行或补空)去掉,结果
  怕不怕重复都一样;给了表结构时「按主键关联 users」和「user_id IN (SELECT id FROM users)」不再说成「A 关联了
  users 表,B 没有关联它」(没给表结构时关联可能把行复制,这句话是对的,照说)。schema_cache 的 unique 也读进来。
- 原文写的函数名在重新生成时不见了、又没有哪个节点记下它(PostgreSQL 的 DATE_PART 解析成 EXTRACT、不留位置;
  date_part 返回浮点数、EXTRACT 返回 numeric),这条语句不在语法树上判等价;CAST 一类(x::INT、MySQL 的
  CONVERT(x, CHAR))不算。
- 规范化按关联条件换成同一列之后,差异说明给人看原文写的那一列(「A 按 users.id 分组」不写成 orders.user_id)。
- 留着的 HAVING TRUE 不说成「分组后只保留 TRUE 的组」(读不懂、也没说到真正的不同),只给兜底。
- _finish 的说明按现在的先后重写。

测试:按主键关联的 LEFT JOIN 收进 _SAME_SCHEMA_R3(不是唯一键、后面还接着别的表的进 _DIFF_R3),DATE_PART 进
REGRESSIONS_R3,补「按主键关联对 IN 子查询」的说法测试。
This commit is contained in:
ThearchyHelios
2026-10-05 14:32:07 +08:00
parent 08f75d4002
commit 4557e72c8e
3 changed files with 140 additions and 30 deletions
+111 -30
View File
@@ -2460,14 +2460,18 @@ def _call_names(tokens: list[Any]) -> list[str]:
_SURFACE = "sql_semantics_surface"
def _mark_surfaces(tree: Any, text: str, read: str | None) -> None:
def _mark_surfaces(tree: Any, text: str, read: str | None) -> bool:
"""函数节点上记下原文写的名字(换算成规范的同义写法,见 _SAFE_RESPELLINGS)——只在它和 sqlglot 重新生成的名字
不一样时记:sqlglot 把方言里意思不同的写法解析成同一个节点时(SQLite 的 MOD(a, 2) 和 a % 2 都是 Mod;PostgreSQL 的
LOG10(x) 和 LOG(10, x) 都是 Log),两边的语法树就因为这一项而不同,不会判等价。解析器在函数节点的 meta 里留着
函数名在原文里的位置(start / end),按位置取原文。"""
函数名在原文里的位置(start / end),按位置取原文。
返回能不能记全:原文写的某个函数名在重新生成的写法里不见了(又不是同义写法),却没有哪个节点记下了它(解析器
换了写法、没留位置,PostgreSQL 的 DATE_PART(…) 解析成 EXTRACT 就是这样)时返回 False,调用方就不在语法树上判等价。"""
from sqlglot import exp
mapping = _SAFE_RESPELLINGS.get(read or "", {})
attached: set[str] = set()
leaves = (exp.Column, exp.Identifier, exp.Literal, exp.Star, exp.Null, exp.Boolean, exp.Var, exp.Placeholder,
exp.Parameter, exp.DataType)
for node in tree.walk():
@@ -2488,6 +2492,16 @@ def _mark_surfaces(tree: Any, text: str, read: str | None) -> None:
found = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(", generated)
if written and (found is None or found.group(1).upper() != written):
node.meta[_SURFACE] = written
attached.add(text[start:end + 1].upper())
try:
tokens = _tokens(text, read)
present = {t.text.upper() for t in _tokens(tree.sql(dialect=read, comments=False), read)}
except Exception: # noqa: BLE001 - 切词、重新生成不了:当成记不全
return False
# CAST 一类(解析成 Cast 节点、不留位置)换写法也是同一个意思(CAST(x AS INT) 和 x::INT、MySQL 的 CONVERT(x, CHAR)),
# 也不会和别的函数撞成同一个节点(SQLite 的 CAST(x AS DATE) 重新生成成 DATE(x),可 DATE(x) 解析出来是 Date 节点)
return all(name in present or name in mapping or name in attached or name in ("CAST", "TRY_CAST", "CONVERT")
for name in _call_names(tokens))
def _respelled(text: str, read: str | None, tree: Any) -> bool:
@@ -2648,6 +2662,7 @@ class _Schema:
columns: set[str] = field(default_factory=set)
not_null: dict[str, set[str]] = field(default_factory=dict)
pk: dict[str, list[str]] = field(default_factory=dict)
unique: dict[str, list[list[str]]] = field(default_factory=dict)
def _schema_info(schema: Mapping[str, Any] | None) -> _Schema:
@@ -2661,6 +2676,7 @@ def _schema_info(schema: Mapping[str, Any] | None) -> _Schema:
types: dict[str, dict[str, str]] = {}
not_null: dict[str, set[str]] = {}
pk: dict[str, list[str]] = {}
unique: dict[str, list[list[str]]] = {}
for name, meta in tables.items():
columns = meta.get("columns") if isinstance(meta, Mapping) else None
cols = [c for c in columns or [] if isinstance(c, Mapping) and isinstance(c.get("name"), str) and c["name"]]
@@ -2672,7 +2688,11 @@ def _schema_info(schema: Mapping[str, Any] | None) -> _Schema:
keys = meta.get("primary_key") if isinstance(meta, Mapping) else None
if isinstance(keys, list) and all(isinstance(k, str) for k in keys):
pk[key] = [k.translate(_ASCII_LOWER) for k in keys]
return _Schema(mapping or None, types, {c for cols in types.values() for c in cols}, not_null, pk)
groups = meta.get("unique") if isinstance(meta, Mapping) else None
if isinstance(groups, list):
unique[key] = [[c.translate(_ASCII_LOWER) for c in g] for g in groups
if isinstance(g, list) and g and all(isinstance(c, str) for c in g)]
return _Schema(mapping or None, types, {c for cols in types.values() for c in cols}, not_null, pk, unique)
def _not_null_column(info: _Schema, table: str, column: str, read: str | None) -> bool:
@@ -2790,17 +2810,22 @@ def _light(root: Any, read: str | None, info: _Schema) -> _Form:
def _finish(tree: Any, read: str | None, info: _Schema, *, level: str, qualified: bool) -> _Form:
"""两级共用的几步,都只做保持语义的变换(为什么保持语义见各函数的说明):
"""两级共用的几步,都只做保持语义的变换(为什么保持语义见各函数的说明),按这个先后:
1. 动来源的:两个来源的 RIGHT JOIN 写成 LEFT JOIN;WHERE 把外连接可空一侧没关联上的行筛掉了的写成内连接;开头那组
内连接的条件挪进 WHERE、写成逗号连接。结果看行出来的先后的一段(见 _Sensitivity)都不做。
0. 原样选出 FROM 里一个子查询全部列、什么别的都不做的一段换成那个子查询(见 _unwrap_passthrough)。
1.5 去重的:SELECT DISTINCT 写成按选出来的列 GROUP BY、已经按选出来的列分组的 DISTINCT 去掉、IN 子查询里的
DISTINCT 和 ORDER BY 去掉(见各函数:只在「相等」就是「一样」、不看行出来的先后时做)。
2. 和来源无关的:COUNT(1) 写成 COUNT(*)、MIN / MAX 里的 DISTINCT 去掉;EXISTS 子查询写成 SELECT 1;IN 列表去重
排序、只有一个常量的写成 =;BETWEEN 写成两个比较;NOT 套比较写成反过来的比较、NOT NOT 去掉;不影响结合的括号、
ORDER BY 的 ASC、OFFSET 0 去掉;最外层 ORDER BY 引用的别名换成表达式,最外层和条件里的子查询选出来的列名去掉。
3. 起规范的名字、排好交换律成立的地方(见 _named)。
0. 原样选出 FROM 里一个子查询全部列、什么别的都不做的一段换成那个子查询(_unwrap_passthrough)。
1. 动来源的:两个来源的 RIGHT JOIN 写成 LEFT JOIN;WHERE 把外连接可空一侧没关联上的行筛掉了的写成内连接;只取可空
一侧跳过空值的聚合时 LEFT JOIN 写成内连接;开头那组内连接的条件挪进 WHERE、写成逗号连接;恒为真的条件、已经
保证不为空的 IS NOT NULL 去掉;没用到、又不怕重复(或者按唯一键关联)的 LEFT JOIN 去掉;(认列之后)按关联条件
相等、类型一样的两列换成同一列。结果看行出来的先后的一段(见 _Sensitivity)这些都不做——只有挪内连接的 ON
条件、去掉 1 = 1 这类常量条件照做(库里一视同仁,扫描的先后不变)。
2. 只看有没有、在不在的子查询:EXISTS 写成 SELECT 1,IN 子查询里不影响取值集合的去重、分组、排序、UNION 去掉。
3. 去重的:SELECT DISTINCT 写成按选出来的列 GROUP BY、已经按选出来的列分组的 DISTINCT 去掉(只在「相等」就是
「一样」、不看行出来的先后时做)。
4. 和来源无关的:COUNT(1) 写成 COUNT(*)、MIN / MAX 里的 DISTINCT 去掉、IIF / IF 写成 CASE;IN 列表去重排序、只有一个
常量的写成 =;BETWEEN 写成两个比较;NOT 套比较写成反过来的比较、NOT NOT 去掉;LIMIT 的同义写法;不影响结合的
括号、ORDER BY 的 ASC、OFFSET 0、重复的排序键去掉;最外层 ORDER BY 引用的别名换成表达式,最外层和条件里的子查询
选出来的列名去掉。
5. 起规范的名字、排好交换律成立的地方(见 _named)。
"""
tree = _unwrap_passthrough(tree, read)
sens = _Sensitivity(tree, read, info)
@@ -2811,7 +2836,7 @@ def _finish(tree: Any, read: str | None, info: _Schema, *, level: str, qualified
_flatten_prefix(tree, sens, read)
_drop_true_conjuncts(tree)
_drop_implied_not_null(tree, read, info, sens)
_drop_unused_left_joins(tree, sens)
_drop_unused_left_joins(tree, sens, info, read)
if qualified:
_substitute_equal_columns(tree, read, info, sens)
_exists_subqueries(tree)
@@ -4204,12 +4229,43 @@ def _dup_insensitive(select: Any) -> bool:
return distinct is not None or _membership_context(select)
def _drop_unused_left_joins(tree: Any, sens: _Sensitivity) -> None:
"""LEFT JOIN 进来的表除了它自己的 ON 之外哪儿都没用到(本段、相关子查询、后面的连接都不引用它),结果又不怕重复
的行(SELECT DISTINCT;聚合都是 MIN、MAX、COUNT(DISTINCT …) 这类去重的;整段是 IN / EXISTS 子查询):这次
LEFT JOIN 去掉结果一样。LEFT JOIN 不会让左边的行变少,只会让关联上好几行的左边的行重复出现,别的列的取值不变。
不去的:有 COUNT(*)、SUM 这类怕重复的聚合,有 RIGHT / FULL JOIN、窗口函数、SELECT *,有不带表名的列(分不清是不是
这张表的),结果看行出来先后的一段(DISTINCT 留下先扫到的那个值、取行,见 _Sensitivity)。"""
def _unique_key_bound(item: Any, conds: list[Any], info: _Schema, read: str | None) -> bool:
"""关联进来的实表 item 的某个唯一键(主键、唯一约束,见 schema_cache 的 primary_key / unique)的每一列,都被 conds
里的「这一列 = 不涉及这张表的表达式」定住了:左边每一行最多关联上它一行(相等的条件匹配不上空值)。SQLite 里只认
整数、文本亲和的键列(没有类型亲和的列能同时存 1 和 '1',比较时按数值转换就能匹配上两行)。"""
from sqlglot import exp
if not isinstance(item, exp.Table) or item.args.get("db") or \
item.name.translate(_ASCII_LOWER) in _visible_ctes(item):
return False
alias, table = item.alias_or_name, item.name.translate(_ASCII_LOWER)
bound: set[str] = set()
for cond in conds:
cond = _unparen(cond)
if not isinstance(cond, exp.EQ):
continue
for x, y in ((cond.this, cond.expression), (cond.expression, cond.this)):
x = _unparen(x)
if isinstance(x, exp.Column) and x.table == alias and y.find(exp.Query, exp.Subquery) is None and \
not any(c.table == alias or not c.table for c in y.find_all(exp.Column)) and not _volatile(y):
bound.add(x.name.translate(_ASCII_LOWER))
keys = ([info.pk[table]] if info.pk.get(table) else []) + info.unique.get(table, [])
types = info.types.get(table, {})
for key in keys:
if key and all(k in bound for k in key) and \
(read != "sqlite" or all(_identity_type(types.get(k, ""), read, None) for k in key)):
return True
return False
def _drop_unused_left_joins(tree: Any, sens: _Sensitivity, info: _Schema, read: str | None) -> None:
"""LEFT JOIN 进来的表除了它自己的 ON 之外哪儿都没用到(本段、相关子查询、后面的连接都不引用它),而且下面两种情况
之一:结果不怕重复的行(SELECT DISTINCT;聚合都是 MIN、MAX、COUNT(DISTINCT …) 这类去重的;整段是 IN / EXISTS
子查询);或者 ON 用它的唯一键(主键、唯一约束)把它定死了、左边每一行最多关联上一行(见 _unique_key_bound)。
这次 LEFT JOIN 去掉结果一样:LEFT JOIN 不会让左边的行变少,只会让关联上好几行的左边的行重复出现(唯一键定死时
连重复都没有),别的列的取值不变。不去的:有 COUNT(*)、SUM 这类怕重复的聚合又没有唯一键,有 RIGHT / FULL JOIN、
窗口函数、SELECT *,有不带表名的列(分不清是不是这张表的),结果看行出来先后的一段(DISTINCT 留下先扫到的那个值、
取行,见 _Sensitivity)。"""
from sqlglot import exp
changed = True
@@ -4218,8 +4274,9 @@ def _drop_unused_left_joins(tree: Any, sens: _Sensitivity) -> None:
for select in list(tree.find_all(exp.Select)):
joins = select.args.get("joins") or []
if not joins or any(str(j.side or "").upper() in ("RIGHT", "FULL") for j in joins) or sens.of(select) or \
any(isinstance(p, exp.Star) for p in select.expressions) or not _dup_insensitive(select):
any(isinstance(p, exp.Star) for p in select.expressions):
continue
insensitive = _dup_insensitive(select)
own = list(_block_nodes(select))
nested = [c for sub in _nested_queries(select) for c in sub.find_all(exp.Column)]
columns = [n for n in own if isinstance(n, exp.Column)] + nested
@@ -4230,7 +4287,8 @@ def _drop_unused_left_joins(tree: Any, sens: _Sensitivity) -> None:
continue
alias = join.this.alias_or_name
on = {id(n) for n in join.args["on"].walk()}
if alias and not any(c.table == alias and id(c) not in on for c in columns):
if alias and not any(c.table == alias and id(c) not in on for c in columns) and \
(insensitive or _unique_key_bound(join.this, _conjuncts(join.args["on"]), info, read)):
select.set("joins", [j for j in joins if j is not join] or None)
changed = True
break
@@ -4299,6 +4357,11 @@ def _drop_implied_not_null(tree: Any, read: str | None, info: _Schema, sens: _Se
select.set(holder.arg_key, None)
#: 换成同一组相等的列之后,原文写的那一列(实表名、列名)记在新列的 meta 里:比等价时不算(见 _fingerprint),差异
#: 说明给人看时照原文写(见 _Explainer.show)
_WRITTEN_COLUMN = "sql_semantics_written_column"
def _substitute_equal_columns(tree: Any, read: str | None, info: _Schema, sens: _Sensitivity) -> None:
"""(认列之后)WHERE 里 AND 连着的「两列相等」(内连接的条件挪进 WHERE 之后也在这里)对 WHERE 之后的每一行都成立:
两列都不为空、而且相等。两列声明的类型完全一样、这个类型下「相等就是一样」(整数、文本这类,见 _identity_type)时,
@@ -4370,18 +4433,19 @@ def _substitute_equal_columns(tree: Any, read: str | None, info: _Schema, sens:
continue
table, name = replace[(col.table, col.name)]
new = exp.column(name, table=table)
new.meta[_WRITTEN_COLUMN] = bases[(col.table, col.name)][:2]
if col.parent is select and col.arg_key == "expressions":
new = exp.alias_(new, col.alias_or_name, copy=False)
col.replace(new)
def _identity_type(declared: str, read: str | None, sens: _Sensitivity) -> bool:
def _identity_type(declared: str, read: str | None, sens: _Sensitivity | None) -> bool:
"""声明的类型下「相等就是一样」、可以拿同一组相等的列互换:SQLite 里整数、文本亲和的列(实数亲和的 0.0 和 -0.0
相等却不一样,数值亲和、没有亲和的列能同时存 1 和 1.0);PostgreSQL、MySQL 按 _Sensitivity._safe_type。"""
upper = declared.upper()
if read == "sqlite":
return "INT" in upper or any(w in upper for w in ("CHAR", "CLOB", "TEXT"))
return sens._safe_type(declared)
return sens is not None and sens._safe_type(declared)
def _int_value(node: Any) -> int | None:
@@ -6191,6 +6255,12 @@ class _Explainer:
copy = node.unalias().copy() if isinstance(node, exp.Alias) else node.copy()
single = len(block.all_rels()) == 1 and not full
for col in copy.find_all(exp.Column):
written = col.meta.get(_WRITTEN_COLUMN)
if written is not None:
# 规范化按关联条件换成了同一组里的另一列(见 _substitute_equal_columns):给人看原文写的那一列
col.set("this", exp.to_identifier(written[1]))
col.set("table", None if single else exp.to_identifier(written[0]))
continue
rel = block.rel(col.table) if col.table else None
if rel is not None:
col.set("table", None if single else exp.to_identifier(rel.shown))
@@ -6323,7 +6393,9 @@ class _Explainer:
all(self.base_columns(c, big) is not None for c in conds) else ""
used = any(t.name == rel.name and id(t) not in {id(i) for i in _items_of(small.select)}
for t in small.select.find_all(exp.Table))
if used and _dup_insensitive(big.select):
item = next((i for i in _items_of(big.select) if i.alias_or_name == rel.alias), None)
if used and (_dup_insensitive(big.select) or
(item is not None and _unique_key_bound(item, conds, self.info, self.read))):
# 另一边在子查询里用到了这张表、这一边又不怕重复的行:可能只是把 IN / EXISTS 改写成了关联(去重之后一样),
# 说不准是不是真的不同。怕重复的(SUM、COUNT(*))关联会把行按关联上的条数复制,那才说
return None
@@ -6366,7 +6438,10 @@ class _Explainer:
if any(c.table == alias and id(c) not in on for c in columns) or any(not c.table for c in columns) or \
any(isinstance(p, exp.Star) for p in select.expressions):
return False
return _dup_insensitive(select)
join = next((j for j in joins if j.this.alias_or_name == alias), None)
return _dup_insensitive(select) or (join is not None and join.args.get("on") is not None and
_unique_key_bound(join.this, _conjuncts(join.args["on"]), self.info,
self.read))
def _outer(self, A: _Block, B: _Block, where: tuple[str, str]
) -> tuple[bool, dict[str, str], set[str], set[str]] | None:
@@ -6976,10 +7051,16 @@ class _Explainer:
def having(self, A: _Block, B: _Block, mapping: Mapping[str, str], where: tuple[str, str]) -> bool:
"""HAVING:同一个对象(聚合)只和常量比的,两边合起来逐段算,留下的组一样的不说(COUNT(*) > 1 和 >= 2、分组
之后 COUNT(*) > 0);只有一边有、另一边 HAVING 里没有别的条件的,说「分组后只保留」。"""
from sqlglot import exp
la, lb = self.sides(where)
only_a, only_b = self._unmatched(A.having, B.having, mapping)
if not only_a and not only_b:
return True
if any(isinstance(_unparen(c), exp.Boolean) for c in only_a + only_b):
# 留着的 HAVING TRUE 只说明「没有 GROUP BY 时它让语句成了只出一行的聚合」(见 _drop_true_conjuncts),说
# 「分组后只保留 TRUE 的组」读不懂,也没说到真正的不同
return False
grouped = bool(A.group) and bool(B.group)
if any(self.base_columns(c, A) is None for c in only_a) or any(self.base_columns(c, B) is None for c in only_b):
return False
@@ -7341,13 +7422,13 @@ def explain_sql_diff(a: str, b: str, dialect: str | None = None, *, schema: Mapp
forms_a: dict[str, _Form] = {}
forms_b: dict[str, _Form] = {}
try:
_mark_surfaces(ra, a.strip(), read)
_mark_surfaces(rb, b.strip(), read)
marked_a = _mark_surfaces(ra, a.strip(), read)
marked_b = _mark_surfaces(rb, b.strip(), read)
if read == "sqlite":
_mark_comma_joins(ra, a.strip(), read)
_mark_comma_joins(rb, b.strip(), read)
forms_a = {} if _lost_in_parsing(a.strip(), read, ra) else _canonical_forms(ra, read, info)
forms_b = {} if _lost_in_parsing(b.strip(), read, rb) else _canonical_forms(rb, read, info)
forms_a = {} if not marked_a or _lost_in_parsing(a.strip(), read, ra) else _canonical_forms(ra, read, info)
forms_b = {} if not marked_b or _lost_in_parsing(b.strip(), read, rb) else _canonical_forms(rb, read, info)
except Exception: # noqa: BLE001 - 规范化本身的缺陷不能连累调用方:当作说不清,记下来修
logger.warning("SQL 规范化失败", exc_info=True)
forms_a, forms_b = {}, {}
+16
View File
@@ -2207,6 +2207,12 @@ _DIFF_R3 = [
"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"),
# 按不唯一的列关联(users.referrer_id、refunds.order_id):LEFT JOIN 会把左边的行复制,去不掉
("r3-left-join-on-non-unique-unused", "SELECT o.id, o.amount FROM orders o LEFT JOIN users u ON u.referrer_id = o.user_id",
"SELECT o.id, o.amount FROM orders o"),
("r3-left-join-on-key-plus-other-table", "SELECT SUM(o.amount) FROM orders o LEFT JOIN users u ON u.id = o.user_id "
"LEFT JOIN refunds r ON r.user_id = u.id",
"SELECT SUM(o.amount) FROM orders o LEFT JOIN users u ON u.id = o.user_id"),
("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"),
]
@@ -2327,6 +2333,12 @@ _SAME_SCHEMA_R3 = [
"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'"),
# 按主键关联、关联进来的表又没用到:左边每一行正好一行,这次 LEFT JOIN 去掉结果一样(不知道是主键时它可能把行复制)
("r3-left-join-on-primary-key-unused", "SELECT o.id, o.amount FROM orders o LEFT JOIN users u ON u.id = o.user_id",
"SELECT o.id, o.amount FROM orders o"),
("r3-left-join-on-primary-key-sum", "SELECT SUM(o.amount), COUNT(*) FROM orders o LEFT JOIN stores s "
"ON s.id = o.store_id AND s.region = 'east'",
"SELECT SUM(o.amount), COUNT(*) FROM orders o"),
]
#: 意思一样、要按语义推理才看得出的:认不出时只许给兜底说明(复验专挑这类对查「说出不存在的差异」)
@@ -2553,6 +2565,10 @@ REGRESSIONS_R3: list[tuple[str, str, str, str]] = [
# 解析时换成同一个节点、方言里却不是一回事的写法
("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 的 date_part 返回浮点数、EXTRACT 返回 numeric(秒数 12.5 和 12.500000),sqlglot 解析成同一个节点、
# 还不留函数名的位置:这样的不在语法树上判等价
("date_part_vs_extract", "postgres", "SELECT DATE_PART('second', paid_at::timestamp) FROM orders",
"SELECT EXTRACT(SECOND FROM paid_at::timestamp) FROM orders"),
# 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",
+13
View File
@@ -1266,3 +1266,16 @@ def test_only_literals_changed_is_still_the_same_template_in_round_three(dialect
同一个模板,字面量各是各的。"""
na, nb = normalize_sql(a, dialect), normalize_sql(b, dialect)
assert na.same_shape(nb) and na.literals != nb.literals
def test_a_join_on_a_primary_key_against_an_in_subquery_is_not_described_with_the_schema():
"""按主键关联 users(每张订单最多关联上一个用户)和 user_id IN (SELECT id FROM users) 是一回事:给了表结构(知道
users.id 是主键)就不说「A 关联了 users 表,B 没有关联它」;没给表结构时关联可能把行复制,这句话是对的。"""
a = "SELECT o.id, o.amount FROM orders o JOIN users u ON u.id = o.user_id"
b = "SELECT o.id, o.amount FROM orders o WHERE o.user_id IN (SELECT id FROM users)"
diff = explain_sql_diff(a, b, "sqlite", schema=sql_pairs.schema_cache())
check_shape(diff)
assert all(d.aspect == "other" for d in diff.items), diff.items
diff = explain_sql_diff(a, b, "sqlite")
check_shape(diff)
assert diff.fallback and diff.items[0].text.startswith("A 关联了 users 表")