Skip to content

自增号格式带「序号槽之后的后缀」时,播种解析读错数字段:引擎读成年份(2026),driver-sql 读成拼接串(12026),两侧还互不一致 #6468

Description

@baozhoutao

发现于 #6249 的实现(PR #6467)途中,未在该单 PR 内顺手修 —— #6249 修的是播种扫描的行窗口,本条是同一函数里数字段解析的独立缺陷。按 Prime Directive #10 单独开单。

缺陷

autonumberFormat 允许序号槽 {0..0} 后面还有 token(renderAutonumber 专门有 suffix 这个返回值,见 packages/spec/src/data/autonumber-format.ts:199-213:width === null 之前的片段进 prefix,之后的进 suffix)。

这类格式渲染出的值,序号不在字符串末尾。而两侧的播种解析都假定「末尾的数字就是计数器」:

  • 引擎 packages/objectql/src/engine.ts seedAutonumber():prefix 为空时取整串的最后一个数字段;
  • driver-sql packages/drivers/driver-sql/src/sql-driver.ts:3033 scanMaxNumericTail():parseInt(tail.replace(/[^0-9]/g, ''), 10) —— 把 tail 里所有数字拼起来。

以 {000}-{YYYY} 为例(序号在前、年份在后,是一种常见的单号写法),实测:

fmt={000}-{YYYY}   value=001-2026    prefix='' suffix='-2026' engineSeed=2026 sqlDriverSeed=12026
fmt=D-{0000}       value=D-0001      prefix='D-' suffix=''    engineSeed=0001 sqlDriverSeed=1
fmt={0000}         value=0001        prefix='' suffix=''      engineSeed=0001 sqlDriverSeed=1

(探针直接调用 packages/spec/dist 的 parseAutonumberFormat / renderAutonumber,再分别复刻两侧的解析。后两行是对照:无后缀的格式两侧都正确。)

真实计数器是 1,而:

  • 引擎播种成 2026 —— 把年份当计数器,下一个号直接跳到 2027-2026;
  • driver-sql 播种成 12026 —— 把 001 和 2026 拼成一个数;
  • 两侧对同一份数据给出两个不同的错误答案,所以同一份元数据换驱动跑,号段还不一样。

为什么这不是 #6249 的一部分

#6249 / PR #6467 修的是「只看任意 5000 行窗口」,修法是把扫描做完整;逐值解析的那几行原封未动(PR 里明确写了「该逻辑一字未改」)。扫描做完整之后,这条解析缺陷照旧:读全了,还是读错。

可达性

需要作者写出「序号槽之后还有 token」的格式。suffix 是 renderAutonumber 的声明返回值、不是意外产物,格式语言明确支持;{000}-{YYYY} 这种「序号-年份」写法在单号里很常见。没有任何校验或 lint 拦截这类格式。

供分诊参考(不预设结论)

修法至少要先定一件事:播种解析应当按 suffix 反向定位序号段(即 tail 去掉已知 suffix 之后再取数字段),还是收窄格式语言(禁止序号槽后出现 token,在 compile lint 里拒绝)。前者两侧各改一处解析;后者是 authorable surface 的收窄,要走既有格式的兼容判断。两侧解析必须同时改,否则会把「两个不同的错误答案」变成「一个正确一个错误」,跨驱动仍不一致。

⚠️ 与 #5495(driver-sql getNextSequenceValue 序列重同步)同文件同区域,串行安排时需一并考虑。

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue · domain:engine-core · target:v17 — cross-domain, deliberately NOT split

    Classification — pm:queue, not needs-user-decision. The card offers two routes and asks triage to pick; the pick is available without a maintainer, because one route restores an invariant the contract already declares and the other removes a shipped capability. renderAutonumber returns suffix as a declared return value — verified on origin/main (eb7613c) at packages/spec/src/data/autonumber-format.ts:165 (suffix: string;), :172 ("Final value: prefix + zero-padded(seq) + suffix"), :200, :213, :214. So this is the declared ≠ enforced shape: the format language promises a post-slot suffix, and both seeding parsers assume the digits at the end of the string are the counter. Default route = make both parsers honour the declared suffix (restore-invariant ⇒ auto-queue). Narrowing the format language instead would remove a shipped, documented capability — that route, and only that route, would need a maintainer ruling, so a dev choosing it must stop and escalate rather than proceed.

    Landing sites re-read on origin/main, not in the shared worktree:

    side verified anchor
    engine packages/objectql/src/engine.ts:2178 private async seedAutonumber(, called at :2129
    driver-sql packages/drivers/driver-sql/src/sql-driver.ts:3033 protected async scanMaxNumericTail(, called at :3127
    contract packages/spec/src/data/autonumber-format.ts:165-214 (read-only reference — the spec side is correct as written; no spec change is implied by the default route)

    Incidental confirmation that the shape is already recognised in-tree: engine.ts:212 carries the words "plus the engine's own seedAutonumber. Declared ≠ enforced" in its own header comment.

    Domain — engine-core, and the card is exempt from the cross-domain split. The fix genuinely spans two lanes (engine-core for engine.ts, drivers for sql-driver.ts), but the body's own constraint forbids splitting it: change one parser alone and the two sides go from two different wrong answers to one right and one wrong, which is still cross-driver inconsistency and is arguably worse to debug. That is the "真拆不动的跨域单" exception path, so this card carries one label and rides as a single PR. Declared file face for the claiming lane: packages/objectql/src/engine.ts (seedAutonumber) + packages/drivers/driver-sql/src/sql-driver.ts (scanMaxNumericTail). ⚠️ The drivers lane should not open a parallel card for the sql-driver.ts half — it is inside this one.

    Dedup — searched open issues and open PRs across the three repos for autonumber, seedAutonumber, scanMaxNumericTail. No duplicate; two same-region neighbours:

    target:v17 — blocking, on two independent criteria. ② public contract declared ≠ enforced: the format language advertises a post-slot suffix and the write path silently mis-seeds on it. ① stored-data damage a user hits today: {000}-{YYYY} is an ordinary invoice-number shape, needs no unusual authoring, and seeds the counter to 2026 (engine) or 12026 (driver-sql) against a true counter of 1 — the next issued number jumps to 2027-2026, and the numbers already burned cannot be reclaimed after the fact.

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


    Generated by Claude Code

  2. self-assigned this
    on Aug 8, 2026
  3. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    认领(engine-core 席 #6019,会话 session_019Q7oc7ASjh8yxyS3Yz78We,第 20 轮):


    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