Skip to content

fix(objectql,driver-sql,driver-memory,driver-mongodb)!: FilterArray 在 engine 门下沉,四驱动数组方言删除 (#5158 拍板 C 第 2 步) - #5329

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5158-lower-filter-array-at-engine-door
Aug 4, 2026
Merged

fix(objectql,driver-sql,driver-memory,driver-mongodb)!: FilterArray 在 engine 门下沉,四驱动数组方言删除 (#5158 拍板 C 第 2 步)#5329
os-zhuang merged 3 commits into
mainfrom
claude/issue-5158-lower-filter-array-at-engine-door

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5158

拍板 C 的第 2 步(engine 半边)。第 1 步(spec 声明 FilterArray 为仅输入授权糖)已由 spec 车道以 #5285 / PR #5306 落地在 b49ccfdfe;本单只消费那份声明,packages/spec/** 一行未改。


1. 现场核对(行号已漂,以现场为准)

issue 与派发评论引用的行号今天都动过,逐条重核如下:

引用 issue/评论里的位置 本 PR 基线 0f1711470 的实际位置 结论
engine Door 2「祝福 expression tree 归 driver 解释」 engine.ts:4309 engine.ts:4429-4443(requireFindOnePredicate 的文档注释 + Array.isArray(where) 判定) 同一处,漂了约 120 行
协议门 Door 1 protocol.ts:4533 protocol.ts:4558-4562(isFilterASTparseFilterAST,否则 malformedFilterArrayError) 同一处
SqlDriver.applyFilters 数组分支 —— sql-driver.ts:6033-6112(改前) 与同日三次改动(#5243 reduceFilterNode#5296 nullSafeNegationOperand#5327 reduceFilterKey{field:{}} 闸门)不重叠:那三处都在对象路径(applyFilterCondition / 归约遍历),本 PR 只删数组遍历
engine.ts 同日两次改动 —— #5270 批量写按行、#5283 单记录 delete 绑 previous 与本 PR 不重叠:两者在写路径主体,本 PR 只在六个入口点的归一化段落插一行

另外核到一个 issue 正文未写、但决定了实现形状的事实:FilterArray 的中缀连接形式([condA, 'or', condB])parseFilterAST 根本表达不了(isFilterAST 对它返回 false,parseFilterAST 返回 undefined),FilterArraySchema 也不接受它。也就是说驱动编译的那套方言里,有一支从来不是 FilterArray 的任何声明形状 —— 它是纯粹的驱动私产。这决定了 Door 2 必须像 Door 1 一样「先 isFilterAST 闸门、再下沉、否则响亮拒收」,而不是无条件 parseFilterAST(后者对拼错的操作符有 $${op} 宽松兜底,会把作者的错误变成一个没人执行的 $sounds_like 条件)。

2. 下沉点的选择与理由

新增模块级函数 lowerWhereFilterArray(object, operation, bag)(packages/objectql/src/engine.ts),在六个入口点各调用一次,紧跟已有的 foldEngineOptionAliases / rejectUnknownEngineOptions:

find / findOne / count / aggregate / update / delete

  • 为什么是这六处而不是更深的一处:这六处正是 engine 的公开入口,也是 foldEngineOptionAliases([P1] filter folds to where in engine.find only — findOne/count/update/delete silently match EVERY row, and the hook docs teach the broken call #4346 的别名折叠)已经站的位置。别名折叠与形状下沉是同一件事的两半 —— 先把 filter 折成 where,再把 where 的值下沉成 FilterCondition —— 放在一起,后面所有代码(token 解析、by-id 快路、middleware、hook)读到的 where 恒为对象。
  • 为什么不塞进 foldEngineOptionAliases 内部:那个函数按 slot 表折,并且刻意在无别名时返回同一引用(withResolvedWhere 的零分配纪律);值形状下沉是另一件事,显式的第二次调用更可 grep,也与 rejectUnknownEngineOptions 的既有风格一致。
  • update/delete 的顺序:下沉必须在 by-id 提取之前 —— 未下沉的数组上 where.id 恒为 undefined,所以 update({ where: [['id','=',x]] }) 过去一律路由到多行路径。

三种到达,三个答案,与 Door 1 逐条对齐:

到达 处理 与 Door 1
[] 删键(「无过滤」) Door 1 留 []where 上继续下传,而 engine 在其下游,所以最终一致
isFilterAST 为真 parseFilterAST 下沉
其余非空数组 抛错,消息带调用方自己的值 + 操作符词表 + 「否则会返回未过滤的结果集」 Door 1 是 400 INVALID_FILTER;engine 直调没有 HTTP 状态,措辞对齐

3. 不可达证明(先证再删)

结论:生产路径结构性不可达;类型层不可达不成立**,如实报告。**

3.1 类型层:证不成立

QueryAST.whereFilterCondition,而 FilterCondition 的非递归半边是 { [key: string]: any } 索引签名 —— TypeScript 里数组对索引签名类型是可赋值的。实测:仓内 15 个驱动测试文件里,where: [['category','=','Electronics']] 这类写法不带任何 cast 也能通过 tsc --noEmit(本 PR 前后 pnpm typecheck 全绿 124/124 都证实了这点)。

所以「类型挡住了数组」是错的,这也正是运行时必须响亮拒收、而不是靠类型的原因。这一点已写进 sql-driver-filter-no-silent-drop.test.ts 的注释,免得下一个人再推一次。

3.2 调用点清点:生产调用方 0

# 生产代码(非 test)里以数组字面量写 where/filter 的位置,全仓
packages/metadata-protocol/src/protocol.ts:1016   where: ['filters', '$filter']      # slot 别名表,不是 filter 值
packages/cli/src/commands/explain.ts:202          filters: [{field,operator,value}]  # view filter 契约,另一形状
packages/plugins/plugin-auth/src/auth-manager.ts  where: [{field,value}] ×5          # better-auth adapter 的 where,另一契约
examples/app-showcase/.../my-work.page.ts:52      filters: [['owner_id','=','…']]    # React prop → Door 1
# 不经 engine 直接调驱动的生产代码
packages/metadata/src/utils/history-cleanup.ts    where: baseWhere / filter          # 均为对象
packages/verify/src/{date-bucket-parity,read-coercion}.ts                            # 均为对象

即:没有任何生产调用方以数组形式到达 SqlDriver.applyFilters —— 与侦察清点的读数一致,本 PR 独立复核后确认。

3.3 探针实测:全仓测试穿过拒收

删除后的驱动分支本身就是探针 —— 任何经 engine/协议到达驱动的数组都会抛 INVALID_FILTER。以此跑全仓 pnpm turbo run test:

 Tasks:    134 successful, 134 total

@objectstack/objectql 1908、@objectstack/runtime 1333、@objectstack/metadata-protocol 388、@objectstack/client 222 全绿 —— 没有一条经 engine 或协议的路径产生数组。

唯一的到达者是驱动测试自己的直调(15 个文件、23 条 driver-sql 用例 + memory/mongo 若干),它们都是 driver.find(...) 直接喂数组。这类调用是存在的,所以删除对直调驱动的消费方不是零行为变化;对产品路径是零行为变化。两句话都写在 changeset 的「可观察的行为变更」里,不含糊。

4. 四驱动删除清单

删除内容 行数
driver-sql applyFilters 的数组遍历分支 + 其比较发射器 protected applyAstComparison(方言私有,唯一调用点在被删分支);canonicalAstOperator 导入随之移除 ~220
driver-memory convertToMongoQuery 的 legacy array 分支 ~62
driver-mongodb translateArrayFilter + translateComparison + mapFieldName ~140
driver-sqlite-wasm 无自有实现,extends SqlDriver 随之变更 0

三处统一替换为 filterArrayReachedDriverError —— 逐字相同的措辞,ADR-0112 信封(INVALID_FILTER / 400),消息指向 parseFilterAST 而不是 SQL builder,并明说中缀形式没有下沉、要写前缀。[] 在三个驱动里仍然提前返回(「无过滤」)。

顺带消失的一条:driver-mongodbcreatedAtcreated_at 字段别名只存在于数组路径(mapFieldName 仅被已删的 translateComparison 调用),对象路径从未应用过。按 PD #12「消费端别名是债务不是模式」不再补回,已在测试里钉住现状。

5. Pin 退役说明(刻意,不是顺手删测试)

5.1 engine-findone-contract.test.ts —— 派发点名的那一条

退役:

it("a non-object where (an expression tree) is the driver's to interpret, not refused")

这是拍板 C 的直接后果,不是碍事的测试。 那句话是 engine 对第二套方言的明文祝福 —— 产品里唯一让 FilterArray 不经下沉抵达驱动的门。C 把门关上后,「归 driver 解释」对任何 where 都不再成立。

而且这条祝福是有代价的,退役同时补上了代价对应的 pin:findOne({ where: [] }) 过去被 requireFindOnePredicate 当作「表达式树 = 有谓词」放行,于是 limit: 1 落在整张表上返回任意一行 —— #4419 要挡的缺陷,活在 #4419 自己的守卫里面。两条替代用例:

  • an array where is LOWERED before the guard, and still selects the record(断言驱动收到 { name: 'Two' },不是数组)
  • `where: []` no longer walks past the guard as "an expression tree"

engine.ts 里那段祝福的注释原文也一并改写,并说明保留的 Array.isArray 分支从此只是纵深防御,不再是契约。

5.2 sql-driver-filter-no-silent-drop.test.ts —— 第二处退役,一并说明

派发只点名了 5.1。但删掉驱动方言必然动到这个 #3948 的 pin 文件,故在此显式报告:

5.3 其余 13 个驱动测试文件:迁移而非删除

统一手法:把原本喂给驱动的裸数组改成 parseFilterAST(<同一个数组>) —— 即显式走声明的那条路。作者写的形状一字不改,覆盖面不减,而且这些用例现在顺带证明了下沉链路本身。涉及 #2704 空值算子、#3777/#4042 日历日上界、#3912 文本存储、#4436 拒收信封、#5041 跨字段引用、ADR-0015 §18 columnMap 等既有 pin,逐条保持原断言。

6. 测试

命令 结果
pnpm turbo run test(全仓) 134 / 134 tasks 通过
pnpm --filter @objectstack/objectql exec vitest run Tests 1908 passed,Test Files 118 passed
pnpm --filter @objectstack/driver-sql exec vitest run Tests 794 passed | 44 skipped,Test Files 61 passed | 4 skipped
pnpm --filter @objectstack/driver-memory exec vitest run Tests 324 passed,Test Files 12 passed
pnpm --filter @objectstack/driver-mongodb exec vitest run Tests 139 passed | 120 skipped(5 个套件需下载 mongod,沙箱无网,预先跳过)
pnpm --filter @objectstack/driver-sqlite-wasm exec vitest run Tests 237 passed,Test Files 17 passed
pnpm --filter @objectstack/client exec vitest run Tests 222 passed,Test Files 17 passed
pnpm turbo run typecheck(全仓) 124 / 124 tasks 通过
pnpm lint(全仓 eslint) 无输出(通过)
pnpm --filter @objectstack/spec check:generated ✓ All 9 generated artifacts are up to date.
check:driver-conformance / check:engine-double-contract / check:adr-anchors / check:route-envelope / check:durability-log-level / check:startup-registry-verdict / check:error-code-casing / check:nul-bytes 全 PASS

新增 packages/objectql/src/engine-filter-array-lowering.test.ts(25 条)。断言落在驱动入参,不是结果行 —— 结果行相同正是本单的目标,只测结果证明不了下沉发生了。测试用的记录型驱动故意只认对象形式,一旦收到数组就抛;仓内四个驱动过去两种都认,这正是没人发现自己拿到了哪一种的原因。

反向验证(把改动 stash 掉,新用例必须失败)

$ git stash push -- packages/objectql/src/engine.ts
$ pnpm --filter @objectstack/objectql exec vitest run engine-filter-array-lowering

 Test Files  1 failed (1)
      Tests  24 failed | 1 passed (25)

AssertionError: expected [Function] to throw error matching /selects no particular record/ but got 'driver received a non-object \'where\…'
AssertionError: expected [Function] to throw error matching /Infix joins .* NOT one of the shapes/s but got 'driver received a non-object \'where\…'
AssertionError: expected [Function] to throw error matching /is not a filter/ but got 'driver received a non-object \'where\…'
AssertionError: expected [Function] to throw error matching /UNFILTERED result set/ but got 'driver received a non-object \'where\…'
AssertionError: expected [ { ast: { where: [ 42 ], …(1) } } ] to have a length of +0 but got 1

25 条里唯一仍通过的是 a FilterCondition object passes through byte-for-byte —— 它本来就不该受影响。

派发点名的四条语义,逐条核对

要求 结果
数组输入经 engine 后驱动收到 FilterCondition 而非数组 ✅ 断言到驱动入参(7 种形状 × it.each)
FilterBuilder 元组 + showcase 5 处形状不回归 FilterBuilder 全部 7 种输出形状逐条钉住;{current_user_id} token 那处形状单独一条;wire 面(Door 1)未改动,metadata-protocol 388 条全绿
[] 仍是「无过滤」 find/count 语义逐字不变(engine 删键、驱动提前返回)。唯一例外 findOne([]),单独报告见下
裸三元组今天抛错 ⚠️ 部分改变,单独报告见下

7. 需要维护者过目的两处语义变化(未擅自扩大,如实列出)

  1. findOne({ where: [] }):放行任意行 → 抛错。 []含义没变(仍是「无过滤」),变的是 findOne 终于看得见它。这正是 ObjectQL silently drops unsupported predicate keys; findOne then returns the first row #4419 守卫的目的,视为修复而非回归;但它确实是行为变化,已在 changeset 和 5.1 里点名。
  2. 裸三元组的答案取决于「哪一道门」。 派发写「裸三元组今天抛错,这条不许变」。现场核对后拆成两半:
    • 驱动层:driver.find(o, { where: ['close_date','before','2024-01-01'] }) 仍然抛(消息从「Unrecognized filter operator "close_date"」变为「A filter ARRAY reached the driver」)—— 不变
    • engine 层:engine.find(o, { where: ['stage','=','won'] })(格式良好的裸三元组)过去由驱动抛错,现在被下沉为 {stage:'won'} 并正常执行。这是有意的:FilterArrayComparison 就是 spec 第 1 步声明的合法输入形状之一(filter-array-declaration.test.tsPRODUCED 表里就有),Door 1 也一直这样接受它。engine 门若在这里比协议门严,就是又造了一个方言 —— 恰是 C 要消灭的东西。操作符不在词表里的裸三元组(before/sounds_like,A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 的原始 case)仍然抛错,只是改由 engine 门抛。

8. 与 cloud RemoteTransport 的合流关系(本 PR 不碰 cloud 仓)

TursoDriver extends SqlDriver(cloud/packages/driver-turso/src/turso-driver.ts:166),原样继承方言;而同仓的 RemoteTransport.buildWhereSQL 自 cloud#1075 起对同一输入响亮拒收(isFilterNode(filters) 为假即 uncompilableWhere)—— 同一产品内部两个答案,两侧零测试。本 PR 删除方言后,TursoDriver 继承到的是同一个 INVALID_FILTER / 400 拒收,分叉自然消灭,cloud 侧无需任何跟进改动

一处残留差异,留给 cloud 车道知悉(本 PR 不动):[] 的读法两侧仍不同 —— framework 三个驱动读作「无过滤」,cloud 的 isFilterNode([]) 为假故拒收。因为 engine 门现在会把 [] 删键,[] 已经不可能经 engine 到达任一驱动,该差异对产品路径不可观察;如果 cloud 车道希望彻底统一,那是一次独立的取舍(framework 侧的「[] = 无过滤」有 sql-driver-filter-no-silent-drop 的明文 pin,不该顺手改)。

9. 范围外发现(已按 PD #10 立单/评论,本 PR 一律未修)

搜索去重后:

10. 硬禁区自查

  • packages/spec/** —— 0 处改动(git diff --stat 可验)
  • content/docs/releases/** —— 0 处改动;变更记录只走 .changeset/filter-array-lowered-at-engine-door.md
  • cloud 仓 —— 未改动(只读核对 TursoDriver / RemoteTransport 用于第 8 节)
  • os-regen merge-driver 管的生成物 —— 未手改;check:generated 9/9 up to date

Generated by Claude Code

…rray at the engine door, delete the drivers' array dialect (#5158)

Maintainer ruling C, step 2. `FilterArray` is INPUT-ONLY authoring sugar and
the spec has said so since #5285; this makes the runtime agree.

Door 2 — a direct in-process engine call — used to pass a `where` array
through verbatim, so four drivers grew a second filter compiler to meet it,
including an INFIX dialect (`[condA, 'or', condB]`) no schema declared and
`parseFilterAST` cannot express. Cloud's `RemoteTransport.buildWhereSQL` has
refused that same input since cloud#1075: one query, two answers, decided by
whether the caller went over the wire.

All six engine entry points (`find`/`findOne`/`count`/`aggregate`/`update`/
`delete`) now run `isFilterAST` -> `parseFilterAST`, the same sink the
protocol face has always used, and refuse an array that cannot be lowered.
`[]` keeps its meaning everywhere: no filter.

The drivers' array compilers are deleted and replaced by a loud
`INVALID_FILTER` / 400 refusal that names the lowering — converging
driver-sql, driver-sqlite-wasm, driver-memory and driver-mongodb with cloud's
transport.

Deliberately retired: `engine-findone-contract`'s pin that a non-object
`where` "is the driver's to interpret". That sentence was the engine's
blessing of the second dialect, and it cost `findOne({ where: [] })` walking
past #4419's own guard to return an arbitrary row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 10:51pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql.

17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/driver-memory)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql, @objectstack/driver-memory)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/driver-memory)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

claude added 2 commits August 4, 2026 22:40
… local mongod skip hid (#5158)

CI caught what the sandbox could not run. `mongodb-driver.test.ts` is gated on
`describe.skipIf(!sharedMongod)`; `mongodb-memory-server` downloads its binary
from fastdl.mongodb.org, which this environment cannot reach, so the suite
skipped locally and ran on CI — where `should filter with legacy array style`
asserted that the driver COMPILES `where: [['age','>=',30], ['role','=','user']]`.

That case tests the dialect itself, so it gets the same treatment as
driver-sql's two compile-asserting cases: a REFUSAL pin, plus a counterpart
proving the identical authored shape still returns the same rows once lowered
through `parseFilterAST`. Migrating it instead would have quietly changed what
the case measures.

The filter document is provably unchanged: the deleted `translateArrayFilter`
folded an all-AND group to `{$and:[{age:{$gte:30}},{role:'user'}]}`, and
`translateFilter(parseFilterAST(<same array>))` produces that document
byte-for-byte — verified locally against `translateFilter`, the only function
`MongoDBDriver.find` uses to build a filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7

Copy link
Copy Markdown
Contributor Author

CI 补漏:driver-mongodb 的方言 pin 退役(成因:本地 mongod 跳过)

6c4398d94。这一条是我在正文 §6 里自己披露过的验证盲点变成的真失败,不是 flake。

失败与成因

Error: A filter ARRAY reached the driver: [["age",">=",30],["role","=","user"]].
 ❯ filterArrayReachedDriverError src/mongodb-filter.ts:42:15
 ❯ translateFilter src/mongodb-filter.ts:80:11
 ❯ MongoDBDriver.find src/mongodb-driver.ts:240:20
 ❯ src/mongodb-driver.test.ts:425:36
Serialized Error: { code: 'INVALID_FILTER', status: 400 }

mongodb-driver.test.ts 挂在 describe.skipIf(!sharedMongod) 上;mongodb-memory-server 要从 fastdl.mongodb.org 下载约 123 MB 的二进制,本沙箱无外网,所以这个套件本地是 skip、CI 上是真跑。我本地读数 139 passed | 120 skipped 里的那 120,正好包含它。

两个 job 都挂在同一条上,不是两个失败点:Test Core (1/3)Temporal Conformance (live PG + MySQL) 都跑 @objectstack/driver-mongodb(后者在 TZ=America/New_York 下跑 non-SQL 那一腿,ci.yml:495-501),两边都是 1 failed | 258 passed (259)

修法:退役,不是迁移

那条用例叫 should filter with legacy array style —— 它测的正是被删掉的方言本身。改成 parseFilterAST(...) 会让它变成「测另一个东西」,覆盖数字不变而语义没了。所以照 sql-driver-filter-no-silent-drop.test.tscompiles a nested condition array / compiles an infix logical join同一手法、同一措辞处理:

  • refuses a raw array where — the dialect is gone (#5158) —— 断言 INVALID_FILTER 拒收
  • the same authored shape still returns the same rows, once lowered (#5158) —— 对照条,parseFilterAST(<一字不改的同一数组>),断言仍是 toBe(2)

保留的注释写明这是刻意退役及其理由,与 §5 的两处一致。

过滤文档可证不变:被删的 translateArrayFilter 对全 AND 组折成 {$and:[{age:{$gte:30}},{role:'user'}]};translateFilter(parseFilterAST([['age','>=',30],['role','=','user']])) 产出同一个文档。fixture 里 Bob(30, user)、Charlie(35, user)恰好 2 行,toBe(2) 成立。

全仓穷尽清点(不只修报错那一行)

用脚本而非肉眼,两轮:

第 1 轮 —— 所有把数组喂给驱动方法的位置(匹配 *.test.ts<driver>.{find,findOne,count,aggregate,update,updateMany,delete,deleteMany}(...) 调用内、跨行的 where: [):

=== test files calling a driver method with an ARRAY-valued `where` ===
packages/plugins/driver-mongodb/src/mongodb-driver.test.ts
   444: driver.find('user', { where: [['age','>=',30], ['role','=','user']] as any }), ).rejects.toThrow(/A …
packages/plugins/driver-sql/src/sql-driver-null-operators.test.ts
   95: driver.find('tasks', { object:'tasks', where: [['assignee','isnull',true]] as any }), ).rejects.toThr…

仅剩两处,都是本 PR 刻意写的拒收 pin

同一轮还扫了直接调用过滤入口的形式(translateFilter( / applyFilters( / convertToMongoQuery( / buildWhereSQL( 后接 [):唯一命中是 mongodb-filter.test.tstranslateFilter([]),即「[] = 无过滤」那条 pin,符合预期。

宽口径复核:把口径放宽到全仓 *.test.ts任何 where:|filters:|filter: 后接 [ —— 87 处,逐一归类,无一是漏网的驱动调用:

类别 处数 说明
spec schema 测试 filters:(已退役的 legacy 键) 12 QuerySchema.parse,不碰驱动
view metadata filter:({field,operator,value} 视图筛选契约) 33 另一形状,与 FilterArray 无关
better-auth adapter where: [{field,value}] 5 另一契约(plugin-auth)
queryast 测试的 filters:(「legacy 键被忽略」用例) 6 驱动只读 query.where,该键从不进 applyFilters
analytics / realtime / lint / 协议 metadata 10 与驱动过滤无关
本 PR 新增的 engine 门用例 19 断言下沉与拒收,应当是数组
本 PR 的驱动拒收 pin 2 同上

第 2 轮 —— 「本地跳过、CI 才跑」的盲区清点(这条比修那一行重要)。先按 describe/it.skipIf|runIfprocess.env.*(PG|POSTGRES|MYSQL|MONGO|REDIS|TURSO|LIBSQL)*OS_TEST_* 扫出 17 个带条件跳过的测试文件,再对其中所有会跑真驱动的 96 个文件做数组清点:

盲区 文件数 CI 里由谁跑 数组调用点
driver-mongodb(skipIf(!sharedMongod),需下载 mongod) 6 Test Core 分片 + Temporal Conformance 2(均在 mongodb-driver.test.ts,即本次修的两条)
driver-sql live PG/MySQL(skipIf(!OS_TEST_POSTGRES_URL/…MYSQL_URL)) 4 Temporal Conformance(带 services: 起真库) 0
packages/qa/dogfood(skipIf(!organizationsAvailable) 等) 96 Dogfood Regression Gate ×3 0
packages/lint(skipIf(!existsSync(dist/…))) 2 Test Core 0(与过滤无关)

即:除已修的这两条外,盲区里没有第二处。第二片、第三片不会再红在同一根因上。

我如何核对那些本地跑不起来的套件(不假装跑过)

沙箱内既无 mongod 二进制(find / -name mongod 无结果、~/.cache/mongodb-binaries 不存在)也无外网,driver-mongodb 的 5 个套件确实无法真跑。核对方式两条,都说清覆盖面:

  1. 静态穷尽清点 —— 上表第 2 轮,模式与结果如上,覆盖 6 个 mongodb 文件 + 4 个 live-dialect 文件 + 96 个 dogfood 文件。
  2. 功能等价验证 —— MongoDBDriver 的每一个读写入口(mongodb-driver.ts 的 240/252/370/441/459/546 行)都只用 translateFilter(query.where, …) 构造过滤器,而 translateFilter 本地可跑。于是写了一个临时 probe(跑完即删,未进提交),把这两个盲区文件里我改动过的原样表达式喂给 translateFilter:
✓ mongodb-datetime-storage.test.ts:144 — prefix AND group lowers to both bounds
✓ mongodb-driver.test.ts — the retired dialect case: raw array refused
✓ mongodb-driver.test.ts — its counterpart: same authored shape, lowered
 Test Files  1 passed (1)      Tests  3 passed (3)

其中第 1 条同时补上了另一个盲区:mongodb-datetime-storage.test.ts 也在那 5 个 skip 套件里,我在首轮迁移过它却从未在本地执行过;probe 证明它下沉后得到 {$and:[{created_at:{$gte:'2026-04-29'}},{created_at:{$lt:'2026-07-29'}}]}(上界按 #4042 整日半开),与该用例上方 $between 条产出同一组行。

这两条是间接证据(证的是过滤文档,不是 mongod 上的真实行),我不声称等同于跑过那些套件 —— 留给 CI 判定。

重跑

命令 结果
pnpm --filter @objectstack/driver-mongodb exec vitest run Tests 139 passed | 121 skipped,Test Files 7 passed | 5 skipped(skip 数 120→121:一条用例拆成两条)
pnpm turbo run typecheck --filter=@objectstack/driver-mongodb Tasks: 4 successful, 4 total
pnpm lint(全仓) 无输出(通过)
复现 Temporal Conformance 的 non-SQL 腿:TZ=America/New_York pnpm --filter core --filter formula --filter driver-memory --filter driver-mongodb --filter service-analytics test core 429formula 357driver-memory 324driver-mongodb 139 passed | 121 skippedservice-analytics 591 —— 全绿
同 job 的 SQL 腿(无 live URL,故 4 个套件仍 skip):TZ=America/New_York pnpm --filter @objectstack/driver-sql test Tests 794 passed | 44 skipped

另:本分支已 merge origin/main(f1cc3a316,两笔均只动 packages/spec / plugin-email,与本 PR 零重叠),merge 后重建 spec 并跑 check:generated✓ All 9 generated artifacts are up to date.

保持 draft,分支未变(claude/issue-5158-lower-filter-array-at-engine-door),未开新 PR。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 23:03
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

PM 复核(会话 session_01Pbu27iNUfQCHeuS551Rqo7):接受,两个开放问题均裁 A(否决窗口开放)

CI 23/23 全绿,含首轮红过的 Test Core (1/3)Temporal Conformance。转 ready 入队。

开放问题 1:findOne({ where: [] }) 从「返回任意一行」变为抛错 → 取 A

[]含义没变(find/count 仍是「无过滤」),变的是 findOne 终于看得见它。旧行为是:requireFindOnePredicate 把数组当作「表达式树 = 有谓词」放行,于是 limit: 1 落在整张表上返回任意一行 —— #4419 要挡的缺陷,活在 #4419 自己的守卫里面。B 等于把那个缺陷留在原地,再加一条只为兼容旧行为存在的特判。取 A,并已按行为变更如实记入 changeset。

开放问题 2:格式良好的裸三元组在 engine 层从「抛错」变为「下沉执行」→ 取 A

派发令里「裸三元组今天抛错,这条不许变」那句是我写错了。 实现方现场核对后的拆分是对的,采纳:

否决窗口:维护者若认为 engine 门应当比协议门严,回一句我即刻转 B 并另派。

复核确认(对 GitHub 实况与本地核对,非照抄报告)

  • 不可达证明给的是否定结果,并如实报告 —— 类型层不可达不成立:FilterCondition 的非递归半边是索引签名,TS 里数组对索引签名可赋值,所以仓内 15 个测试文件裸写 where: [[...]] 不带 cast 也能过 tsc。这一点写进了测试注释,免得下一个人再推同一个错误论证,也正是必须靠运行时响亮拒收、不能靠类型的理由。生产不可达另由调用点清点 + 全仓 134/134 探针实测确立。
  • 文档面无漂移 —— 我核过 content/docs/protocol/objectql/query-syntax.mdx:1022:那段 Callout 早就写着数组是「UI builder 输入格式、不是规范协议格式」、进入协议或 IDataEngine必须parseFilterAST() 转换。这份已发布文档描述的世界,正是本 PR 刚刚变成真的那个。 此前是文档教一套、driver-sql 私下执行另一套。
  • 中缀形式的归属查清了 —— [condA, 'or', condB] parseFilterAST 表达不了、FilterArraySchema 也不接受,所以它是纯粹的驱动私产,从无契约背书。这决定了 Door 2 必须「先 isFilterAST 闸门、再下沉、否则拒收」,而不是无条件 parseFilterAST(后者对拼错操作符有 $${op} 宽松兜底,会把作者的错误变成一个没人执行的条件)。

首轮 CI 红的处理

mongodb-driver.test.tsshould filter with legacy array style 漏迁,成因是本地 driver-mongodb 有 120 个套件因无法下载 mongod 而跳过 —— 该盲点实现方在首份报告里已如实披露,不是事后找补。补漏时按正确的分类处理:该用例测的是方言本身,故改为断言拒收 + 补下沉对照(与 sql-driver-filter-no-silent-drop.test.ts 的两条同手法),而不是迁移成 parseFilterAST(那会变成测另一个东西)。


Generated by Claude Code

Merged via the queue into main with commit c7406b0 Aug 4, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5158-lower-filter-array-at-engine-door branch August 4, 2026 23:15
This was referenced Aug 4, 2026
os-zhuang pushed a commit that referenced this pull request Aug 5, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants