Repository navigation
{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 4, 2026 分诊(engine/services 车道 PM,会话
session_01Pbu27iNUfQCHeuS551Rqo7):挂needs-user-decision上交 —— TRUE / FALSE / 拒收三个候选各有成文依据且互相矛盾,拒收还意味着收紧 spec 声明合法的形状,属公开契约定调,PM 不代答。正文两轴分析已完整,PM 复核后同意其推荐序:拒收 > FALSE,不取 TRUE —— 拒收让 AI 写出的「选了字段没选操作符」形状在编写期响亮爆炸(与 #5041 顶层闸门同一先例,只是补进组合子内),FALSE 至少跟随两个参照实现且 fail-closed;TRUE 会把一个几乎必然是笔误的形状静默升格为全表。补一条 triage 输入:UI 筛选器「半填状态」是最可能的 producer(#5159/#5114 的 filter-builder 一族正在 spec 车道手里,拍板时可顺带核实)。拍板后四后端对齐 + case 进FILTER_LOGIC_CASES可与 #5239 同批执行。
Generated by Claude Code
维护者拍板:取 拒收
{ field: {} }(零个操作符的字段约束)在四个后端一律响亮拒收(INVALID_FILTER),不取 TRUE、不取 FALSE。needs-user-decision摘除。依据:它让 AI 生成的元数据在编写期就炸,而不是在某个后端上安静地多返回或少返回几行 —— 与 #5041 已在 driver-sql 顶层建立的先例一致,本次只是把同一道闸门补进组合子内部,顺带消灭 driver-sql 自身「顶层拒收、包进
$or就静默 TRUE」的内部不自洽。排期:串行等 #5146
本单与 #5146(engine 半边,NULL-safe
$not)改的是同一个文件的同一个函数(sql-driver.ts的applyFilterCondition),按同文件严格串行的纪律,本单待 #5146 的 PR 合入后下轮派发,不并行。保持pm:queue。给下一次派发的已定范围(避免重查):
- engine 半边(本车道):driver-sql 组合子分支补闸门;driver-memory
checkCondition、formulaevalField两处现有的 FALSE 改为同码拒收;四后端同一 error code,各包各自 pin。注意 formula 那条是 RLScheck的求值路径,改为抛出后落在 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775「求不出值 = 该次操作失败」的既定姿态上,PR 里要写明这条连带。 - spec 半边(spec 车道):该形状今天在
FilterConditionSchema的z.record半边是声明合法的,拒收 = 收紧已声明契约,需 spec 侧写明;case 进FILTER_LOGIC_CASES—— 建议与 [spec] FILTER_LOGIC_CASES 补空组合子的布尔单位元四条 —— 需与 driver-mongodb 的单位元归约同时落地 #5239、$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 的 spec 半边同批落。
Generated by Claude Code
- engine 半边(本车道):driver-sql 组合子分支补闸门;driver-memory
- added and removed
on Aug 4, 2026 认领 + 派发(engine/services 车道 PM,会话
session_01Pbu27iNUfQCHeuS551Rqo7)分支
claude/issue-5240-empty-field-spec-reject,专用 worktree/home/user/objectstack-issue-5240,域domain:engine。
文件面:packages/plugins/driver-sql/src/sql-driver.ts、packages/plugins/driver-memory/src/memory-matcher.ts、packages/formula/src/matches-filter.ts+ 三包各自测试 + changeset。串行前置已解除:#5146 的 PR #5296 已合入
origin/main(5aae79096,fix(driver-sql): $not 取反前先把操作数编译成全域谓词),applyFilterCondition的同文件同函数占用解除,本单可派。同日 churn 警示 ——
sql-driver.ts今天被改过两次,开工必须基于最新origin/main并逐条重核 issue 正文引用的现场:- fix(driver-sql): 空
$and/$or/$not按布尔单位元编译,$or: []不再返回全表 (#5134) #5243(SqlDriver.applyFilterCondition 丢弃编译成空的 $and/$or 子过滤器,而不是套用布尔单位元 —— 与同仓 matchesFilterCondition / driver-memory 相反 #5134):新增reduceFilterNode/reduceFilterKey结构性三值归约,空$or/$not→1 = 0。关键:该归约把带字段键的节点一律判为'clause'(不是'true'),这正是它当时刻意不替本单裁决的方式 —— 本单的闸门要接在归约之后的编译分支里,不要去改归约的判定。 - fix(driver-sql):
$not取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296($not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146):nullSafeNegationOperand及其逐算子极性表。注意它已经为本单留了位置:nullGuardForFieldSpec对entries.length === 0显式返回'none',注释写明「Guarding it would turn a shape that emits nothing into a liveIS NULLpredicate — i.e. would RULE on{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 from here」。本单改为拒收后,那段注释与分支要一并更新到新事实。
已定范围(维护者已拍板取「拒收」,不要重新论证 TRUE/FALSE):
- driver-sql 组合子分支(
applyFilterCondition的typeof value === 'object'分支)补闸门,与顶层assertCompilableComparand([spec]$field跨字段比较:spec 声明 + cel-to-filter 产出,但无任何 SQL 执行层实现 —— enforce-or-remove 裁决位 #5041)同码;顺带消灭「顶层拒收、包进$or就静默 TRUE」的内部不自洽。 - driver-memory
checkCondition、formulaevalField两处现有的 FALSE 改为同码拒收。 - 四后端同一 error code(
INVALID_FILTER),各包各自 pin。 ⚠️ 必须在 PR 正文写明的连带:formula 那条是 RLScheck的求值路径,改为抛出后落在 hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775「求不出值 = 该次操作失败」的既定姿态上 —— 这是一次可观察的行为变更(原本静默 FALSE 的 check 现在让该次操作失败),要如实写进 changeset 与 PR 正文,不要轻描淡写。
⛔ 不碰
packages/spec/**—— 收窄FilterConditionSchema与FILTER_LOGIC_CASES归 spec 车道(建议与 #5239、#5146 spec 半边同批)。engine 半边先落,契约收窄随后。
⛔ 不碰packages/services/service-analytics/**—— 那是 #5297 的在飞文件面(它的read-scope-sql.ts是独立的 RLS 降解器,不属于本单的「四后端」)。
⛔ 不碰content/docs/releases/**。
Generated by Claude Code
- fix(driver-sql): 空
实现完成 —— draft PR #5327(会话
session_01Pbu27iNUfQCHeuS551Rqo7,分支claude/issue-5240-empty-field-spec-reject)按拍板取拒收。四个后端(driver-sql / driver-memory / formula / driver-sqlite-wasm)对
{ field: {} }一律抛INVALID_FILTER/ 400,消息指名位置(如filter.$or[0].stage)。现场核对:正文那张四路径表有一行需要修正。
driver-memory那行指的是memory-matcher.ts,而它不是驱动的实时路径 ——InMemoryDriver.find只 import 了getValueByPath,过滤实际交给 mingo,后者把{ a: {} }读作「字段深等于空文档」(实测:特意播的a: {}那行被选中)。所以那条路径给的不是 FALSE,而是另一个 filter,只在正常数据上看着像 FALSE。两个过滤面都补了闸门,否则用户真正走的那条仍然静默。其余三行逐条实测成立。闸门位置与派发单有一处偏差(已在 PR 正文说明):接在归约的校验遍历里(与
assertFilterNode并列),而不是归约之后的编译分支 —— 因为{ $or: [ { a: {} }, {} ] }会被{}这个 TRUE 析取项在编译前短路掉,编译分支永远见不到它,拒不拒收就取决于兄弟节点。归约的判定一行未改(字段键仍是'clause')。#5296 的nullGuardForFieldSpec空 spec 分支与注释已随本单删除。RLS
check连带已如实写进 changeset 与 PR 正文:{ $or: [ { a: {} }, {…} ] }与{ $not: { a: {} } }这两类策略,原本 FALSE 被吸收 → 写入放行,现在抛出 → 写入失败。即有原本能成功、现在会失败的写入。验证:driver-sql 786 / driver-memory 320 / formula 357 / sqlite-wasm 237 / objectql 1882 / plugin-security 731 / service-analytics 555 / service-storage 283 / plugin-sharing 347 全绿,typecheck + eslint 干净;反向验证(stash 源码)四个后端的新用例全部失败,原文在 PR 正文。已 merge 最新
main(04fab5eaf)后重跑一轮。spec 半边未动:实现现在比已声明契约更严,收窄
FilterConditionSchema与补FILTER_LOGIC_CASES仍归 spec 车道(#5239 / #5146 同批)。范围外新发现,已另开 #5324(unassigned):driver-memory 的实时路径根本不支持
$not—— 原样透传给 mingo,而 MongoDB 没有文档级$not,任何位置的$not都抛无code/status的MingoError(500 形状);CEL!expr降下来的 RLS scope 在该驱动上直接报错。至今没测出来是因为FILTER_LOGIC_CASES对该后端只经参考匹配器跑,没穿过真驱动。
Generated by Claude Code
- added a commit that references this issue
on Aug 5, 2026 18 remaining items
- added 3 commits that reference this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 15, 2026 - added a commit that references this issue
on Sep 24, 2026 - added a commit that references this issue
on Sep 29, 2026 - added 5 commits that reference this issue
on Oct 7, 2026
修 #5134(driver-sql 的空
$and/$or/$not单位元)时按纪律核对「编译成空」的全部成因,发现这一处不属于本单范围的分叉。按 Prime Directive #10 单独记在这里,unassigned。现象
{ a: {} }—— 一个字段,后面跟零个操作符。FilterConditionSchema的非递归半边是z.record(z.string(), z.unknown()),所以这个形状是声明合法的。同一个 filter,同仓四条路径给三个答案:{ a: {} }的答案driver-sql,顶层sql-driver.tsapplyFilters的 plain-map 分支 →assertCompilableComparandINVALID_FILTER/ 400(#5041 加的闸门)driver-sql,组合子内($or/$and/$not的分支里)sql-driver.tsapplyFilterCondition的typeof value === 'object'分支formulamatches-filter.tsevalField:if (keys.length === 0 ¦¦ …) return falsedriver-memorymemory-matcher.tscheckCondition:落到JSON.stringify(value) === JSON.stringify(condition)driver-sql自己内部就不自洽:同一个{ a: {} },写在顶层被响亮拒收,包进一层$or就变成静默的 TRUE。为什么这是个 bug 而不是洁癖
在
$or里,一个「什么都不产出」的分支会被 knex 连同分组一起丢掉,于是编译成
(b = 2)—— 而按{}是 TRUE 析取项的算法(#5134 刚刚为空节点确立的),它应当匹配全部行;按 formula/driver-memory 的 FALSE 判定,它应当只匹配b = 2。两种读法结论不同,而当前 SQL 的结果是碰巧和后者一致、却是经由「丢弃子句」而非任何一种语义得到的 —— 和 #5134 修掉的那个成因完全同源。为什么 #5134 没顺手修
因为两个候选答案各有依据、且互相矛盾,这是语义定调不是实现细节:
{}(零个键的节点)正是这么定的,{ a: {} }是它的直接推论。check用的就是它),driver-memory也给 FALSE。两个「参照实现」当前一致地说 FALSE。#5134 的实现刻意不碰它:归约函数把带字段键的节点一律判为
'clause'(而非'true'),所以这次的单位元修复既没有把{ a: {} }悄悄升格为「匹配所有行」,也没有替它做任何裁决 —— 行为与修复前逐字节相同。代码里留了注释指向本单。建议
三选一,需维护者/spec 车道拍板;拍板后四个后端一起对齐,并把该 case 补进
FILTER_LOGIC_CASES(见 #5239,那一单已经在扩这张表)。个人倾向 拒收:它让 AI 生成的 metadata 在编写期就炸,而不是在某个后端上安静地多返回或少返回几行 —— 与 #5041 已经在 driver-sql 顶层建立的先例一致,只是把同一条闸门补到组合子内部。若嫌收紧契约代价大,次选 FALSE(跟随两个参照实现,且方向 fail-closed)。未验证的部分:我没有找到今天真的会产出
{ field: {} }的 producer(UI 筛选器里「选了字段还没选操作符」是最可能的来源,未实测)。严重度请 PM 按 triage 定,不代表我判断它低。关联:#5134(同函数、同「编译成空」家族,已修空组合子)、#5041(顶层那道闸门的由来)、#5239(一致性表扩条)、#3774。