fix(driver-mongodb): 空 $and/$or/$not 归约成布尔单位元,非 filter 节点先响亮拒收 (#5239) - #5323
Conversation
…tity, refusing non-nodes first (#5239) `translateFilter` passed combinator arrays through verbatim, and MongoDB answers an empty one with neither TRUE nor FALSE but a third behaviour: it refuses the query (`$and/$or/$nor must be a nonempty array`). So `{$and: []}` and `{$or: []}` reached find/count/updateMany/deleteMany as a server error carrying no ADR-0112 code, while driver-sql (#5134), driver-memory and formula all answered them as identities. Replaced with the same STRUCTURAL three-valued reduction: reduce the whole tree to true/false/clause first, then emit. Empty `$and` becomes TRUE (no condition); empty `$or` becomes FALSE and emits a real zero-row condition (`{_id: {$in: []}}`) — emitting nothing would be `{}`, which find/updateMany/ deleteMany read as EVERY document, the opposite answer. Every `$and`/`$or` array emitted is therefore guaranteed non-empty. Shape rejection lands in the same change and runs BEFORE any identity: measured on main, `{$or: [new Date()]}` translated to `{$or: [{}]}` (every document) and `{$or: 'x'}` / `{$not: null}` translated to `{}` (every document). updateMany and deleteMany translate the same `where`, where that is data loss rather than a wrong row count. Non-nodes now raise INVALID_FILTER / 400 naming the position; the gate judges by PROTOTYPE, since Date/RegExp/class instances satisfy `typeof x === 'object'` while enumerating empty. spec is documentation only: FilterConditionSchema's contract TSDoc now states the NULL-safe `$not` semantics ruled in #5146, and filter-logic-conformance.ts records the measured matrix for the three ruled-but-not-yet-enrolled case families. The four FILTER_LOGIC_CASES rows #5239 asks for are deliberately NOT added: read-scope-sql and the analytics filter-normalizer, both enrolled backends, refuse empty combinators fail-closed by design and pinned test, which contradicts the identity ruling — escalated as #5322. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
PM 验收(session_01ErbEDVAg1No9gdg1pgDAGB)——通过。
进入串行落地链链尾(当前序:#5304 → #5306 → #5308 → #5318 → #5319 → #5321 → #5314 → #5312 → 本 PR)。轮到时由 PM 通知重建 worktree 同步 + 全量重生成 + 兄弟断言,再翻 ready 并 arm auto-merge。 Generated by Claude Code |
…ity-batch Textual conflict: packages/plugins/driver-mongodb/src/mongodb-filter.ts — both sides rewrote translateFilter/translateCondition (#5239 reduction vs #5329 array-dialect deletion + #5368 $null gate/path threading). Resolution keeps both: main's array refusal and path threading, this branch's three-valued reduction and shape gates; the three helpers both sides defined (unsupportedFilterError, describeFilterOperand, safeShapePreview) are de-duplicated onto main's copies. Semantic reconciliation the textual merge could not see (AGENTS.md s10): main's #5347 $null comparand gate sat in the emitter, and this branch's reduction makes emitters skippable by a boolean identity — { $or: [ {}, { stage: { $null: 'yes' } } ] } would have translated to match-all while driver-sql refuses it. The gate's load-bearing copy moved onto the validating walk (reduceFilterKey), mirroring driver-sql's #5368 placement; the emitter arm keeps its local check. Pinned in mongodb-null-comparand-refusal.test.ts (three identity-sibling fixtures). Fixture triage: the 'legacy array dialect is untouched' pin in mongodb-filter-boolean-identity.test.ts pinned a dialect #5329 deleted — replaced wholesale with the surviving boundary ([] = absent filter = match-all, non-empty array refused before the reduction runs). The reduceFilterKey field-arm comment on { field: {} } is recalibrated to current main: #5327 gated the shape on the other four backends; this driver remaining the one still answering it is now tracked by #5376. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
本 PR 的 spec 半边是契约文档,机械合并会把 base 时代的论断带上 main; 逐条对当前 origin/main 实测后校订: - FilterConditionSchema 的 NULL-safe $not 合规段:read-scope-sql 已由 #5326 对齐(#5297 关闭)、filter-normalizer 已由 #5335 对齐(#5325 关闭),七个面全部一致 —— 「尚未合规、指向 #5297」改写为已闭合的事实。 - 「Deliberately NOT declared here」:空组合子单位元由「两立场对峙、 上交 #5322」改为「#5322 已拍板取单位元,实施在 #5365(排在本 PR 之后 合入);main 上两个 analytics 编译器今天仍拒收,故本 PR 仍不在此声明, 声明随 #5365 翻正」;{ field: {} } 由「无后端设闸」改为「#5327 已闸 四家,driver-mongodb 是唯一还在作答的后端(#5376)」。 - filter-logic-conformance.ts 族 2/3 状态行同步重测:族 2 的后端阻塞 已清零,唯余 fixture 工作;族 3 的四家闸门已落,阻塞改为表形扩展 + mongodb(#5376)。族 1 段落一字未动 —— 由 #5365 在其同步轮删除, 已约定分工。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
…bee2) 合并 origin/main 后重建时由 gen:schema 写出(先 commit merge 再跑生成, #5370 的锚点倒退陷阱按序避开):baseRev 28ad90e → cdfbee2,随锚点带入 #5312 的 api/ApiEndpoint 键面。check:generated 9/9 up to date, check:authorable-surface 绿。非手改。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
同步轮(2026-08-05,接力,会话
|
| 位置 | 原论断(base 时点) | 校订后(main 实测) |
|---|---|---|
filter.zod.ts NULL-safe $not 合规段 |
read-scope-sql / filter-normalizer「仍发裸 NOT(…),tracked by #5297」 | 七个面全部一致:read-scope-sql 由 #5326 对齐(#5297 已关),filter-normalizer 由 #5335 对齐(#5325 已关);段落改记「缺口已闭合」 |
filter.zod.ts「Deliberately NOT declared here」 |
空组合子单位元「两立场对峙……上交 #5322(未裁定)」 | 「#5322 已拍板取单位元(2026-08-04);实施在 #5365(排在本 PR 之后);main 上两个 analytics 编译器今天仍拒收空 $and/$or,故声明随 #5365 翻正,不在本 PR」 |
同上,{ field: {} } 半句 |
「#5240 拍板拒收但无后端设闸」 | 「#5327 已闸 driver-sql / sqlite-wasm / memory / formula;driver-mongodb 是唯一还在作答的后端,新立 #5376 追踪」 |
filter-logic-conformance.ts 族 2 |
「两编译器仍裸 NOT,tracked by #5297」 | 后端阻塞清零(#5326/#5335),唯余 fixture 工作(FILTER_LOGIC_ROWS 无可空列,需七个 harness 同步声明) |
filter-logic-conformance.ts 族 3 |
「无后端设闸,repo 四个答案」 | #5327 已落四家;mongodb 仍作答(#5376);进表阻塞改为表形扩展(expectRejection)+ spec 侧收窄 |
filter-logic-conformance.ts 族 1 段落(含 @175d789 矩阵) |
— | 一字未动,由 #5365 在其同步轮删除(已约定分工) |
mongodb-filter.ts reduceFilterKey 字段 arm 注释 |
「ruled REJECT but not yet gated in any backend」 | 「#5327 已闸四家;本驱动唯一还在作答(#5376)」;归约判定一行未动(字段键仍 'clause') |
PR 正文第 1、2 节的 @175d789 矩阵与立场叙述保留为历史记录(在当时为真;时间线见上表与 #5322)。
验证(merged 态,全部 flock 串行 + --maxWorkers=2)
@objectstack/spec 310 files / 7934 tests 全绿;check:generated 9/9 up to date
(含 check:authorable-surface,锚点由 gen:schema 前移至
cdfbee2f0 —— 先 commit merge 再跑生成,#5370 陷阱按序避开);
tsc --noEmit 通过;check:exported-any 1813 types + 1560 schemas 干净
@objectstack/driver-mongodb 9 files passed | 5 skipped;190 passed | 129 skipped;typecheck 通过
@objectstack/driver-sql 63 passed | 4 skipped / 851 passed | 44 skipped
@objectstack/driver-memory 16 files / 424 tests 全绿
@objectstack/formula 16 files / 357 tests 全绿
@objectstack/driver-sqlite-wasm 18 files / 246 tests 全绿
@objectstack/service-analytics 46 files / 723 tests 全绿
check:driver-conformance 4 drivers x 5 case-sets,20 covered,0 DEBT
eslint(改动 5 文件) 无告警;check:nul-bytes OK + 改动文件控制字节自扫无命中
mongodb 的 129 skipped 不是 pass:需要真 mongod 的那半边(mongodb-memory-server 二进制本容器取不到),译文断言半边(发出的 document 即语义)190 条全跑全绿;CI 有真 mongod 时另半边照常执行。兄弟面(#5306/#5308/#5318/#5319/#5321/#5314/#5312)保持:本分支对 main 的 delta 仅 6 个文件(本 PR 的 5 个 + mongodb-null-comparand-refusal.test.ts 的 3 条新 pin),spec 全量 7934 绿即其证据。
范围外
- 新立 driver-mongodb 是
{ field: {} }唯一还在作答的后端:#5240 拍板拒收、#5327 已闸四家,mongodb 仍译成「字段深等于空文档」 #5376(unassigned):driver-mongodb 是{ field: {} }唯一还在作答的后端。 - driver-mongodb 带着 #5328 的同一条缝:形状错误的
$between/ 非数组$and、$or/ 非对象$not全被静默吞掉,且它的算子拒收没有 ADR-0112 信封 #5346 第 2 项(非数组$and/$or、非对象$not静默吞掉)由本 PR 的assertFilterNodeList/assertFilterNode顺带闭合;其余项不在本 PR,另在该单留言对账。
保持 draft,未挂 auto-merge。
Generated by Claude Code
Generated by Claude Code
- filter.zod.ts:按 #5323 同步散文预留的交接("The declaration flips to stated contract with that PR"),空组合子单位元从「Deliberately NOT declared」段转为正式契约段;{field:{}} 半边保持未宣告(#5376 仍开)。 - filter-refusal-envelope.test.ts(#5366 新到):空数组两行从 REFUSALS 翻入 ACCEPTED(单位元树断言),同一守卫点的非数组拼写补位 REFUSALS,信封不变。 - filter-logic-conformance.ts:族 1 段落按分工删除(四行已进表),族 2/3 原样。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
Fixes #5239
同批携带 #5146 spec 半边(已落地)与 #5240 spec 半边(受限携带 → 实测后不进表)。
1. 前提复核(先做的事)
按仓规对
origin/main(175d789)逐后端实测四条 case,而不是照抄 issue 正文。实测方法本身有阳性对照:我的 driver-sql 探针复现了 PR #5296 / #5243 已落地 pin 的逐字结果({$not:{stage:'won'}}→ 行 2,3,4;{$or:[]}→ 零行),说明测的是真行为不是我的假设。{$and:[]}{$or:[]}{$or:[{a},{}]}{$not:{}}formuladriver-memorydriver-sql(#5134/PR #5243)driver-sqlite-wasmdriver-mongodb(改前)read-scope-sqlfilter-normalizerissue 正文点名的「四个后端」里就含
read-scope-sql,而它和同包的filter-normalizer各红四格。两者都在册跑全量FILTER_LOGIC_CASES(read-scope-sql-conformance.test.ts、native-sql-filter-logic-conformance.test.ts),所以四条一进表就是八格红。2. 为什么四条没进表
那两处不是「没跟上」,是成文的相反立场:
filter-normalizer.ts的错误消息逐字写着「An empty combinator has no defensible reading — dropping it widens the query, and treating it as "match nothing" silently empties a chart」,而「match nothing」正是 #5134 给$or: []定的答案;read-scope-sql.test.ts:86还用toThrowError(/non-empty array/)把这个抛错钉住了。「响亮拒收」本身是有先例的答案 —— #5240 对
{ field: {} }取的就是它。所以这是两条已落地立场必须有一条让步,属公开契约定调,不是实现细节。已立 #5322(含两轴分析与推荐:取单位元 + 可选地在 publish/lint 面对字面量空组合子拒收,这样运行期语义单一而作者错误仍在编写期爆炸)。派发词的硬要求是「批次 PR 必须对自己的内容全绿」,所以红行不进表,实测矩阵原样留在filter-logic-conformance.ts里,下一位不必重测。同理另两族也没进表:
$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 NULL-safe$not:driver-sql(PR fix(driver-sql):$not取反前先把操作数编译成全域谓词,NULL 行不再被静默排除 (#5146) #5296)、driver-sqlite-wasm、driver-memory、formula、driver-mongodb五家已一致(实测{$not:{stage:'won'}}→ 行 2,3,4),但read-scope-sql/filter-normalizer仍发裸NOT (…),只返回行 2 —— 即 read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297,已在该单补注「filter-normalizer.ts是同缺陷的第二份拷贝」。另外这一族还要改 fixture:FILTER_LOGIC_ROWS每一列都非空,得先加一个可空列并在七个 harness 里同步声明。{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240{ field: {} }:四后端闸门未实现,今天实测有四个不同答案。更根本的是FilterLogicCase没有办法表达「期望拒收」 ——expected是行 id 列表,空列表的意思是「匹配到零行」,恰恰是裁定没取的 FALSE 答案。要进表得先扩表结构(加expectRejection判别式或另立姊妹表),按派发词不擅自发明,只留注记。3. 本 PR 实际改了什么
driver-mongodb(#5239 的硬约束半边)translateFilter原样透传组合子数组,而 MongoDB 对空数组既不答 TRUE 也不答 FALSE,是第三种行为:拒绝整条查询。改成与 #5134 同一套结构性三值归约:先把整棵树判成true/false/clause,再据此产出 —— 而不是「编译完再问有没有产出条件」,后者分不清「本来就是空」和「有东西没编译出来」。$and→ TRUE,不产出条件。$or→ FALSE,产出真实的零行条件{ _id: { $in: [] } }。这一格是关键:「什么都不产出」等于{},而find/updateMany/deleteMany把{}读作全部文档,方向正好相反。{}作$or分支仍是 TRUE 析取项,{$not: {}}仍是零行 —— 这两条 MongoDB 本来就与布尔代数一致,所以归约按结构做而不是只判length === 0(只判长度会有一半是蒙对的)。$and/$or数组因此都保证非空(丢掉单位元成员 ≠ 发一个空数组)。形状拒收在同一改动里,且先于归约。单位元把「这个节点没有谓词」读作「匹配全部文档」,所以空节点必须只有一个成因。改前实测,这一格比 driver-sql 当年更糟 —— 不是「被静默忽略」,是已经在放宽:
{ $or: [new Date()] }{ $or: [{}] }{ $or: 'x' }{}{ $not: null }{}{ $or: ['x'] }{ $or: [{ '0': 'x' }] }updateMany/deleteMany走同一个 translate 层,在那里「放宽到全部文档」不是行数不对而是数据丢失。现按 ADR-0112 以INVALID_FILTER/status: 400拒收并点出位置(filter.$or[0]),消息不带[mongodb]前缀(#3867)。判定按原型而非typeof——Date/RegExp/ class 实例都满足typeof x === 'object'却枚举为空。一处刻意不动:
{ field: {} }仍归为'clause',译文逐字节不变,不替 #5240 做任何裁决。packages/spec(仅文档,零运行时改动)FilterConditionSchema的契约 TSDoc 写明$not的语义在 driver-sql 与 driver-memory / formula 之间分叉:NULL 行的去留相反,$not: {}一个是 TRUE 一个是 FALSE #5146 拍板的 NULL-safe$not:被比较列为 NULL 的行不满足被否定的条件、应当被返回,即NOT (…) OR col IS NULL;并说明守卫必须下推到叶子(hoist 到顶会重新放行经由另一分支满足内层$or的行)。按 PD chore: version packages #10 如实写明read-scope-sql/filter-normalizer尚未合规并指向 read-scope-sql 的$not有两处与 SQL 驱动分叉:非 NULL-safe(#5146 后的最后一个异类),且{ $not: {} }编译成空 → RLS 整表放行 #5297 —— 声明出来正是让这个缺口从隐形变成在册。{field:{}}),避免制造新的 declared ≠ enforced。filter-logic-conformance.ts记下三族「已裁定但未进表」的 case、实测矩阵与阻塞单号。FilterArray为仅输入的授权糖(#5158 拍板 C 第 1 步) #5306 的FilterArray声明块(该 PR 仍未合入,且落点在parseFilterAST之后,与本 PR 无重叠)。4. 验证
消费半径全跑(表钉住的每一个后端 + 本次改动包):
skip 不是 pass:
driver-mongodb的 128 skipped 是需要真 mongod 的那半边 —— 本容器取不到mongodb-memory-server的二进制。所以新 pin 分两半,与本包既有惯例一致:译文断言永远跑(对这四条,发出的 document 就是语义:{}= 全部,{_id:{$in:[]}}= 零),真 mongod 那半边回答「服务端是否同意」,在 CI 能取到二进制时才跑。反向验证 —— 方向先预测,再跑
预测:把
mongodb-filter.ts还原成origin/main,新 pin 里除「existing translation is untouched」那组控制项外全红。实测 28 红 / 9 绿,方向与预测一致。唯一偏差是我预测绿的数量写成 8 而实际 9 —— 差的那一条是
a field constrained by zero operators is still not ruled on (#5240),它本就该在还原后保持绿(它钉的正是「{stage:{}}前后逐字节不变」),是我数自己的控制项时漏数了一条,不是方向反了。红的 28 条覆盖三组单位元、嵌套组合、以及全部 15 条形状拒收。Generated by Claude Code