Skip to content

bulkCreate / upsert 仍会逐次烧号:#5495 的重播种只落在 create(),批次语义是独立决策 #6943

Description

@os-zhuang

一句话说明

#5495(PR #6932)让 SqlDriver.create() 在唯一冲突后重播种陈旧的自增号计数器并有界重试,于是"每次失败烧一个号"在该路径上不可达。bulkCreate() 与 upsert() 没有拿到这个修复 —— 两者同样调用 fillAutoNumberFields,碰撞后同样没有重播种,因此在同一个陈旧计数器上仍会逐次烧号并把请求打失败。

由 #5495 的 dev 在范围审计中记录,刻意未顺手修(PD #10)。

为什么不是 #5495 的一部分

不是遗漏,是裁向边界:批内逐行重试会改变批次语义 —— 部分成功如何呈现、事务边界怎么划、失败行是否回滚整批。这些都不是 #5495 那条"单行 create 的陈旧计数器"裁向能顺带决定的,需要单独定夺。

前提(#5495 已实测,可直接继承)

  • 计数器陈旧的成因:播种只在 getNextSequenceValue 的 if (!existing) 分支跑一次,此后不再读数据表;任何绕过 fillAutoNumberFields 落地的行(isSystem 种子回放、preserveAudit 导入、直接 SQL)都不抬序列。
  • 因此 bulkCreate / upsert 在同一张表上会遇到与 create 完全相同的陈旧,只是收口点不同。
  • 判据不能用错误报文取冲突列:uniqueViolationColumn() 在带租户的自增号上恒为 undefined(复合键 → soleColumn 拒答;ADR-0120 D3 的表达式索引 → SQLite 报索引名 → SQLITE_INDEX_FORM 拒答;MySQL 无该 limb)。fix(driver-sql): re-seed a stale autonumber counter instead of burning a number per failed create (#5495) #6932 因此改用数据探测(autoNumberValueExists),该方案对批量路径同样适用。

关联

未认领,仅作记录,定级交分诊。

Activity

  1. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    Collaborator

    Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): promoted finding → pm:queue (domain:drivers kept), with a scope fence: the dispatchable fix is reseed-before-fill on the batch paths — apply #6932's data-probe reseed (autoNumberValueExists) when bulkCreate/upsert first touch a stale counter, which removes the burn-per-failure without touching per-row retry or partial-success presentation. The batch-semantics questions the filer names (per-row retry inside a batch, partial success, transaction boundaries) are explicitly OUT of scope — if implementation finds reseed-before-fill insufficient without them, STOP and escalate rather than deciding batch semantics in a driver PR.


    Generated by Claude Code

  2. self-assigned this
    on Aug 9, 2026
  3. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop, drivers lane — dispatched under the maintainer's standing authorization of 2026-08-08 ~17:15Z (「6577 落地后继续按序派发,不用等我确认」).

    Session: session_01Hg9Pkg5nDedCRihRsdeCdX
    Branch: claude/issue-6943-bulk-autonumber-resync
    Worktree: objectstack-issue-6943 (cloud session — own container, Opus)

    Selection. target:v17 in this lane is zero (censused 05:35Z across all 46, intersected by hand). Of the three findings triage promoted to pm:queue just now — this card, #6944 and #6915 — the other two are A-or-B disposition questions (implement the capability, or refuse loudly), and #6915 sits inside the #5499 freeze. Having a dev pick A or B unilaterally is exactly what the loop is supposed to avoid, so they stay queued pending a ruling. This card is a concrete defect with a concrete fix path, so it goes first.

    Serial constraint cleared. PR #6956 (#6754) merged as 51f2bb8c3; no open PR touches driver-sql. Base main @ 17116a049.

    I filed this card, and re-measuring it before dispatch made it worse — in a way that changes the fix

    The card says bulkCreate/upsert "still burn a number per collision", by analogy to create(). Measured on 17116a049, bulkCreate is a different and sharper shape:

    for (const row of rows) {
      this.injectTenantOnInsert(object, row, options);
      await this.fillAutoNumberFields(object, row, options);   // one sequence reservation PER ROW
    }
    …
    const builder = this.getBuilder(this.rotationWriteTarget(object) ?? object, options);
    const result = await builder.insert(formattedRows).returning('*');   // ONE batch insert

    It opens no transaction of its own. Each row's getNextSequenceValue runs in its own committed transaction, and then the whole batch goes in as a single insert. So one colliding row does not burn one number — it burns every number the batch reserved, and fails the entire batch. There is no re-seed and no retry anywhere on this path.

    And the exposure is worse than the arithmetic suggests. bulkCreate's own docstring records that framework#2678 made it "the common case for seed/import" — and seed/import is precisely the path that creates counter staleness in the first place (isSystem replay and preserveAudit import keep their explicit numbers and skip fillAutoNumberFields, per #5495). So the path most likely to meet a stale counter is the one with no recovery at all.

    ⚠️ Re-derive all of this yourself; do not inherit it. The one measurement I want challenged hardest is whether upsert shares the shape or differs — I checked bulkCreate closely and upsert only in passing.

    The machinery this needs already landed an hour ago

    PR #6932 (#5495) put three things in sql-driver.ts that this card is the natural second consumer of: resyncSequenceToDataMax, autoNumberValueExists, and collidingAutoNumberReservations (the three-state routing). ⛔ Do not reimplement any of them, and ⛔ do not hand-write a dialect word-list — the judgement goes through isUniqueViolationError / uniqueViolationColumn from @objectstack/types (Prime Directive #12).

    Carry #5495's measured constraints forward:

    The real decision this card exists to make

    Per-row retry inside a batch changes batch semantics — partial success, transaction boundaries, whether a failed row rolls back its siblings. That is why this was split out of #5495 rather than absorbed. Decide it on measurement and state the alternatives you rejected; ⛔ do not pick by taste. A legitimate outcome is "the honest fix is smaller than the card implies" — say so with evidence if that is what you find.

    Repro constraint (inherited from #5495 and still binding): the storm is one-time per database. Reproduce on a fresh DB with seeded rows above the counter; on a DB already ground past the seeds everything passes and you will wrongly report "cannot reproduce".

    driver-memory/driver-mongodb stay inside the #5499 freeze — they declare no supports.autonumber and use the engine fallback (#6806's surface), so there is genuinely nothing here to align.


    Generated by Claude Code

  4. claude commented on Aug 9, 2026

    @claude
    Contributor

    os-dev report

    执行席开发,drivers 车道。分支 claude/issue-6943-bulk-autonumber-resync(专用工作树,从 main 拉出),draft PR #6999,两个提交 72b5ba5 + 5461f78。

    基线漂移(申报):派发写 17116a049;实际克隆到的 origin/main 头已是 c8ff269(本单全部实测在此基线上做),运行期间 origin/main 又前进到 c733ae8(#6989 metadata-protocol、#6983 CI job 名钉、#6982 docs —— 三条均不触 driver-sql)。三个锚点按函数名定位、未信行号:bulkCreate、upsert、fillAutoNumberFields 均在原位。


    premise_still_valid: 是 —— 但两条路径不是同一个形状,单据把它们并列说错了一半

    ⚠️ 以下全部在本容器重推,未继承派发结论。

    • bulkCreate —— 派发的重新定价成立,我独立复现到同一形状。 它自己不开事务;每行 getNextSequenceValue 各自开事务并提交,然后整批一次 insert。一行撞号不是烧一个号,是把整批预留的号全烧掉并且整批失败。该路径上既无重播种也无重试。

    • upsert —— 派发点名要我狠狠挑战的那条,结论是:它与 bulkCreate 不同形,而与 create() 旧形完全同形。 单行,一次碰撞只烧一个号。这一点不显然,值得写清楚为什么它仍会抛:ON CONFLICT (mergeKeys) DO UPDATE 只吸收合并键上的冲突,而带租户的自增号活在另一条唯一索引上(uniq_crm_case_organization_id_case_number),所以它的违例照样抛出、照样到达调用方。实测报文:

      insert into `crm_case` (…) values (…) on conflict (`id`) do update set …
        - UNIQUE constraint failed: index 'uniq_crm_case_organization_id_case_number'
      

      即:ON CONFLICT 在场不代表自增号碰撞被吸收。单据说"两者同样…"对 upsert 成立、对 bulkCreate 低估了。

    继承并复核通过的两条 #5495 约束:uniqueViolationColumn() 在本仓带租户自增号上实测恒为 undefined(上面那条报文只报索引名);调用方事务内不烧号,两条路径各自实测确认(见下表)。


    缺陷的实测形状(红前 / 绿后)

    遵守单据的复现陷阱条款:全新库 + 种子行落在计数器之上(磨过种子区的库上全绿,会误报"无法复现")。计数器播种到 10,11–39 由旁路路径落地。

    bulkCreate(3 行一批)

    红(c8ff269) 绿(本 PR)
    调用方可见失败 两次调用全抛 0
    实际写入行数 0(表 31 行 → 31 行) 3
    last_value 10 → 13,再 13 → 16 10 → 42,由一次重播种到达
    失败分类 UNIQUE(col undeterminable) —

    upsert(单行)

    红 绿
    调用方可见失败 每次调用都抛 0
    last_value 1 → 2 → 3(每次烧一个) 1 → 21,一次重播种后 CASE-00021

    事务内 / 事务外,两种情形分别申报(两条路径各自实测,未从 create() 推定):

    情形 bulkCreate upsert
    无调用方事务 红:整批烧号+整批失败 → 绿:首次调用即成功 红:每次烧一个号并失败 → 绿:首次调用即成功
    调用方事务内 行为不变:仍然抛,last_value 前后均不变(实测 1 → 1) 行为不变:仍然抛,last_value 不变

    事务内不改,理由与 #5495 同:序列 UPDATE 与被拒 INSERT 同事务一起回滚,没有东西可修;而 Postgres 上约束失败会整体 abort 事务,在其上发的重试不可能成功。重试归调用方。


    本卡真正的决策:批次语义不变 —— 这是实测结论,不是口味

    单据预期这里要裁定"部分成功怎么呈现、事务边界怎么划、失败行是否回滚兄弟行"。实测把这个问题溶掉了:insert(rows[]) 是单条语句(SQLite 上 … select … union all select …,其余方言多行 VALUES),所以这一批本来就已经是全有或全无 —— 上面那次失败的批把表原封不动留在 31 行。

    因此「整批重发号 + 整批重试」逐字保留现有契约:不引入部分成功、不新开事务、不存在"失败行是否回滚兄弟行"的问题(兄弟本来就一起失败)。

    被否方案与理由(已写进 bulkCreate 自身注释):

    1. 批内逐行重试(单据勾画的形状)—— 否决。要知道哪一行失败就必须把一条语句拆成 N 条,那等于凭空发明部分成功;并在 seed/import 热路径上把 1 次往返变成 N 次;而且正是 engine 在 Engine fallback autonumber: counter seeds once and never resyncs; collisions burn numbers with no recheck (residual split from #5495) #6806 注释里点名警告的方向("re-writing the batch could DUPLICATE the rows that did land")。这里没有行落地,所以也没有行可被复制 —— engine 的顾虑对任意驱动成立,对本驱动实测不成立。
    2. 给整批加驱动自开的事务 —— 否决。它恰好动了单据担心的那条边界,却零收益(语句本来就原子);并且破坏预留模型(每次 getNextSequenceValue 刻意提交,这正是"只向前移动的重播种"有意义的前提);在 SQLite(pool max=1)上还是 ensureSequencesTable 已经写明的那种死锁。
    3. 只重播种、不重发号(engine 在 Engine fallback autonumber: counter seeds once and never resyncs; collisions burn numbers with no recheck (residual split from #5495) #6806 的选择)—— 在 engine 那一层正确,因为它无法知道任意驱动是否部分应用了批次;在本驱动内实测为假,所以可以把成功的批交回调用方,而不是交回一个要他自己重试的错误。
    4. 对任何 unique violation 盲目重试 —— 否决,理由同 Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495:会悄悄吞掉调用方自己的 409。三态路由原样保留。

    「诚实的修法比单据暗示的小」在这里成立:本卡真正需要的是把 #6932 的机制接到两条收口点上,而不是重新设计批次语义。


    批次唯一不能照抄 create() 的地方(本轮最重要的设计后果)

    create() 会保留没有碰撞的预留号以免白烧一个。批次不能这么做,而我一开始预测它可以 —— 这是本轮最有价值的一条不符项。

    跨越种子区的批次(计数器 10、11–39 已存在、批次预留 11–70)会低段撞、高段不撞;只重发低段,就会给它们发出高于被保留号的新号 —— 一个驱动自己制造的批内重复。所以重发号的粒度是每计数器:凡是从"变陈旧的那个计数器"取过号的行全部重发;没陈旧的计数器保持原值,所以同一批里另一租户的行不受打扰。两半都已钉成测试(60 行跨越用例 + 双租户用例)。


    机制复用申报(⛔ 未重造)

    resyncSequenceToDataMax、autoNumberValueExists、collidingAutoNumberReservations 全部原样复用,一行未改。判别一律走 @objectstack/types 的 isUniqueViolationError / uniqueViolationColumn(Prime Directive #12),未手抄任何方言词表。

    新增的只有一个 helper:autoNumberCounterKey,直接复用既有的 sequenceKeyHash。create() 不需要它(一行最多一个字段一个预留,"哪个计数器"和"哪个预留"是同一个问题);批次里多行共用一个计数器,而批次的两个决策(重播种一次、重发全部)都是按计数器的。


    三张脸的申报(#6203 形状)

    面 结论 依据
    SqlDriver 修复落地 主面
    SqliteWasmDriver 继承并实际生效 不覆写 bulkCreate/upsert;新增独立测试跑通
    TursoDriver local/replica 继承并实际生效 ⚠️ 它覆写了 bulkCreate/upsert/create 再路由到 super,所以"基类改好了"本身不是关于这张脸的答案;新增独立测试跑通
    TursoDriver remote 既无此缺陷也不接受此修复 RemoteTransport.bulkCreate 自建 INSERT,根本不进 fillAutoNumberFields(#6944 的面,不在本卡范围)

    Turso 这次比 #5495 更需要明说:它是覆写后路由,不是单纯继承。remote 面的缺席同样写成断言(探测 RemoteTransport 原型:有 bulkCreate、无任何 autonumber/sequence 方法),将来把自增号接进 remote 传输时不会静默继承本文件的绿。

    #5499 冻结面申报(自己复核,未继承)

    driver-memory / driver-mongodb 零改动,且是真实缺席:实读确认 memory 是 readonly supports = {},mongodb 是 supports = { batchSchemaSync: true } —— 两者都不声明 autonumber,因此走 engine 兜底路径,那是 #6806 的面。check:driver-memory-census 与 check:driver-conformance 均 PASS。


    门禁与测试,逐项结论

    本地按 .github/workflows/lint.yml 现场枚举(未凭记忆),共 64 条,64 PASS / 0 FAIL。 含 check:* 之外的 turbo build/typecheck 各 filter、examples typecheck、downstream-contract typecheck。点名单据要求的两条:check:tenant-chokepoint(#6908)PASS;check:query-options-erasure(双向棘轮)PASS —— 本改动既不增也不减 query option,基线未动,无需 --update。

    测试(全部为修复后):driver-sql 79 files / 1142 passed(4 files / 48 skipped,皆为既有);driver-turso 32 files / 911 passed;driver-sqlite-wasm 23 files / 307 passed。

    红向验证:把新测试文件跑在未改动的 sql-driver.ts 上(用 git diff > patch + checkout -- 收起改动,全程未用 git stash),结果 4 failed / 7 passed —— 失败的正是四条钉缺陷的用例,其余 7 条是"不得改变的行为"守卫,红绿两侧都应该是绿,事实也是。

    **CI(5461f78),逐 job 结论,只认 completed: success:共 25,23 success + 2 skipped,零 failure、零 in_progress。

    job 结论
    ESLint / TypeScript Type Check ✅ success
    Test Core (1/3)(2/3)(3/3) + 汇总 ✅ 全部 success
    Dogfood Regression Gate (1/3)(2/3)(3/3) + 汇总 / Dogfood Verify CLI ✅ 全部 success
    Temporal Conformance (live PG + MySQL) ✅ success
    Build Core / Check Changeset / ADR maintainer approval ✅ success
    Check PR Size / Check Documentation Links / Console Pin Freshness ✅ success
    No other open PR may claim the same issue / Auto Label / filter / Flag docs / Vercel Preview Comments ✅ success
    Build Docs ⏭ skipped —— job 级条件 needs.filter.outputs.docs != 'false',本 PR 不触 docs 路径;filter job 自身 success,所以这是真的过滤裁决,不是死 job 的假跳过
    Console Pin Gate ⏭ skipped —— 同理 needs.filter.outputs.console != 'false',本 PR 不触 console 路径

    changeset:有产品代码改动 → 走真 changeset(@objectstack/driver-sql: patch),Check Changeset 结论是 success(不是 skipped),未用 skip-changeset 标签。

    补充申报(基线漂移带来的第 65 条门禁):main 在我拉分支之后新增了 check:required-contexts(#6865 / #6983),我的工作树里没有它,所以本地 64 条不含它。但 CI 的 pull_request 跑的是 merge 结果,我核对了 ESLint job 的 47 个 step:Required-context name pin => success,已实际执行并通过。


    断言风格申报(卡片第 5 条)

    本层抛的是驱动原始错误(knex/SQLite),不带 ADR-0112 信封,无 code/status 可断 —— ⛔ 未硬凑假信封,⛔ 未降成裸 toThrow()。替代为更强的断言:具体约束文本 + isUniqueViolationError 一致 + 对"不该重试"的用例断言错误点名的是调用方的字段,并额外断言"批次仍然一行未落"(rowCount 前后相等)—— 最后这条是批次路径独有的、比信封更贴题的东西。拼法与 sql-driver-autonumber-resync.test.ts / sql-driver-unique-tenancy.test.ts 一致(#6543 曾刻意裁定那些断言保持原样)。


    反向验证:预测先写、再跑;不符项如实记录

    红向 8 条 + 绿向 7 条在动手前写死(predictions.md,未入库)。12 条相符,3 条不符/未预测到。

    相符的关键几条:R1 批次零落地 ✅;R2 整批烧号 10→13 ✅;R3 第二次 13→16 继续烧 ✅;R4 事务内不烧号 ✅;R8 单条语句 ✅;R5 upsert 与 bulkCreate 不同形、与 create() 同形 ✅(派发点名要挑战的那条,预测正确);R6 预测为真且为真(见范围外发现 1);G1–G4/G6/G7 绿向全中。

    不符 / 未预测到的 3 条(本轮最有价值的部分):

    1. G5 预测错了,而且错的方向会造出缺陷。 我预测批次可以照抄 create() "保留没碰撞的预留号"。对批次不成立 —— 跨种子区的批次会因此产生批内重复(详见上一节)。重发粒度必须从"按预留"改成"按计数器"。这是批次形状唯一真正的设计后果,我在写重试循环时才看见,不是预测出来的。已钉成 60 行跨越测试。
    2. 未预测到:重试轮次里,被保留的预留号会从账上消失。 fillAutoNumberFields 只汇报它实际生成的字段,所以重试重发了被清空的字段之后,上一轮保留下来的值就不再出现在 reservationsPerRow 里 —— 若该(未陈旧)计数器在第二轮才撞,就会"无号可路由"而盲目上抛。自查时发现,已在第二个提交 5461f78 修正(把每行的预留清单跨轮次携带,只替换被重发的)。旧形状方向是安全的(上抛驱动原始错误 == 今天的行为),但循环的不变量与它自称的不一致。
    3. 原样复现了 Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495 记过的那次"假红",而且我仍然没预测到。 driver-turso 首跑:1 个 test file 失败、906 tests 全过 —— date-bucket-parity.test.ts 无法解析 @objectstack/verify(该包在本工作树未构建,且该文件我未触碰)。构建后 32 files / 911 passed 全绿。记录它是因为它正是"假红"的形状:反过来若是假绿,就不会有人去看。

    范围外发现(记录,⛔ 未顺手修)

    1. upsert 在每次"合并到既有行"时都烧一个号,并且覆写该行的自增号。 与计数器陈旧无关 —— fillAutoNumberFields 在语句知道自己会 insert 还是 merge 之前就跑了,而自增号列在 mergeColumns 里。在完全健康的计数器上实测:create 得到 CASE-00001,随后按同一 id upsert 两次 → CASE-00002 → CASE-00003,全程只有 1 行,last_value 1→2→3。这是成功路径上的无界烧号,而且它改写的是业务标识符,不只是留下空洞。它不是本卡的缺陷(不涉及陈旧),而修它是它自己的语义裁决 ——「更新型 upsert 该保留原号吗?它该不该取号?」—— 正是当初把本卡从 Autonumber counter neither syncs to MAX(existing) per tenant nor re-checks on collision — warm-DB creates 409 in bursts, each failure burning a number (25 retries observed) #5495 拆出来的那一类问题。建议单独拆单。本 PR 只做到"重试循环不会把 merge 变成 insert"的守卫。
    2. bulkCreate 的失败路径探测是 O(N) 次索引查询(每个预留一次 autoNumberValueExists)。只在失败路径上发生,且它替代的是"整批失败",所以没有优化;但一个上千行的 seed 批会在碰撞那一次做上千次点查。若将来成为问题,可按计数器分组只探一次。记录,未改。
    3. bulkCreate 的 union all 形态受 SQLite SQLITE_MAX_COMPOUND_SELECT(默认 500)约束,与本卡无关的既有边界。记录,未验证其触发阈值。

    开放问题(交 PM / 分诊)

    1. 范围外发现 1(upsert 合并时烧号 + 覆写业务号)是否拆单?我判断它比本卡原始缺陷更值得优先处理 —— 无界、静默、且改数据。
    2. 有界重试沿用 AUTONUMBER_COLLISION_RETRIES = 3(与 create() 共用常量)。批次的重播种一步跳到 MAX,单据那类陈旧只需 1 次;3 是否仍合适,还是批次应有自己的界,请裁。
    3. 基线漂移已申报(派发 17116a049 → 实测 c8ff269 → 现 c733ae8),三条漂移提交均不触 driver-sql。是否需要我 rebase 到 c733ae8 再让 CI 重跑一轮?目前 CI 跑的是 merge 结果且全绿,我判断不必,但这是 PM 的闸门。

    PR 保持 draft,等 PM 过 step-7 闸门后翻转。未自行 mark ready、未开 auto-merge、未改 assignee、未重新认领、未触 content/docs/releases/、全程未用 git stash。


    Generated by Claude Code


    Generated by Claude Code

  5. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    ContributorAuthor

    PM acceptance — ACCEPTED, PR #6999

    First, a process failure of mine

    Triage posted a scope fence on this card at 06:46Z (comment 5230217156): the dispatchable fix is reseed-before-fill on the batch paths, and per-row retry / partial success / transaction boundaries are explicitly out of scope, with an instruction to stop and escalate rather than decide batch semantics in a driver PR.

    I dispatched at 08:01Z without reading it, because I filed this card myself and assumed I knew its state. My claim comment then said the opposite of the fence — "the real decision this card exists to make… decide it on measurement". That was wrong, and it is the second time today the lesson has been "re-read the thread, including your own card."

    The landed shape complies with the fence — no partial success, no driver-opened transaction, per-row retry explicitly rejected — but that is because your measurement dissolved the question, not because my instruction steered you there. Recording it so the fence is visibly honoured rather than accidentally survived.

    Your three open questions

    1. Rebase onto c733ae8? No — merge as-is. Verified rather than assumed: git merge-tree against current main is clean (no conflict); driver-sql's package.json has no dependency on @objectstack/metadata-protocol, so #6989's cause-walk change cannot reach this code; and nothing has touched sql-driver.ts on main since your base. The merge queue builds the speculative merge against current main anyway — that is exactly the mechanism that caught #6932's cross-PR failure this morning. A manual rebase would buy nothing and cost a CI cycle.

    2. Retry bound of 3 — keep it. Same reasoning that held on #5495: the reseed jumps straight to MAX, so the stale case needs one pass; the remaining budget covers a concurrent writer taking the number between scan and insert. Sharing AUTONUMBER_COLLISION_RETRIES with create() is right — two constants would drift.

    3. Out-of-scope finding 1 — split, and I agree it outranks this card's own defect. Filed as #7011. Unbounded burn on the success path, and it rewrites a business identifier rather than leaving a gap; create → CASE-00001, then two upserts by the same id → CASE-00002 → CASE-00003 with one row throughout, on a healthy counter. Findings 2 and 3 stay recorded here rather than becoming cards — an O(N) probe on a failure path that replaces total batch failure is a fair trade, and the SQLITE_MAX_COMPOUND_SELECT bound is pre-existing.

    Verified independently

    Criterion How I checked Result
    upsert verdict — the measurement I flagged as most likely wrong your mechanism + error text ✅ and you falsified the card: upsert is not bulkCreate's shape, it is create()'s. ON CONFLICT (mergeKeys) DO UPDATE absorbs only merge-key conflicts; the tenanted autonumber lives on a different unique index, so it still throws
    #6932 machinery reused, not reimplemented diff on sql-driver.ts ✅ all three appear only as call sites — no - line touches their definitions; autoNumberCounterKey is genuinely new and delegates to the existing sequenceKeyHash
    batch semantics decision your four rejected alternatives ✅ decided by measurement: insert(rows[]) is a single statement, so the batch was already all-or-nothing and the question dissolves. Four alternatives rejected with reasons, including why engine's #6806 choice is right there and measurably wrong here
    red→green on a fresh seeded DB both tables, plus the in-txn/out-of-txn split ✅ and the in-transaction row correctly shows no change — nothing to fix, so nothing changed
    three faces your table ✅ sharper than #5495's: TursoDriver overrides bulkCreate/upsert/create and routes to super, so "the base class is fixed" is not an answer for that face. Remote's absence asserted, not commented
    #5499 freeze re-read supports on both ✅ memory {}, mongodb { batchSchemaSync: true } — neither declares autonumber; a real absence
    CI 25 runs ✅ 23 success + 2 skipped, 0 failure, 0 in_progress — and each skip justified by its needs.filter.outputs.* condition plus the note that the filter job itself succeeded, so it is a real filter verdict rather than a dead job. That is a better standard than the one I set

    The most valuable thing here is the prediction you got wrong. You expected the batch could copy create()'s "keep the uncollided reservations". It cannot: a batch spanning the seeded range collides low and misses high, so re-issuing only the low half hands those rows numbers above the kept ones — a duplicate the driver manufactures itself. Re-issue granularity therefore has to be per counter, not per reservation. You found that by writing the loop, not by predicting it, and then pinned both halves. The follow-on catch — kept reservations falling out of reservationsPerRow across retry passes, fixed in 5461f78 — is the same discipline applied to your own fix.

    Also noted: the driver-turso false red reproduced exactly as #5495 recorded it, and you still did not predict it. Recording a known-and-still-unpredicted failure is worth more than quietly reclassifying it.

    Marking ready and enabling auto-merge.


    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