ThearchyHelios
a78affa2f1
目录修改提案落在已驳回的项上时,改前写出被驳回的原值:plan_patch 交回的 before 带上驳回的值、before_status 写 rejected(只改给人看的信息,算改后、算状态照旧按没有这一项,保存时照旧换成人工确认的新值);助手流的提案卡片、表助手的提案卡片和待保存的修改写「已驳回:<原值>」,和导入预览「目标里这一项已经驳回」一致;check-catalog-loop、check-stream 补对应检查
2026-10-14 14:07:15 +08:00
ThearchyHelios
eb8d73b6da
表助手不再把数据源的起草要求当成使用者的原话:交给模型的背景里起草要求标明「不是这次对话里使用者说的话」,系统提示词和 propose_changes 的说明写清 reason 里引用的原话只能是这次对话里使用者的消息,依据来自起草要求时写「起草要求写着:……」
2026-10-14 14:07:15 +08:00
ThearchyHelios
20bef14520
保存目录修改提案时依据随项保存:表助手、助手流里「建议更新数据目录」的提案经 /catalog/{table}/patch 保存,写了依据的项备注记成「依据:……」(超出目录文字项的上限 500 字截断),没写依据的照旧;值不变、保存即确认的项和沿用了测量备注的关联关系,依据写在原来的备注前面(关联关系的剖析备注再剖析时整段换掉,放在前面才留得住)。表详情里人工确认的项有备注时也看得到:表级的项写在值下面(同有效记录条件的说明),列的一格、关联关系悬停看;check-catalog-loop 的假服务端照服务端记下依据,补保存后看得到依据、人工确认项的说明两处检查
2026-10-14 14:07:14 +08:00
ThearchyHelios
fc25c6367c
起草任务写进目录的项,修改时间取那一次写入的时间:宽表逐段落盘的每一段、整张表收尾各取一次当时的时间,不再整个任务都记成开始的那一刻(以前只在任务开始时取一次、截到秒,比任务的开始时间还早,也看不出哪一段是什么时候写进去的);补一条用假时钟逐段核对的测试
2026-10-14 14:07:14 +08:00
ThearchyHelios
6baebe821a
合入数据目录闭环第一步(后端 B):表助手(表详情页对话,流式返回,成批提出经校验的修改提案,只在开了剖析的源上经只读守卫跑查询、结果先遮);审阅队列(按使用次数、风险、来源排序,批量确认驳回逐表乐观锁,表被哪些运行用过,审阅进度);数据目录导出导入(先预览,确认和驳回的照带、冲突保留目标、推断只填空缺、对不上的跳过并写原因);一次修改上限提到 200 项
2026-10-14 10:16:53 +08:00
ThearchyHelios
d3bd80211a
数据目录闭环第一步(后端 A)复核修正:没收下的有效记录条件不算失败,在起草任务逐表的 log 里记一句,接口看得到(以前只进服务端日志)——没通过校验的写原因(「有效记录条件里不能有子查询,模型提议的这一条没有收下:……」),剖析核对排除 0% 或 100% 的写数到了多少行;只在请模型写表级的项的那段里记,一张表最多 10 句。CatalogDraftTask 加 log 列(存量任务回填 NULL,接口给空列表)
2026-10-14 10:00:10 +08:00
ThearchyHelios
7383877ee7
数据目录闭环第一步(后端 A)复核修正:模型提议的有效记录条件存整理过的写法——原文照旧(NOT IN、IS NOT NULL 不被改写),只把列换成表结构里的真名、不带表名、按数据源的方言要加引号的加引号,剖析核对的就是存下的这一条。以前存的是模型的原文,核对时却按真名加了引号:PostgreSQL 上混合大小写的列("OrderStatus")存成 orderstatus <> 'X',一跑就报列不存在,SQL 检查还会让模型照抄。列名先照原样对表结构,只差大小写的几列写不清是哪一列的不收;加了引号超过 500 字的不收(写进去会让整张表写入失败);不合规的说法用界面上的叫法「有效记录条件」
2026-10-14 09:59:52 +08:00
ThearchyHelios
9c30bf0307
数据目录闭环第一步(后端 A)复核修正:起草提示里「已确认的目录项」不再带遮罩列的码值,人工确认过的也不给(码值就是这一列的取值),开没开剖析都一样;这一列别的确认过的项照给。和新加的码值背景、表助手的背景、导入目录的口径一致
2026-10-14 09:59:35 +08:00
ThearchyHelios
a490365bcf
数据目录闭环第一步(后端 A)复核修正:按比例止损只看每次调用的最终结局。服务方嫌额度太大退回 8192、结构化输出不支持或回了说明文字改走纯文本、429 / 5xx 退避重试之前的那次照旧记进用量和提醒,不进止损的窗口——它不花额度,也说明不了模型写不出来;重试用完仍出错的那一次照常算。以前额度上限 8192、不支持 json_schema 的 OpenAI 兼容网关上,并发的头几次调用全是这种重来,第一次额度用完就凑满 10 次里 7 次,那一段直接记没写出来、不再拆小
2026-10-14 09:59:19 +08:00
ThearchyHelios
813522ec7b
表助手:待保存的修改不设上限、有效记录条件按起草的规矩核对。宽表上逐列补中文名、含义,几轮下来待保存的修改常常超过一次保存的 200 项,以前请求里超过 200 项就 422、对话用不了;现在交多少都收,背景里只列最近的 200 项(界面上新的提案接在后面)并写明另有几项,码值照全部待保存的修改合并。模型提的有效记录条件是 SQL:按 sqlcheck.valid_filter_problem 核对(一个条件、只用这张表的列、不能有子查询、聚合、窗口、分号,只能用几个函数),sqlcheck 里还没有这条规矩时至少核对用到的列都是这张表的、都在表结构里、没有子查询,不过的不交给界面、原因交回模型;系统提示里先写明这几条写法
2026-10-14 09:52:37 +08:00
ThearchyHelios
9b4663afa8
导入核对有效记录条件用到的列:有效记录条件是表级的一项、却写着列名,按列跳过的规矩拦不住它。按目标数据源的方言解析出条件里用到的列,不在目标的表结构里、是别的表的列、带子查询(没法核对子查询用到的表和列)、解析不了或不止一句的跳过并写原因;人工写的条件不限函数,列名大小写和目标不同、带本表表名(文件里的写法、目标的写法都认)的照带。核对用的 filter_columns_problem 放在 catalog_transfer,表助手在没有起草的校验规矩时也用它
2026-10-14 09:52:13 +08:00
ThearchyHelios
864e63dadd
审阅队列与进度按约定改:field_label 写给人看的完整位置(「表的有效记录条件」「列 status 的码值」「指向 members 的关联关系」,和表助手提案、导入冲突的说法同一个函数),列级的项另带 column;排在这里的原因写「被 N 次运行用到」,数的是运行(同「用过这张表的运行」的 total),同一次运行里查了几次、使用次数比运行次数多时两个都写(「被 2 次运行用到,使用 5 次」),table_usage 和优先级照旧按使用次数(同目录清单的「使用 N 次」和排序);进度里的常用 20 张表和起草的默认范围是同一批(使用次数从多到少,用到的不够 20 张时按表结构的顺序补齐),top 只给 tables、items_total、items_confirmed,all 只给约定的四项;表助手背景里写这张表被几次运行用到、查询几次
2026-10-14 09:51:49 +08:00
ThearchyHelios
87b6cb4090
数据目录闭环第一步(后端 A):宽表收尾那次写不进去时,写进目录的段数照库里逐段写进去的记
...
整张表收尾的写入一直撞上别人刚改过(写入冲突)时,最后一段没进目录:parts_written 不再按引擎写出来的段数虚报,留着逐段写入时记下的数
2026-10-14 08:35:44 +08:00
ThearchyHelios
4647fc9ff3
数据目录闭环第一步(后端 A):防错事实的提议——起草时提议有效行过滤和去重规则
...
- 提示词:表级的项多要两项,valid_filter(有效行过滤,condition 写一段 SQL 条件、只用这张表的列,函数只用 UPPER、LOWER、TRIM、COALESCE)和 dedup(去重规则,rule 写一句话),各附一句 reason;写清只在表结构、注释、码值能看出来时才写,拿不准、没有就不写;业务日期照旧。结构化输出的约定、纯文本那条路的格式说明、只写表级的项那一行都带上这两项
- catalog._from_model:两项记模型起草、推断,依据写进 note(「依据:……」);有效行过滤按数据源的方言解析(sqlcheck.parse_valid_filter),不过就丢掉、日志记一条,不算这张表失败;只写了一段文字的也认
- 起草引擎:表级那段写完核对有效行过滤。开了数据剖析的源经剖析的只读守卫数排除比例(catalog_profile.check_valid_filter),排除 0% 或 100% 的不收,收下的 note 写上「剖析核对:排除 3.2% 的行(共 12,345 行)」;没开剖析、连不上的照收,写明没有核对的原因;目录里人工确认、驳回过的不再查库
- 开了数据剖析的源:目录里记下的码值(剖析取过的取值和已知含义)交给模型做背景,要写表级的项的给全部列的、只写几列的给这几列的,遮罩的列、驳回的码值不给,已确认的不重复
- 测试核一遍 SQL 检查的规矩:起草来的有效行过滤是推断,漏了它只是提示;人工确认以后才是警告,驳回以后不报
2026-10-14 08:32:44 +08:00
ThearchyHelios
7c3484adba
数据目录闭环第一步(后端 A):起草遗留——宽表逐段落盘、按比例止损、块进度、停止后的说法、同步起草与任务互斥
...
- 宽表逐段落盘:每写出一段(表级那段、每个列分段)就把这张表到这时拿到的内容并入目录(catalog_jobs.write_part,这一次合并不删任何东西);整张表写完(ok、partial 都一样)再按完整覆盖的规则收尾一次,只删已经不在表结构里的列的模型项(write_draft 对 partial 也照删)。停止、服务重启时已经写进去的段留着;写过段的表照旧记 cancelled,model_error 写「已停止:已写进目录 N 段,剩下 M 段没起草」(中断写「已中断:…」)。新增、更新、删除的计数是各次写入的合计
- CatalogDraftTask 加 parts_written(已写进目录的段数)、chunks_total / chunks_done(这张表实际分成的块数和已结束的块数,段被缩小的上限分开、拆成两半后会变多;不用模型时为 null),升级时补列
- 止损按比例:这次起草最近 10 次调用有 7 次以上没写出来时,之后的段不再拆小、每块只问一次(不补问漏掉的列、截断后不再补剩下的、空壳不再原样再问,拆出来还没问的块也不再问),直到比例降下去;去掉会被偶尔一次成功清零的「连续 2 段」「接连超时」两条。结构化输出只给空壳的,这次起草还没换纯文本问过的照旧问一次;额度被拒、结构化输出不支持、网络错误这些通信上的重来照旧
- 同一张表接连 3 个原始段都一项没写出来:剩下没开始的段跳过不问,原因写「这张表接连 3 段都没写出来,剩下的 N 段已跳过,可以换一个起草模型后只重试这张表」
- 原因里「已拆小重试仍没写出」只跟在真拆过的那几段后面说:说到的段只有一部分拆过时写「其中第 3 段已拆小重试仍没写出」
- 目录清单的 last_draft 加 blank_columns_from(最近一次结束于 ok / partial 的用模型起草记下的留空列数;之后有起草写进了新内容时为 null)和 parts_written
- 同步起草和后台任务双向互斥:同步起草进行期间同一个源建任务、重试、导入(ensure_idle)、再开一次同步起草都 409 catalog_draft_busy(进程内登记)
2026-10-14 08:31:35 +08:00
ThearchyHelios
447c66649c
数据目录闭环第一步(后端 A):有效行过滤的校验和剖析核对
...
- sqlcheck.parse_valid_filter / valid_filter_problem:按数据源的方言用 sqlglot 把有效行过滤解析成一个条件,只能引用这张表的列(不带表名或带这张表的表名,不分大小写),不能有子查询、聚合、窗口函数、分号、注释、模板占位、参数占位、算术,只允许比较、IN(值的列表)、BETWEEN、LIKE、IS NULL、AND / OR / NOT 和 UPPER / LOWER / TRIM / COALESCE,不超过 500 字,至少用到一列;不认识的数据源类型不收
- sqlcheck.render_valid_filter:把条件按方言重新写出来,列换成表结构里的真名、按剖析查询的规矩加引号,拼进核对查询用
- catalog_profile.check_valid_filter:开了数据剖析的源经剖析的只读守卫(guard.check(readonly=True)、单条时限、查询次数上限)发一条查询,只取条件里的列、先取前 max(抽样键值数, 整表统计行数上限) 行,数出总行数和被排除的行数;排除 0% 或 100% 的不收,收下的写「剖析核对:排除 3.2% 的行(共 12,345 行)」,只数了前若干行的写明按前几行统计;没开剖析、数据源停用、条件里有遮罩的列、视图、取值拼不进查询、查询失败或超时照收,写明没有核对的原因
2026-10-14 08:30:14 +08:00
ThearchyHelios
8c270987bd
表助手:绑定工具出错(模型不支持工具调用之类)也在流里照实交一条错误事件,不让流半路断掉
2026-10-14 08:28:30 +08:00
ThearchyHelios
77ddbe96ea
表助手的背景在宽表上不至于太长:这张表的目录最多列 600 项(表级的项、关联关系在前),其余写明还有几项没列出;导入时界面直接交来的文件原文(JSON 文字)也认,不是 JSON 的 422;导入冲突说明里的状态用界面的叫法
2026-10-14 08:26:05 +08:00
ThearchyHelios
65369b8a6e
表助手:SDK 不再自己重试(同起草,网络错误、429、5xx 只由表助手在还没输出时重试,退避 3、10 秒);测试补上取模型的规格(起草设置、保留思考、额度 32768 / 8192、单次时限、不重试)和两个工具在 OpenAI 兼容、Anthropic 模型上都绑得上;query_snapshot_tables 不进 catalog 的 __all__(按模块属性取用,少和起草那边的改动撞在一起)
2026-10-14 08:23:08 +08:00
ThearchyHelios
a5d9ee0a16
表助手:表详情页里和使用者对话完善一张表的数据目录(POST /catalog/{table}/assistant,SSE:正文增量、只读查询、修改提案、完成、出错);交给模型表结构、目录现状(每项的来源和状态)、关联关系(含别的表指过来的)、剖析结论、起草要求、使用次数、最近一次起草留空的列和界面上待保存的修改,遮罩列只给列名;提案按 plan_patch 核对(公式不收、列名必须在表结构里、遮罩列不给码值、已确认的同值不提),待保存修改里的码值并进新提的码值,只交给界面不写目录;只读查询只在开了数据剖析的源上给,经剖析的只读守卫和单条时限、每次请求最多 5 条、50 行交给模型,结果和报错先遮再交;模型取法、说法同起草(用不了 409),保留思考、额度 32768 可退回 8192、单次时限同起草,还没输出时网络错误和 429、5xx 重试,等待时发 SSE 注释保活,停止时正在进行的调用跟着停;PATCH 一次最多 200 项(预览和保存)
2026-10-14 08:20:02 +08:00
ThearchyHelios
2298cf848a
数据目录导出导入:导出为「<数据源名>-数据目录.json」附件(全部状态的项、起草要求);导入先预览再写入,表名、列名按表结构的写法再忽略大小写对(带 schema 前缀的去掉前缀再对),关联关系两端换成目标的写法并重算编号;已确认、已驳回的项照带(来源、状态不变),目标里已确认、已驳回而不一样的保留目标的并记冲突,推断、有确证的只填空缺;表结构里没有的列、两端对不上的关系、目标遮罩列的码值、不合规的项跳过并写原因;逐表乐观锁、一张表写不进去不影响别的;有起草任务时 409,文件格式不对 422,没有表结构 409;起草要求只在勾选且目标还没有时写入
2026-10-14 08:06:45 +08:00
ThearchyHelios
adc56da6cf
数据目录审阅队列:推断的项按「使用次数权重 × 风险权重 × 来源权重」排好(有效记录条件、去重规则、业务日期、关联关系最高,模型起草、命名推断比库注释优先),带排在这里的理由,可按影响数字的项 / 名称和说明、按表筛选和分页;批量确认、驳回、撤销审阅逐表按版本乐观锁,同一张表的几条一次写入,一张表冲突不影响别的表;表被哪些运行用过(新的在前,取法同使用次数,抽出 query_snapshot_tables 共用);审阅进度(常用 20 张表、全部、影响数字的项)
2026-10-14 07:59:51 +08:00
ThearchyHelios
7bc7fc685f
起草按复核修正:读回复出错也记进用量;补问时列出了、字段留空不算没写出来;留空的列看目录里已有的;一项都没写出来的宽表逐段写范围;提醒照约定的说法
...
- 读回复时出了意料之外的错:这次调用照样记进用量(算一次出错),再交给 _guarded 记这一块没写出来;以前 _take 抛异常
时这次调用不进 calls、token、failed_calls,也不进提醒的窗口。校验(catalog._from_model)里该是文字的地方写成了
数组、对象的(列名、表类型、业务日期的列、度量类型,columns 不是数组的)直接丢掉、不抛异常:实测列名写成数组时
抛 TypeError,整张表记「起草这张表时出错」。
- 补问时请写的列都列出来了、字段留空:表上算拿不准(记留空),用量和提醒里也不再算一次「只给空壳」没写出来;以前
提醒会因此劝人换模型,真正该做的是补充起草要求。
- 留空的列不算目录里已经有中文名或含义的(不论来源:以前模型起草、按「重新起草不丢内容」留下的,库注释,人工;驳回
的不算有);写入时(write_draft)再按并入目录之后的结果筛一遍,起草进行中有人填了的同样不算。以前照界面的建议补了
起草要求再起草,模型这次没再给的老列又被说成「拿不准、留空了」,提示一直消不掉。
- 一项都没写出来的宽表同样逐段写上列的范围:「这张表 95 列,分 4 段起草,第 1 段(表级的项)、第 2 段(c001 到
c032,共 32 列)……都没写出来:原因」;只有一段的表照旧只写原因(这一段就是整张表)。
- 提醒照约定写「建议在「设置 → 偏好设置」的「起草模型」换一个模型、缩小起草范围,或者停止这次起草」。
2026-10-13 21:25:59 +08:00
ThearchyHelios
84405977b6
起草引擎按复核修正:每个原始段连同分开的各块一共 8 次;一直超时的模型几轮就停;止损后纯文本不再原样再问
...
- 封顶管到原始段:段大小减半以后,新开始的段按上限分开的各块(窄表重新拼成的各组、单表均分的各块)共用这一段的
调用次数(_Budget 在 _work 里按原始段建,_start、_guarded 接过去),不再每块各有 8 次。以前上限缩到 10 列后一直
只给空壳,4 张窄表拼成的 40 列段调用 20 次,最坏是「段列数 / 上限 × 8」。止损的计数(连续几段没写出来)同样按
原始段数。
- 超时的两半同时问:带表级项的前一半不再先写完才问后一半(一次超时就是一个时限,40 → 20 → 10 串着问要好几个时限);
额度用完、只给空壳的照旧先写前一半、后一半拿它做背景。
- 超时另有一条止损:拆到每段 10 列(MODEL_MIN_PART_COLUMNS)的调用接连超时 2 次(MODEL_GIVE_UP_AFTER),超时就和
段大小无关了,后面超时的不再拆小,超时拆出来、等名额时还没问的块也不再问(次数退回);有一次调用写出来就恢复。
只管超时:额度用完、只给空壳照旧拆小。原因后面补「拆到每段 10 列仍然超时,已停止拆小重试,建议更换起草模型」。
用假模型量:每次都超时的 40 列表从 8 次调用、约 7 轮(420 秒时约 49 分钟)降到 6 次、3 轮;首轮同时起草的 3 段从
24 次、约 8 轮降到 12 次、4 轮;40、20 列超时而 10 列写得完的照旧拆到 10 列全部写出。
- 止损以后本来能拆小的段只给空壳:已经走纯文本的不再原样再问(约定只对拆不开的段再问一次);结构化输出的照旧换
纯文本问一次。
- resolve_draft_model 的说明照实写:超时不由引擎重试,当作这一段太大、拆小再问。
- 止损的说明改成文字(_Why.halted),两种止损各说一次。
2026-10-13 21:25:27 +08:00
ThearchyHelios
1e1aa3f64a
起草引擎:提醒第一次出现时记一条日志(最近几次调用有几次没写出来、原因);模块说明补上补问时才列出、字段留空的列也算拿不准
2026-10-13 20:30:02 +08:00
ThearchyHelios
975dde1146
起草引擎按验收修正:列分段均分、超时拆小、段大小随模型减半、每段 8 次封顶、提醒、超时与没写出来的计数、模型留空的列、不插队、原因写清哪几段哪几列
...
- 均分:超过 40 列的表,列分段数是列数除以 40 向上取整,各段列数平均(相差不超过 1),不再留下 2 到 8 列的尾段(实测
会思考的模型常对这种小段只给空壳);表级那段、窄表拼段、parts_total 不变。
- 拆不开的段(不到 10 列)只给空壳:再问一次,结构化输出换纯文本,已经走纯文本的原样再问,仍是空壳才记没写出来。
- 超时当作这一段太大:不再原样重试同样大小的段,按额度用完的办法拆小再问;网络错误、429、5xx 仍原样退避重试。
- 段大小随模型调整:一次起草里额度用完或超时累计 2 次,之后新开始的段列数上限减半(40 → 20 → 10,最低 10),新开始
的段超过上限就先按上限均分开(多表的段按表重新拼);任务记当前的上限 part_columns。
- 封顶:一段(连同它拆出来的、补问的、重试的)最多调用 8 次,到了次数还没写完的记没写出来、原因写清。止损照旧。
- 提醒:最近 6 次调用有 4 次以上没写出来(额度用完、超时、空壳、出错),任务的 notice 写一句提醒(次数、原因,建议在
「设置 → 偏好设置」的「起草模型」换一个模型、减少表或停止);之后最近 6 次里少于 2 次就清掉。
- 用量:进度回调 usage 换成 call_done(CallReport:用量、没写出来的原因种类、当前的提醒和列数上限),任务的用量加
timeouts、failed_calls;超时的调用 token 照实记 0。
- 留空的列:请模型写了、模型也列出了,中文名和含义却都没给(或者给的都没通过校验)的列记在 ModelOutcome.blank,逐表
记 blank_columns 和最多 5 个列名 blank_sample,任务上是合计,目录清单的 last_draft 也带列数;表的状态不因此变。已确认
的、库注释已经说清的列不算。补问时把漏的列列出来、字段却留空的,算留空,不再记成「漏了这些列」。
- 公平排队:宽表的表级那段写完,它的列分段排到已经排着的段后面(先来先服务),不再插到队首让窄表一直等。
- 原因:几段因为同一个原因没写出来时说「这几段」,同一段拆出来的几块仍是「这一段」;只有真拆小过才补「已拆小重试仍
没写出」(截断后只补剩下的不算);每段写上列的范围,例如「第 5 段(列名 到 列名,共 33 列)」,窄表写出是哪几列。
- 库:catalog_draft_jobs 加 timeouts、failed_calls、part_columns、notice,catalog_draft_tasks 加 blank_columns、
blank_sample,旧库启动时补列(存量回填 0 和空)。假模型加 hang(头几次调用一直不回,用来模拟超时)。
2026-10-13 20:30:02 +08:00
ThearchyHelios
645064fe47
数据目录重新起草不丢内容;库注释就是名字本身或标识符写法的不当中文名
...
- 合并(merge_notes)只改模型起草这一个来源的「覆盖即删」:模型这次对某一项没给值(留空,或者给的值没通过校验、在
_from_model 里就丢掉了),以前模型起草的项留着,只有给了合格的新值才替换。只有这一轮完整起草过(covered 含 llm)、
而这一项说的列已经不在表结构里(列级的项看列名,业务主键、业务日期看它们指着的列)才删;write_draft 把表结构里的
列名交给合并。注释、外键、命名推断、剖析各自的规矩不变,人工确认、驳回的不动。删除数因此只在真删时才有。
以前整库重跑一次,模型这次没给的几百项全被删掉,有的表覆盖率从八成多掉到五成,表仍记「已完成」。
- 库注释(comment_role):和表名(列注释和列名)相同(忽略大小写、下划线、连字符、空白,表名带 schema 前缀的也比
不带前缀的),或者是标识符写法(只有字母、数字、下划线、点,不带空格),不当中文名、也不当说明,不记进目录;交给
模型的表结构里照旧有这条注释。以前这种注释短、不带句读就当中文名,库注释的级别又高于模型起草,模型起草的中文名
永远进不了目录。已有目录不另做迁移:起草到那张表时,旧的那条随这一轮的注释删掉,模型起草的中文名就能进来。
2026-10-13 20:30:02 +08:00
ThearchyHelios
4857c578f6
起草的止损和公平:连续 2 段(MODEL_GIVE_UP_AFTER)拆到底仍一项没写出来,这次起草后面的段不再拆小重试,没写出来就直接记下,原因后面补「模型连续 2 段都没写出来,已停止拆小重试,建议更换起草模型」;有一段写出来就恢复拆小。每次起草同时只起草 3 段(MODEL_CONCURRENCY),一段连同拆小、补缺做完再取下一段,宽表的表级那段写完,它的列分段排到最前面一段一段起草;同时在等全进程名额的调用每次起草最多 3 个,几次起草(不同数据源的任务、同步起草)交替排队,后开始的不再等前一个排着的上百段全部调完。实测一直只给空壳的 294 列宽表从 168 次调用降到 81 次,20 张 120 列的宽表一直用完额度从约 900 次降到 122 次
2026-10-13 16:46:52 +08:00
ThearchyHelios
c3105bba4f
起草引擎按失败原因处理的三处修正:结构化输出回了一段说明文字(不是 JSON、也没停在额度上,例如网关接受了 response_format 却没照做)不再因为带着思考就当成额度用完去拆,也不直接判失败,这次起草后面改走纯文本、这一段换纯文本问一次(纯文本另带格式说明);只有停在额度上、或者正文为空而有思考才算额度用完。补问之后整列没列出来的列记没写出来(「模型的回复里漏了这些列,补问一次仍没有给出」),表是 partial、covered 不含 llm,以前起草过的待确认模型项不再被删(以前补问只给空壳也记 ok,还把这一轮没给的旧项删掉);列出了、字段留空的照旧算拿不准;补问时整张表还一项都没有的,照只给空壳的规矩拆小、换纯文本,不直接记失败。几处没写出来的原因按基础原因去重,拆小重试过的在最后补一句,不再出现同一句说两遍。假模型照提示词把请写的列都列出来(字段留空是拿不准),要模拟漏列的传 list_all=False
2026-10-13 16:42:45 +08:00
ThearchyHelios
ec6838dc10
起草模型的 SDK 不再自己重试:ModelSpec 加 max_retries,build_chat_model 一直传到 SDK 客户端(Bearer 鉴权换上的 Anthropic 客户端也带上);数据目录起草建的大、小两个模型都是 max_retries=0,429、5xx、网络错误、超时只由起草引擎按失败原因重试(最多 2 次)。以前 openai、anthropic 的 SDK 各自先重试 2 次,叠在引擎的重试上,一段最多发 9 次请求,报错却写「已重试 2 次」,网关限流时还放大流量。不写 max_retries 的调用照 SDK 默认,配方 AI 起草不动
2026-10-13 16:40:07 +08:00
ThearchyHelios
bf0619a03c
目录清单的 last_draft 改在库里挑:每张表最近一次起草任务里的那一项用窗口函数取,起草进行中清单页反复刷新时不再把最近一百个任务的逐表记录全读出来
2026-10-13 15:38:27 +08:00
ThearchyHelios
b988a5ae18
起草引擎和后台任务的几处收紧:提示词要求请写的每一列都列出来(拿不准的字段留空、列不漏),免得模型留空的列再被补问;纯文本那条路的格式示例改用「表类型」「度量类型」占位,不让模型照抄示例值;从正文取 JSON 时依次试多个括号作开头,说明文字里带括号、外面多包一层都认得;用量记在单次调用的时限之外,落库慢了不会把已经回来的调用算成超时;后台任务收尾写库出错只记日志;补上从正文取 JSON、任务开始时模型已用不了(整体 failed)的测试
2026-10-13 15:36:24 +08:00
ThearchyHelios
c1a70ecf38
数据目录起草改成后台任务:新表 catalog_draft_jobs、catalog_draft_tasks 存任务和逐表状态,同一个源同时只有一个排队或进行中的任务(接口先查、部分唯一索引兜底);POST /catalog/draft-jobs 建任务(202),GET /draft-jobs/active、/latest、/{id} 读任务,/cancel 停止(排队的直接停,进行中的停掉正在进行的模型调用,已写进目录的不回滚),/retry 只重试失败、只写出一部分、停止了的表;同源 409 catalog_draft_busy、起草模型用不了 409 catalog_draft_model_unavailable、已结束 409 catalog_draft_finished;每张表一结束就并入目录,进度、计数、用量随时落库;服务启动时把没做完的任务标成中断、不自动续跑,关停时同样;目录清单每行加 last_draft,顶层加 guidance、active_job;数据源级的起草要求存在 options 里,经 PUT /catalog/guidance 维护(最多 2000 字,表名恰好叫 guidance 的整份提交照旧),数据源接口不显示、整份回写 options 时留着,拼连接串时摘掉;本次说明最多 1000 字,任务记下起草要求和本次说明的快照,同步起草也带上数据源的起草要求
2026-10-13 15:33:00 +08:00
ThearchyHelios
6b1f63a9dc
数据目录模型起草换成分段、按失败原因处理的引擎(data/catalog_draft.py):每次最多 4 张表、40 列,窄表按顺序拼段,宽表拆成「表级一段 + 每 40 列一段」,列分段带着表注释和表级起草结果做背景,最后合并照旧逐项校验;思考和输出用完额度、只给空壳时拆成两半再问(最少 5 列、最多 3 层),拆不开的空壳换纯文本问一次,写出来了后面都走纯文本;被截断的 JSON 先捡回已完整的表和列、只补剩下的;缺了的表和列只补缺的、补一次;服务方嫌额度大时退回 8192、结构化输出不支持时改走纯文本,都管到这次起草结束;网络错误、429、5xx、超时退避重试最多 2 次;单次调用 420 秒;全进程最多 3 个调用同时进行;报错写原因和办法(第几段、共几列没写出来、可以只重试这张表);提示词加上起草要求一段(数据源说明、起草要求、本次说明,规矩在前、冲突以规矩为准)和「思考时不必逐列草拟」;同步起草改用新引擎,写入抽成每张表一结束就能调的 write_draft
2026-10-13 15:19:06 +08:00
ThearchyHelios
b940b27e24
起草模型设置:新增设置组 catalog_draft(接入、模型),点名的接入不存在或已停用时保存回 422;数据目录起草和按配方导入的 AI 起草共用一处取模型(起草模型 > 助手的模型 > 默认接入),取自起草模型时报错照实说起草模型那一项;数据目录起草不再关思考,额度 32768,服务方嫌大时退回 8192(之后都用小额度),单次调用时限 420 秒交给 SDK;配方 AI 起草的额度、思考设置不动
2026-10-13 15:00:38 +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
3c51c40e9d
点名的模型接入:发布前检查给每一处配修复,提示指到界面上真能改的地方,复核发现的几处漏查、误查补齐
...
多数节点(模型调用、Agent、报告撰写、多 Agent 协作、条件分支、结构校验、知识检索)、协作成员和全图默认的接入在检查器里没有下拉框,以前点名的接入坏了只能去设置里建一个同名接入。现在门禁接受接入表(governance.lint_for_publish 的 providers,接口层读好传进来,同数据目录 SQL 检查器),点名接入的问题成了门禁的一部分,自动修复给每一处一条要人选的修复:改用一个启用的接入,或不指定接入;选的接入不提供这一处写着的模型时模型一并去掉。一处一条(修复 id 带字段,复核只认那个字段),协作节点自己的接入和成员的接入各修各的。校验和发布检查的提示按位置写:有下拉框的说「在节点中换一个接入」,别处说「在发布前检查中改用其他接入」;剩下的只有接入问题时交给助手的说明也改成指向这些修复。
复核发现一并修:核查节点自己的开关关着、结构校验不许重试时不再查(执行时根本不取模型,以前会挡住发布);报告没写裁判配置时查全图默认里的那一份(以前放行,正式运行却全部未裁判);接入名不是字符串时查助手模型的接口不再 500,设置接口对门控、助手两组的这类值回 422 带字段;助手、表格导入、数据目录起草用的模型点名的接入不存在或已停用时照实说是哪个接入、去哪里改(以前说「尚未配置完成」,停用接入的下一步丢了),删掉两处已走不到的分支。
测试补齐:结构校验让模型修复、知识检索让模型重排、按需裁判各路径的不存在与已停用,助手、定时正式运行、裁判的两种情况,反方审稿节点上点名的接入;修复的候选、应用、拒收和图级默认。
2026-10-06 15:14:26 +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