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,内存库):

filter origin/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-turso RemoteTransport 的形状,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) 静默错答案 / 裸 TypeError INVALID_FILTER / 400,点名 index NStringOperatorSchema
analytics where 门(caller 授权) 静默错答案 INVALID_FILTER / 400
analytics read-scope(admin policy,非 caller 输入) 静默 over-reach READ_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

claude added 6 commits August 6, 2026 11:45
…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

vercel Bot commented Aug 7, 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 7, 2026 1:22pm

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

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 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