发现于 #5495 的前提复核(engine-core 车道),未在该单 PR 内顺手修,按 Prime Directive #10 单独开单。
缺陷
packages/objectql/src/engine.ts 的 seedAutonumber()(:2111 起)用一次普通 find 播种内存计数器:
const rows = await this.find(object, {
fields: ['id', field],
limit: 5000,
context: execCtx,
} as any);
三个问题叠在一起:
limit: 5000 是硬上限,且
- 没有
sort —— 取的是驱动返回的任意 5000 行(SQL 上通常是插入顺序的前 5000 行),再在这个窗口里求 max;
- 没有按
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 的两条都修好,超过 5000 行的对象仍然会播种出低号。#5495 报告的 HotCRM 现场行数远小于 5000,所以本条不是那次风暴的成因,是同一函数里发现的另一个洞。
修法的难点(供分诊参考,不预设结论)
「读真正的 MAX」在引擎层没有现成的便宜写法:
sort: [{ field, direction: 'desc' }] + limit: 1 取的是字符串字典序最大值。对定宽补零、且在同一前缀 scope 内的格式,字典序最大 == 数值最大;但对 prefix 为空的 legacy 路径(取整串最后一个数字段),字典序最大不等于数值最大。
- 因此正确做法多半要按 scope 下推
startsWith(prefix) 过滤 + 定宽假设,或者走 aggregate 的 max。两条都要先决定引擎与驱动之间的契约面,不是一行改动。
影响面
只影响引擎兜底路径(驱动未声明 supports.autonumber)。driver-sql/driver-turso/driver-sqlite-wasm 走各自的 _objectstack_sequences,其播种是 scanMaxNumericTail(SQL 侧 where field like 'prefix%',无 limit),不受本条影响。
发现于 #5495 的前提复核(engine-core 车道),未在该单 PR 内顺手修,按 Prime Directive #10 单独开单。
缺陷
packages/objectql/src/engine.ts的seedAutonumber()(:2111 起)用一次普通find播种内存计数器:三个问题叠在一起:
limit: 5000是硬上限,且sort—— 取的是驱动返回的任意 5000 行(SQL 上通常是插入顺序的前 5000 行),再在这个窗口里求 max;prefix下推过滤 —— 前缀筛选是在 JS 侧if (prefix && !s.startsWith(prefix)) continue;做的,所以对日期/{field}分组格式,5000 行窗口还要先被其他 scope 的行占掉。结果:对象行数一旦超过 5000(或某个 scope 的行排在窗口之外),播种得到的 max 低于库内真实 MAX,计数器从一个已被占用的号段起号。对声明了
unique的自增号字段,这就是直接发出撞号值。实测(fake driver 捕获引擎实际下发的查询):
—— 无
sort、无filters,limit恒为 5000。与 #5495 的关系
#5495 记的是另外两个缺陷(计数器不按分区同步、撞号烧号)。本条是第三个、独立的播种正确性缺陷:即使 #5495 的两条都修好,超过 5000 行的对象仍然会播种出低号。#5495 报告的 HotCRM 现场行数远小于 5000,所以本条不是那次风暴的成因,是同一函数里发现的另一个洞。
修法的难点(供分诊参考,不预设结论)
「读真正的 MAX」在引擎层没有现成的便宜写法:
sort: [{ field, direction: 'desc' }] + limit: 1取的是字符串字典序最大值。对定宽补零、且在同一前缀 scope 内的格式,字典序最大 == 数值最大;但对prefix为空的 legacy 路径(取整串最后一个数字段),字典序最大不等于数值最大。startsWith(prefix)过滤 + 定宽假设,或者走aggregate的max。两条都要先决定引擎与驱动之间的契约面,不是一行改动。影响面
只影响引擎兜底路径(驱动未声明
supports.autonumber)。driver-sql/driver-turso/driver-sqlite-wasm走各自的_objectstack_sequences,其播种是scanMaxNumericTail(SQL 侧where field like 'prefix%',无 limit),不受本条影响。