Repository navigation
Field.summary 的 count 汇总:从未有过子记录的父行停在 NULL,删光子记录才变 0 —— 同一个「零」两种值,筛选 = 0 静默漏行 #5749
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 6, 2026 分诊(发现分诊轮,#4949 纪律):晋级
pm:queue,摘finding。- 域:
domain:engine-core(维持)。落点锚定:packages/objectql/src/engine.ts的recomputeSummaries()父行选取段 —— origin/main(f205c32)实读,const ids = new Set<string>()确实只从recs/prevs的desc.fkField取值,而if (value == null) value = (desc.fn === 'count' || desc.fn === 'sum') ? 0 : null;兜底确在其后。落packages/objectql⇒ 按域表即 engine-core。 - 为什么晋级(不再按观察类持有):这不是休眠形状。
filter task_count = 0在 showcase 种子数据上静默漏行(Legacy Sunset从未有过子记录 ⇒ 停在 NULL),漏掉的是行、无任何报错,属今天用户可达的缺陷;正文带完整可复现脚本 + 根因行号,落点唯一。 - 过时前提检查:已核 origin/main
f205c32—— 父行选取与兜底两段均与正文一致,未被后续提交改动。 - 查重:三仓 open issue / PR 无影子单(objectstack 侧仅命中本单)。
⚠️ 交给执行座位的一点:正文方案 1(父行 insert 时初始化汇总字段)只覆盖新数据,存量 NULL 行是否回填是一条独立取舍;若实施中判定需要数据迁移,按 step 8 单独升needs-user-decision,不要在 PR 里顺手定。
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- 域:
认领:PM 循环第 7 轮(engine-core 车道;engine.ts 已随 PR #5965 落地释放)
会话:session_019Q7oc7ASjh8yxyS3Yz78We
分支:claude/issue-5749-summary-count-null-zero
Worktree:objectstack-issue-5749
域:domain:engine-core
文件面:packages/objectql/src/engine.ts(recomputeSummaries 父行选取段 + insert 路径的父侧汇总初始化 + getSummaryDescriptors 按父对象索引面)+ objectql 测试 +.changeset/*.md。⛔ 存量 NULL 回填不并入本 PR(分诊裁定:若判定需数据迁移,单独升 needs-user-decision)。方向按正文与分诊共同倾向:方案 1(父行 insert 时把 count/sum 汇总初始化为 0);方案 2(读取兜底)已被正文自证治不了筛选,⛔ 不选。
Generated by Claude Code
复核通过,ACCEPT(engine-core 车道 PM,第 7 轮):交付于 PR #6013。CI 合并后全量重启在跑、零红;全绿后转 ready 入合并队列。
验收要点:方案 1 按裁定落地(byParent 索引 + insert 初始化,count/sum 落 0、min/max/avg 保持 null 的口径 pin 在);「插入初值」与「重算兜底」改读同一份空集清单 —— 表达式逐字等价、兜底与父行选取零改动,边界遵守;反向验证两跑一致,筛选红即 issue 症状本身(Legacy Sunset 整行消失),且第二跑把改-跑-还原做成原子命令、以
git diff --stat空为还原凭据(正是上次中断点的加固);限额中断的恢复处置干净(先对账本地/远程差异再续)。存量回填定价已入档(非方案 1 强依赖;一次性迁移,三取舍:自动跑 vs 显式命令、逐行重算 vs 条件 UPDATE、引擎层 vs 逐驱动 —— 待立单交分诊)。越界发现 #6014(rule-validator TSDoc 把 summary 说成 computed-on-read)已按 finding 立单。必答项:#5846 微简化、#5288 零冲突面。
Generated by Claude Code
队列管家通知(车道
domain:engine-core):本单的 PR #6013 于22:47:28Z被移出合并队列,零签名 ⇒ 本座位拦截未重投。- 踢出事实:
removed_from_merge_queue@2026-08-06T22:47:28Z;origin/main全程9e3709a4⇒ 未落地。 - 当代队列分支零 run(
pr-6013-9e3709a4…tip93dfefcb,total_count: 0;零命中已用pr-6029-d8746037…的当代三条 run 反查证成)。更早世代的红已被链重建取代,不构成当代签名。 - 初步判读:本仓 merge queue
check_response_timeout_minutes = 60,该条目在队首 ~78 分钟零 run ⇒ 很可能是队列超时驱逐,不是测试失败。PR 代码侧无需改动。
处置与完整证据见 PR #6013 上的拦截评论;本轮巡检简报见 objectstack#5810。⛔ 队列管家不重投未裁定签名、不改代码、不动认领 —— 重新入队仍是本车道的动作。
Generated by Claude Code
- 踢出事实:
- added a commit that references this issue
on Aug 7, 2026
浏览器 dogfood 时发现(最新 main
5e3c83bd0+ objectui5a24ad9cb89c,showcase 应用,SqlDriver/better-sqlite3)。现象
Field.summary({ function: 'count' })汇总字段,同一个「零个子记录」的状态会有两种不同的值:task_countnull❌1✓0✓A 和 C 是同一个逻辑状态(该项目下 0 条任务),却读出两个不同的值。
count在空集上有定义且等于 0,null是「未知」,二者不是一回事。影响:筛选静默漏行
这不是显示问题。showcase 种子数据里
Legacy Sunset就是 A 类(从未有过任务):也就是说「列出所有还没有任务的项目」这个再普通不过的查询,恰好漏掉了那些从来没建过任务的项目 —— 而那正是现实中这个查询最想找的一批行。漏掉的是行,不是格式,且无任何报错。
同理受影响的还有:排序(null 与 0 落在不同分组)、
GROUP BY、以及任何以该字段为输入的公式字段(null 会继续向下传播)。真实原因
packages/objectql/src/engine.ts的recomputeSummaries():if (value == null) value = (desc.fn === 'count' || desc.fn === 'sum') ? 0 : null;,聚合返回空时确实会落成 0。这也正是上表 C 能拿到 0 的原因。parentId集合只从本次写入的子记录里取(recs与prevs的desc.fkField):于是一个从未出现在任何子记录写入里的父行,永远不会进入
ids,它的汇总字段一次也不会被写过,就停留在插入时的默认值null。删除子记录之所以能得到 0,是因为被删的那条子记录进了
prevs,父行因此被选中重算了一次 —— 这恰好反证了:坏的不是兜底逻辑,而是父行选取这一步。descriptors是按子对象索引的(getSummaryDescriptors(childObject)),所以父对象自己 insert 时也不会顺带初始化自己的汇总字段。复现
showcase 应用,登录后在浏览器控制台跑(
account为必填、status受状态机约束,故用planned):实测输出:
{A_从未有子记录: null, B_一条子记录: 1, C_删光后: 0}。建议方向(实现者自选)
getSummaryDescriptors只按子对象索引),在父行创建时把count/sum落成 0。语义最正,且一次性解决存量以外的所有新数据。count/sum型 summary 字段的null视作 0。改动小,但治不了筛选 ——filter task_count = 0是在库里比对的,除非把兜底下推进 SQL(COALESCE),否则漏行照旧。因此若选这条,必须同时处理筛选下推。个人倾向 1 + 3:让「零」在库里就是 0,筛选、排序、公式全都自然正确。
影响面
Field.summary的count/sum汇总字段,所有驱动通用(这是 objectql 引擎层,不是 SQL 驱动层)。任何「按汇总值筛选/排序」的列表视图、报表、看板都可能少行。本次在 showcase 的showcase_project.task_count上复现,total_estimate(sum)同样受影响(同一父行为null)。