Skip to content

fix(driver-turso): RemoteTransport 拒收条件层的 $-算子键 —— 不再编译成静默空集/全表写 (#5769) - #5921

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5769-turso-remote-operator-key-refusal
Aug 6, 2026
Merged

fix(driver-turso): RemoteTransport 拒收条件层的 $-算子键 —— 不再编译成静默空集/全表写 (#5769)#5921
os-zhuang merged 1 commit into
mainfrom
claude/issue-5769-turso-remote-operator-key-refusal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5769

前提复核(先做的事)

单子的前提在 origin/main(5c94f833c)上逐条实测确认,并且比正文所述更严重一格。捕获客户端 + 三行 fixture(d_won/d_lost/d_open):

{ $eq: 'won' }                 → SELECT * FROM "deal" WHERE "$eq" = ?         → []
{ $gt: 5 }                     → SELECT * FROM "deal" WHERE "$gt" = ?         → []
{ $null: true }                → SELECT * FROM "deal" WHERE "$null" = ?       → []
{ $where: 'return true' }      → SELECT * FROM "deal" WHERE "$where" = ?      → []
{ $and: 'x' }                  → SELECT * FROM "deal" WHERE "$and" = ?        → []
{ $or: [{}, { $where: 'x' }] } → SELECT * FROM "deal"(整句没有 WHERE)        → 全部三行

前五行就是维护者实测过的静默空集。补一条机理:SQLite 把「解析不到列的双引号标识符」降级成字符串字面量 → 语句编得出、跑得通、一行不匹配;而在关掉该规则的构建上(本仓测试用的 better-sqlite3 即 SQLITE_DQS=0)会报 no such column,RemoteTransport.find 自己的未知列兜底又把它吞成 []两条路,一个答案,调用侧都区分不出「没有匹配的行」和「过滤器根本没编出来」。

最后一行是单子正文没有预料到的一格,也是真正付出代价的一格,而且不依赖任何方言怪癖:{}$or 的 TRUE 单位元,整组被吸收,连同那个畸形兄弟已经编出来的子句一起被丢掉,语句整个丢掉了 WHERE。实测:

updateMany({ $or: [{}, { $where: 'x' }] }, { stage: 'ARCHIVED' })
  → UPDATE "deal" SET "stage" = ?   (无 WHERE)
  → stages ["won","lost","open"] → ["ARCHIVED","ARCHIVED","ARCHIVED"]

一个一行都没点名的过滤器改写了整张表。这就是单子正文里「写路径上谓词恒假或恒真,取决于落侧」的恒真那一侧,而且它落在了「连谓词都没有」这个更宽的位置上。

改了什么

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' } 结局逐字相同。不修它就等于把闸开在它赖以定义的那三个键上留个洞;SqlDriverassertFilterNodeList 早已按名拒收,补上后 local/remote 在这里也一致。$not 取单个操作数而非列表,按形状由下一层的 buildSubFilterSQL 拒收,措辞不变(用例钉住了这条边界)。

闸放在发射循环里,而不是另起一趟校验遍历 —— 与 SqlDriver 的落点相反,理由是这个编译器的性质不同。SqlDriver 的发射器会被布尔单位元整段跳过,所以闸放发射器上会变成「取决于兄弟节点」的条件闸(PR #5368 的完整论证)。本编译器没有这个缺口:它的 $and / $or 分支是先把每个元素都编完再应用单位元规则的 —— 上面第六行实测就是这个事实本身({ $where: 'x' } 被编了出来,然后才被丢掉)—— 所以条目循环本身已经走遍整棵树。(c) 组的三个用例正是这一点垮掉时会红的用例。

为什么 remote 是最后一面

objectstack#5348 裁定拒收,PR #5368SqlDriver 的校验遍历落地,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:462 Unsupported aggregate functionError,实测后没有在本 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-memorydriver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not 整条丢,$between/$startsWith/$null/$regex 因无 cube 映射而丢 —— 聚合结果被放大 #5345 已经为过滤器算子画过这条线;合成一句会告诉一个写 count_distinct 的看板作者说他打错字了。

两面同改 + 第二类怎么答,需要一次会波及四个后端的裁定,已按 Prime Directive #10 拆为 #5907(未指派)。

验证

pnpm --workspace-concurrency=2 --filter @objectstack/driver-turso typecheck
  → tsc --noEmit(无输出)

pnpm --workspace-concurrency=2 --filter @objectstack/driver-turso test -- --maxWorkers=2
  → Test Files  21 passed (21)
  →      Tests  691 passed (691)

均在合入最新 main(73571306b)、重装依赖并重建上游依赖包之后跑过(合入的 5 个提交未触及 driver-tursopackages/spec/src/data)。node scripts/check-nul-bytes.mjs OK,改动文件另做过控制字符自查。

合法过滤器一个字节都没变:三个组合算子的嵌套、$and: [] / $or: [] / $or: [{}, …] / $not: {} 的布尔单位元、字段层算子、隐式相等、IS NULL,以及六种既有拒收各自的措辞 —— (f)、(g) 两组把它们逐条钉住。


🤖 Generated with Claude Code

https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx


Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 6, 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 6, 2026 11:40am

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-turso.

6 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-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-turso)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-turso)

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-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 13:26
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit d82b85f Aug 6, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5769-turso-remote-operator-key-refusal branch August 6, 2026 13:38
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/l tests tooling

Projects

None yet

2 participants