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.

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