Skip to content

fix(driver-turso): remote 分页读补齐确定性排序,与 local 面共用同一条规则 (#5653) - #5689

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5653-turso-remote-pagination-tiebreaker
Aug 6, 2026
Merged

fix(driver-turso): remote 分页读补齐确定性排序,与 local 面共用同一条规则 (#5653)#5689
os-zhuang merged 2 commits into
mainfrom
claude/issue-5653-turso-remote-pagination-tiebreaker

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5653

前提复核(对 origin/main c36abfe98

成立,逐条核对过:

  • remote-transport.tsbuildSelectSQL 确实把调用方 orderBy 原样 map 进 SQL、不追加唯一列,紧接着无条件拼 LIMIT ? / OFFSET ?,两段之间没有任何「这是分页读」的判断;
  • local 面走 SqlDriver.orderKeysFor() + paginationTieBreaker(),按 分页读取在没有 orderBy 时同样不确定:tie-breaker 只覆盖了「排了序的翻页」 #4363 的三态表办事;
  • remote 面建表由 RemoteTransport.buildCreateTableSQL 负责,开头无条件写 "id" TEXT PRIMARY KEY —— 所以 issue 说的「paginationTieBreaker 的前提在 remote 也成立」属实。

但有一处 issue 没写、而修法必须处理的机制paginationTieBreaker 读的是基类 managedObjectFields,那张表只由 SqlDriver.initObjects 填。remote 模式的 DDL 走 RemoteTransport.syncSchemasBatch从不经过那个方法registerRemoteFieldMetadata 只调 registerExternalObject,它不碰 managedObjectFields)。所以直接复用 orderKeysFor 会对每个对象都答「不是我建的表」,等于一个空修。本 PR 把这一格补上了。

修法:复用,不造第二套

按 PM 指示优先复用。orderKeysFor / paginationTieBreaker 都是 protectedTursoDriver extends SqlDriver,可见性没有问题,未改动 driver-sql 一个字节

TursoDriver.find / findOne 现在先经 toRemoteReadQuery() 用继承来的 orderKeysFor() 解析出完整排序键,再交给传输层;三态规则只有一份实现:

orderBy 分页 结果
非空 任意 调用方的键 + id
limit/offset 单独 id
都没有 不加 ORDER BY(#4363 carve-out,同批搬过来)

为什么落点不在 buildSelectSQL(issue 的建议落点)

因为 RemoteTransport.findOne 自己把 id 查询拼成 find(object, { ...query, limit: 1 })。等 SQL 建到 buildSelectSQL 那一层,findOne 与「页大小为 1 的第一页」已经不可区分,而两者要的东西正相反 —— ORDER BY id LIMIT 1 正是让计划器放弃谓词自身索引改走主键的形状(SqlDriver.findRows 里记着的 2M 行 Postgres 实测:0.08 ms 变 7.8 ms)。在 driver 这一层两个调用方还分得开,singleRowLookup 就是它们表明身份的方式 —— 和 local 面完全一致。

传输层仍是纯粹的 SQL 组装器:buildSelectSQL 上补了一段文档说明「这里的 ORDER BY 是渲染,不是决策」,并写明不要在此处长出第二套 tie-breaker 规则以及原因。

唯一列的判定

新增 paginationTieBreaker override:规则不重述,只回答 remote 特有的那一个事实 —— 这张表是不是本驱动建的。事实记在 remoteManagedObjectsregisterRemoteFieldMetadata 里登记,两个 remote DDL 入口都经过它,且在 DDL 成功之后)。基类那份保守全部保留:不是自己建的表返回 null、不发明排序列,并按既有机制每对象告警一次。

测试

新增 remote-pagination-tiebreaker.test.ts(11 条,语句级)—— 共享套件在 12 行内存表上分不清「真 tie-breaker」和「计划碰巧」,所以按发出的 SQL 逐格钉三态表,外加 findOne、limit 单独出现、方向跟随、调用方已按 id 排序、未托管表五种情形。

#5590 的两条机制 pin 按 PM 要求翻转:从钉插入序改为钉新机制的具体序列(组内 id 序 / 无序分页单 id 序),并各自 not.toEqual 它取代的那个 storage-order 序列,所以不是靠「什么都没产生」而绿。

同文件第三条 page boundaries are invisible with NO orderBy 属于「整条替换」处置而非改写:它把无序分页 walk 和未分页无序读作对比,原先两边都不排序才绿;修完后两者理应不同(walk 是 id 序的分区,未分页读保持 #4363 carve-out)。继续比对等于把 carve-out 的缺席钉死,所以换成 local 孪生文件里那条 walks an unsorted read in id order

反向验证(方向前置)

预测:还原修法后,两条翻转的 pin、三条替换的 walk、语句级套件里判定改变的那几格应变红;而 carve-out 那几格应保持绿,因为这次改动没有触碰它们。实测把 find/findOne 还原成 toRemoteQuery 后:12 红 / 620 绿,红的正是 3 条 walk + 2 条 pin + 7 条语句断言;保持绿的 4 条恰好是新旧行为本就重合的格子 —— 未分页无序读不加 ORDER BY、未托管表不加、findOne 无序、以及调用方已经按 id 排序时输出相同。与预测一致。

验证输出

pnpm --filter @objectstack/driver-turso test   →  Test Files 20 passed (20) / Tests 632 passed (632)
                                                  (基线 621,本 PR +11)
pnpm --filter @objectstack/driver-turso typecheck →  tsc --noEmit,无输出
pnpm check:driver-conformance →  25 covered cell(s), 0 DEBT, 0 exempt(5x5 全 ok,与改前一致)
pnpm check:query-options-erasure →  test surface: 267 site(s) in 51 file(s) — at the ceiling
                                     baseline key set verified against c36abfe: no files added
node scripts/check-nul-bytes.mjs →  OK (scanned 5603 tracked text file(s))
eslint(5 个改动文件)→  exit 0

另附控制字符自扫(grep -naP 覆盖 gate 扫不到的 0x01 等):改动文件全部干净。

影响面

改动全部在 packages/drivers/driver-turso/ 内 + 一个 changeset(patch,published 包的行为修复)。已按消费半径查过 rule 的调用方:RemoteTransport 全仓仅本包使用,仓内其它 libsql:// 出现处(cli 的 URL 解析/banner、spec 与 service-settings 的字符串)都不走 remote 读。未触碰 cloud。

🤖 Generated with Claude Code

https://claude.ai/code/session_01WyvqvKMG6asi9aXjKE6xtx


Generated by Claude Code

…aging rule local mode has (#5653)

`RemoteTransport.buildSelectSQL` mapped the caller's `orderBy` verbatim and
appended no unique column before `LIMIT`/`OFFSET`, so a paged read over a
non-unique sort key had no tie-breaker and an unsorted paged read had no ORDER
BY at all. SQLite promises nothing about how equal keys are arranged across two
statements, so walking pages could serve one row twice and never serve another
— while every page is full and every row legitimate.

The same driver's local face has answered this since #4363, which left ONE
driver giving two different ordering guarantees for the same query, selected by
URL alone (the ADR-0053 D-A1 seam).

Reuse, not a second rule: `TursoDriver.find`/`findOne` now resolve the complete
sort key list through the inherited `SqlDriver.orderKeysFor()` and hand the
transport a query that already carries it, so the three-state table (incl.
#4363's unpaged-unordered carve-out) has one implementation. Resolving it at
this seam rather than inside `buildSelectSQL` is also what keeps `findOne`
unsorted: the transport injects its own `limit: 1`, which by SQL-build time is
indistinguishable from page one of a walk.

`paginationTieBreaker` is overridden to answer the one remote-specific FACT the
rule needs — whether this driver created the table — from the objects synced
through `RemoteTransport` (its CREATE TABLE always writes `"id" TEXT PRIMARY
KEY`), since remote DDL never reaches the `initObjects` that fills the base
class's `managedObjectFields`. Tables this driver did not create keep prior
behaviour exactly and warn once.

#5590's two "records the measured mechanism" pins are flipped with it: they now
assert the concrete sequences the mechanism produces, each against the
storage-order arrangement it replaced.

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 12:22am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels 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.

5 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/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.

@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 00:56
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit a58c0b5 Aug 6, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5653-turso-remote-pagination-tiebreaker branch August 6, 2026 01:08
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/m tests tooling

Projects

None yet

2 participants