fix(drivers): undefined 比较数一律拒收 —— 闸落在任何发射器/守卫之前,两个毛病同闸消灭 (#6050) - #6142
Merged
Conversation
… 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 7, 2026 03:44
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 #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构造的:SqlDriver)RemoteTransport){ d: undefined }Undefined binding(s)(code/status均 undefined)['3','4']{ d: { $eq: undefined } }['3','4']{ $not: { d: undefined } }['1','2']{ d: { $ne: undefined } }['1','2']['1','2']{ $not: { d: { $ne: undefined } } }[]['3','4']{ d: { $in: [undefined] } }[]{ d: { $nin: [undefined] } }['3','4']{ d: { $gt: undefined } }[]{ $or: [{ d: undefined }, { a: 'x' }] }对照组
{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 的分歧变成不可达,而不是「被修好」。
driver-sql:reduceFilterKey的校验走查(与$null/$exists拒收并排),外加applyFilters的平铺映射分支 ——{ d: undefined }进不了走查(typeof undefined不是'object',构不成hasMongoOperators),而它恰是这个 bug 最常见的拼写。两处共用一个函数,{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 已经为「同一形状两处两判」付过一次账。driver-turso:buildWhereSQL入口做一次整棵子树的前置走查。必须前置:逐键编译会先把$not的操作数交给nullSafeNegationOperand(一个守卫)。== null/|| === undefined的拼写统一收严成=== null($null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 收紧$null臂时给的理由:宽松拼写在闸被挪走后会悄悄恢复回答一个没人裁决过的取值)。拒收位置逐个清点:直接比较数、单值算子比较数(
$eq/$ne/$gt/$gte/$lt/$lte与 LIKE 族)、列表算子数组的成员($in/$nin/$between),以及嵌在$and/$or/$not里的以上各位。$null/$exists的undefined保持它们自己的布尔域拒收措辞(那条消息更贴切 —— #5240「一个条件一种措辞」两个方向都适用)。两驱动的拒收句子逐字一致。⛔
null一字未动{ f: null }、{ $eq: null }→IS NULL;{ $ne: null }→IS NOT NULL;$null: true/false不变;null仍是合法$in成员;$not下同。三个测试文件各带一段null对照块钉这件事。调用方清点(issue 的前置要求)
where/filter字面量里的可选链与动态装配点 —— 生产面用的都是if (x) where.k = x或?? null,即裁决建议的作者侧修法本身,零个依赖旧行为。@objectstack/dogfood的semantic-roles.dogfood.test.ts在满载下beforeAll10s 超时,单独重跑 5/5 通过 —— 与本改动无关的负载抖动。两个 example app 均 build 通过,dogfood 会真正把 showcase stack 起起来跑。⇒ 没有依赖 remote 现行 IS NULL 行为的生产调用方,不触发「停手」条件。测试
packages/drivers/driver-sql/src/sql-driver-undefined-comparand-refusal.test.ts(28 例)packages/drivers/driver-turso/src/remote-transport-undefined-comparand-refusal.test.ts(29 例)turso-local-remote-null-parity.test.ts补上 undefined 格(fix(driver-turso): remote 模式对齐 NULL-safe 语义 —— $not/$ne/$nin/$notContains 四算子 + $exists 拒收闸 (#5903) #6047 刻意未覆盖的那一列):13 个位置 × 两 transport,同码、同 status、同句子,外加一段null对照。反向验证(方向先预判,再实测)
预判:不是一个方向。拒收用例该红,对照用例该绿,而且 driver-sql 侧的红该由两种不同机制产生 —— 因为未修的驱动本来就用两种方式回答这一族。
实测(只删两处
assertDefinedComparands调用,其余不动):Undefined binding(s)—— 一个没有code/status的 Error,所以refusalOf正常返回、只有信封断言失败。code与status的原因。其余($ne、LIKE 族、{ $not: { … $ne: undefined } })红在答了:从未抛过,且{$not:{$ne:undefined}}答[],即毛病 B 的恒真式。refusalOf的「本该拒收,却编译成了 …」分支 —— 即全部靠「答出来」,与预判一致(这个 transport 从不抛)。留绿的 9 个是对照:null块、节点位拒收、where缺席、布尔域用例、常规词表。门禁
实测但未改的其余无值语义面 → #6125
issue「落点」节要求裁决后逐格实测其余面。做完了,记录如下 —— 同一个
{ d: undefined },仓内五种读法:{ d: undefined }driver-sql/ turso LOCAL['3','4']undefined ≡ nulldriver-memory(mingo)['3','4']undefined ≡ nullformulamatchesFilterConditionundefined ≢ nullservice-analyticsread-scope-sql"t"."d" = ?boundnull= 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