Skip to content

Report the business identifiers already minted twice by the tenancy split — an operator-facing inventory, on installs the #8686 backfill cannot repair #8928

Description

@hotlong

Follow-up deliverable required by the #8844 maintainer ruling (comment 5302919977), binding point 3, filed as its own card rather than folded into the fix:

Already-minted duplicates: report, do not rewrite — same posture as #8686. The implementing dev files the duplicate-reporting deliverable as its own follow-up card (dedupe first) rather than folding renumbering into this fix.

Dedupe: searched open and closed issues for an operator-facing duplicate-identifier report (keyword and file-path over the autonumber/tenancy family — #8686, #8844, #5495, #6806, #6249, #8323, #3696, #7287). Nothing covers this. The closest existing thing is backfillSeedTenancy's collisions array (packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts), which is the seed of the answer but not the deliverable — see "What exists already" below.

Why this is needed, and why it is not the fix

Two producers of untenanted rows have now been closed:

Neither touches the damage already done. On the reporting install the duplicated numbers have propagated into notification bodies, approval trails and a cross-reference field, so renumbering is off the table there — and both rulings say the same thing: a business identifier that has already been handed out is not the platform's to rewrite. What the operator needs instead is to know which ones they are, per object and per field, so they can decide case by case.

The urgency is that the evidence is perishable. The moment either repair merges the two partitions, the two rows become indistinguishable: the organization_id = NULL marker that says "this one came from the untenanted side" is exactly what the repair overwrites. backfillSeedTenancy already runs its collision probe before the stamp for this reason. An install that upgrades without ever running a report loses the ability to produce one.

What exists already, and what is missing

backfillSeedTenancy returns collisions: [{ object, field, value, rows }] and a buildCollisionProbeSql(object, field) the operator can re-run themselves. That covers exactly one entry point and one moment:

gap why it matters
only reachable during a backfill run the report is a side effect of a repair. An operator who wants to know the damage without repairing anything has no door.
only on single-tenant installs the backfill returns skipped-multi-tenant before the collision probe runs, so the population that most needs a manual decision gets no list at all.
only on the __global__ / organization seed split it enumerates from _objectstack_sequences rows, so a duplicate on an object whose counter was since merged, or one produced by a different partition pair, is invisible.
no persisted record the result lives in one log line at boot. There is nothing to hand an auditor, and nothing survives the next restart.
sys_/cloud_/ai_ filtered out correct for a repair (platform seeds stay global), but a report should not silently omit a real duplicate.

What a fix has to decide

  1. Where the door is. A CLI command (os …, next to os migrate) is the obvious shape — it is the surface an operator already uses for exactly this class of question, and it does not need a running server. An admin REST endpoint and a boot-time report are the alternatives; they have different audiences and different disclosure properties.
  2. What "duplicate" means for the report. The narrow reading is the ruled one: values held by more than one row within one object/field, across the partitions COALESCE(organization_id, '__global__') separates. A wider reading (any unique-declared field with repeated values, whatever the cause) would also catch damage from other sources — worth deciding explicitly rather than by accident.
  3. Output shape and durability. Machine-readable (JSON/CSV, one row per duplicated value with its holder ids, organizations and creation timestamps) so it can be diffed and archived, versus a human summary. Persisting it as data would itself be a schema decision.
  4. Whether it also reports the CONDITION, not just the damage. An install still holding a __global__ counter beside an organization-scoped one for the same object is about to mint more duplicates. On a version carrying both Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA) #8686 and System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844 that should no longer happen, so its presence is a useful signal in its own right.
  5. Scope beyond the driver. _objectstack_sequences is driver-sql's table (driver-turso inherits it). A report keyed on it does not cover a non-SQL driver, and the data-side probe (group by value, having count greater than 1) does — that is a design choice, not an oversight to discover later.

⛔ Explicitly out of scope, per both rulings: renumbering, deduplicating, or otherwise rewriting any already-minted identifier. This card produces an inventory and nothing else.

Related

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions