ThearchyHelios
c3173909db
整理:前两次提交里超过 120 字的两行折行,absent_message 进 source_objects 的 __all__
...
只动排版和导出清单,不改行为。
2026-10-17 08:42:26 +08:00
ThearchyHelios
caeab0fad6
SQL 差异说明认得同一列上的条件改写:说清「这次对 X 的条件是 …,口径是 …」
...
验收:口径 status = 'paid'、这次 status IN ('paid', 'refunded'),explain_sql_diff 只给兜底「两条 SQL 的写法不同,
未能归纳成具体差异」。原因是差异说明只说四类,同一列上两边各写一套条件(等号对 IN、取值集合不同、范围端点不同)
不在其中:_extra_filters 看到另一边也用了这一列就放弃。
改法:_Explainer 加 _rewrites,排在 _times 之后、_extra_filters 之前。同一张实表的同一列两边 WHERE 里都有条件、
至少一边有没配上的,就说「A 对 X 的条件是 …,B 是 …」;两边都只是取值集合、又有共同的取值时补「(这次多了
'refunded')」。两边都是时间条件时 aspect 是 time_range(FacetDiff 说明里早有这个方面、engine/compare 也按它
排序),否则是 filter 且 a、b 两边都有——dual_check_facts 对两边都有的 filter 本来就说「两边各有不同的筛选条件」
不写目录,ledger/stats、评测页的方面叫法都已认得这两个名字。
为了不说出不存在的不同,只在这些条件都满足时说:两边用到这一列的条件都只涉及这一列、比的是那一列本身;
按取值逐段算过两边留下的取值确实不同(_Range.compare 加 never_null:表结构里确定不为空的列不拿空值比;
x >= 100 AND x >= 50 对 x >= 100 这类冗余写法算出来一样,不说);不在 LEFT JOIN 可空一侧;WHERE 之外(ON、
HAVING、CASE、聚合参数)都没用到这一列(SUM(CASE WHEN status = 'paid' …) 配上 IN 可能和只筛 paid 一样);不是
认不准的时间窗口;没换表;有认不出列的条件时整段不说。
只改说明、不碰等价判定:说明只在已经判了不等价之后才跑。新测试把 _rewrites 换成什么都不做,逐对(语料全部
加几对改写、两种先后、给不给表结构)比等价与否和判定的级别,一字不差;IN 只有一个值、IN 列表去重排序照样判等价。
语料执行守门(意思一样的对不许说出具体不同)全过。
测试:test_sql_semantics 新增三组(各方言说两边、可能不影响结果的不说、等价判定不变);原来断言这两类只给
兜底的用例改成「算出来的值上换了值」(LOWER(status))仍只给兜底;test_sql_text_diff、test_dual_check_facts 跟上
(同一列上的时间范围现在是 time_range,照样不写目录;换了筛选的值不写目录)。去掉 _rewrites 的调用回跑,新测试
挂 5 条。
双盲复算的前端夹具 dual_check.json 里「说不清的」那个变体原来就是同一列上的时间范围,现在说得出了:换成套着
DATE() 的时间范围(认不出取值范围,仍只给兜底),test_dual_check_fixture 重新生成,check-verify 对应的说法和
高亮那一行的断言跟上。
2026-10-17 08:17:12 +08:00
ThearchyHelios
68ea3a7f47
口径账本内核第一步:sql_semantics 加参数绑定与等价证明(bind_template / match_template)
...
为什么:口径账本要认出「这次执行的 SQL 是不是某一版口径的取数」。只比 normalize_sql 的文本会错给信任(status = 'paid'
和 'refunded' 文本相同),拿模板原文直接比 explain_sql_diff 又永远不等价(参数不同)。做法照 next-round 02 做法 1:
- 绑定只是启发:模板里的 {{ input.<字段> }}(可带 trim / compact)按位置换成哨兵,依次在 optimizer、light、parsed 三级
规范形式上抹掉字面量的值比结构,结构一样就按位置配字面量(半截字面量按正则取,写死的常量必须相等,同一槽位取同一个值);
三级都配不上时退回「同一列、同一运算符」的启发式配对(只给说明和预填)。
- 等价只由按绑定 render_template(运行时同一个渲染器)渲染出的口径 SQL 交给 explain_sql_diff 判;有槽位既没取到值又没有
缺省值时不证明(渲染出 EXISTS (SELECT FROM …) 这种口径实际不会执行的 SQL,sqlglot 照样能解析)。
- 不可比照实给原因:引用 vars. / nodes. / file_text、参数写在名字里、别的过滤器。
- 等价时按位置给列映射(外层别名不计、列先后计,SELECT * 按表结构展开)。
测试:新增 test_sql_binding.py(认得出、不错给信任、不可比、列映射);test_sql_equivalence.py 加「口径模板」一节,语料里
每一对把 B 的头两个字面量换成槽位当模板拿 A 比,判了等价的在虚构库上执行结果必须一致、explain_sql_diff 也必须判等价。
去掉「槽位没值不证明」那一道回跑,这一节抓到 11 对执行报错。
2026-10-16 13:39:15 +08:00
ThearchyHelios
6d4f452f36
M4 复核修正:规范化后不同的行带上所在的子句,ON 挂在 JOIN 那一行的末尾
...
复核发现上一版把 ON 单独排成一行后,「规范化后不同的行」丢了关联条件的上下文:只差一个关联条件时那一栏只剩
「AND refunds.state = 'done'」,有两个 LEFT JOIN 时看不出是哪个 JOIN 的,也看不出是关联条件还是筛选条件;两边 JOIN
的表不同时,两边都有的那一行「ON」被 difflib 当成相同的行吃掉,表名下面直接跟条件;关联子查询换成两张表时也一样。
这几种都只给兜底说明,审核人全靠这一栏逐行对照。
- sql_semantics:ON 挂在 JOIN 那一行的末尾(表名、子查询的右括号后面),条件各占一行缩在下面,仍比 WHERE 的条件
深一级(ON 里和 WHERE 里的同一个条件不当成同一行)。text_diff 里不同的行前面带上它的各级上级行(排在前面、缩进比
它浅的最近一行,一层层数到顶格),以「 」开头、两边相同,只为标出不同的行在哪个 WHERE、哪个 JOIN 的 ON、哪层
子查询里;截到 40 行时末尾落单的上级行不留。只动排版:拿测试里的 SQL 语料(990 对,4 种方言、有无表结构、两个
方向)逐对比过,equivalent、items、fallback、level 和改之前完全一致,有没有不同的行也一致,不再有单独一行的 ON。
- SqlDiff 的约定多一条:「 」开头的是上下文行,只跟着不同的行出现。test_sql_semantics 的 check_shape 原来只认
「- 」「+ 」,改成也认「 」、并要求最后一行是不同的行(374 个用例只因这一行断言变红,判定和说明都没变)。
- 审批卡(DualCheckApproval):两栏各自只放用得着的上级行(这一边不同的行往上数,缩进更浅的最近一行,一层层往上),
只有另一边用得着的不放;上级行不高亮、用淡一级的字色(同原文栏里没标的行),标题后面说明「淡色的行两边相同,标出
所在的子句」,悬停也是这句;这一边没有不同的行时照旧说没有,老工件没有上级行,照旧。lineDiff 注释里「条件并成
一行」的旧说法改正。
- 测试:test_sql_text_diff 里 ON 那条的期望改成带 JOIN 的上级行,补两个 LEFT JOIN 只差一个关联条件(两个方向)、
两边 JOIN 的表不同、关联子查询换成两张表、同一个条件在 ON 和 WHERE 里、顶格的行不带上下文、40 行截断几条;去掉
上级行、去掉「ON 挂在行末」、去掉截断后的清理,各自让对应的用例变红。
- 夹具 dual_check.json 按 test_dual_check_fixture 的办法重新生成(只变 text_diff),另加 join_condition 变体(只差
一个关联条件、只给兜底);check-verify 查两栏的上级行(WHERE)列在不同的行前面、不高亮、标题有说明,join_condition
下原流程一栏是带 ON 的 JOIN 加缩进的条件、盲算一栏不画只有原流程用得着的上级行,深浅色对比度、390px 不溢出。
2026-10-07 01:34:59 +08:00
ThearchyHelios
5b18eab310
M4:双盲复算「规范化后不同的行」每个条件一行,两边相同的条件不再被标成差异
...
验收时审批卡把两边完全相同的日期区间标成了差异:_text_diff 逐行比两边 sqlglot pretty 的输出,pretty 按宽度决定
换不换行,两个合取的 WHERE 排成一行、三个的拆成三行,同一个条件在两边落在不同的行上。原文 SQL 都是单行时原文栏
一行都不标,这一栏是唯一逐行指出差在哪的地方,审核人会以为统计期也不一样。
现在逐行比之前两边按同一套排版写(_diff_lines / _line_generator,按方言给 sqlglot 的生成器加一处改动):WHERE、
HAVING、ON 的合取式每个条件一行,各层子查询里的也一样;条件本身是 OR 连起来的一律加括号(单独一个和与别的并列时
写法一样);ON 照 WHERE 的样子单独一行、条件缩进在下面(ON 里和 WHERE 里的同一个条件不当成同一行)。比的时候
不看条件前面的 AND(一边多出来的条件排在最前面时,另一边同一个条件只差在有没有 AND),列出来的仍是各自原样的行。
只动排版:判不判等价、差异说明不变(拿测试里的 SQL 对照语料按三种方言、有无表结构逐对比过,items、equivalent、
fallback、level 全部一致),sql_semantics、sql_equivalence 原有测试全过。
前端夹具 dual_check.json 里的 text_diff 是服务端的真实输出,按 test_dual_check_fixture 的办法重新生成(只变了
text_diff);check-verify 的对照行一节另查「只差结束日期时开始日期不标」「多出的两个条件各占一行」。
2026-10-07 00:12:31 +08:00
ThearchyHelios
2eddd8236a
SQL 差异说明收尾:前缀的 NOT 后面同一段里既有谓词、又有算术 / 拼接 / 按位运算时(NOT status IN (…) * amount),SQLite 里 NOT 最松、sqlglot 读成 (NOT …) * amount,不在语法树上判等价、只给兜底;48 种组合两种方言都不再判等价,常见的 NOT … IN、NOT … = 照样认得出等价改写。模块说明照实列出仍做不到的几类说明准确度问题(Unix 秒的时间列、带 time 的时长列、被函数定死的列)
2026-10-06 09:18:10 +08:00
ThearchyHelios
095cc4c8ab
SQL 差异说明收尾(第 2 轮):PostgreSQL 符号运算符和 || 混用不判等价;条件写在聚合里、时间窗口、反连接、经主键定死的列不再说错
...
- 保真守卫(_misread):PostgreSQL 写成符号、sqlglot 却当成 LIKE 那一级来读的运算符(~、~*、~~、~~*、@@、-|-、@> 等)
和别的二元运算混用时拦下(PostgreSQL 里它们和 ||、& 同级、从左往右,status ~ 'p' || 'x' 是 (status ~ 'p') || 'x');
另拦 PostgreSQL 的前缀 @、|/、||/、COLLATE 后面的 ::;不知道是哪种库时拦块注释里套着的 /*、紧跟着字的 --、#、双引号
括起来的名字(MySQL 默认的 sql_mode 里是字符串)。都只给兜底,附原文的逐行对照。
- 条件写在聚合的参数里(SUM(status = 'paid')、COUNT(… OR NULL)、COUNT(NULLIF(…))、SUM(amount * (1 - is_deleted)))
算作这一边对这一列有条件(_Usage 的 case),另一边多出同一列上的 WHERE 条件时不再说「这一边没有这个条件」,只给兜底。
- 时间:timed 认 BETWEEN 和时间列上 LIKE 像日期的模式(paid_at LIKE '2026-09%'),时间窗口不再被说成有效行过滤;
「另一边没有按时间筛选」之前看另一边可能是时间窗口的条件(聚合参数里的时间比较、date_key、stat_month 这类列、
20260901 / 202609 这类整数、上下界一对),有就不说;认不准是不是时间列的窗口(_window_like)不说成有效行过滤。
两边的时间列在不同的表上时写成「表.列」。
- 反连接:用到 LEFT JOIN 可空一侧的列的条件(WHERE r.id IS NULL、r.order_id IS NULL)不说成有效行过滤,时间列在
可空一侧时也不说按它筛时间。
- 给了表结构时,聚合参数所在的表的键在组里定死了(按 u.id 分组时的 u.city),MAX 对 MIN、AVG(DISTINCT) 对 AVG 不说。
- 另一边原文也 LEFT JOIN 了、规范化因为结果不怕重复去掉了的表,不说「另一边没有关联」。
- 回归语料:收尾第 1 轮之后复验报的全部 blocker / major 收进 sql_pairs(_DIFF_R5、_DIFF_TIME_R5、_SAME_HARD_R5、
_SAME_SCHEMA_R5 执行;REGRESSIONS_R5、EXPLAIN_TRAPS_R5 只查结论),新增对应的测试。
- 模块说明照实补上:新增的守卫、时间条件的认法、只给兜底的情况、收窄换来的代价、MySQL 按默认 sql_mode 读的假设。
认出等价的比例:语料(SQLite,给表结构)169 对里 157 对,和改动前一样;PostgreSQL、MySQL 也不变;不知道是哪种库时
少认 1 对(same:quoted-names,双引号括起来的名字)。
2026-10-06 08:10:41 +08:00
ThearchyHelios
6767b5f2fc
SQL 差异说明收尾(第 1 轮):不靠约束改写、解析保真守卫、具体说法只在简单形状上说四类
...
- 去掉所有依赖表约束的改写:凭主键 / 唯一键去掉 LEFT JOIN、去掉 DISTINCT,COUNT(列) 当 COUNT(*),凭 NOT NULL 去掉
IS NOT NULL(_unique_key_bound、_constraint_rewrites、_not_null_source 删掉)。SQLite 的类型亲和、COLLATE、部分唯一
索引、MySQL 的隐式转换都会让「唯一」不成立,宁可认不出;约束只拿来让说明少说(schema_cache 的外键也读进来)。
- 解析保真守卫(_misread,按方言切词、按括号深度分层):MySQL / MariaDB / 不知道方言时字符串里的反斜杠转义、1_000 和
0o17 这类数字、挨着写的字符串常量、同一层里没加括号地混用比较运算和 IS / IN / LIKE / BETWEEN / 另一个比较、按位运算
(含 COLLATE)和别的二元运算混用、XOR、不知道方言时的 ||、MySQL 以外的 &&:不在语法树上判等价,只给兜底,text_diff
给原文的逐行对照;normalize_sql 逐词规范化时数字和紧跟着的名字算一个字面量。
- 具体说法重写(_Explainer):只在两边都是简单形状(一段 SELECT、实表、普通连接,没有派生表、合并、窗口函数)时说,
只说四类——底层表上的时间列不同、一边多一个只涉及一张表的列和常量比的筛选条件(另一边对这些列没有任何条件、不是
被蕴含的、按取值逐段算过不是恒为真)、顶层聚合函数或去重不同、关联的表或关联类型不同(后面没有内连接引用可空一侧);
每句先确认真会影响结果(条件恒为假、结果看不见行、参数在组里定死、只取一行、表结构的键 / 外键说明不起作用时不说),
别的一律兜底。
- 回归语料:第 3 轮补充之后的复验报的全部 blocker / major 收进 sql_pairs(_DIFF_R4、_SAME_HARD_R4、_SAME_SCHEMA_R4
执行;REGRESSIONS_R4、DIALECT_SAME_R4、CONSTRAINT_TRAPS_R4 只查结论,约束陷阱另造小库执行),执行比对复验者列的 9 个
改松后拦不住的反例也收进来;旧的具体说法测试按新规矩改写。
- 模块说明照实写:等价怎么判、不靠约束、保真守卫、只给兜底的情况、已知做不到的。
认出等价的比例(给表结构):169 对里 163 → 157(96% → 93%),少的 6 对都是要靠约束才成立的;不给表结构 148 / 156 不变。
2026-10-05 17:35:51 +08:00
ThearchyHelios
3e5fc7bc84
SQL 语义:去掉 _Usage._bases 里没用到的 import(pyflakes 报的,第 2 轮就留下的)
2026-10-05 15:14:03 +08:00
ThearchyHelios
c38177aea6
SQL 差异说明第 3 轮补充:只在一边被去掉、被按内连接比的 LEFT JOIN 不说成「另一边没有」「用 INNER JOIN」
...
规范化在一边认出了「这次 LEFT JOIN 有没有都一样」(去重之后不怕重复、按主键关联、只取可空一侧跳过空值的聚合),另一边
写法不同没认出来时,两边的规范形式一个有关联、一个没有(或者一个内连接、一个 LEFT JOIN),以前会照规范形式说「A 没有
关联 refunds 表」「A 用 INNER JOIN」,可原文两边都写着 LEFT JOIN。现在去掉的关联记在那一段上(_DROPPED_JOINS)、按内
连接比的记在那张表上(_NULL_BLIND),差异说明碰到就不说;表结构里有唯一键、又没用到的那张表,ON 写得认不出是不是按
唯一键关联(s.id IN (o.store_id))时也不说。补了三种写法在三种方言、给不给表结构下的测试。
2026-10-05 15:09:10 +08:00
ThearchyHelios
87f0979a90
SQL 差异说明第 3 轮补充:NOT 按德摩根律往里推;结果看行出来先后的一段不拆 BETWEEN、不去 MIN / MAX 里的 DISTINCT
...
- NOT (p AND q) 写成 NOT p OR NOT q、NOT (p OR q) 写成 NOT p AND NOT q(p、q 是条件时,三值逻辑下照样成立),在
外连接改写、唯一键判断之前也做一遍。复验者的「意思一样却说了具体的不同」压测脚本在本轮的按主键去掉 LEFT JOIN
上找到了:ON 写成 NOT (NOT (s.id = o.store_id) OR NOT (s.region = 'east')) 时认不出按主键关联,说成「B 用 LEFT
JOIN 关联 stores 表,A 没有」;往里推之后两边一样。
- 结果看行出来先后的一段不拆 BETWEEN、不去掉 MIN / MAX 里的 DISTINCT、不按德摩根律改写(一个 BETWEEN 和两个比较
估算出的行数可能不同,换一种扫描的先后,GROUP_CONCAT 拼出来的先后就跟着变)。
- 测试:德摩根律的等价改写进 _SAME_R3,NOT (a AND b) 对 NOT a AND NOT b 进 _DIFF_R3;「排除」那条说法跟着往里推
之后的写法改。
2026-10-05 15:06:48 +08:00
ThearchyHelios
f91615624d
SQL 差异说明第 3 轮补充:靠主键认出 COUNT(主键) 和带主键的 DISTINCT;结果看行出来先后的一段不再去重条件、去重排序键、换 COUNT
...
- 给了表结构时:这一段里一定不为空的列(主键、NOT NULL,不在 LEFT JOIN 可空的一侧)上的 COUNT(x) 就是 COUNT(*);
只有一张表、选出来的列包含它的主键(或者每一列都不为空的唯一约束)时 DISTINCT 去掉(_constraint_rewrites)。
- 在真实的 MySQL 8.0 上压测时看到:GROUP_CONCAT 那一段同一个条件写两遍,MySQL 按条件估算的行数变了、换了关联的
先后,拼出来的先后就不同。结果看行出来先后的一段(见 _Sensitivity)不再去掉重复的条件、重复的排序键,不把 COUNT(x)
换成 COUNT(*),外层条件不挪进这样的分组子查询的 HAVING;有 GROUP_CONCAT、窗口函数、既不分组也不聚合的列时不去掉
负数 LIMIT。差异说明里只差在重复写的分组键、排序键上的不说成不同。
- 测试:COUNT(主键)、带主键的 DISTINCT 进 _SAME_SCHEMA_R3;可空一侧的主键、不带主键的 DISTINCT 进 _DIFF_R3。
2026-10-05 14:45:39 +08:00
ThearchyHelios
4557e72c8e
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 子查询」的说法测试。
2026-10-05 14:32:07 +08:00
ThearchyHelios
08f75d4002
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'。
2026-10-05 14:19:55 +08:00
ThearchyHelios
02b08f9ad4
SQL 差异说明重做(第 2 轮补充):在真实的 PostgreSQL / MySQL 上复查,浮点数求和的一段不调换先后、外层条件只把聚合结果和「相等就是一样」的分组键挪进 HAVING;排列数先算再展开
...
为什么:第 2 轮的主体提交只在 SQLite 上执行比对。这次私起了 PostgreSQL 18 和 MySQL 8.0(复验者语料的那份数据),
拿复验者的全部语料和自己针对新改写造的 22 对重跑,抓到两处在真实的库上结果不同、却判了等价的:
- 套在子查询里的 UNION ALL 调换各支,外面 SUM:浮点数加的先后变了,1902.2 对 1902.2000000000003(PG、MySQL)。
- 外层 WHERE BINARY t.city = 'SH' 挪进分组子查询的 HAVING:MySQL 自己会把外层只涉及分组键的条件推进子查询的 WHERE
先筛行,不分大小写时和 HAVING 结果不同。
做了什么(backend/app/data/sql_semantics.py):
- _Sensitivity:PostgreSQL、MySQL 里对不知道是不是精确数(整数、定点小数)的值 SUM / AVG / STDDEV / VARIANCE,这一段
算「结果看行出来的先后」,连同给它供行的子查询、CTE 不调换来源、不排分组键、不并子查询。SQLite 不算(它用
Kahan-Babuska-Neumaier 求和,关联先后也不跟着 FROM 变;语料 120 对在 7 组数据上按精确值比没有差别)。
- 外连接按 WHERE 变成内连接、内连接的条件挪进 WHERE:三种库的优化器自己也做,执行先后不变,看先后的一段也照做;
只有 SQLite 写了 CROSS JOIN 的不挪。
- 外层 WHERE 挪进分组子查询的 HAVING 只挪聚合结果和「相等就是一样」的分组键上的条件;引用了好几次的简单 CTE 有
一处在看先后的一段里就不拆开(PostgreSQL 会先把它算出来存着)。只筛行选列的子查询照样并进求浮点数和的外层:三种库
自己也会并(PostgreSQL 的 subquery pull-up、MySQL 的 derived_merge、SQLite 的 flattening),执行先后一样(在两个
库上核对过结果一字不差)。
- 同名来源的排列先算个数再展开:十几个同名表时以前先把十几的阶乘个排列都列出来(一条 12 张表自连接的语句跑了两分钟
以上),现在超了上限直接按排好的先后。
- 模块说明照实写清上面几条和 SQLite 求和的假设。
测试:REGRESSIONS_R2 补这两处(PG、MySQL 的 UNION ALL 求和、MySQL 的分组键条件);DIALECT_SAME 里两对需要在求和的
一段调换关联先后的 PG / MySQL 改写移到 KNOWN_UNRECOGNIZED(只给兜底说明)。
验证(最终代码):tests/test_sql_semantics.py + tests/test_verify_agent.py 601 passed,tests/test_sql_equivalence.py
375 passed,全量 -n 8:7856 passed、11 skipped。等价改写 124 对认出 120 对(97%),不给表结构 117 对(94%)。复验者
vsadv 全部语料(928 次判定,SQLite / PG / MySQL 执行)无 blocker、判等价的没有一对执行结果不同,自己针对新改写的 32 对
同样;advfp 180 对等价对上具体说法 0 条;vsprobe 变异压测(种子 9、10)约 1.6 万对无误判等价、无异常;自己的压测
约 1.65 万条可执行,规范形式相同的组执行结果全一致。
2026-10-05 12:16:20 +08:00
ThearchyHelios
f16860f704
SQL 差异说明重做(第 2 轮):规范化只留能讲清为什么保持语义的几步,结果看行出来先后的一段不动来源;具体说法先确认两边真的不同、另一边真的没有,其余给兜底
...
为什么:第 2 轮复验抓到第 1 版规范化里一批不保持语义的改写(15 个 blocker),说明里一批假话、矛盾话(10 多个
major)。根子是借用了 sqlglot simplify 等现成改写、又把「相等」当成「一样」、没管结果看行出来先后的查询;说明这边
是先改名再比、只看这一段有没有那个条件。这一轮把改写收窄到每一步都讲得清为什么保持语义,说明这边每句话都先验证。
规范化(backend/app/data/sql_semantics.py):
- 去掉 sqlglot 的 simplify(Decimal 折叠 0.1 + 0.2、-(-x) 化成 x、三项加乘拍平合常量、BETWEEN SYMMETRIC 丢了
SYMMETRIC)、pushdown_projections、HAVING 挪进 WHERE(MySQL 不分大小写时同组的值各不一样)、只有一列的 IN 列表
写成 =(SQLite 里 a IN (b) 是 a = +b)。只留自己写的几步:AND / OR、比较两边、一个节点的加乘两边排序,NOT 套
比较翻过来,BETWEEN 拆开,IN 常量列表去重排序,COUNT(1)、MIN/MAX(DISTINCT)、IIF / IF 写成 CASE、CASE ELSE NULL、
恒真的 1 = 1 去掉,IS NOT NULL / NOT LIKE 各方言统一成 NOT (…)。
- 新增 _Sensitivity:取行(含有 ORDER BY 并列)、DISTINCT ON、窗口函数、GROUP_CONCAT 这类聚合、聚合里的散列、当成标量
的子查询,以及去重、分组、MIN/MAX 留下的值「相等却不一样」(MySQL 字符串、PG numeric、算出来的表达式)的一段,
连同给它供行的子查询、CTE,都不调换来源、不排 GROUP BY 键、不改 DISTINCT、不并子查询。SQLite 的逗号连接和 CROSS
JOIN 按原文分开(SQLite 不调换 CROSS JOIN 两边)。
- 列名表(CTE、子查询)写进里面那段的别名,实表别名带列名、递归 CTE(含 SQLite 不写 RECURSIVE、CTE 引用自己或后面的)、
BETWEEN SYMMETRIC、FETCH WITH TIES 不走规范化;FETCH FIRST 写成 LIMIT;USING 只在简单写法下写成 ON。
- EXISTS 改写前先查聚合(含 ORDER BY 里的)和 OFFSET;合并只调换最外层不取行的 UNION ALL(PG/MySQL 另加按位置排序、
各支列名一样的嵌套);后面有 RIGHT / FULL JOIN 时内连接不挪进 WHERE、开头的来源不调换。
- 子查询并进外层自己写(_merge_derived:只并内连接一侧、只筛行选列的),引用多次的简单 CTE 每处写回一份后整条语句
的别名重新改成互不相同的(压测撞到的:同一个 CTE 并进外层和相关子查询两处,里层别名遮住外层);外层 WHERE 只挪进
分组子查询的 HAVING(同一个值)。几个来源的 SELECT * 先写成各来源的 t.*(sqlglot 展开 * 时把实表排在子查询前面)。
- 解析丢了写法的不在语法树上判等价:0x10 和 x'10' 这类字面量、换了名字的类型名(SQLite 的 STRING 是数值亲和);
SQLite 不带表名、带双引号、表结构里没有的列名保留大小写和引号(找不到这一列时被当成字符串)。
差异说明:
- 换了表的一边照它自己的表说(以前把 B 改名成 A 的表,中文名、detail 归错表)。
- 新增 _Range:同一个对象只和常量比的条件按取值逐段算,留下的行一样的不说(IN 和 OR、冗余 IS NOT NULL、拆成两段的
时间、COUNT(*) > 1 和 >= 2);关联条件比相等关系的分组;聚合只在参数是列时说。
- 新增 _Usage:说「另一边没有这个条件 / 没有按时间筛选」之前,看另一边整条语句(含相关子查询、CTE、CASE、判空时连
聚合参数)是否用这一列筛选;条件子查询里不带表名的列没给表结构时不拿来说具体的。
- 子查询要外面写法一模一样(含 NOT、IN 左边的列)才配对,位置照实说「id NOT IN 的子查询」;EXCEPT 按先后逐支比;
order 只在最外层、没取行、没 DISTINCT ON / 窗口函数时说;LEFT 对 INNER 只在最外层、可空一侧没进 WHERE、结果不只是
可空一侧的聚合时说;DISTINCT 对同列 GROUP BY、MySQL 表名只差大小写不说成不同;互相矛盾的一对不说。
测试:tests/fixtures/sql_pairs.py 加 nums 表和一组专挑写法差异的数据(0.1 + 0.2、像数字的文本、'SH' / 'sh'),
第 2 轮反例收进语料(_SAME_HARD 20 对、_SAME_R2 11 对、_DIFF_R2 36 对进执行比对;REGRESSIONS_R2 21 对、
DIALECT_SAME_R2 6 对、KNOWN_UNRECOGNIZED 4 对各方言只查结论);执行比对共 7 组数据、372 对,标 same 而没认出的
不许有具体说法;test_sql_semantics.py 补第 2 轮的说法用例和 normalize_sql「只改参数」的用例。这些新用例在上一版
上有 109 处失败。
验证:test_sql_semantics + test_verify_agent + test_sql_equivalence 973 passed;全量 -n 8:7853 passed、11 skipped。
等价改写 124 对认出 120 对(97%),不给表结构 117 对(94%)。复验者的脚本:vsprobe 变异压测 6 个种子约 4.3 万对无
误判等价;在私起的 PostgreSQL 18、MySQL 8.0 上重跑 vsadv 全部语料无 blocker;advfp 的 180 对等价对上具体说法 0 条;
自己的压测 3 个种子各约 1.5 万条可执行,规范形式相同的组执行结果全一致。
2026-10-05 12:01:08 +08:00
ThearchyHelios
c55b1983f6
SQL 差异说明重做(第 1 轮):先判等价、再说差在哪——规范形式一字不差才算等价,具体说法只说有把握的,其余给兜底并附规范化后不同的行;等价结论拿 SQLite 执行比对守门
...
为什么重做:过去五轮按方面归纳任意 SQL,每轮修好一批又冒出一批(第 5 轮还回退过「只在 IN / EXISTS /
标量子查询里用到的 CTE 里的不同」),收不拢。改成先判两条 SQL 是不是一回事,不是的话只说有把握的不同。
做了什么(backend/app/data/sql_semantics.py):
- 新增 explain_sql_diff(a, b, dialect=None, *, schema=None, names=None, labels=("A", "B")) -> SqlDiff
(equivalent、items、fallback、text_diff、level)。两边各自规范化,每一步只做保持语义的变换;同一级的两个
规范形式语法树逐节点一样才算等价(不比生成的 SQL:sqlglot 生成 SQLite 时把 CAST(x AS DATE) 写成 DATE(x)),
否则一律不等价、条目一定不为空。三级:optimizer(sqlglot 的 qualify、eliminate_subqueries、merge_subqueries、
eliminate_ctes、pushdown_projections、simplify,给了数据源的 schema_cache 就按它认列、展开 SELECT *)、
light(不认列)、parsed(只统一大小写、空白、引号)。
- 不用的 sqlglot 规则:unnest_subqueries(OR 里的标量子查询改成 CROSS JOIN,子查询没有行时整条都没了)、
canonicalize(日期比较加 CAST,SQLite 里 CAST(… AS DATE) 取的是数字)、normalize(合取范式人读不懂);
simplify 去掉同一列上比较的合并、字符串常量比较、等式移项、DATE_TRUNC 改区间(取决于列类型、排序规则、方言);
merge_subqueries 不并外连接可空一侧算出来的列。
- 本模块自己的几步:去括号、ASC、OFFSET 0,COUNT(1)→COUNT(*),IN 列表去重排序,EXISTS 写成 SELECT 1,
DISTINCT↔GROUP BY,原样透列的子查询换掉,RIGHT JOIN→LEFT JOIN,WHERE 筛掉了没关联上的行的外连接写成内连接,
内连接写成逗号连接,起规范的名字(CTE 叫 cte1…、CTE 的列叫 c1…、别名按表名和层数,同名来源排列着试),
交换律成立的地方排序。
- 具体说法的门槛:两段读的来源对得上(一样,或只差一处说得清的)才逐项比时间列、筛选条件、聚合与去重计数、
选出来的列、分组键、HAVING、取行和排序、UNION 还是 UNION ALL;条件里的子查询两边各一个、外面一样的往里比
(第 5 轮回退的 IN-CTE 反例在这里说出来);原文是 LEFT / RIGHT JOIN、规范化成内连接或调了方向的照原文说;
牵涉 CTE 算出来的列、自己关联自己、说不准的子查询的不说具体的;两边给人看一样的不说。
- 兜底:一条具体说法都没有时给 aspect="other"「两条 SQL 的写法不同,未能归纳成具体差异,请对照原文」,只覆盖
一部分时追加「另有未能归纳的差异,请对照原文」,fallback 为真;text_diff 是 difflib 比两边 sqlglot pretty
的输出,最多 40 行。
- diff_facets 签名不变,交给 explain_sql_diff 比两份 facets 记下的原文,返回它的 items。normalize_sql 判
「只有参数不同」不变,补了测试。
守门(tests/test_sql_equivalence.py、tests/fixtures/sql_pairs.py):虚构库(users、stores、orders、
orders_archive、order_items、refunds、payments、shipments)上 5 组固定种子的数据加 1 组极端数据(空值、重复、
边界值、悬空外键、空表)。305 对 SQL(113 对已知等价、192 对不许判等价,含复验者以前造的、在这几组数据上结果
不同的 91 对)在不给和给表结构两种情况下各判一次:结果不同的绝不判等价,标 diff 的不判等价,换个先后问结论
一样。旧测试五轮复核里报过不同的 234 对、意思一样的 56 对(SQLite / PostgreSQL / MySQL)原样收进来只查结论。
压测(变异 + 等价改写,每个种子约 1.87 万条)时找到、已经堵上的不保持语义的改写:
- merge_subqueries 把外连接可空一侧的常量列并进外层;merge 时同一个 CTE 并进外层和相关子查询两处互相遮住;
- 没给表结构时 qualify 把 WHERE / GROUP BY / HAVING 里和列别名同名的名字当成别名(SQLite 先认列),把 SELECT
列表里和前面别名同名的名字展开(库里不认 SELECT 列表里的别名):这时不出 optimizer 这一级;
- UNION(去重)、INTERSECT 调换各支:去重留先出现的值(SQLite 里 2 和 2.0、MySQL 不分大小写时 'SH' 和 'sh');
UNION ALL 带 LIMIT、按名字 ORDER BY 的也要看哪支在前,SQLite 合并结果的排序规则取最左一支:现在只在调换
不改变结果时排;SQLite 从子查询读 UNION 的类型亲和;
- 一元加号在 SQLite 里去掉类型亲和、解析时却丢了;ORDER BY 别名换成表达式后撞上另一个别名;
- 没有 ORDER BY 的 LIMIT、聚合里既不分组也不聚合的列:调换来源、DISTINCT 写成 GROUP BY、RIGHT 写成 LEFT
都会改变实际取到的行,这样的一段不做;两边都写了 COLLATE 的比较不对调。
验证:tests/test_sql_semantics.py + tests/test_verify_agent.py 519 passed,tests/test_sql_equivalence.py
307 passed,全量 -n 8:7706 passed、11 skipped。已知等价的 113 对认出 111 对(98%),不给表结构时 109 对
(96%)。压测 4 个种子各约 1.5 万条可执行:规范形式相同的组执行结果全一致,能执行的规范形式结果和原文一致
(带 CAST 的不比:生成 SQLite 时写法变了),生成的等价改写认出 2113/2115;复验者留下的 1201 对里可执行的 440 对没有误判等价,结果相同的 302 对认出 276 对。
仍做不到的(只给兜底或认不出等价)见模块说明:IN 子查询和 EXISTS 互换、窗口函数前后按分区键筛、三项以上的
加乘换结合、和方言有关的同义写法、SQLite 里套一层子查询的 UNION、去重 UNION 调换各支、LATERAL、递归 CTE、
没给表结构时别名和列同名的。假设列没有声明非默认的排序规则(SQLite 比较两列取左边那列的,表结构里看不到)。
2026-10-05 09:28:08 +08:00
ThearchyHelios
007a13d72b
SQL 语义复核修正(第 5 轮):读同一张表的派生来源不再透成一张表、相关子查询分内外层、条件分组连接聚合按层计数,去重、UNION、最后出几行进比较
...
上一轮(3e0ab05)之后复验清单里还有一批用例返回空列表(A24 只报了一半)。最初的问题 1–3(paid / refunded 两个 CTE 换用哪一个的漏报、多一个 vip CTE 时配错、内连接换先后报出自相矛盾的两句)在第 3、4 轮已经修好,这次仍失败的是另外几类,根因各不相同:
读同一张表的几个派生来源:两个原样透出的 CTE(已付、未付,SELECT * FROM orders WHERE …)透过去,p.amount、o.amount 都成了 orders.amount,SUM、WHERE、时间列、ORDER BY、GROUP BY、关联条件、派生列的定义里用的是哪一份都分不出来(CTE 链、子查询套子查询同样);CTE 和它自己读的 orders 关联(父单、子单)也一样;同一个 CTE 关联两次时 _roles 设的 role 透过去就丢了。现在算完一段的来源后看各来源一路透过「只读一个来源」的派生来源最后读的是谁(_base),同一段里有几个读到同一个来源的,其中的派生来源标 opaque(_seal):through、时间列都不透过去,照写两边按结构配好的共同编号(已付配已付、未付配未付),给人看写名字(paid.amount),单看一条语句「只用到一张实表」时也照样写出是哪一份。时间条件给调用方对照数据目录的 detail 另一直换到实表(TimePredicate.table)。只有一个来源读那张表时照旧透过去。
相关子查询读外层那张表:EXISTS、标量子查询、LATERAL 里 c.parent_id = o.id 和 c.id = o.parent_id 都写成 orders.id = orders.parent_id(有子单还是有父单、之前还是之后有过订单分不出)。现在比较用的写法里外层的列在列名后面带「^」(_depth,往外几层带几个;带在列名上,排比较两边的先后只在两边表名一样时才受影响),给人看时内外读同一个来源的照写别名、留着表别名;两边各剩一个 EXISTS(NOT EXISTS)时配成一对说。
按整条语句去重:各段的条件、分组键、连接、聚合原来去重成一个集合,外层的 status = 'paid' 和 IN / EXISTS 子查询、关联进来的汇总 CTE、UNION 另一支里的同一个条件合成了一个,外层少了它报不出来(分组键、连接、聚合同理;聚合去重后只剩「多算了 SUM」一句,算出来的列又被压掉)。现在每一项记下在哪一层(Part / JoinFacet / Aggregate 的 scope,见 _layers:最外层、UNION 第几支、派生来源的共同编号、条件子查询「?」;只筛行的 CTE 和用到它的那一段同层,读同一张表的几个同段 CTE 各算各的层),按(层, key)去重;比的时候同一项在一边出现在几层算几个(_Compare.leftovers):同一层的先配,剩下的不管层也配(条件挪一层常是同一个口径),配不完的才报,另一边也有、只是层少了的说清是哪一层(「原流程在外层也只保留 status = 'paid' 的行,盲算在外层没有这个条件」)。条件子查询那一层多出来的不单说:外层那个条件已经报了,原来还会多出一句读起来像外层也这么筛的「只保留 state = 'done' 的行」。时间条件在列、范围都一样时也按层数比。
去重、合并、最后出几行:DISTINCT、UNION / UNION ALL 原来不进任何一面,分组键挪一层也看不出来。SELECT DISTINCT(这一段没有分组、聚合时)当这一段的分组键收:和按同几列 GROUP BY 一样的不报,一边在子查询里先去重报「去重」;各段的 UNION / INTERSECT / EXCEPT 记下合并方式和支数(SetOpFacet,同层的先配,挪了层的同样写法也配上);最后出来的行按什么分(Facets.grain:分组键 / 只出一行 / 逐行)在分组键都配得上、两边都汇总了时比(先在子查询里按用户分组、外层数用户数,对外层按用户分组)。
说清楚:自己关联自己、关联的方向反了,两边都写 a.amount 时说清各自的 a 是关联条件里出哪一列的那个(Part.roles,实表自连接同样)。
模块说明、Part / JoinFacet / TimePredicate / Aggregate / Facets / FacetDiff、unmatched / leftovers / joins / render 的文档按现在的做法改写,做不到的照实列出:读同一张表的几个派生来源同段时只认配好的编号(一边两个 CTE、另一边把其中一个换成关联实表再在 WHERE 里筛,算不同;同一个 CTE 关联两次对定义成两个一样的 CTE 也算不同);挪了层又多一层时挑哪一个说成多出来的有顺序约定;条件子查询那一层多出来的不单说;UNION 各支按位置配;不加表前缀引用外层的列认不出;LATERAL 里的相关条件两边各说一句;最后出几行只在两边都汇总时比;COUNT(DISTINCT x) 对数 DISTINCT 子查询行数照报不同(有空值时确实不一样)。
测试:复验清单里的用例逐个补成测试(三个方言,CTE 和子查询写法),另加意思一样、不该报的反例(关联换先后、改名、单个透出的 CTE、自连接换别名、相关子查询换别名、两边都两层、条件挪进汇总 CTE、分组写在 CTE 里、GROUP BY 对 DISTINCT、一样的 UNION、最外层 UNION 对包一层)。新加 132 项,其中 119 项在 3e0ab05 上失败,其余 13 项防误报的两边都通过;原有 192 项和 test_verify_agent 照旧通过,全量 7245 项通过。另拿测试里出现过的每条 SQL 在三个方言下和自己比(1051 次),都没有不同。复验清单最后一条只到「b_s」就截断了,看不到全文,按开头补了 DISTINCT 子查询对不去重、对 COUNT(DISTINCT) 的对照。
2026-10-05 06:11:40 +08:00
ThearchyHelios
3e0ab059b1
SQL 语义复核修正(第 4 轮):外连接按主表比、算出来的列按定义比、时间列透过 CTE 认、筛的先后照报、没用到的 CTE 不算,同名却不是同一个的派生来源说清各是什么
...
上一轮(db91c9e)给派生来源加了结构标识、按结构配对之后,复验还有一批用例返回空列表或者报得自相矛盾。按根因分几类改:
外连接:joins 第二遍按涉及的来源配对、不分方向,orders LEFT JOIN refunds 和 refunds LEFT JOIN orders 配成一对后类型、条件都一样,一句不说(db91c9e 引入的回归),RIGHT JOIN 对调、两个 CTE 对调、子查询写法同样漏。现在 JoinFacet 记下主表(keep)和可空那一侧(nullable),LEFT / RIGHT 按主表比:对调了报「原流程以 orders 为主表外连接 refunds,盲算以 refunds 为主表外连接 orders」,LEFT JOIN 和两边对调的 RIGHT JOIN 是同一回事、不再报成类型不同(原来列为做不到)。同一段 WHERE 里拿可空那一侧的列比较、IN、LIKE、BETWEEN、IS NOT NULL 的(JoinFacet.rejects),这次外连接按内连接比:盲算把可空一侧的条件写进 WHERE、LEFT JOIN 实际成了内连接的照报;IS NULL、OR、套在 COALESCE / CASE 里的不认。
算出来的列(Computed,新的 expression 一面):聚合只按去重后的 (函数, 参数) 集合比,SUM(p.total) - SUM(r.total) 对反过来、退款率分子分母对调报不出;派生来源里不是聚合的列不进任何方面,net.v 谁减谁、x.total 取的是付款还是退款合计、amount * 0.9 对 0.8、CASE 的口径、窗口函数的分区和排序、RANK 对 ROW_NUMBER 都是空列表。现在最外层选出来的列(先剥掉 ROUND、CAST、COALESCE 给默认值这类外壳)按算法比,用到的聚合一样、位置换了照报;派生来源里被外面用到、又不是原样透出一列的列按定义比。只差在聚合上、聚合那一面已经报了的,按透过去的那一列筛时间、时间那一面已经报了的,不再报第二遍。比较时加法、乘法的两项排好先后,DATE()、CAST(… AS DATE)、::date 统一成一种写法,免得写法不同被报成算法不同。
时间:时间条件的列透过「同一列套着日期函数、CAST、MIN / MAX」算出来的派生列,一直换到实表(TimePredicate.via 记下路过的列):日期表达式写在 CTE 里,按支付时间还是下单时间筛照样报出来,带上表名、列名给调用方对照数据目录(三个方言的 DATE()、CAST … BETWEEN、两个同名 CTE 对调后的 MAX(paid_at) 对 MAX(refunded_at))。
结构标识和配对:_Derived 里往里套着的派生来源原来一律写「@」,net 是 paid.total - refunded.total 还是反过来写出来一样。现在按结构写的 shape 写法里引用派生来源的地方写它自己的结构标识(里面那段按 shape 写出来再取摘要),配对之前比较的两边也按结构标识排先后;配对之后仍按共同编号排(配成一对的两个派生来源里面稍有不同时结构标识就不同,按它排会把同一个比较排成两个方向)。_align 同一级先看条件重合、再看名字:两边都叫 paid、一边筛已付一边筛未付时不再按名字配反。配成一对的派生来源里两边起名不同的列按定义配成同一列(total 对 sum_amt,原来列为做不到;_renamed),两边各剩一列时也配上,定义的不同照报。
筛的先后:派生来源取前几行(LIMIT、OFFSET)或算了窗口函数(这一列又不是分区键,含 DISTINCT ON)时,外面的条件不再透成里面那张表的条件;Part 另记一路透到底的写法(pierce)和挡住它的是什么(stage),两边只差在这里时报「原流程先算 t 的窗口函数、再按 status = 'paid' 筛选,盲算先按 status = 'paid' 筛选、再算 t 的窗口函数」「先取 t 的前 10 行、再筛」,时间条件同样。
没用到的 CTE:原来逐段收集连定义了没用的 CTE 里的条件也收,盲算定义了只筛已付的 CTE 却直接查 orders 报不出来,没用的 CTE 里多一个条件又会被报成多筛了一遍。现在只收从最外层一路引用到的段(_Statement._enliven);例外是两边都定义了同一个 CTE、一边把它关联进来一边没用到,没用到的那边照样收它,免得它里面的聚合、分组被说成一边独有(连接那一面已经说了关联了它)。
说清楚:两边同名的派生来源其实不是同一个时(t1 / t2、a / b 定义对调),「按 t1.total > 100 筛选」两边写法一模一样,读起来自相矛盾。Part 记下用到的派生来源各列的来历(origins,一直换到实表的列),给人看的写法依次退到一律写表名、带上来历(t1.total 即 SUM(orders.amount))、来历也一样(两个 CTE 只差里面的条件)时说「盲算的 paid 对应原流程的 open」。
自己关联自己:同一个 CTE、同一张表在一段里出现两次,两个别名按各自在关联条件里出的列区分(_Source.role),b1.total > b2.total 写反了报得出来,换了别名、换了关联先后不报(原来列为做不到)。CTE、子查询自己写了列名的(WITH t(n) AS …,递归 CTE 常这么写)按写的列名认列。
模块说明、Part / JoinFacet / TimePredicate / Computed / Facets / FacetDiff、unmatched、joins 的文档按现在的做法改写,做不到的照实列出:两边起名不同、各多出不止一列且定义都不同的列仍算两列;全换了的派生来源不配对;条件改得一个都不重合又各起了名字的同形派生来源只能按先后配;自连接两个别名在关联条件里出的列一样时分不出;三项以上的加法乘法换序、括号展开算不同算法;QUALIFY、GROUP BY 之后才筛的先后不认;外连接链里连带变成内连接的、套在函数里或写在外层 WHERE 的不认;不是 facets 算出来的 Facets 只能按位置编号比。
测试:复验清单里的用例逐个补成测试(各方言、CTE 和子查询写法),另加意思一样、不该报的反例(LEFT / RIGHT 对调、WHERE 筛主表、外连接链换序、0.9 * amount、DATE 对 CAST、按分区键在窗口前后筛、两边都先取前 10 行、列起名不同、自连接换别名换先后、没用到的 CTE)。新加 64 项,其中 48 项在 db91c9e 上失败,其余是防误报的,两边都通过;原有 128 项和 test_verify_agent 照旧通过,全量 7113 项通过。复验清单最后一条只到「WITH base AS (SE」就截断了,看不到全文,按开头和已知缺口一并处理了 CTE 自连接。
2026-10-05 04:58:17 +08:00
ThearchyHelios
db91c9eaf6
SQL 语义复核修正(第 3 轮):两边的 CTE、子查询先按结构配好对再比,不再拿「不写编号」的写法补配
...
上一轮给派生来源(CTE、FROM 里的子查询)只编了位置编号,编号错开时再按不写编号的写法(派生来源一律写「@」)补配一遍。复验发现三处问题,根因相同:派生来源没有结构上的标识。一是漏报:两个 CTE 都算出 total(付款合计、退款合计),编号一致时 SUM(p.total) 对 SUM(r.total)、WHERE p.total > 100 对 r.total > 100、ORDER BY、HAVING、差值写反,都被补配吞掉、返回空列表。二是误报:盲算多一个排在前面的 CTE(vip),原流程的 t(@1)按编号配给了盲算的 vip(@1),报出「关联 t 表的条件是 orders.user_id = users.id,盲算是 users.id = users.id」「盲算关联了 t 表」。三是误报:内连接换了先后(FROM paid JOIN refunded 换成 FROM refunded JOIN paid),编号跟着对调,比较的两边按编号排成两个方向,报出两条自相矛盾的话。
现在 diff_facets 先给两边每个派生来源算一份结构标识(_Derived):里面读到的实表(FROM / JOIN,往里套着的也算)、选出来的各列规范化后的定义式(total 是 SUM(orders.amount))、分组键、里面的条件、整段规范化后的写法和起的名字;按从严到宽几级一对一地配:整段一样,表和各列定义、分组一样,表和列名一样,表和名字一样,最后三级只在两边各剩一个时才配(只是表一样、只是名字一样、只是列名一样);同一级几对都配得上时先配同名的、再配条件重合多的、最后看先后,哪一级都配不上的不配,没用到的 CTE 也参加配对。配好之后第二条 SQL 的派生来源改用第一条里对应的编号(配不上的另起编号),facets 记下原文(Facets._origin,不进比较和 as_dict),按共同的编号重新分析一遍再比:比较两边谁在前、关联条件的先后都按共同编号排,不再受位置编号左右。_Compare 去掉了不写编号的补配(删掉 Part.anon),loose 里派生来源的编号照写,筛选按同一列配对时列也带编号。
连接:内连接、CROSS JOIN 按涉及的来源和条件比(JoinFacet.sides),不按哪一侧是关联进来的,实表的内连接换了先后也不再报;配成一对的两边名字不同时两个名字都说;一边独有的内连接、关联进来的那一侧另一边也有时,报它接上的另一头(FROM stores JOIN orders、另一边只查 orders:多出来的是 stores,不是 orders;Facets.sources 记各段 FROM / JOIN 的来源拿来对照,条件里子查询的来源不算),已经说了「关联了」的表不再另说一遍「用到了」。取行:CTE、子查询里的取行按所属派生来源配好的编号(LimitFacet.scope_key)配对,一边多一个限了行数的 CTE 不再和另一边的 CTE 凑对;LATERAL 子查询照写它的别名。筛选、HAVING:两边各自独有、读起来却一样的条件(p.total > 100 和 r.total > 100 都只读得出 total > 100)配成一对、改用带来源名字的写法说,不再各说一句。
模块说明、Part / JoinFacet / LimitFacet / Facets、unmatched、joins 的文档按现在的做法改写,做不到的照实列出(外层按名字引用的列两边起名不同算两列;读同一张表、各列定义一样的两个派生来源两边都改了条件又改了名时可能配反;全换了的两个派生来源不配对;自连接分不出两个别名;LEFT JOIN 对调成 RIGHT JOIN、内连接的单表条件写在 ON 还是 WHERE、三张以上不带条件的表换先后仍算不同)。测试补上三处问题的反例(CTE、FROM 子查询、改名又改先后的写法)和上面各项,原有 99 项照旧通过。
2026-10-05 03:36:12 +08:00
ThearchyHelios
d5e9ef39ae
SQL 语义复核修正(第 2 轮):CTE、子查询起的名字不再进比较,条件里的子查询选出来的列名也不算;派生来源按位置编号,编号错开时再按不写编号的写法配一遍
...
上一轮只去掉了条件里子查询的表别名,子查询整段写进外层条件的 key 时还带着各自起的名字:标量子查询、IN 子查询给选出来的列起的名字(AVG(amount) AS avg_amount),子查询里 FROM 的子查询别名(s 换成 agg),以及外层引用 CTE、FROM 里的子查询算出来的列时的来源名(派生来源的 key 就是别名)。CTE 改个名,外层的 total > 100 就被报成两边各有一个、互相矛盾的条件;双盲复算里盲算的名字本来就是自己起的,会被说成换了口径。
现在派生来源(引用 CTE 的表、FROM / JOIN 里的子查询和 LATERAL 子查询)按位置编号作比较用的 key:从外往里一层一层地编,CTE 在第一次用到的地方编号,写成 CTE 还是 FROM 里的子查询、CTE 定义的先后都不影响;给人看的写法照写名字。不拿里面那段查询的写法作 key:里面差一个条件,外层按它的列写的条件就全跟着变,同一处不同会报好几遍。条件里的子查询写进外层 key 时,引用 CTE 的表、子查询(含 LATERAL)、子查询自带的 CTE 定义都换成编号,只看取值的子查询(IN、EXISTS、标量子查询)去掉选出来的列名;FROM 里的子查询、CTE 定义选出来的列名留着,外层按名字引用它们。LATERAL 的别名原来没登记成来源,按「别名写错了」照写,一并改成派生来源。
编号只看位置:一边多出一个排在前面的 CTE(比如盲算先关联一个筛 VIP 的 CTE),后面的编号就错开了,同名的 CTE 反倒比原来按名字比时多报出两条自相矛盾的话。Part 加 anon(派生来源一律写「@」、不写编号),diff_facets 每个方面先按编号比,比不上的再按 anon 一对一地配,配上的不算不同;比较两边的先后先按 anon 排、分不出再按编号;连接先配完全一样的再按关联的表配,免得错开的两个 CTE 被配成一对、说成关联条件不同。编号对得上时照样分得清是哪个 CTE 的同名列(p.total > r.total 写反成 r.total > p.total 照报)。筛选条件配对改按列的比较写法(Part.column_keys,CTE、子查询的列写「@.列」),CTE 名不同、阈值不同时说成「按 total > 100 筛选,盲算按 total > 200 筛选」,不再拆成两条各说各的。
仍按名字比的只有 CTE、子查询选出来、外层按名字引用的列(一边叫 total、一边叫 sum_amt),写进了模块说明。
2026-10-05 02:37:33 +08:00
ThearchyHelios
a427b53874
SQL 语义复核修正:同一个口径不再因为子查询里的别名、HAVING 里的别名被报成不同;取哪几行逐段比,连同 OFFSET 和取前几行时的排序
...
条件里套着的子查询(EXISTS、IN (SELECT …))原来按外层的来源整个照抄进外层那个条件,别名原样留着:带别名和不带别名的同一条 NOT EXISTS 被报成两处筛选不同,双盲复算就会说盲算换了口径。现在每段 SELECT 在语法树上记段号(复制出来的子查询靠它找回原树上那一段算好的来源,相关子查询照样到外层找),改写时每一列按它所在那一段认,子查询里的表去掉别名;比较两边排序时也按那一段的来源排。HAVING 引用 SELECT 别名(HAVING s > 100)换成别名指的表达式,和 GROUP BY 别名一个读法,聚合函数里的同名列不换。
取哪几行原来只看最外层 LIMIT 的行数:CTE 里 LIMIT 10 和 LIMIT 100、OFFSET 0 和 OFFSET 10、取前 10 行时降序和升序都报成没有不同,取到的却是不同的行。Facets 加 limits,逐段收集(最外层、CTE、FROM 里的子查询;条件里的子查询已经整个写在条件里,不单算,免得报两遍)行数、跳过的行数,以及这一段的排序(ORDER BY 1、别名换成 SELECT 里的表达式);OFFSET 0 算没写,排序只在限了行的段里比。原来的 limit(最外层行数)照旧保留。
2026-10-05 01:46:55 +08:00
ThearchyHelios
f1c068d6a9
核查工具箱收尾:连接的方面补上另一侧(关联条件里出现的别的表,CROSS JOIN 写它之前的来源),双盲复算对照数据目录里的关系时用得上;只读 agent 收尾前先拿到收尾用的模型再预留金额,模型拿不到时不在每日账上白记一次调用;收尾消息的参数改名 limit_words,说清是哪个上限的说法
2026-10-05 00:42:02 +08:00
ThearchyHelios
031aae9343
SQL 语义:data/sql_semantics.py 给双盲复算、运行对比、反方审稿共用。normalize_sql 按方言解析、重新生成(去注释、统一关键字和空白、按方言转换不加引号的名字),再把字符串和数字换成占位符:只改了字面量的两条 SQL 规范文本相同(定时运行每期日期不同,不算模板变了),字面量按规范文本里的先后列出;解析不了退回逐词规范化,照样认得出只改了字面量。facets 把查询拆成表、连接(类型 / 关联进来的表 / ON 条件)、WHERE 合取式、时间条件(列 / 运算符 / 取值)、分组、聚合(函数 / 参数 / 去重)、HAVING、LIMIT:别名换成真实表名,比较两边排好顺序,GROUP BY 1 和别名换成表达式,CTE、子查询逐段收集,只透出一张表的 CTE 换回那张表的列,逗号连接写在 WHERE 里的等式算关联条件,相关子查询到外层找别名;解析不了带 parse_error,从不抛。diff_facets 每处不同给一句中文(「A 按支付时间 paid_at 筛时间,B 按下单时间 created_at」「A 排除了 is_deleted = 1 的行,B 没有」「A 对 order_id 去重计数,B 没去重」),两边叫法可以换成「原流程」「盲算」,列的中文名可以用 catalog_labels 从数据目录取;只换了一张表时说一次「查的是哪张表」,其余按不写表名的写法比。复用 sqlcheck 的 dialect_of、_conjuncts、_bare_column、_unparen
2026-10-05 00:22:51 +08:00