fix(driver-turso): RemoteTransport 拒收条件层的 $-算子键 —— 不再编译成静默空集/全表写 (#5769) - #5921
Merged
os-zhuang merged 1 commit intoAug 6, 2026
Merged
Conversation
…5769) `buildWhereSQL` recognised only `$and` / `$or` / `$not`; every other `$`-prefixed key of a filter node fell into the FIELD arms and was quoted into SQL as a COLUMN of that name. Measured on origin/main (5c94f83) with a capturing client and a three-row fixture: { $eq: 'won' } -> WHERE "$eq" = ? -> [] { $where: 'return true' } -> WHERE "$where" = ? -> [] { $and: 'x' } -> WHERE "$and" = ? -> [] { $or: [{}, { $where: 'x' }] } -> no WHERE at all -> every row The first three are the silent empty set: SQLite degrades a double-quoted name that resolves to no column into a string literal, so the statement compiles, runs and matches nothing — and where that rule is disabled, `find()`'s own `no such column` backstop swallows the error into `[]` anyway. The fourth needs no dialect quirk: a `{}` disjunct absorbs its `$or` to TRUE, the malformed sibling's compiled clauses are discarded with it, and the statement loses its WHERE entirely — a whole-table write on deleteMany/updateMany, measured as all three rows rewritten. A node-position `$`-key that is not one of the three declared combinators is now refused with INVALID_FILTER / 400, with no statement executed, on all six WHERE-building entry points. Two tails, one verdict: a misplaced FIELD operator is told to name its field, an undeclared key is refused by name. `$and` / `$or` carrying something other than a list is refused too — it fell through the same `Array.isArray` tests into the same field arm. This is objectstack#5348's ruling, already landed on SqlDriver's validation walk by PR #5368 and inherited by driver-sqlite-wasm and Turso LOCAL. RemoteTransport is an independent compiler and inherited none of it, so one driver answered one filter two ways depending only on `url` — strict locally, silent remotely. A parity block pins that fork closed. The gate sits inline in the entry loop rather than on a separate walk: unlike SqlDriver's emitter, this compiler's `$and`/`$or` branches compile EVERY element before applying their identity rule, so the loop already visits every node. `remote-transport-not-operator.test.ts`'s `$not: { $eq: 'won' }` case is replaced rather than re-spelled: it pinned exactly this limb, under a comment naming #1077 (now #5769) as the successor that would change it. 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 1 package(s): 6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 6, 2026
os-zhuang
marked this pull request as ready for review
August 6, 2026 13:26
os-zhuang
deleted the
claude/issue-5769-turso-remote-operator-key-refusal
branch
August 6, 2026 13:38
This was referenced Aug 6, 2026
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 #5769
前提复核(先做的事)
单子的前提在
origin/main(5c94f833c)上逐条实测确认,并且比正文所述更严重一格。捕获客户端 + 三行 fixture(d_won/d_lost/d_open):前五行就是维护者实测过的静默空集。补一条机理:SQLite 把「解析不到列的双引号标识符」降级成字符串字面量 → 语句编得出、跑得通、一行不匹配;而在关掉该规则的构建上(本仓测试用的 better-sqlite3 即
SQLITE_DQS=0)会报no such column,RemoteTransport.find自己的未知列兜底又把它吞成[]。两条路,一个答案,调用侧都区分不出「没有匹配的行」和「过滤器根本没编出来」。最后一行是单子正文没有预料到的一格,也是真正付出代价的一格,而且不依赖任何方言怪癖:
{}是$or的 TRUE 单位元,整组被吸收,连同那个畸形兄弟已经编出来的子句一起被丢掉,语句整个丢掉了 WHERE。实测:一个一行都没点名的过滤器改写了整张表。这就是单子正文里「写路径上谓词恒假或恒真,取决于落侧」的恒真那一侧,而且它落在了「连谓词都没有」这个更宽的位置上。
改了什么
buildWhereSQL的条目循环开头加一道节点级闸:条件层任何$开头、又不是三个已声明组合算子的键,一律INVALID_FILTER/ 400 拒收,且不发出任何语句。六个建 WHERE 的入口(find / findOne / count / aggregate / deleteMany / updateMany)全部覆盖。消息分两种,裁定只有一个。 节点位置的
$eq和节点位置的$where不是同一个错误:前者是真算子写高了一层(手写或 AI 生成的过滤器漏掉字段名时最常见的形态),修法是把字段名补上;后者协议在任何层级都没有声明,修法是别用。两者都以同一个INVALID_FILTER/ 400 和同一句开头拒收 —— 首句与driver-sql/driver-memory逐字相同(#5240「一个条件一种措辞」),只有收尾的指路方向不同。没有任何东西 branch 在这个区分上:MISPLACED_FIELD_OPERATORS只决定印哪句话,永远不决定是否拒收,所以往 spec 的FILTER_OPERATORS里加名字改不动本闸的判决(#5701 那条「它是运行时 allowlist 不是词表」的告诫)。顺带补上的同族缺口:非数组的
$and/$or。{ $and: 'x' }此前从两个Array.isArray判断底下漏过去,落进同一条字段路径、编成WHERE "$and" = ?—— 和{ $eq: 'won' }结局逐字相同。不修它就等于把闸开在它赖以定义的那三个键上留个洞;SqlDriver的assertFilterNodeList早已按名拒收,补上后 local/remote 在这里也一致。$not取单个操作数而非列表,按形状由下一层的buildSubFilterSQL拒收,措辞不变(用例钉住了这条边界)。闸放在发射循环里,而不是另起一趟校验遍历 —— 与
SqlDriver的落点相反,理由是这个编译器的性质不同。SqlDriver的发射器会被布尔单位元整段跳过,所以闸放发射器上会变成「取决于兄弟节点」的条件闸(PR #5368 的完整论证)。本编译器没有这个缺口:它的$and/$or分支是先把每个元素都编完再应用单位元规则的 —— 上面第六行实测就是这个事实本身({ $where: 'x' }被编了出来,然后才被丢掉)—— 所以条目循环本身已经走遍整棵树。(c) 组的三个用例正是这一点垮掉时会红的用例。为什么 remote 是最后一面
objectstack#5348裁定拒收,PR #5368 在SqlDriver的校验遍历落地,driver-sqlite-wasm与 Turso local 继承。RemoteTransport是独立的过滤器编译器,继承不到任何一点,于是同一个TursoDriver、同一个过滤器,只因构造时的url不同就给两个答案,而且方向是反的:local 严、remote 松 —— 这是最容易让 bug 一路走到生产还没被测出来的方向。新增的 local/remote 一致性 describe 块把这条叉钉死:六个形态,两个 transport,同样的code/status。反向验证(方向是事先定的:红)
新用例整份跑在未打补丁的
origin/main源码上:59 个用例 46 红 13 绿。绿的 13 个正是控制组:既有行为的字节一致性断言、六个邻接拒收各自的措辞、以及 —— 值得单独说 ——
$nor那条 local/remote 一致性用例。它在补丁前就已经是绿的,因为 remote 侧的$nor是被 #1058/#1066 的比较值守卫偶然拦下的(数组不可绑定),信封恰好也对。这正是单子正文点名的那条「偶然拦下」,补丁后它改由本闸按正确的措辞拒收(消息从「'deal.$nor'的比较值是数组」变成「where.$nor不是已声明的组合算子」),不再把$nor报成deal的一个字段。fixture 处置
remote-transport-not-operator.test.ts的$not: { $eq: 'won' }一例是整例替换,不是改写期望值。它此前钉的正是本 PR 删掉的那条肢体,而且它自己的注释就写着理由:「残留的"$eq"列是调用方写高了一层的内层条件,本地SqlDriver也一样把条件层$eq当列名交给 Knex —— 收紧它是 #1077,刻意不在本次做」。那条理由已经过期:#5348 / PR #5368 让 local 变严了,remote 成了最后一面,而 #1077 就是本单。替换后它仍然证明这个文件本来要证明的事:拒收点名的是内层的$eq、位置是where.$not.$eq,$not一路到底都还是它被声明的那个算子。其余 fixture 无一需要动。已按规则消费半径排查:
RemoteTransport只被TursoDriver消费(全仓 grep,包外引用全是注释),toRemoteFilter对$键原样透传、不合成任何节点层$键,全仓也没有别的 fixture 在条件层写$算子键(service-analytics的$nor用例走它自己的编译器)。共享一致性表
验收条款里的「若共享一致性表已覆盖 remote 面则加轴」经核不适用:
FILTER_LOGIC_CASES没有「这条必须被拒收」的表达方式 ——FilterLogicCase.expected是行 id 列表,空列表意思是「匹配到零行」,恰恰是本裁定没有取的那个答案。这一点是该表自己的文档写明的(「Enrolling this case needs the shape extended first … deliberately not invented here」,针对 #5240 的同类困境)。所以新用例按本包既有惯例(一缺陷族一文件:comparand-refusal / null-comparand-refusal / top-level-where / not-operator)落在remote-transport-node-operator-refusal.test.ts,与driver-sql的孪生文件sql-driver-out-of-contract-filter-input.test.ts对齐。:462的顺带项:不做,已拆单 #5907维护者点名的
remote-transport.ts:462Unsupported aggregate function裸Error,实测后没有在本 PR 收尾,理由是实测推翻了「remote 单面」这个前提:packages/drivers/driver-sql/src/sql-driver.ts:6753(mapAggregateFunc,由SqlDriver.aggregate:3103调用)逐字相同的裸Error,code/status同样undefined。只修 remote 会让 remote 变INVALID_QUERY/400、local 仍是裸Error—— 一个条件、两种线上身份、取决于url,正是本单花整单去关的那类叉,方向反过来。median,调用方写错了)和协议声明了、没有任何 SQL 后端编译的函数(count_distinct/array_agg/string_agg,AggregationFunction声明 8 个、两处各编 5 个)。后者是能力缺口不是调用方的错,driver-memory的 driver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not整条丢,$between/$startsWith/$null/$regex因无 cube 映射而丢 —— 聚合结果被放大 #5345 已经为过滤器算子画过这条线;合成一句会告诉一个写count_distinct的看板作者说他打错字了。两面同改 + 第二类怎么答,需要一次会波及四个后端的裁定,已按 Prime Directive #10 拆为 #5907(未指派)。
验证
均在合入最新
main(73571306b)、重装依赖并重建上游依赖包之后跑过(合入的 5 个提交未触及driver-turso或packages/spec/src/data)。node scripts/check-nul-bytes.mjsOK,改动文件另做过控制字符自查。合法过滤器一个字节都没变:三个组合算子的嵌套、
$and: []/$or: []/$or: [{}, …]/$not: {}的布尔单位元、字段层算子、隐式相等、IS NULL,以及六种既有拒收各自的措辞 —— (f)、(g) 两组把它们逐条钉住。🤖 Generated with Claude Code
https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx
Generated by Claude Code