Skip to content

fix(driver-sql,service-analytics)!: 两类无意义比较对象不再编译成「静默空谓词」—— $in/$nin 的对象成员与 LIKE 族的对象比较值一律拒收 (#5234) - #6296

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-5234-silent-empty-predicates
Aug 7, 2026
Merged

fix(driver-sql,service-analytics)!: 两类无意义比较对象不再编译成「静默空谓词」—— $in/$nin 的对象成员与 LIKE 族的对象比较值一律拒收 (#5234)#6296
os-zhuang merged 6 commits into
mainfrom
claude/issue-5234-silent-empty-predicates

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#5234

前提复核:两个形状在最新 origin/main 仍在,且比 issue 记的更严重

按 os-dev 规程,先把 issue 正文的前提在 origin/main(探针跑于 70f132c15)上实测了一遍,再动代码。探针建了四行 fixture,其中 r_literalname 就是字面文本 [object Object] —— 它是把「静默零行」这个说法变成可证伪命题的那一行。

driver-sql 实测(better-sqlite3,内存库):

filterorigin/main 的答案判读
{status: {$in: ['a', {foo: 1}]}}['r_a']编译通过、执行、答得就像第二个成员从没被写过
{status: {$nin: [{foo: 1}]}}['r_a','r_b','r_five','r_literal'] —— 全表该排除的一行都没排除:方向是放宽,不是 issue 说的 fail-closed
{name: {$contains: {}}}['r_literal']命中了一条真实记录。不是「零行」,是错行
{name: {$notContains: {}}}['r_a','r_b','r_five']反过来:为一个没人记录的理由排除了一条真实记录
{name: {$startsWith: {}}}['r_literal']同上,LIKE 族整族如此
{name: {$contains: ['al','be']}}[](绑 %al,be%)见下方「数组是唯一一个拒收即消分叉的形状」

service-analytics 实测(同一 commit):

{name: {$contains: {}}}{name: {$contains: ['al','be']}}
read-scope-sql.compileOperator%[object Object]%%al,be%
where 门(fieldLeaves → 发射器读 values[0])leaf values: [{}]%al%

所以:前提成立(assertCompilableComparand 头注释的 "Deliberately NOT extended" 与 applyLikeString(value) 都还在),而且 issue 里「fail-closed、风险较低」的定级实测不成立——$nin / $notContains 方向是放宽,$contains 给的是错行。同时确认了 08-05 评论所说的第三份实现,并新测到一条评论里没有的事实:数组比较数在 service-analytics包内部就已经有两个答案(%al,be% vs %al%)。

对照组(必须继续工作的形状)在 origin/main 上是 $contains: 5%5%$contains: null%null%$in: ['a','b'] → 正常,改后逐条不变。

守卫落在哪里:每个包自己的收口点,不是三个 String() 发射器

调度单要求实测后判定「进两处发射器」还是「更上游收口」。判定是每个包自己的收口点,理由三条:

  1. convertFilter 是生产者,不是消费者。objectql-strategy.tsString(v0) 处收紧没有意义:那里把对象洗成一个类型完全正确的 '[object Object]' 字符串再交给驱动,下游再严格的驱动也永远看不到它该严格的那个形状。守卫必须在 leaf 生成之前。
  2. fieldLeaves 是本包唯一的 leaf 生产者,所以一处拒收同时覆盖三个消费方:NativeSQLStrategy(真正执行的语句)、ObjectQLStrategy.generateSql(/analytics/sql 回显)、ObjectQLStrategy.convertFilter(引擎路径)。read-scope-sql.ts 编译的 FilterCondition 不经过这个门,所以它自带同一套检查、包在它自己的信封里(policy 编译失败是 500 fail-closed,不是 400)。
  3. 更上游(engine seam)这次不行,因为它会动冻结面。packages/objectql 今天确实有一个引擎级闸(数据 API:集合算子 not_in/in 的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869 / PR fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 (#5869) #6209 刚落地的 filter-comparand-shape.ts),但把成员检查放到那里,必然改变 driver-memorydriver-mongodb 的可观察行为(它们今天按深等值接受对象成员),而 [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 明令这两个面零行为改动。值得一提:fix(objectql): 集合算子的标量比较值答 400 INVALID_FILTER 并点名期望形状,不再 500 (#5869) #6209 自己的头注释就把这件事让给了本单——原话是 $in/$nin 的成员类型 "are driver-sql:两类无意义比较对象仍编译成「静默空谓词」—— $in/$nin 的非 $field 对象成员,与 LIKE 族的对象比较值(String 成 [object Object]) #5234's subject, on the driver-sql object-syntax face, and are not re-judged here"。两个闸互补而非重复:它判「比较数到底是不是列表」,本单判「列表的每个成员能不能绑」。

like-pattern.tsapplyLike 里的 String(value)原样保留并各自加了一段说明:它们不再是缺陷所在,因为门前已经没有渲染不出来的值能到达。两包的谓词由 like-metacharacter-escape.test.ts逐值互锁(该文件本来就用同一手法锁转义表达式)。

合法形状全集:逐条实测后决定,默认只拒对象/数组

调度单要求每条留一行依据。围栏是 allow-list(抄 driver-tursoRemoteTransport 的形状,cloud#1004 / #1058:deny-list 会把下一个被发明出来的值形状悄悄放进来)。

比较数裁决依据(实测)
数字 {$contains: 5}保留driver-sql / driver-memory / analytics 两个面今天都给 %5%,#5526 刻意未拒。单面收紧即制造分叉
null{$contains: null}保留现行 %null%#5526 收敛、filter-value-type-fidelity.test.ts 钉住,⛔ 不打破
布尔保留与数字同一条渲染路径,三面一致
Date保留turso 的 allow-list 把它作为唯一的对象转换保留;拒收会重新叉开本地与远程
binary(Uint8Array/Buffer)$in 成员保留、LIKE 拒收绑得上(与写路径 formatInput 同一套分类)但渲染不出作者想要的东西 —— 这正是 isBindableComparand / isRenderableTextComparand 是两个谓词而不是一个带 flag 的原因
undefined保留不可授权(JSON 没有 undefined),analytics 门按 #5526 / #5332 归一为 null;在 driver 拒收会造出一个分歧而不是消除一个
对象 {} / {foo: 1}拒收String({})'[object Object]',本 PR 顶部表格的错行来源
数组(LIKE 族)拒收唯一一个「拒收即消分叉」的形状:read-scope-sql(与 driver-sql)绑 %al,be%,analytics 的 where 门绑 %al%(读 values[0],其余成员静默丢弃)。同一个包对同一条 filter 两个答案

{$eq: {…}}不在本次范围,仍按 toSqlBindValue 绑 JSON(#5526 钉住的行为);NULL/undefined 语义(#5298 / #6050)、#5921 条件层闸、#6204 聚合区一律未触。driver-memory / driver-mongodb 冻结面 diff 为零

降级表

位置改前改后
driver-sql(caller 授权的 filter)静默错答案 / 裸 TypeErrorINVALID_FILTER / 400,点名 index NStringOperatorSchema
analytics where 门(caller 授权)静默错答案INVALID_FILTER / 400
analytics read-scope(admin policy,非 caller 输入)静默 over-reachREAD_SCOPE_COMPILE_FAILED / 500,fail-closed,消息不外泄(#5367 已建的通道)

信封体例照 PR #5921 / #6142 / #6204;闸位在发射器之前(#6142 的论证同理适用)。

反向验证(方向在跑之前先定,计数重测于 d367f03)

方向就是平凡的 before-green / after-red:守卫是新增臂,不是 ?? 链重排,下游也没有 count 形状的闸,所以 #5046 / #5018 那两种反常方向不适用。逐臂停用实测:

⚠️ 一条如实记录:上述回退下 like-metacharacter-escape.test.ts保持绿。这不是漏,是分工——它锁的是两包谓词之间的一致,拒收文件锁的是门确实调用了谓词。两者都需要:只有前者就是 #4984 那种「fixture 拼的是被拒的写法,规则死了测试还绿」的形状。这一点已写进测试头注释。

首写时记的通过数是 32 / 36(当时两文件共 40 个测试);失败数两次都不变,是承重的那一半,通过数随邻测增删而漂移 —— 已在注释里说明,以免下一个读者以为计数漂移意味着臂失效。

验证

  • pnpm --filter @objectstack/driver-sql test68 passed | 4 skipped (72 files),942 passed | 46 skipped
  • pnpm --filter @objectstack/service-analytics test63 passed (63 files),1235 passed
  • 消费半径清扫 --filter '...@objectstack/driver-sql' --filter '...@objectstack/service-analytics'(前缀=下游)test + typecheck 全绿,覆盖 objectql / rest / runtime / plugin-auth / plugin-security / plugin-sharing / driver-sqlite-wasm / service-datasource 等 20+ 包
  • pnpm exec turbo build → 72/72
  • pnpm check:query-options-erasure → 棘轮持平,test surface 267 未抬,baseline key set verified against d367f03: no files added
  • pnpm check:type-check-debt(全仓 re-measure)→ OK,34 条台账无一高于记录值
  • node scripts/check-nul-bytes.mjs → OK;另对本分支所有改动文件做了越过闸门的自查 grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]',零命中
  • pnpm --filter @objectstack/spec check:generated → 10 个生成物均最新

并行/基线说明


🤖 Generated with Claude Code

https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx


Generated by Claude Code

…hat compiled to a silent nonsense predicate (#5234)
An `$in`/`$nin` list member that cannot be bound, and a LIKE-family comparand
that `String()` cannot render, both compiled to valid SQL that answered with a
predicate the caller never wrote:
- `{status: {$in: ['a', {foo: 1}]}}` answered as if the second member had
never been written; `{status: {$nin: [{foo: 1}]}}` excluded NOTHING, so the
exclusion the caller wrote silently did not happen.
- `{name: {$contains: {}}}` bound `LIKE '%[object Object]%'` and MATCHED a row
whose text really was `[object Object]`; `$notContains` excluded it.
#5041 (PR #5223) recorded both as "Deliberately NOT extended" on the grounds
that they were fail-closed. Measured, neither premise held: the `$nin` /
`$notContains` direction is wider, not narrower, and the answers were wrong
rather than empty.
The guard lands at each package's own chokepoint rather than at the three
`String()` emitters, so one `$contains` still means one thing on every face:
`assertCompilableComparand` in driver-sql, `fieldLeaves` for the analytics
`where` door (the only leaf producer, so it covers all three consumers of the
tree), and `compileOperator` for the read-scope lowering. The fence is an
allow-list, copied from driver-turso's RemoteTransport, which has refused these
same two shapes since cloud#1004 / #1058 — local and remote SQLite answered the
same query differently until now.
Primitives are deliberately untouched: `{$contains: 5}` and `{$contains: null}`
agree across every backend today and #5526 pinned the latter. Arrays are refused
because they already forked inside service-analytics (`%al,be%` at the read
scope, `%al%` at the `where` door).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
Resolved: #5298's read-scope NULL-safe negation (`nullSafeNegative`) and
#5298's `notSet OR <leaf>` rewrite in `fieldLeaves` both landed after this
branch's first commit. Both sides survive — the #5234 comparand-shape gate runs
BEFORE the NULL-safe rewrite reads the value, which is the required order: the
rewrite consults `nullValueSatisfiesOperator(opKey, v)` on the raw comparand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
- `find()` 调用去掉 `object` 键:#6210 把驱动查询参数收窄为 `DriverQuery`,
对象名只由第一个实参给出。没有用 `as any` 绕过——拒收类测试一旦用越契约的
输入构造查询,就不再是在验证调用方真能发出的形状。
- 反向验证计数重测:LIKE 臂停用 → 8 failed / 40 passed;成员臂停用 →
4 failed / 44 passed(此前记的 32 / 36 是首写时对 40 个测试测的)。失败数
两次都不变,是承重的那一半;通过数随邻测增删而漂移,已在注释里说明。
- 补记 analytics 侧同法实测:两个门回退到 main 版本 → 17 failed / 41 passed,
而 `like-metacharacter-escape.test.ts` 在该回退下保持绿——它锁的是两包谓词
之间的一致,拒收文件锁的是门确实调用了谓词,两者都需要。
Refs #5234
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 7, 2026 1:22pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/service-analytics.

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

  • content/docs/api/data-api.mdx(via @objectstack/service-analytics)
  • content/docs/api/index.mdx(via @objectstack/service-analytics)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/driver-sql, @objectstack/service-analytics)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/service-analytics)
  • content/docs/plugins/anatomy.mdx(via @objectstack/driver-sql)
  • content/docs/plugins/packages.mdx(via @objectstack/driver-sql, @objectstack/service-analytics)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/driver-sql)
  • content/docs/releases/implementation-status.mdx(via @objectstack/driver-sql, @objectstack/service-analytics)
  • content/docs/releases/v17.mdx(via @objectstack/service-analytics)
  • content/docs/releases/v9.mdx(via @objectstack/service-analytics)

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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

2 participants

@os-zhuang@claude