fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 (#5869) - #6209
Merged
baozhoutao merged 2 commits intoAug 7, 2026
Merged
Conversation
…calar comparand (#5869) `FieldOperatorsSchema` declares `$in`/`$nin` as arrays and `$between` as a [min, max] tuple, but nothing enforced that on the way in: `isFilterAST` checks only the OPERATOR and `parseFilterAST` lowers whatever comparand it is handed, so `['status', 'not_in', 'done']` became `{ status: { $nin: 'done' } }` and reached the driver, where `driver-sql` handed a scalar to `whereIn()` and answered 500 DATABASE_ERROR -- a server-fault code for a filter the caller can fix, naming neither the operator nor the field. The gate goes at the engine's single filter collection point rather than in each driver: three backends answered three different ways for one declared contract, and both driver families are under an investment freeze (#5499). It runs on the LOWERED condition, which is what makes it cover the door the defect was actually measured through: the protocol face runs its own isFilterAST -> parseFilterAST and hands the engine a FilterCondition OBJECT, so a guard on the array branch alone would have left the reported 500 exactly where it was. `$between` arity is hoisted to the same seam -- driver-sql and driver-memory each already refuse it (wording kept verbatim), while driver-mongodb's arm falls through without emitting a range predicate. Not judged here: empty lists (declared predicates), list MEMBER types (#5234, another face), and non-collection operators. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
… to `any` `check:query-options-erasure` went red: "test surface grew 267 -> 289". The 22 new engine call sites in engine-filter-array-lowering.test.ts all carried a bare `as any`, which the #4918 ratchet counts. Both remedies the gate names are used, split by what `tsc` actually says about each input rather than applied uniformly: - `as unknown as EngineQueryOptions` (via the `asFilterArrayQuery` helper, and the `EngineCountOptions` / `EngineAggregateOptions` twins on count/aggregate) for the FilterArray inputs. Those are off-contract BY DECLARATION -- `where` is a FilterCondition / Record<string, unknown> that an array is not assignable to, because FilterArray is INPUT-ONLY sugar the spec excludes (#5285). - The assertion simply DROPPED on the malformed-comparand cases (`{ stage: { $nin: 'won' } }`). Those type-check fine, because `where` is declared loosely on purpose -- which is precisely why the runtime gate this file pins has to exist. Erasing them would have hidden that they are type-legal. The 23 pre-existing sites in this file are untouched, as are the baseline JSON and eslint.config.mjs -- the ceiling is met by fixing the new sites, not by raising the number. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
baozhoutao
marked this pull request as ready for review
August 7, 2026 11:16
baozhoutao
enabled auto-merge
August 7, 2026 11:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5869
前提复验(origin/main
80f7dc6a3)单据前提成立,且复验中发现一处会决定落点成败的细节,先记在最前面。
探针实测(
isFilterAST/parseFilterAST直调):isFilterAST只看算子(VALID_AST_OPERATORS.has(filter[1])),不看比较值形状;parseFilterAST照单下降。三种拼法同一条路径,与单据实测一致。关键细节 —— 收口点不能只装在数组分支上。 分诊说的收口点(
lowerWhereFilterArray)现行代码第一句是if (!Array.isArray(where)) return bag;。但单据实测的那道门(HTTP)走不到数组分支:协议面metadata-protocol/protocol.ts:5187自己跑了一遍isFilterAST→parseFilterAST,交给引擎的已经是下降完的FilterCondition对象。所以只判数组分支的守卫,恰好漏掉本单实测的那道门。本 PR 因此把守卫装在下降后的 condition 上,两道门同一份判据。为什么必须收在一处(实测的三家答案)
$in/$nin标量whereIn(f, scalar)→ 500 DATABASE_ERROR$between非二元组driver-memory的形状门(filter-refusal.ts)实测逐格结果:一条声明(
FieldOperatorsSchema写着$in: z.array、$nin: z.array、$between: z.tuple),三家三个答案,且那份形状门只有driver-memory自己读 —— 这正是「收口点唯一」的论据本身。⛔ 未在任何 driver 侧加拒收(两族冻结,#5499)。改动
packages/objectql/src/filter-comparand-shape.ts—— 只判「集合算子的比较值整体是不是列表」的走查。独立成文件,让engine.ts的 diff 只有 17 行(本周 fix(objectql): seedAutonumber 把非「表未建」的读故障上抛,不再从 0 重发自增号 (#5979) #6114 / fix(objectql)!: 事务句柄不再跨数据源穿透 —— 业务写响亮拒绝、系统账本移出事务落盘 (#5351) #6171 / feat(objectql,cli): os migrate 新增 summary count/sum 存量 NULL 回填迁移 (#6063) #6158 刚改过它,且都不在这一带)。engine.ts在lowerWhereFilterArray的两个出口各调一次(对象门 / 数组门)。六个入口(find/findOne/count/aggregate/update/delete)因此一致。400+StandardErrorCode.enum.INVALID_FILTER(标准目录码,不需要动ERROR_CODE_LEDGER—— 那是扩展码的账本)。⛔ 未改
packages/spec。 这是本单最值得记的一条:期望形状早就声明了,缺的只是入口处的强制。ViewFilterRuleSchema.value不按算子约束形状确实是真的,但修 500 不需要动它 —— 详见下方「扩测项」。错误信息(实测,穿过 REST 后客户端收到的正文):
点名了算子(同时给作者实际书写的
not_in/nin/notin拼法 —— 没人往 metadata 里写$nin,只报下降后的名字会让作者去找一个他文件里没有的键)、字段、收到的值与位置、可直接粘贴的正确形状、以及标量比较该用的替代算子。收益穿过 HTTP 边界后仍在(实测)
mapDataError对声明了 4xx 的错误原样透传:CLIENT_MESSAGE_MAX(500)截断,掉的正好是尾部那句「filter was NOT applied」—— 即这条拒收存在的理由本身。已把信息压到 392 / 372 / 355,并加了一条钉住该性质的用例(toBeLessThan(500)+ 尾句仍在),因为那个上界在另一个包里。反向验证(方向先写死,再跑)
肢 B 的意义:它证明点名断言绑的是点名,而不是顺带被「拒收发生了」满足 —— 否则「只把 500 改成 400 不给可操作信息」会照样绿。
肢 A 另有一条值得记的观察:测试替身的匹配器对
$in: 'won'并不抛错(JS 里'won'.includes('won')为真),即无守卫时替身面的失败模式是静默错答,而 driver-sql 面才是 500。同一形状两种坏法,更说明判据该在引擎侧。测试
新增 16 条,落在既有
engine-filter-array-lowering.test.ts(贴现有结构),含正反两侧:四种拼法的标量拒收、点名断言、对象门、null/数字/对象比较值、$and/$or/$not嵌套走查(带 path)、六入口一致、$between三格、REST 截断上界;以及必须保持工作的回归:合法数组形状原样下达、$in: []/$nin: []仍是合法谓词、列表成员不复判、$eq里形似算子图的深等值比较值不误伤、非集合算子标量不受影响(含$gt的 ISO 日期字符串)。消费半径扫描(该判据对全仓每一次引擎查询生效,不能只测本包)。全仓 grep 标量
$in/$nin命中三处夹具,均在 analytics read-scope 面(scope:/filter:,不是where),且该面本就自带同族拒收(⑧$in需数组 / ⑨$nin需数组 / ⑩$between需[min,max])—— 与本 PR 无交叠,实测全绿:顺带说明:本单未新增 fake engine 写动词,
assertEngineDeleteDispatch不适用;复用的是既有makeRecordingDriver(driver 替身,非 engine double),check:engine-double-contract已绿。必答项
#5234(driver-sql 对象语法列表成员)—— 定价不变。 不同轴:本 PR 判「比较值整体是不是列表」,#5234 判「合法列表内部成员的类型」。
{ status: { $in: ['a', { foo: 1 }] } }是数组,本守卫原样放行(已加用例钉住这条边界)。LIKE 族的对象比较值也不在本守卫的算子集内。#5234 的落点(packages/drivers/driver-sql)、测量范围、文件面均不受影响;其分支claude/issue-5234-silent-empty-predicates(85ef43d02)尚未并入 main,与本 PR 零文件交叠。唯一的间接影响是非减负:经引擎路由的查询里非数组比较值不再抵达 driver-sql,但直调 driver 的路径仍在,所以接手者要做的事没有变少。#5905(having-filter 第五求值面)—— 未触及。 如预期。
lowerWhereFilterArray只读bag.where,having是 aggregate AST 上的另一个键,不经过本守卫。且两者判据正交:#5905 是无值行的取值语义($nin对undefined该判真还是假),本守卫只问「比较值是不是列表」,即便跑在 having 面上也不会改动 #5905 的任何一格。engine-aggregate-having.test.ts/having-filter.test.ts在全量里保持绿。扩测项
between的实测结论。 与 in/not_in 同族但不同格,如实报告:它不是 500 ——driver-sql(arr.length !== 2)与driver-memory(isBetweenComparand,#5328)各自已经答 400 INVALID_FILTER,且措辞已经点名算子/字段/期望形状。但driver-mongodb的case '$between'是if (Array.isArray(value) && value.length === 2) {...}且无 else,即不拒收也不发射区间谓词。所以本 PR 仍把它收进同一收口点:判据是纯 arity(与那两家逐字相同的条件,不发明更严的契约),首句措辞逐字保留,收在收口点后三家答案一致。是否需要改 spec:否 —— 三格都不需要。 期望形状
FieldOperatorsSchema已经声明($in/$nin是z.array,$between是z.tuple),缺的只是入口强制,所以本 PR 完全没进packages/spec,无需转 spec 座位。ViewFilterRuleSchema.value不按算子约束形状仍然是事实 —— 本 PR 把它从「发布通过 → 运行时 500」改善成「发布通过 → 运行时可操作的 400」,但作者仍要到运行时才知道写错了。在发布期就按算子拒收是domain:spec的活,且是明显更大的一刀(要给每个算子定 value 形状并迁移存量 metadata),按纪律未做 rider、也未擅自立案。偏差与风险
$between现在由引擎先答,driver 侧那两条拒收在经引擎路由时不再被触达(直调 driver 仍触达)。全仓仅driver-memory自己的用例断言该措辞,且为直调,保持绿;首句逐字保留也是为此。Generated by Claude Code