Skip to content

fix(drivers): undefined 比较数一律拒收 —— 闸落在任何发射器/守卫之前,两个毛病同闸消灭 (#6050) - #6142

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6050-undefined-comparand-refusal
Aug 7, 2026
Merged

fix(drivers): undefined 比较数一律拒收 —— 闸落在任何发射器/守卫之前,两个毛病同闸消灭 (#6050)#6142
os-zhuang merged 1 commit into
mainfrom
claude/issue-6050-undefined-comparand-refusal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#6050

按 issue #6050 的裁决落档(2026-08-07,B 案:undefined 比较数一律响亮拒收,INVALID_FILTER / 400,#5347-A 同族信封)实施。

前提复核(先做的第一件事)

五格矩阵在最新 origin/main(cba7454df,立单时记的是 efedd289f)逐格复现,而且多量了四格。同一个 TursoDriver、同一条 filter、同一份 fixture(d 在 1-2 有值、3-4 为 NULL),答案取决于它是用哪个 url 构造的:

filterLOCAL(继承 SqlDriver)REMOTE(RemoteTransport)
{ d: undefined }抛裸 knex Undefined binding(s)(code/status 均 undefined)['3','4']
{ d: { $eq: undefined } }同上['3','4']
{ $not: { d: undefined } }同上(compiling WHERE)['1','2']
{ d: { $ne: undefined } }['1','2']['1','2']
{ $not: { d: { $ne: undefined } } }[]['3','4']
{ d: { $in: [undefined] } }抛裸 knex[]
{ d: { $nin: [undefined] } }抛裸 knex['3','4']
{ d: { $gt: undefined } }抛裸 knex[]
{ $or: [{ d: undefined }, { a: 'x' }] }抛裸 knex全表

对照组 {d: null} / {$eq: null} / {$ne: null} 两面一致且正确 —— 毛病只在 undefined 这一个值上。

两个毛病,一道闸

A —— 抛出的那几格没有 ADR-0112 信封。 knex 的 Undefined binding(s) detected when compiling SELECT 既无 code 也无 status,mapDataError 落默认分支,一条「调用方 filter 写坏了」的错误以不透明 500 到达客户端。#1116 / #4436 为这条通路清点过同类形态,唯独漏了这格。

B —— 守卫与它自己的发射器分裂。$ne 发射器读 coerced == null(宽松 ⇒ undefined 编译成 IS NOT NULL,一条 TOTAL 谓词),而必须钉住这个发射器的两张极性表读 === null(严格 ⇒ 判它「不 total」且「NULL 行满足它」)。nullGuardForFieldSpec 于是把一条已 total 的谓词包成 d IS NULL OR d IS NOT NULL —— 恒真 —— 取反后恒假,答 []。这正是 #5298 立的不变量在它自己的定义处被破坏:每张极性表钉的是它自己发射器的拼写。

闸落在比较数进入任何发射器或守卫之前,两个毛病同闸消灭 —— knex 再也见不到 undefined 绑定,守卫与发射器对 undefined 的分歧变成不可达,而不是「被修好」。

拒收位置逐个清点:直接比较数、单值算子比较数($eq/$ne/$gt/$gte/$lt/$lte 与 LIKE 族)、列表算子数组的成员($in/$nin/$between),以及嵌在 $and/$or/$not 里的以上各位。$null / $existsundefined保持它们自己的布尔域拒收措辞(那条消息更贴切 —— #5240「一个条件一种措辞」两个方向都适用)。两驱动的拒收句子逐字一致。

null 一字未动

{ f: null }{ $eq: null }IS NULL;{ $ne: null }IS NOT NULL;$null: true/false 不变;null 仍是合法 $in 成员;$not 下同。三个测试文件各带一段 null 对照块钉这件事。

调用方清点(issue 的前置要求)

  • 静态:grep 仓内 where/filter 字面量里的可选链与动态装配点 —— 生产面用的都是 if (x) where.k = x?? null,即裁决建议的作者侧修法本身,零个依赖旧行为。
  • 实测:全仓 build(71 tasks 全过)+ 全仓 test(135 个任务 134 成功)。唯一失败是 @objectstack/dogfoodsemantic-roles.dogfood.test.ts 在满载下 beforeAll 10s 超时,单独重跑 5/5 通过 —— 与本改动无关的负载抖动。两个 example app 均 build 通过,dogfood 会真正把 showcase stack 起起来跑。⇒ 没有依赖 remote 现行 IS NULL 行为的生产调用方,不触发「停手」条件。

测试

反向验证(方向预判,实测)

预判:不是一个方向。拒收用例该红,对照用例该绿,而且 driver-sql 侧的红该由两种不同机制产生 —— 因为未修的驱动本来就用两种方式回答这一族。

实测(只删两处 assertDefinedComparands 调用,其余不动):

  • driver-sql:22 failed / 6 passed(共 28)。留绿的 6 个正好是全部对照块。红的机制是混合的:多数位置红在抛出 knex 的裸 Undefined binding(s) —— 一个没有 code/status 的 Error,所以 refusalOf 正常返回、只有信封断言失败。⚠️一个只断言「它抛了」的测试会在本 issue 所针对的那个驱动上保持绿色 —— 这就是每一例都断言 codestatus 的原因。其余($ne、LIKE 族、{ $not: { … $ne: undefined } })红在答了:从未抛过,且 {$not:{$ne:undefined}}[],即毛病 B 的恒真式。
  • driver-turso:20 failed / 9 passed(共 29),20 个全部红在 refusalOf 的「本该拒收,却编译成了 …」分支 —— 即全部靠「答出来」,与预判一致(这个 transport 从不抛)。留绿的 9 个是对照:null 块、节点位拒收、where 缺席、布尔域用例、常规词表。

门禁

pnpm --filter @objectstack/driver-sql --filter @objectstack/driver-turso typecheck → Done / Done
pnpm --filter @objectstack/driver-sql test → Test Files 65 passed | 4 skipped Tests 906 passed | 46 skipped
pnpm --filter @objectstack/driver-turso test → Test Files 24 passed Tests 788 passed
node scripts/check-nul-bytes.mjs → OK (5860 files, no raw ASCII control bytes)

实测但改的其余无值语义面 → #6125

issue「落点」节要求裁决后逐格实测其余面。做完了,记录如下 —— 同一个 { d: undefined },仓内五种读法:

求值面{ d: undefined }读法
driver-sql / turso LOCAL抛裸 knex
turso REMOTE['3','4']undefined ≡ null
driver-memory(mingo)['3','4']undefined ≡ null
formulamatchesFilterCondition只匹配键缺失的行undefined ≢ null
service-analyticsread-scope-sql"t"."d" = ? bound null= NULL → 匹配零行

三条都是语义争议而非实现细节,派工单明确要求「有语义争议只记录留后续单、不扩大 diff 赌时限」:formula 的读法会顺手替 #5299 拍板;read-scope-sql 只会抛 READ_SCOPE_COMPILE_FAILED / 500(与本裁决的 400 不是一个信封,且 read scope 是平台自己编译的、不是调用方输入);driver-memory / mongodb 是 #5499 的冻结面,按裁决零行为改动。已如实立单 #6125(finding,未指派,带 Blocked-by: #6050),其中也写明了本 PR 落地后 driver-memory 会在这一格上与 driver-sql 不一致 —— 那是裁决接受的代价,不是回归。

changeset

.changeset/undefined-comparand-refusal.md(driver-sql + driver-turso 双 patch),升级说明写明「undefined 比较数从静默/崩溃变为 400 拒收,作者侧修法是显式判空或用 null / $null」,并附两种正确写法。


🤖 Generated with Claude Code

https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx


Generated by Claude Code

… or guard reads it (#6050)
`undefined` in a comparand position had two answers in one driver, chosen by
the url a TursoDriver was constructed with. Measured on origin/main
(cba7454), four rows with `d` valued on 1-2 and NULL on 3-4:
{ d: undefined } LOCAL: knex "Undefined binding(s)" REMOTE: ['3','4']
{ d: { $eq: undefined } } LOCAL: knex "Undefined binding(s)" REMOTE: ['3','4']
{ $not: { d: undefined } } LOCAL: knex "Undefined binding(s)" REMOTE: ['1','2']
{ d: { $ne: undefined } } LOCAL: ['1','2'] REMOTE: ['1','2']
{ $not: { d: { $ne: undefined } } } LOCAL: [] REMOTE: ['3','4']
{ d: { $in: [undefined] } } LOCAL: knex "Undefined binding(s)" REMOTE: []
{ d: { $gt: undefined } } LOCAL: knex "Undefined binding(s)" REMOTE: []
Two defects. (A) The throwing rows carried no ADR-0112 envelope: knex's error
has neither `code` nor `status`, so mapDataError served an opaque 500 for a
caller mistake in a filter (#1116/#4436 catalogued the shape; this one was
missing from the list). (B) driver-sql's `$ne` emitter read `coerced == null`
while its own polarity tables (operatorIsNullTotal, nullValueSatisfiesOperator)
read `=== null`, so nullGuardForFieldSpec wrapped an already-TOTAL predicate in
`d IS NULL OR d IS NOT NULL` and its negation answered []. That is #5298's
invariant — a polarity table pins the spelling of its own emitter — broken at
its own definition.
Ruled REFUSED on 2026-08-07 (#6050 ruling B, the #5347-A disposition):
FieldOperatorsSchema declares no undefined comparand, `{ f: undefined }` is
indistinguishable from an absent key while meaning the opposite, and undefined
cannot survive JSON so it is always an in-process authoring bug — the
`{ owner_id: ctx.user?.id }` that silently matched every env-wide row.
One gate, placed before any emitter or guard, kills both: knex never sees an
undefined binding, and guard-vs-emitter disagreement about undefined becomes
unreachable rather than repaired.
- driver-sql: on the reduceFilterKey validation walk, beside the $null/$exists
refusals, plus applyFilters' plain-map branch (which the walk cannot see —
`{ d: undefined }` cannot make hasMongoOperators true). One function, two
call sites.
- driver-turso: a whole-subtree pre-walk at buildWhereSQL entry, which is what
makes `{ $not: { d: undefined } }` hold — compiling key by key would reach
nullSafeNegationOperand with the undefined still in it.
- Both sides' `== null` / `|| === undefined` spellings tightened to `=== null`
for #5347's reason: a lenient test silently resumes answering if the gate
ever moves.
Positions enumerated, not swept: direct comparand, single-value operators, and
members of $in/$nin/$between arrays, at every depth. $null/$exists keep their
own boolean-domain refusal (#5240: one condition, one wording, both ways). The
two drivers' refusal sentences are verbatim identical.
null is untouched in every position: { f: null }, $eq/$ne: null, $null, and
null as an $in member all compile exactly as before, pinned by control blocks
in all three test files.
Callers measured, per the issue's precondition: full repo build (71 tasks) and
full repo test (134/135) pass, both example apps build, and the dogfood suite
boots the showcase stack — no production caller depends on the old behaviour.
Other no-value surfaces measured and NOT changed: formula reads undefined as
key-absence, read-scope-sql compiles `= NULL`, driver-memory reads it as null.
Three further readings, each a separate semantic ruling with its own envelope —
filed as #6125 rather than widened into this diff.
Co-Authored-By: Claude Fable 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 3:23am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/drivers.mdx(via @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/deployment/cli.mdx(via @objectstack/driver-turso)
  • content/docs/deployment/environment-variables.mdx(via @objectstack/driver-turso)
  • content/docs/deployment/self-hosting.mdx(via @objectstack/driver-turso)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/driver-sql)
  • content/docs/plugins/anatomy.mdx(via @objectstack/driver-sql)
  • content/docs/plugins/packages.mdx(via @objectstack/driver-sql, @objectstack/driver-turso)
  • 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)

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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 7, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 7, 2026 03:44
@os-zhuang
os-zhuang added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit acf34e3Aug 7, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6050-undefined-comparand-refusal branch August 7, 2026 04:02
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