Skip to content

[finding] a failed insert permanently burns its autonumber (TK-0001 → fail → TK-0003) — is a gapless series in scope at all? #8283

Description

@baozhoutao

Recording an observation for the platform to rule on — this may well be an accepted trade-off, but it is currently neither documented nor decided anywhere I could find.

An insert that fails for a reason unrelated to the autonumber still consumes the number that row had reserved. Measured on both dialects, same shape:

sqlite:   first=TK-0001  → insert fails (duplicate `slug`)  → next=TK-0003
postgres: first=TK-0001  → insert fails (duplicate `slug`)  → next=TK-0003

TK-0002 exists in no row and will never be issued. Any rejected write — a unique violation on another field, a validation rule, a beforeInsert hook throwing — leaves a permanent hole in the business identifier.

This is not multi-tenant-specific (a failed insert in tenant A does not disturb tenant B — verified), and it is the ordinary behaviour of database-native sequences. But ObjectStack's autonumber is not a native sequence: it is a counter this driver owns and updates itself, and the driver's own docs frame the value as a business identifier —

packages/drivers/driver-sql/src/sql-driver.ts:4523 — "an autonumber is an immutable business identifier"

For a surrogate key, gaps are free. For a business identifier — a contract number, an invoice number, an order number — gaps are sometimes a compliance problem, and are frequently the first thing a customer's finance team asks about.

The decision

Three coherent answers, and the platform should pick one rather than leave it implicit:

  1. Gaps are fine and guaranteed-unique-only is the contract — then say so in the autonumber docs, so nobody promises a customer a gapless series.
  2. Gaps are worth avoiding on the cheap path — reserve the number only after the row is known to be insertable, or return it on a clean rejection.
  3. Gapless is a supported option — an opt-in mode that pays the concurrency cost (serialized issuance) for objects that need it.

Related: #8269 is the pathological version of the same mechanism — there the counter advances and the whole batch fails, so a fresh tenant's numbering can start at TK-0004.

Environment

Probed via SqlDriver directly against postgres:16 and file-backed sqlite, branch claude/multi-org-service-testing-a75937, 2026-08-12. Probe files were temporary and have been removed; the two lines above are the complete result.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions