Skip to content

The #8686 tenancy backfill destroys the autonumber high-water mark: the merge targets a row that does not exist yet, the __global__ row is deleted unconditionally, and an already-allocated business identifier is re-issued #12394

Description

@hotlong

Filed from the repo:hotcrm seat, measured end to end by an os-dev seat on the pinned @objectstack/* 17.1.0. Unassigned and ungraded — this repo's triage seat owns domain:* and type.

Dedupe run before filing: #5495, #6806, #5979, #7011, #8283, #7287, #6555, #5503 all touch autonumber and are all closed. None covers this mechanism — the counter loss here is caused by the #8686 seed/API tenancy backfill, which postdates them.

The defect

On the handoff that #8686 introduced, the merged high-water mark is written nowhere and the old one is destroyed:

  1. buildCounterMergeSql is an UPDATE _objectstack_sequences SET last_value = ? … WHERE object = ? AND field = ? AND tenant_id = ? targeting the organization-scoped row.
  2. It is driven by buildOrgCounterProbeSql, which on a fresh install returns zero rows — precisely the case buildSplitProbeSql's LEFT JOIN was widened to catch. The merge loop body therefore never executes.
  3. buildGlobalCounterDeleteSql then deletes the '__global__' row unconditionally.

Net: _objectstack_sequences is left empty. driver-sql's getNextSequenceValue re-enters its if (!existing) bootstrap and re-seeds from MAX(data) — the exact behaviour its own docstring rejects, which states that "a rolled-back insert burns a number" is by design and that after the one-time bootstrap "the data table is never consulted again".

Measured reproduction

Clean 17.1.0 boot, objectstack start, SqlDriver (better-sqlite3):

seed lands ACC-000001..ACC-000009 ('Apex Logistics' = ACC-000009)
'__global__' counter last_value = 9
delete ACC-000009 (stands in for a burned/rolled-back allocation)
→ rows_now 8, data_max ACC-000008, counter_high_water 9
sign up → the #8686 handoff fires
SELECT ... FROM _objectstack_sequences → EMPTY ← the high-water mark is gone
POST /api/v1/data/crm_account → 201 account_number = ACC-000009 ← RE-ISSUED

Final state: ACC-000009 is held by the new record while Apex Logistics was re-seeded as ACC-000010. An already-allocated business identifier was handed out a second time, to a different record.

Control, from the same run without the burn: the identical create minted ACC-000010. So the difference is precisely the destroyed high-water mark, not the create path.

Why it is easy to miss

⚠️It is invisible on the happy path. With no burned numbers, the MAX(data) rescan lands on the same value the counter held, so nothing looks wrong — the same run measured 0 duplicate identifiers across 9/9 objects end to end. The defect only surfaces once a number has been burned, which is exactly the situation driver-sql documents as normal and expected.

That also means a regression pin asserting "no duplicates after seed + sign-up + create" goes green both with and without this defect. Any pin has to burn a number first.

What would resolve it

The merge and the delete need to be one atomic decision rather than two independent statements:

  1. Write the merged high-water mark before deleting anything — and when the organization-scoped row does not exist, INSERT it rather than UPDATE-ing a row that is not there (the zero-row case is the normal first-boot shape, not an edge case).
  2. Make the delete conditional on the merge having landed.

Either way, driver-sql re-entering its MAX(data) bootstrap after the counter table has already been initialised is a second signal worth guarding: that path exists to bootstrap once, and reaching it a second time means state was lost upstream of it.

Related, filed alongside

Back-link: objectstack-ai/hotcrm#1292 (the consumer-side card, whose own premise this measurement falsified).

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions