Skip to content

fix(objectql): seedAutonumber 播种扫描覆盖 scope 内每一行,不再取 5000 行窗口 (#6249) - #6467

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-6249-seed-autonumber-window
Aug 8, 2026
Merged

fix(objectql): seedAutonumber 播种扫描覆盖 scope 内每一行,不再取 5000 行窗口 (#6249)#6467
baozhoutao merged 1 commit into
mainfrom
claude/issue-6249-seed-autonumber-window

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #6249

引擎兜底路径(驱动未声明 supports.autonumber)的自增号播种,此前是一次 limit: 5000无排序、无过滤find,把「任意 5000 行窗口内的最大值」当成了全表 MAX。对象超过 5000 行、或某个 scope(日期 / {field} 分组)的行被其他 scope 的行挤出窗口时,播种出低于真实 MAX 的号,计数器从一个已被占用的号段起号 —— 对声明了 unique 的记录号字段,就是直接发出重复的业务标识符。

前提复核(动手前逐条实测 origin/main@48f98b0)

前提 结论 证据
P1 seedAutonumber 仍是窗口形状 ✅ 成立 engine.ts:2142(#6456 落地后行号漂移,按内容定位):find({ fields: ['id', field], limit: 5000, context }) —— 无 orderBy、无 where
P2 影响面 = 引擎兜底路径专属 ✅ 成立 engine.ts:2083 if (driverOwnsAutonumber) return;;sql-driver.ts:3033 scanMaxNumericTail.where(field, 'like', prefix%) + whereNotNull,无 limit;driver-turso / driver-sqlite-wasm 继承 SqlDriver。⛔ 驱动侧零改动
P3 串行核对 #5495 ✅ 无冲突 #5495 现为 pm:queue、assignee 已清空(PM 于 12:16Z 采纳前提证伪后释放),评论区无在飞认领对;其真凶经实测落在 driver-sql.getNextSequenceValue,与本单零文件交集
P4 #6114 loud-fail catch 仍在且不回退 ✅ 成立且未触碰 isMissingTableError 判别 catch 原样,git diff} catch (error) { 之后无任何 diff;两条 pin 测试在反向验证中保持绿

选型

采用:keysetWalk 游标分页全扫 + 前缀下推,数值最大值在引擎侧逐值解析。

  • 分页用仓内既有的 keysetWalk(packages/types/src/keyset-walk.ts),与 summary-backfill 同一把,而不是再手抄一份游标合并;
  • id 游标 seek 而非 offset —— 该模块自己的理由(分页读取在没有 orderBy 时同样不确定:tie-breaker 只覆盖了「排了序的翻页」 #4363):offset walk 无法保证访问过每一行,而这里漏掉的一行正好就是不能漏的那个号;
  • prefix 非空时下推 { [field]: { $startsWith: prefix } },让日期 / {field} scope 只读自己的行;JS 侧的 startsWith 复核保留为权威,这样匹配更宽松的驱动(大小写不敏感的 LIKE)无法用别的 scope 的行抬高 max;
  • 页大小沿用 5000,但语义从「硬上限」变成「页大小」,单次读的驱动侧成本不变。

这与 SQL 驱动自身的播种形状(scanMaxNumericTail,无 limit 的 scope 内全扫)一致 —— 兜底路径不再比它弱。

被否路线:

路线 否决理由
orderBy desc + limit 1 取值最大 文本排序。字典序等于数值序,仅当 scope 内所有值补零到同一定宽 —— 格式语言不保证:{0} 不补零;任何定宽在计数器越过后溢出(CASE-99999 排在 CASE-100000 之上);legacy 空前缀路径取整串最后一个数字段,根本没有对应的字典序读法。要救它就得再加一道「本格式是否定宽」的判定加溢出逃生口 —— 更多假设,而猜错的代价是重复的业务标识符
aggregatemax 继承完全相同的文本排序陷阱(SQL / Mongo 的 MAX 对文本列即字典序),另外还要依赖驱动的 aggregate 支持与引擎的内存聚合兜底(后者本身仍要读全部行)。收益为零,依赖面更大
分页全扫但用 offset keyset-walk.ts 模块注释已写明:offset walk 会漏行且是 O(n²/p)

未新增任何驱动能力声明,因此不触发分诊指定的 needs_decision 出口。查询词汇全部对着仓内词表核实:where / orderBy / limit / fields 均在 ENGINE_FIND_OPTION_KEYS(engine.ts:271);$startsWith / $gt / $and 均在 FILTER_OPERATORS / LOGICAL_OPERATORS(filter.zod.ts:1085);排序节点用 order 而非 direction —— 后者被 SortNodeSchema 按名拒绝(#4721),派单示例里的写法正是词表外的那个。

字典序陷阱的处置

结构上不进入。 排序只用于游标推进(按 id),从不用于判定最大值;数值最大值由既有的数字段解析逻辑(有前缀取紧随其后的数字串,无前缀取最后一个数字串)逐值算出,该逻辑一字未改。因此定宽、不定宽、溢出、legacy 空前缀四种形态走同一条正确路径,不需要豁免任何一种。

有一条专门的 pin 测试守住这一点:legacy 裸计数器 '7' '8' '9' '10' 的字典序最大是 '9',据此播种会重发已存在的 '10';测试断言得到 '11'。任何未来把这段换成排序捷径的改动都会撞红它。

无法正确计算时的处置

扫描走不完(行缺 id 游标、或驱动未执行游标谓词)时,walk.truncated 为真 —— 此时「已读部分的最大值」是下界而不是最大值。不静默播这个低号,而是抛错、不分配号、不写入,与 #6114 对读故障的处置同族。该拒绝错误刻意不匹配 isMissingTableError 的任何模式,不会被兜回「播种 0」。

反向验证(方向先写死,再运行)

预测写在 predictions.md 后才执行:回退窗口修复 ⇒ 9 红 4 绿。实测与预测逐条相符。

用例 预测 实测
(1) 超页对象取真实 max expected 'D-5001' to be 'D-6001'
(1) 游标分页而非停在首页 expected 1 to be greater than 1
(1) 只播种一次后内存计数 expected 'D-5001' to be 'D-6001'
(2) 被挤出窗口的 scope 不从 1 重来 expected 'APAC-0001' to be 'APAC-0004'
(2) scope 前缀下推 expected undefined to deeply equal { ticket_no … }
(2) scope 之间互不干扰 expected 'APAC-0001' to be 'APAC-0004'
(3) 无前缀时跨页扫描 expected '5001' to be '6001'
(5) 拒绝用不完整扫描播种 红(旧码直接 resolve,不抛)
(5) 该拒绝不被吞成 missing table 红(旧码直接 resolve)
(3) 取数值最大而非字典序最大 绿 绿
(4) 对照:supports.autonumber 驱动无播种扫描 绿 绿
(5) 读故障仍上抛、不写入 绿 绿
(5) 表未建仍播种 0 绿 绿

⚠️ 第 10 行是刻意预测为绿的,不是漏网。 它不是窗口守卫:旧码同样在 JS 侧对返回的 4 行算数值最大值,4 行不触发窗口。这条守的是未来换成 orderBy desc + limit 1 / 聚合 max 的捷径。按报告纪律照实写出来,而不是硬凑成红。

(4)(5) 的绿正是它们该有的方向 —— 驱动侧对照与 #6114 行为都不该被本改动移动。

APAC-0001 这一条尤其值得看:它不是「号偏小」,是与库内已存在行逐字相同的值。

命令输出

pnpm --filter @objectstack/objectql test
  Test Files  144 passed (144)
       Tests  2427 passed (2427)

新增文件单跑(13 例全绿):
  Test Files  1 passed (1)
       Tests  13 passed (13)

pnpm --filter @objectstack/objectql typecheck
  tsc --noEmit        (无输出 = 通过)

pnpm check:engine-double-contract
  OK — 80 pinned, 133 in the DEBT ledger, 4 exempt.

pnpm check:query-options-erasure
  OK: 83 unswept non-test site(s) in 19 file(s), none new.
  test surface: 263 site(s) in 49 file(s) — at the ceiling.      ← 新测试零新增擦除
  baseline key set verified against 48f98b0: no files added.

pnpm check:type-check-debt(先跑全量 build,70/70 successful)
  OK — 62/77 workspace packages type-checked, 15 in the DEBT ledger.
  @objectstack/objectql: TEST_DEBT records 355, tsc now reports 352 (-3)  ← 未抬
  --re-measure: OK — 34 ledger entr(ies), none above its recorded number.

node scripts/check-nul-bytes.mjs
  OK (scanned 6089 tracked text file(s); no raw ASCII control bytes).
  自扫补充:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' 改动三文件 —— 无命中

npx eslint engine.ts engine-autonumber-seed-scan.test.ts   exit=0

文件面


Generated by Claude Code

引擎兜底路径的自增号播种此前是一次 `limit: 5000`、无排序、无过滤的
`find`,把「任意 5000 行窗口内的最大值」当成全表 MAX。对象超过 5000 行、
或某 scope 的行被其他 scope 挤出窗口时,播种低于真实 MAX,计数器从已被
占用的号段起号 —— 对 `unique` 记录号字段就是直接发出重复业务标识符。

改为完整扫描:`keysetWalk` 按 `id` 游标分页(非 offset,#4363),前缀
下推为 `$startsWith`,数值最大值在引擎侧逐值解析。刻意不委托给
`orderBy desc + limit 1` 或聚合 `max` —— 两者按文本排序,字典序等于
数值序仅当 scope 内全部补零到同一定宽,而格式语言不保证(无 `{0..0}`
槽位时渲染裸计数器,`'9' > '10'`;定宽越过后溢出)。

扫描无法走完时拒绝播种并大声失败,不用已读部分的下界起号 —— 与 #6114
对读故障的处置同族。声明 `supports.autonumber` 的驱动不受影响。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
@vercel

vercel Bot commented Aug 7, 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 7, 2026 11:46pm

Request Review

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

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

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 7, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review August 7, 2026 23:59
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit 2bd4e5e Aug 8, 2026
25 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-6249-seed-autonumber-window branch August 8, 2026 00:15
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