Skip to content

driver-turso remote transport 的分页读没有 tie-breaker:ORDER BY 原样透传、无序分页读完全不排序 —— 与同驱动 local 面(SqlDriver orderKeysFor)分叉,IDataDriver.find 的确定性分页 MUST 在 remote 面不成立 #5653

Description

@os-zhuang

发现于 #5590(给 driver-turso 补 PAGINATION_CASES / PAGINATION_UNORDERED_CASES 共享套件)。⛔ 按 #5590 的边界,那个单只补套件、不动实现,所以这条另立。

机制

packages/drivers/driver-turso/src/remote-transport.ts 的 buildSelectSQL(:945)把调用方的 orderBy 原样拼进 SQL,然后直接接 LIMIT / OFFSET:

  • :960-968 —— ORDER BY 段:只 map 调用方给的 orderBy 条目,不追加任何唯一列;
  • :970-977 —— LIMIT ? / OFFSET ?,与上面那段之间没有任何"这是分页读"的判断。

同一个驱动的 local 面走的是 SqlDriver.orderKeysFor()(packages/drivers/driver-sql/src/sql-driver.ts:6675)+ paginationTieBreaker()(:6628),它按 #4363 的三态表办事:

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

remote 面这三格全都落在"原样透传"上。于是 TursoDriver 一个驱动的两条传输对同一个分页查询给出不同的排序保证,而传输模式只由 URL 决定 —— 正是 ADR-0053 D-A1 / #937 一路在关的那条缝。

实测(分支 claude/issue-5590-turso-conformance-suites,hermetic sqlite stub)

ORDER BY status ASC 走 12 行 fixture(PAGINATION_ROWS):

remote: r03,r09,r02,r10 | r01,r12,r08,r06 | r07,r11,r05,r04

每个 status 组内部是插入顺序,不是 id 顺序 —— 直接证明没有追加 id tie-breaker(local 面同一查询组内是 id 序)。无序分页读同理,返回的是 rowid 序:

remote unordered: r07,r03,r11,r01,r09,r05,r12,r02,r08,r04,r10,r06

那为什么 #5590 的共享套件是绿的

因为 hermetic stub 是 better-sqlite3 上的 12 行内存表:计划固定、排序器在这个规模上表现稳定,所以"每行恰好访问一次"这条性质成立。这是运气,不是保证,而且 case-set 自己的文档就写明了这一点不算合规(packages/spec/src/data/pagination-conformance.ts):

SQL leaves the row order of an unordered read to the plan (insertion order in practice on a small table, and not once it goes parallel, switches to an index scan, or is VACUUMed)

driver-memory 那条"存储自身顺序在两次读之间稳定"的豁免针对的是 JS 数组,不是 SQL 计划。契约原文(packages/spec/src/contracts/data-driver.ts:68)把无 orderBy 的分页读称为同一缺陷的满强度形态。

所以 #5590 的套件在 remote 面记录的是"当前机制"而非"契约达成";那两个套件里各有一条 pin 测试把上面这个插入序的事实钉住,并指到本单。

影响

真实 Turso remote endpoint 上,表大到走索引扫描 / 计划变化时,ORDER BY status LIMIT 50 OFFSET 50 的并列行排布不被 SQLite 承诺在两条语句之间一致:用户翻页时某条记录出现两次、另一条永远不出现。每一页都是满的、每一行都真实且合法,所以从任何单个响应里都看不出来 —— 这正是 #4363 / objectui#3106 要消灭的那种失败。

建议修法(不是这单的实现承诺,供 triage)

在 buildSelectSQL 里补上与 local 面同一条规则,而不是第二套:分页读(limit 或 offset 存在)时在调用方排序键之后追加唯一列;未分页且无 orderBy 时保持不加 ORDER BY(#4363 的 carve-out 必须一起搬过来,否则会给系统里绝大多数读改计划)。remote 面的表由 syncSchema / syncSchemasBatch 自己建,id 是已知主键,所以 paginationTieBreaker 的"只对自己建的表下判断"前提在这里也成立。落地后 #5590 里两条 pin 测试要一起改(它们钉的就是当前的无 tie-breaker 行为)。

Blocked-by: #5590

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊:pm:queue + domain:drivers + pm:blocked(Blocked-by: #5590 已补正文)。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 5, 2026
  3. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    解锁 + 认领:PM 循环第 6 轮(drivers 车道)

    解锁依据:分诊的 Blocked-by: #5590 — 该单已随 PR #5656 合入 main 关闭(两条「records the measured mechanism」pin 已落盘),pm:blocked 已摘。

    会话:session_01WyvqvKMG6asi9aXjKE6xtx
    分支:claude/issue-5653-turso-remote-pagination-tiebreaker
    Worktree:objectstack-issue-5653
    域:domain:drivers
    文件面:packages/drivers/driver-turso/src/(remote-transport.ts 的 buildSelectSQL + 同包两条机制 pin 的翻转 + 相应套件断言)。


    Generated by Claude Code

  4. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    复核:ACCEPT → PR #5689(CI 23/23 全绿;ready + auto-merge 因 GraphQL 配额暂满排队执行,恢复即入队 — 不影响判定)。留档要点:

    • 复用而非复制:三态规则(分页读取在没有 orderBy 时同样不确定:tie-breaker 只覆盖了「排了序的翻页」 #4363 含 carve-out)单源自继承的 SqlDriver.orderKeysFor(),driver-sql 零改动;
    • 比 issue 深一层的机制发现:直接照建议落点复用会是空修 — paginationTieBreaker 读的 managedObjectFields 只由 local 面 initObjects 填,remote DDL 从不经过。dev 以 remoteManagedObjects 登记(两个 remote DDL 入口共同经过、DDL 成功后)+ 窄 override(只答「表是否本驱动所建」,规则不重述,未托管表保守如旧)补上;
    • 落点上移的理由成立:SQL 组装层无法区分 findOne 与页大小 1 的分页读,误判会触发有实测锚的计划器退化(0.08ms→7.8ms);buildSelectSQL 已补「只渲染不决策」防再犯文档;
    • pin 翻转承重:两条机制 pin 改钉新序列并 not.toEqual 被取代的 storage-order 序列;一条测试整条替换的理由正当(继续比对等于钉死 carve-out 的缺席);
    • 反向验证 12 红 / 620 绿,红绿分区与前置预测逐条吻合,保持绿的 4 格恰为新旧行为重合处;
    • 新增 11 条语句级测试(SQL 逐格钉三态表);conformance 25/25 不变;erasure 天花板 267 未涨;changeset(patch)在。

    修复将随下次 pin bump 流入 cloud。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions