Skip to content

seedAutonumber 的播种扫描是「取前 5000 行、不排序、不按前缀过滤」的 MAX —— 超过 5000 行的对象会播种出低于真实 MAX 的号 #6249

Description

@baozhoutao

发现于 #5495 的前提复核(engine-core 车道),未在该单 PR 内顺手修,按 Prime Directive #10 单独开单。

缺陷

packages/objectql/src/engine.tsseedAutonumber()(:2111 起)用一次普通 find 播种内存计数器:

const rows = await this.find(object, {
  fields: ['id', field],
  limit: 5000,
  context: execCtx,
} as any);

三个问题叠在一起:

  1. limit: 5000 是硬上限,且
  2. 没有 sort —— 取的是驱动返回的任意 5000 行(SQL 上通常是插入顺序的前 5000 行),再在这个窗口里求 max;
  3. 没有按 prefix 下推过滤 —— 前缀筛选是在 JS 侧 if (prefix && !s.startsWith(prefix)) continue; 做的,所以对日期/{field} 分组格式,5000 行窗口还要先被其他 scope 的行占掉。

结果:对象行数一旦超过 5000(或某个 scope 的行排在窗口之外),播种得到的 max 低于库内真实 MAX,计数器从一个已被占用的号段起号。对声明了 unique 的自增号字段,这就是直接发出撞号值。

实测(fake driver 捕获引擎实际下发的查询):

PROBE2 seedQuery={"fields":["id","case_number"],"limit":5000,"object":"crm_case"}

—— 无 sort、无 filters,limit 恒为 5000。

#5495 的关系

#5495 记的是另外两个缺陷(计数器不按分区同步、撞号烧号)。本条是第三个、独立的播种正确性缺陷:即使 #5495 的两条都修好,超过 5000 行的对象仍然会播种出低号。#5495 报告的 HotCRM 现场行数远小于 5000,所以本条不是那次风暴的成因,是同一函数里发现的另一个洞。

修法的难点(供分诊参考,不预设结论)

「读真正的 MAX」在引擎层没有现成的便宜写法:

  • sort: [{ field, direction: 'desc' }] + limit: 1 取的是字符串字典序最大值。对定宽补零、且在同一前缀 scope 内的格式,字典序最大 == 数值最大;但对 prefix 为空的 legacy 路径(取整串最后一个数字段),字典序最大不等于数值最大。
  • 因此正确做法多半要按 scope 下推 startsWith(prefix) 过滤 + 定宽假设,或者走 aggregatemax。两条都要先决定引擎与驱动之间的契约面,不是一行改动。

影响面

只影响引擎兜底路径(驱动未声明 supports.autonumber)。driver-sql/driver-turso/driver-sqlite-wasm 走各自的 _objectstack_sequences,其播种是 scanMaxNumericTail(SQL 侧 where field like 'prefix%',无 limit),不受本条影响。

Activity

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