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),不受本条影响。

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions