Repository navigation
Commit 1e4d73a
committed
fix(driver-sql)!: unique 按租户物化,不再与分裂的 autonumber 序列冲突 (#3696)
`unique: true` used to become a single-column global index that ignored
`tenancy` entirely, while the autonumber sequence table is keyed by
`(object, tenant_id, field, scope)` and hands every tenant its own counter
starting at 1. Two subsystems of the same platform contradicted each other:
tenant B's `PROD-00001` was rejected by an index it could not see, with no
user error involved. The rejection doubled as a cross-tenant existence
oracle — a UNIQUE violation told tenant B that some OTHER tenant held the
value, enumerable by probing emails/codes/names.
Contract:
- `unique: true` + tenant column -> composite `(tenantField, field)`,
unique WITHIN the tenant. Tenant column first, so the index also serves
the `WHERE tenant = ?` prefix scans every tenant-scoped read issues.
- `unique: true`, no tenant column -> single-column. Single-tenant
deployments get byte-identical DDL to before.
- `unique: 'global'` -> single-column, always. Platform-wide
uniqueness is the special case and now has to say so (a DNS hostname, a
reserved slug, an external provider id, a device identity).
Field-level `unique` had three DDL paths that each inlined their own
single-column SQL — createTable/alterTable via `createColumn`, and the
SQLite drift rebuild. That is how a tenant-scoped field could silently
regain a global index after any drift reconcile. All three now converge on
one tenancy-aware path (`uniqueIndexesFromFields` + `syncTableIndexes`);
`createColumn` owns type/nullability/defaults only, since a column builder
cannot express a composite at all.
Declared object-level `indexes[]` are deliberately NOT rewritten: the author
already names the columns, tenant-scoped ones have always been written
explicitly (`fields: ['organization_id', 'code']`), and many are legitimately
platform-wide. Auto-injecting a tenant column there would have broken global
slug reservation, hostname allocation and external provider ids. `'global'`
is accepted there as a synonym of `true` for one vocabulary across both
spellings.
Legacy indexes (`<table>_<col>_unique` from knex, `uniq_<table>_<col>` from
the rebuild path) are retired inline at sync time. This is a pure relaxation
— the old global constraint is strictly stronger than the per-tenant one, so
existing data satisfies the replacement by construction: no dedup, no
cleanup, no possible failure. It converges at sync rather than waiting for a
deliberate `os migrate` run because a deployment that never ran migrate would
otherwise stay broken, and the failure mode is a rejected insert. Postgres
materializes `col.unique()` as a CONSTRAINT rather than a bare index, so the
drop tries `DROP CONSTRAINT` before `DROP INDEX` — `DROP INDEX` alone would
have made this a no-op on exactly the deployments that matter.
driver-mongodb accepts the new type but keeps single-field indexes: it
implements no row-level tenancy at all (no tenant predicate on read, no
tenant stamp on write), so a `(tenant, field)` index would advertise an
isolation it does not deliver. Tracked separately.
BREAKING CHANGE: on a tenant-scoped object, field-level `unique: true` is now
unique per tenant instead of platform-wide. Fields that need platform-wide
uniqueness must declare `unique: 'global'`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uu13EPsmLey7drXSMjuNq1 parent 6169615 commit 1e4d73a
7 files changed
Lines changed: 756 additions & 26 deletions
File tree
- packages
- plugins
- driver-mongodb/src
- driver-sql/src
- spec
- src/data
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
18 | 30 | | |
19 | 31 | | |
20 | 32 | | |
| |||
0 commit comments