Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
420fb8c
wip(plugin-webhooks): declared webhooks read from the registry; seede…
claude Oct 19, 2026
e44c642
test(plugin-webhooks): pin the registry read, the ledger switch and t…
claude Oct 19, 2026
8189a84
wip: liveness ledger re-anchored to the registry read; dogfood proof …
claude Oct 19, 2026
e412cb8
docs+ledger: the registry read named across spec comments, liveness, …
claude Oct 19, 2026
bdad70b
spec(liveness): state counts regenerated; isActive named at the dispa…
claude Oct 19, 2026
723af97
fix(plugin-webhooks): a declared webhook is deployment-wide when the …
claude Oct 19, 2026
e0a6dc9
fix(gates): tenant-audit census regenerated for the seeder's two reti…
claude Oct 19, 2026
6b3042a
test(plugin-webhooks): the fake engine refuses a WHERE combinator it …
claude Oct 19, 2026
820121c
fix(lint,showcase): the dead label/description verdict is carried to …
claude Oct 19, 2026
b378ff5
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
c3b1452
test(dogfood): the R2 harness pins one delivery per task change from …
claude Oct 19, 2026
9bce0aa
test(dogfood): the harness case switches the declaration on, so it ca…
claude Oct 19, 2026
7e580dc
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
fa6a837
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
b4ae8e6
Merge remote-tracking branch 'origin/main' into claude/issue-24529-8b…
claude Oct 19, 2026
206d2bc
Merge remote-tracking branch 'origin/claude/issue-24529-8b0g-webhooks…
claude Oct 19, 2026
f89c0f2
wip: birth marker, shapeGeneration, boot guard rule
claude Oct 19, 2026
6e14afa
wip: ceremony steps 5a-5d, inventory, flips, differ ownership
claude Oct 19, 2026
50a2d34
wip: open the ceremony command, ports for webhooks and NOT NULL
claude Oct 19, 2026
9ea4312
wip: existing ceremony tests follow the new step set; NOT NULL routed…
claude Oct 19, 2026
719ab13
wip: the guard's pins follow the birth marker and shapeGeneration
claude Oct 19, 2026
036a2a1
wip: the birth half in every CLI boot, so a CLI-created fresh store b…
claude Oct 19, 2026
0128f07
test(cli): pin the generation-1 ceremony steps over an 18.0-shaped da…
claude Oct 19, 2026
8e6a33a
test: the tenant column's NOT NULL is the ceremony's, not drift; the …
claude Oct 19, 2026
32d1e22
test(dogfood): R2 and R3 restated — the 18.0 and v17 databases take t…
claude Oct 19, 2026
13446cb
fix(cli): a rotation shard's NOT NULL is reported unsupported, never …
claude Oct 19, 2026
06e0556
docs,qa: the open ceremony and the birth marker in the CLI docs and t…
claude Oct 19, 2026
e0c1d2b
chore(spec): regenerate the api-surface, export-origin, declaration-m…
claude Oct 19, 2026
a0fa816
test(platform-objects,service-settings): sys_secret is deployment-lev…
claude Oct 19, 2026
e7a788d
chore: regenerate the platform-object tenancy census (sys_secret and …
claude Oct 19, 2026
f1264d7
chore: the gates the sweep named — the guard double's findOne predica…
claude Oct 19, 2026
a3064ef
test(cli): fixtures standing in for an earlier release's database car…
claude Oct 19, 2026
d6195b9
Merge origin/main into claude/issue-24581-ceremony-open
claude Oct 19, 2026
0232108
docs(qa): the ceremony item's revision 3 cites its PR
claude Oct 19, 2026
dceaa56
fix(driver-turso): the remote face counts what its schema doors creat…
claude Oct 19, 2026
33b7943
test(runtime): the remote-face guard pin reads driver-turso's source
claude Oct 19, 2026
d28f4cf
chore(changeset): the remote face's schema-sync count
claude Oct 19, 2026
edb2db1
test(dogfood): the upgrade ceremony over the 18.0 fixture is a shared…
claude Oct 19, 2026
1699ec5
Merge origin/main after #24667 (step 4) into the ceremony branch
claude Oct 20, 2026
b5010d9
test(dogfood): ceremoniedFixture boots the 18.0 bytes once, refused, …
claude Oct 20, 2026
0f83a8d
fix(text): no shipped text says the ownership ceremony is closed; an …
claude Oct 20, 2026
81ebc64
Merge remote-tracking branch 'origin/claude/issue-24581-ceremony-open…
claude Oct 20, 2026
c6f775c
fix(text): the delta review's text items — fresh installs restated, t…
claude Oct 20, 2026
30473b4
wip(driver-sql, objectql): retire the tenant scope's NULL-organizatio…
claude Oct 20, 2026
543fe91
test(driver-sql): the tenant-scope pins assert the 18.x rule — an org…
claude Oct 20, 2026
6c43845
test(objectql): the retention global pass on organization_id carries …
claude Oct 20, 2026
a746877
fix(objectql): the retention partition decides its global pass's NULL…
claude Oct 20, 2026
08c7e35
test(driver-turso): the local/remote tenant-scope parity holds both f…
claude Oct 20, 2026
e476042
test(driver-turso): the inherited-members fixture holds no organizati…
claude Oct 20, 2026
9987cf8
test: pins that asserted the retired NULL-organization reach assert t…
claude Oct 20, 2026
0012a4c
Merge commit 'c6f775cbd70cb9c373db8f328a3fd0a7948fc746' into claude/i…
claude Oct 20, 2026
1f360ec
Merge commit 'a43405d596d4d670dc761e90c9fc20822dc806a9' into claude/i…
claude Oct 20, 2026
2ee8e8d
Merge commit '307ce4c4b42503347d7f85825f572e4ca058b05f' into claude/i…
claude Oct 20, 2026
5124ae0
Merge commit 'c3a8e07b6aca321c0114f49944b0564baf0d2c0f' into claude/i…
claude Oct 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/24583-7a1-tenant-scope-null-arm-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@objectstack/driver-sql': minor
'@objectstack/objectql': minor
---

feat(driver-sql)!: a tenant-scoped read no longer answers a row with no organization; the driver's `OR organization_id IS NULL` arm is retired

Clause-②: no (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) A read-scope narrowing in the SQL driver's tenant chokepoint and the lifecycle sweep's global pass: no key of any metadata schema is removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. No export, type or error code is added, removed or renamed. The data it stops reaching is retired by the 18.x upgrade ceremony, which certifies zero organization-less rows before a database boots. -->

**BREAKING** read-scope narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D1, D14). Step 7 of the 18.x organization-ownership line: it lands after the upgrade ceremony opens.

- **Before.** `SqlDriver.applyTenantScope` compiled a tenant scope as `(organization_id = :tenant OR organization_id IS NULL)`, and the `group` posture's union as `(organization_id IN (…) OR organization_id IS NULL)`. That was the v17 reading, in which a row with no organization stood for platform state, and every tenant's scoped read, count, update and delete reached it. The Turso remote face compiles the same scope, so it answered the same rows.
- **Now.** The scope is `organization_id = :tenant`, or `organization_id IN (…)` under `group`. NULL is not an owner: a row with no organization is no tenant's, and only an unscoped read (no tenant on the call) reaches it. The lifecycle sweep's global retention and archive pass over an `organization_id` partition is a bare `$nin` of the overriding tenants: it no longer selects organization-less rows.
- **Unchanged.** A call with no tenant context and an object with no tenant column are unscoped, as before. The `single` posture's driver reads carry no tenant scope at all (ADR-0131 D8). The `sys_audit_log` attribution partition (`tenant_id`, ADR-0131 D7) keeps its NULL arm: a deployment-level ledger row is about no organization by design, and the global pass still reaps it. Insert stamping (`injectTenantOnInsert`) is unchanged.

**Who is affected.** Only a database that still holds organization-less rows in a tenant-scoped table, and only under a walled posture (`group` / `isolated`) or a caller that passes `tenantId` to the driver itself. A certified 18.x database holds none: a v17 database is refused at boot until `os migrate organization-ownership` certifies it, and a fresh install never writes one.

**The remedy.** If a scoped read misses a row you expect, the row has no organization: run the upgrade ceremony (`os migrate organization-ownership` to review its plan, then the same command with `--apply`), which stamps each such row with its owner, or stamp it yourself (`organization_id = <the owning organization>`).
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,22 @@ function suite(face: Face) {
]);
});

it('a batch with no tenant on the call answers every row it stored, the organization-less one included', async () => {
// [ADR-0131 D1] The tenant scope carries no `IS NULL` arm, so the rows'
// own union (`IN ('org_b')`) would miss the unstamped row this call just
// wrote. The read is scoped by that union only when every row carries a
// tenant; here the call's own options (none) decide, and the ids bound it.
const batch = await driver.bulkCreate(TABLE, [
{ id: 't-m1', title: 'named', organization_id: 'org_b' },
{ id: 't-m2', title: 'unstamped' },
]);
expect(batch.map((r) => [r.id, r.organization_id ?? null])).toEqual([
['t-m1', 'org_b'],
['t-m2', null],
]);
expect(batch[1]).toEqual(await stored('t-m2'));
});

it('inside a caller transaction, create and bulkCreate answer the uncommitted rows and the rollback keeps', async () => {
const trx = await driver.beginTransaction();
let single!: Record<string, unknown>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@
* answering `[]` to an unscoped admin read is a different defect, not a fix. So
* each exposure case is stated beside the read it must not become.
*
* The NULL-org row is here for the same reason. `applyTenantScope` emits
* `(field = ? OR field IS NULL)` on purpose (#2734: strict equality hid every
* platform-seeded RBAC row from every tenant admin). A door "fixed" with a bare
* equality would pass a naive cross-tenant assertion and silently lose p1, so
* p1 is what distinguishes routing through the chokepoint from reimplementing
* a worse copy of it.
* The NULL-org row is here for the same reason. The measurement above is the
* v17 chokepoint, `(field = ? OR field IS NULL)` (#2734). [ADR-0131 D1, D14]
* 18.x retired that arm after the upgrade ceremony: NULL is not an owner, so
* `applyTenantScope` emits `field = ?` alone and no scoped read reaches p1. p1
* now holds every door to the SAME rule from both sides: absent under a
* tenant, present with none. A door that kept its own copy of the old arm, or
* that dropped the no-tenant early-out, answers p1 where the chokepoint does
* not.
*/

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
Expand Down Expand Up @@ -136,8 +138,8 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
await driver.create(TABLE, { id: 'a2', organization_id: 'org_a', name: 'A2', balance: 20 } as any);
await driver.create(TABLE, { id: 'b1', organization_id: 'org_b', name: 'B1', balance: 30 } as any);
await driver.create(TABLE, { id: 'b2', organization_id: 'org_b', name: 'B2', balance: 40 } as any);
// Platform row: belongs to no tenant, so it belongs to no OTHER tenant
// either and must stay visible (#2734).
// An organization-less row: no tenant's, so no scoped read reaches it
// (ADR-0131 D1); only an unscoped one does.
await driver.create(TABLE, { id: 'p1', name: 'P1', balance: 50 } as any);

await driver.create(UNSCOPED_TABLE, { id: 'l1', name: 'L1', balance: 1 } as any);
Expand All @@ -155,16 +157,16 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
const rows = await driver.findWithWindowFunctions(TABLE, { ...WINDOW } as any, {
tenantId: 'org_a',
} as any);
expect(ids(rows)).toEqual(['a1', 'a2', 'p1']);
expect(ids(rows)).toEqual(['a1', 'a2']);
expect(ids(rows)).not.toContain('b1');
expect(ids(rows)).not.toContain('b2');
});

it('gives each tenant its own rows, and only its own', async () => {
const a = await driver.findWithWindowFunctions(TABLE, { ...WINDOW } as any, { tenantId: 'org_a' } as any);
const b = await driver.findWithWindowFunctions(TABLE, { ...WINDOW } as any, { tenantId: 'org_b' } as any);
expect(ids(a)).toEqual(['a1', 'a2', 'p1']);
expect(ids(b)).toEqual(['b1', 'b2', 'p1']);
expect(ids(a)).toEqual(['a1', 'a2']);
expect(ids(b)).toEqual(['b1', 'b2']);
});

it('agrees with `find()` on the same read — one driver, one answer', async () => {
Expand All @@ -173,23 +175,23 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
expect(ids(viaWindow)).toEqual(ids(viaFind));
});

it('keeps the NULL-org platform row visible — the chokepoint, not a bare equality (#2734)', async () => {
it('does not reach the organization-less row under a tenant — NULL is not an owner (ADR-0131 D1)', async () => {
const rows = await driver.findWithWindowFunctions(TABLE, { ...WINDOW } as any, { tenantId: 'org_a' } as any);
expect(ids(rows)).toContain('p1');
expect(ids(rows)).not.toContain('p1');
});

it('honours the ADR-0105 D2 union posture the same way the chokepoint does (#3623)', async () => {
const rows = await driver.findWithWindowFunctions(TABLE, { ...WINDOW } as any, {
tenantId: 'org_a',
tenantIds: ['org_a', 'org_b'],
} as any);
expect(ids(rows)).toEqual(['a1', 'a2', 'b1', 'b2', 'p1']);
expect(ids(rows)).toEqual(['a1', 'a2', 'b1', 'b2']);

const narrow = await driver.findWithWindowFunctions(TABLE, { ...WINDOW } as any, {
tenantId: 'org_a',
tenantIds: ['org_a'],
} as any);
expect(ids(narrow)).toEqual(['a1', 'a2', 'p1']);
expect(ids(narrow)).toEqual(['a1', 'a2']);
});

it('still applies the caller\'s own `where` beside the tenant predicate', async () => {
Expand All @@ -198,10 +200,9 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
{ ...WINDOW, where: { balance: { $gte: 20 } } } as any,
{ tenantId: 'org_a' } as any,
);
// `p1` (balance 50) is the NULL-org platform row and is legitimately
// visible to org_a — it passes the filter AND the chokepoint. b1/b2
// (30/40) pass the filter and must not survive the tenant predicate.
expect(ids(rows)).toEqual(['a2', 'p1']);
// `p1` (balance 50) and b1/b2 (30/40) pass the filter, and none of them
// survives the tenant predicate: p1 is no tenant's (ADR-0131 D1).
expect(ids(rows)).toEqual(['a2']);
});

it('still computes the window function itself', async () => {
Expand Down Expand Up @@ -245,7 +246,7 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
describe('distinct — the third read door, found by the gate rather than by the card', () => {
it('does not return another tenant\'s values', async () => {
const values = await driver.distinct(TABLE, 'name', undefined, { tenantId: 'org_a' } as any);
expect(values.sort()).toEqual(['A1', 'A2', 'P1']);
expect(values.sort()).toEqual(['A1', 'A2']);
expect(values).not.toContain('B1');
expect(values).not.toContain('B2');
});
Expand All @@ -260,9 +261,9 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
const values = await driver.distinct(TABLE, 'name', { balance: { $gte: 20 } } as any, {
tenantId: 'org_a',
} as any);
// `P1` is the NULL-org platform row (balance 50) — see the window-door
// case above. `B1`/`B2` also pass the filter and must not survive.
expect(values.sort()).toEqual(['A2', 'P1']);
// `P1` is the organization-less row (balance 50) — see the window-door
// case above. It and `B1`/`B2` pass the filter and must not survive.
expect(values.sort()).toEqual(['A2']);
});

it('is UNSCOPED with no tenantId — the admin / seed path stays open', async () => {
Expand Down
47 changes: 30 additions & 17 deletions packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,30 +133,43 @@ describe('SqlDriver tenant scope (organization_id)', () => {
});
});

describe('NULL tenant column = GLOBAL/platform row (#2734)', () => {
// [ADR-0131 D1, D14] NULL is not an owner. The v17 reading (#2734) answered
// an organization-less row to EVERY tenant's scoped read, as platform state;
// 18.x retired that arm with the upgrade ceremony, after which no such row
// exists on a certified database. A row written with no organization is
// therefore no tenant's: only an unscoped read reaches it.
describe('NULL tenant column = no tenant\'s row (ADR-0131 D1)', () => {
beforeEach(async () => {
// A platform-seeded row with no organization (bootstrap positions,
// permission sets, business units, pre-org first-boot seeds).
await driver.create('account', { id: 'g1', name: 'Global', tier: 'global' });
await driver.create('account', { id: 'g1', name: 'Orphan', tier: 'none' });
});

it('a scoped read still sees the org-less row (any tenant)', async () => {
it('a scoped read does not see the organization-less row, under any tenant', async () => {
const rowsA = await driver.find('account', {}, { tenantId: 'org_a' });
const rowsB = await driver.find('account', {}, { tenantId: 'org_b' });
expect(rowsA.map((r: any) => r.id)).toContain('g1');
expect(rowsB.map((r: any) => r.id)).toContain('g1');
// …while cross-tenant rows stay hidden exactly as before.
expect(rowsA.map((r: any) => r.id).sort()).toEqual(['a1', 'a2', 'g1']);
expect(rowsA.map((r: any) => r.id).sort()).toEqual(['a1', 'a2']);
expect(rowsB.map((r: any) => r.id).sort()).toEqual(['b1', 'b2']);
});

it('a scoped by-id read resolves the global row', async () => {
it('a scoped by-id read does not resolve it', async () => {
const row = await driver.findOne('account', { where: { id: 'g1' } }, { tenantId: 'org_a' });
expect(row?.id).toBe('g1');
expect(row).toBeNull();
});

it('a scoped count includes the global row', async () => {
it('a scoped count excludes it', async () => {
const a = await driver.count!('account', {}, { tenantId: 'org_a' });
expect(a).toBe(3); // a1, a2, g1 — never org_b's rows
expect(a).toBe(2); // a1, a2
});

it('a scoped update or delete does not reach it', async () => {
await driver.update('account', 'g1', { name: 'Touched' }, { tenantId: 'org_a' }).catch(() => undefined);
await driver.delete('account', 'g1', { tenantId: 'org_a' }).catch(() => undefined);
const row = await driver.findOne('account', { where: { id: 'g1' } });
expect(row?.name).toBe('Orphan');
});

it('an unscoped read still sees it — no tenant context is no scope', async () => {
const rows = await driver.find('account', {});
expect(rows.map((r: any) => r.id)).toContain('g1');
});
});

Expand Down Expand Up @@ -464,14 +477,14 @@ describe('SqlDriver tenant scope (organization_id)', () => {
expect(rows.map(r => r.id)).not.toContain('c1');
});

it('keeps the NULL-tenant global-row carve-out (#2734) on the union path', async () => {
await driver.create('account', { id: 'g1', name: 'GLOBAL' });
it('the union path does not reach an organization-less row either (ADR-0131 D1)', async () => {
await driver.create('account', { id: 'g1', name: 'ORPHAN' });
const rows = await driver.find(
'account',
{},
{ tenantId: 'org_a', tenantIds: ['org_a'] } as any,
{ tenantId: 'org_a', tenantIds: ['org_a', 'org_b'] } as any,
);
expect(rows.map(r => r.id).sort()).toEqual(['a1', 'a2', 'g1']);
expect(rows.map(r => r.id).sort()).toEqual(['a1', 'a2', 'b1', 'b2']);
});

it('an empty or malformed tenantIds falls back to tenantId equality (fail toward isolation)', async () => {
Expand Down
62 changes: 30 additions & 32 deletions packages/drivers/driver-sql/src/sql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7616,11 +7616,16 @@ export class SqlDriver implements IDataDriver {
* are never overwritten), a read scoped to the caller's ACTIVE org would miss
* a row that really landed. A batch may name several tenants, so the scope is
* their union through `tenantIds`, which `applyTenantScope` already compiles
* as `IN (…) OR IS NULL`; the caller's own `tenantIds` membership set is
* replaced, not widened. With no tenant field, or no tenant on the rows and
* none on the call, `applyTenantScope` leaves the read unscoped by its own
* contract. The ids are this call's own and `id` is the PRIMARY KEY, so the
* read cannot answer a row this call did not write, in any organization.
* as `IN (…)`; the caller's own `tenantIds` membership set is replaced, not
* widened. A row written with no tenant is no tenant's, and no tenant scope
* reaches it (ADR-0131 D1), so the rows' union scopes the read only when
* every row carries one. Otherwise the call's own options decide, and a call
* with no tenant leaves a row unstamped only because it carries none:
* `injectTenantOnInsert` fills every absent column on a call that does. With
* no tenant field, or no tenant on the call, `applyTenantScope` leaves the
* read unscoped by its own contract. The ids are this call's own and `id` is
* the PRIMARY KEY, so the read cannot answer a row this call did not write,
* in any organization.
*
* # A row that is not there
*
Expand All @@ -7646,11 +7651,12 @@ export class SqlDriver implements IDataDriver {
),
]
: [];
const scopeOptions: DriverOptions = {
...options,
tenantId: writtenTenants[0] ?? options?.tenantId,
tenantIds: writtenTenants.length > 0 ? writtenTenants : undefined,
};
const everyRowCarriesTenant =
tenantField !== null
&& written.every((row) => row[tenantField] !== undefined && row[tenantField] !== null && row[tenantField] !== '');
const scopeOptions: DriverOptions = everyRowCarriesTenant
? { ...options, tenantId: writtenTenants[0], tenantIds: writtenTenants }
: { ...options };
const builder = this.getBuilder(writeTable, options);
this.applyTenantScope(builder, object, scopeOptions);
const stored: Record<string, any>[] = await builder.whereIn(
Expand Down Expand Up @@ -11475,8 +11481,8 @@ export class SqlDriver implements IDataDriver {
//
// Placed BESIDE `getBuilder` and above the caller's `where`, which is the
// position `findRows()` uses: the predicate has to be on the builder before
// anything reads it, and `applyTenantScope` is what owns the NULL-org
// platform-row and ADR-0105 D2 union semantics. Re-deriving either here
// anything reads it, and `applyTenantScope` is what owns the equality and
// ADR-0105 D2 union semantics. Re-deriving either here
// would be a second, worse copy of the wall.
this.applyTenantScope(builder, object, options);

Expand Down Expand Up @@ -15600,27 +15606,19 @@ export class SqlDriver implements IDataDriver {
? rawIds.filter((v: unknown) => typeof v === 'string' && v !== '')
: [];
if (tenantIds.length > 0) {
return builder.where((b) => {
void b.whereIn(field, tenantIds.map(String)).orWhereNull(field);
});
return builder.whereIn(field, tenantIds.map(String));
}
// `(field = :tenantId OR field IS NULL)`. The NULL arm is the v17 reading
// (#2734), in which an org-less row stood for platform state (seeded
// positions and permission sets, business units, first-boot seeds).
// ADR-0131 replaces that reading with three rules. A managed package's
// metadata is not in the database: its definitions are sealed, and
// customization is environment-level state (D2, D6). A row with an
// organization column was written by that organization, and NULL is not
// an owner (D1); references to declared items are by machine name (D4). A
// table with no organization column is deployment-level or code-level,
// protected by permissions rather than by this wall (D7). So on a fresh
// 18.0 `single` install, where every insert carries its organization, the
// arm matches nothing. The arm is removed on the 18.x line, after the
// upgrade ceremony (ADR-0131 D14). Rows stamped with a DIFFERENT tenant
// stay invisible either way.
return builder.where((b) => {
void b.where(field, String(tenantId)).orWhereNull(field);
});
// `field = :tenantId`, and nothing else. [ADR-0131 D1, D14] NULL is not an
// owner: an organization-less row is no tenant's, so no tenant scope reaches
// it. The v17 reading (#2734) added an `OR field IS NULL` arm here, under
// which such a row stood for platform state; ADR-0131 replaced that reading
// (D2, D6, D7) and the 18.x upgrade ceremony retired the rows it served: a
// certified database holds no NULL organization, a fresh install never
// writes one, and a v17 database is refused at boot until it is certified.
// A composition that registers no organization object can still write
// organization-less rows, and it never reaches this line with a tenant: no
// session, guest or system write there carries an organization.
return builder.where(field, String(tenantId));
}

/**
Expand Down
Loading
Loading