You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 0f38ab0
Browse filesBrowse the repository at this point in the historyBrowse files
fix(driver-memory,driver-sql): an explicit tenancy opt-out survives a partial syncSchema re-registration (#16729) (#17221)
* fix(driver-memory,driver-sql): keep the explicit tenancy opt-out sticky across a partial re-registration
`InMemoryDriver.syncSchema` recomputed its uniqueness constraints from whatever
schema that call carried, so a second registration without a `tenancy` block
fell through to the implicit `organization_id` heuristic and moved a `unique`
field from one row per install to one row per organization. A duplicate the
declaration refuses then landed, silently.
`SqlDriver` has kept a sticky `tenantOptOutByTable` since #3249; this package
had mirrored the inner `computeTenantField` and not the wrapper that consults
the record.
- driver-memory: publish `computeAndRecordTenantField` + `TenantOptOutRecord`;
`InMemoryDriver` holds one record per instance and hands both declaration
surfaces the same resolved column. `tenantFieldOf` is unchanged.
- driver-sql: the shard leaf resolves through the record, keyed by the base
table, instead of the bare `computeTenantField`.
- objectql: `LifecycleObjectLike` declares `tenancy`, so the published type no
longer refuses a key the Archiver's `cold.syncSchema` reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
* test(driver-memory): guard the declared-index surface at the driver door too
The field-level and declared-index constraints are wired through one resolved
tenant column; the door-level pin covered only the field surface, so a fix that
routed half of it would have stayed green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
* test(objectql): pin the tenancy declaration reaching the cold store
The Archiver hands the cold driver the object it was given, and a driver
resolves a uniqueness partition from `tenancy`. Pins the key on the published
`LifecycleObjectLike` and on the `syncSchema` call in one assertion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(driver-memory,driver-sql): an explicit `tenancy.enabled: false` opt-out is sticky, so a partial `syncSchema` re-registration no longer flips a platform-global object's UNIQUE partition (#16729)
8
+
9
+
## What was wrong
10
+
11
+
`InMemoryDriver.syncSchema` recomputed its uniqueness constraints from whatever
12
+
schema THAT call happened to carry. A second registration without a `tenancy`
13
+
block — the `{ name, fields }` shape — fell through to the implicit
14
+
`organization_id` heuristic, so a `unique` field moved from **one row per
15
+
install** (`scopeField: null`, which is what `tenancy.enabled: false` declares)
16
+
to **one row per organization**. A duplicate the declaration refuses then
17
+
landed. Measured at the driver door on `origin/main``d61139f1ba`:
18
+
19
+
| sequence | second `key: 'K'`, different organization |
20
+
|:--|:--|
21
+
| register with `tenancy.enabled: false`|`REFUSED` — `UNIQUE_VIOLATION` / 409 |
22
+
| …then re-register with `{ name, fields }`|**`LANDED`**|
23
+
24
+
`SqlDriver` running the same sequence refuses in **both** cases: it has kept a
25
+
sticky `tenantOptOutByTable` since #3249. `driver-memory` had mirrored the inner
26
+
`computeTenantField` and not the wrapper that consults the record, so "mirrors
27
+
`computeTenantField` arm for arm" stayed literally true while the pair diverged.
28
+
29
+
It is silent in both directions — nothing logs the flip, and the refusal names
30
+
the field, never the partition. That is the declared-vs-enforced shape Prime
31
+
Directive #10 forbids, reached by a state change rather than by a missing check.
32
+
33
+
## What it does now
34
+
35
+
-**`@objectstack/driver-memory`** gains `computeAndRecordTenantField`, the
36
+
sticky resolver, and the `TenantOptOutRecord` type for the per-instance record
37
+
a driver owns. `InMemoryDriver` holds one and resolves through it, handing
38
+
BOTH declaration surfaces — field-level `unique` and declared `indexes[]` —
39
+
the same resolved column. `uniqueConstraintsFromFields` and
40
+
`uniqueConstraintsFromDeclaredIndexes` accept that column as an optional
41
+
second argument; called with one argument they answer exactly as before.
42
+
`tenantFieldOf` is unchanged and still a pure function of its argument.
43
+
-**`@objectstack/driver-sql`**: the shard leaf resolved its tenant column with
44
+
the BARE `computeTenantField`, so a `rotateShards` sweep carrying no `tenancy`
45
+
block gave a shard an organization key part the base table's index does not
46
+
have — one object, two partitions, decided by which physical table a row
47
+
landed in. It now resolves through the record, keyed by the base table.
48
+
-**`@objectstack/objectql`**: `LifecycleObjectLike` declares `tenancy`. The
49
+
Archiver hands that object straight to `cold.syncSchema`, and the published
50
+
type refused the key while the driver below read it — so an author writing a
51
+
fresh literal was pushed into producing exactly the partial re-registration
52
+
above. Same correction #16711 made where the shard leaf narrowed the key off
53
+
the object it was handed.
54
+
55
+
The record is deliberately narrow. Only the explicit OPT-OUT is sticky: a
56
+
declared `tenancy.tenantField` is not recorded, matching `SqlDriver`. An object
57
+
that never declared the opt-out never enters the record, so a genuinely
58
+
org-scoped object keeps its `organization_id` partition across a partial
59
+
re-registration — an implementation answering `null` more often would not be
60
+
stickier, it would be tenant isolation switched off. A carried `tenancy` block
61
+
stays authoritative in both directions and CLEARS a recorded opt-out.
62
+
63
+
`@objectstack/driver-memory` is `minor` for the two new public-entry exports.
64
+
The behaviour repairs themselves are `patch`: each restores an implementation to
65
+
the `tenancy.enabled: false` contract (`isTenancyDisabled`, ADR-0066) it was
66
+
already declaring, rather than replacing one legal published answer with
67
+
another. The `objectql` entry is a published type WIDENING — a key the interface
68
+
refused is now accepted, and nothing that compiled before stops compiling.
0 commit comments