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:
- 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.
- 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.
- 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.
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:
TK-0002exists in no row and will never be issued. Any rejected write — a unique violation on another field, a validation rule, abeforeInserthook 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 —
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:
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
SqlDriverdirectly againstpostgres:16and file-backed sqlite, branchclaude/multi-org-service-testing-a75937, 2026-08-12. Probe files were temporary and have been removed; the two lines above are the complete result.