fix(driver-turso): remote 分页读补齐确定性排序,与 local 面共用同一条规则 (#5653) - #5689
Merged
os-zhuang merged 2 commits intoAug 6, 2026
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…so-remote-pagination-tiebreaker
os-zhuang
marked this pull request as ready for review
August 6, 2026 00:56
os-zhuang
deleted the
claude/issue-5653-turso-remote-pagination-tiebreaker
branch
August 6, 2026 01:08
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 #5653
前提复核(对 origin/main
c36abfe98)成立,逐条核对过:
remote-transport.ts的buildSelectSQL确实把调用方orderBy原样 map 进 SQL、不追加唯一列,紧接着无条件拼LIMIT ?/OFFSET ?,两段之间没有任何「这是分页读」的判断;SqlDriver.orderKeysFor()+paginationTieBreaker(),按 分页读取在没有 orderBy 时同样不确定:tie-breaker 只覆盖了「排了序的翻页」 #4363 的三态表办事;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都是protected,TursoDriver extends SqlDriver,可见性没有问题,未改动 driver-sql 一个字节。TursoDriver.find/findOne现在先经toRemoteReadQuery()用继承来的orderKeysFor()解析出完整排序键,再交给传输层;三态规则只有一份实现:orderByidlimit/offsetid为什么落点不在
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 规则以及原因。唯一列的判定
新增
paginationTieBreakeroverride:规则不重述,只回答 remote 特有的那一个事实 —— 这张表是不是本驱动建的。事实记在remoteManagedObjects(registerRemoteFieldMetadata里登记,两个 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排序时输出相同。与预测一致。验证输出
另附控制字符自扫(
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