Repository navigation
Commit 5e9b100
feat(driver-sql,driver-turso)!: 18.x step 7 part 7b-0 — a table the driver creates carries its tenant column NOT NULL (#24940)
Part of #24583 (18.x step 7, part 7b-0; claim 6285142472; governing
text: execution record 6285122667, ADR-0131 D1, ruling ② A 6269955919).
Clause-②: yes (narrowing) — a table the driver creates refuses a write
that names no organization, at the database.
Metadata-protocol: none — the constraint changes what a data writer can
store, not what a metadata author can write.
## Organization delete: resolved on main (#24948, ruling B)
Earlier on this branch, deleting an organization answered 500. The
engine's `set_null` cascade on the injected tenant lookup hit the NOT
NULL column. The same defect is live in 18.0.0 on a ceremonied database
(measured; see #24948).
The fix landed on main as #25016 (688052e), and this branch now
carries it. On this head:
- A delete refuses before any write while the organization owns records:
`409 DELETE_RESTRICTED` with per-table `dependents`.
- The delete door removes the organization's own auth structure
(members, invitations, teams, team members and org-admin grants) as one
unit.
- The engine refuses where it used to `set_null` a tenant column.
Pointer columns (`active_organization_id`, `parent_organization_id`)
keep `set_null`.
The two dogfood pins that were red here are green on that contract, with
no 500. #25016 rewrote them:
- `organization-delete-federated-fixture`: 6/6. An organization that
owns nothing is deleted with 200 (the positive control). An organization
that owns a record is refused with 409 and its counts. The Default
Organization cases are kept.
- `org-record-admin-reach`: 7/7.
## What changes
- **driver-sql `createColumn`**: a new `tenantColumn` argument makes the
column NOT NULL on both exits (scalar and multi-value). Only the two
CREATE TABLE sites pass it:
- `initObjects`' create branch: `name === tenantField`;
- `ensureShardTable`'s create branch: asked of the base's declaration
through the recording resolver. The index sync now reuses that same
answer.
An ADD COLUMN never passes it, so a column added to a populated table
stays nullable. That table is the ceremony's.
- **driver-turso remote face**: `buildCreateTableSQL` (used by
`syncSchema`, `syncSchemasBatch` and `initObjects`) asks the driver's
`computeTenantField` resolver. It emits ` NOT NULL` on the tenant column
only. ADD COLUMN is unchanged.
- **spec**: `TENANT_SCOPE_FIELD_DEF` keeps its shape. Its description no
longer says "NULL on single-tenant stacks", and its docblock says why
the column has no `storage.notNull`.
- **No metadata change and no differ change.**
`ceremonyOwnedNullability` (from #24681) already keeps the
declared-nullable / physical-NOT-NULL difference out of drift. Pinned
for a fresh table, a new shard and the remote face.
## Pins
- `driver-sql/src/sql-driver-tenant-column-not-null.test.ts`, on SQLite
and live PostgreSQL 16 (MySQL cell declared un-run):
- a fresh table refuses a raw NULL tenant insert at the database, and
reports no drift;
- a newly rotated shard does the same, with no drift on the shard;
- boundaries: an opted-out object keeps `organization_id` nullable; a
tenant column added to an existing table stays nullable; a pre-existing
shard keeps its nullability.
- `driver-turso/src/turso-remote-tenant-column-not-null.test.ts`: a real
libSQL `file:` client behind the remote face (no hosted remote reached).
- Through all three schema doors, the remote CREATE matches the local
face's `notnull` flags, refuses a raw NULL tenant insert, and reports no
drift on either face.
- Boundaries: an opted-out object, and an ADD COLUMN on an existing
remote table.
- Ablation, by reverting the `|| tenantColumn` term: driver-sql 2 cases
go red, Turso remote 4 cases go red; restored, green.
## Fixtures (no assertion weakened)
About 200 test files across 25 packages wrote organization-less rows
through a fresh table. Each was fixed one of three ways, chosen by what
the test is about:
1. **The write names its organization**, where the organization was
incidental (for example a decorated driver that dropped `options`, or a
raw insert).
2. **A lean engine gets what a real `single` install has**:
`installDefaultOrganization` (a `managedBy: 'better-auth'`
`sys_organization` and its row, seeded at rest). The engine's D9
derivation then stamps the writes. In cli, a fixture plugin is spliced
in ahead of the app's seed.
3. **A pre-18 store**, where organization-less rows ARE the subject:
NULL-bucket uniqueness, the driver's NULL arm, global grants and
positions, legacy seeds, backfills. The table is relaxed through the
driver's own `relax_not_null` drift door
(`pre-18-tenant-column.testkit.ts`). The Turso remote face has no such
door, so there the testkit creates its table with the transport's own
pre-18 CREATE. These are the readers 7a-1/7a-2/7a-3/7b-2 retire. Their
tests keep measuring the shape they were written for.
## Size, for the approval
3352 changed lines (+3271 / -81) in 367 files, against main edcc8ce:
- driver and spec code: 55 lines (`sql-driver.ts` 30,
`remote-transport.ts` 16, `injected-system-column-provenance.ts` 9);
- the two new pins: 319 lines;
- testkits: 1235 lines in 27 files, mostly the same two small kits
copied into each package that needs them
(`default-organization.testkit.ts`, `pre-18-tenant-column.testkit.ts`),
because a package's tests do not import another package's test files;
- fixture re-stamps in existing tests: 1631 lines;
- the changeset: 20 lines.
## A database the published 18.0.0 created
- `ORGANIZATION_OWNERSHIP_SHAPE_GENERATION` is unchanged (1). NOT NULL
on new tables is not a shape the guard certifies.
- The harness's fixture-18.0.0 leg
(`upgrade-18x-harness.dogfood.test.ts`, a real 18.0.0-born database:
nullable tenant columns, birth marker generation 1) is green on this
head, 4/4. HEAD plans no destructive change over it and boots it with no
refusal. `ceremonyOwnedNullability` keeps its nullable tenant columns
out of drift.
- Such a database does not have to stay nullable. On a copy of that
fixture, HEAD's `os migrate organization-ownership` (plan, then apply,
with auth composed) answered `complete: true, blocking: 0` and tightened
62 of its 65 tenant tables to NOT NULL. The 3 it left nullable:
- a rotated activity shard (the ceremony reports it cannot tighten a
shard);
- `sys_email_template` (mirror-deletion fate);
- `sys_sso_provider` (tenancy disabled, with an authored
`organization_id`).
This is a measurement only; nothing here changes the ceremony.
## Merge resolutions (main 7169612, then 688052e)
- Seven test files conflicted with 7a-2's rewrites. Each takes #24893's
resolution (4dadd7e) verbatim, which keeps both rules: a grant
applies only in its own organization, and a fresh tenant column is NOT
NULL.
- Main's version wins, and the pre-18 relax goes, where 7a-2 now writes
the grant WITH an organization:
- `parent-derived-write-refusal-not-visible`
- `predicate-write-unreadable-not-matched`
- `write-door-unreadable-is-not-found`
- `sys-user-default-row-scope`
- The relax stays where main still lands an organization-less row:
- `sso-register-platform-admin-gate`
- `default-organization-statements.golden`
- `sharing-rule-org-less-caller`
- `sharing-rule-criteria-required` takes main's version verbatim. Both
sides had added the organization key, which would have been a duplicate
property.
- `notification-display-title.test.ts` (service-messaging): it keeps
main's removal of the retired `NotificationSubscription` import (#25024)
and this branch's Default Organization testkit import.
- Main's new `webhook-delivery-real-sql-driver` case (plugin-webhooks)
gets the lean engine's Default Organization. Its assertion is unchanged.
## Measured (merged tree: main 7169612, with #24681, 7a-1, 7a-2, 7a-3,
7b-3, 7c, 7z and #24911)
- On the latest merge (head 0b58ee3, main 688052e with #25016):
- driver-sql 3707 passed; driver-turso 2437; service-messaging 602;
objectql 8566; plugin-auth 3376;
- dogfood: the upgrade harness (including the fixture-18.0.0 leg) and
every organization and org-* dogfood file, 19 files and 131 tests, none
skipped;
- workspace `turbo run typecheck`: 151/151;
- `dispatch-gates --commands`: 103 commands, all exit 0;
- `check-tenant-chokepoint`: exit 0.
- On the merge before it (main 7169612):
- upgrade harness 12/12, and the merge-touched dogfood files 71/71;
- driver-sql 3696 passed; driver-turso 2437; plugin-webhooks 227;
- workspace `turbo run typecheck`: 151/151;
- `check-tenant-chokepoint`: exit 0.
- Every package's suite is green except the two organization-delete
dogfood cases above. cli: 6046 passed. `dispatch-gates --commands`: 103
commands, all exit 0. Typecheck of the 23 touched packages: clean. `pnpm
lint`: exit 0.
- Main's own new fixtures were also brought to the NOT NULL column.
Among them is 7b-3's "a lean embedding keeps `__global__`" pin: it now
runs on a pre-18 store, because a fresh table refuses the
organization-less row at the database. That is a cross-part question for
the seat; see the report.
- **R1**: fresh `pnpm dev -- --fresh` on random ports, for showcase, crm
and todo. Each answered health 200 and seeded-admin sign-in 200, with 0
ERROR lines and 0 NOT NULL refusals. The fresh database's tenant columns
are all NOT NULL.
- **R2 + R3**: `upgrade-18x-harness.dogfood` (8, over the ceremonied
fixture) and `organization-ownership-boot-refusal-order.dogfood` (4) are
green.
- **R4**: a production `os serve` under `OS_TENANCY_POSTURE=isolated`,
with the real `@objectstack/organizations` linked into a scratch app,
answered health 200, owner sign-up 200 and sign-in 200, with 0 error
lines. Step 4's seven walled e2e cases
(`serve-organizations-host-resolution` 3 +
`serve-organizations-mount-failure` 4, run with `OS_TEST_TIERS=nightly`)
pass 7/7.
- Known NULL writer, made loud: a production `os serve` with no
`OS_AUTH_SECRET` (showcase) still answers health 200, and its inline
seed is refused 172 times at the database. Before, it landed NULL rows.
Part 7b-3 makes that boot refuse.
- Lean SQL embedding (`@objectstack/objectql/core` + `SqlDriver`, no
organization object): an author sees `NOT NULL constraint failed:
account.organization_id` on the first insert. ADR-0131 D9 prescribes an
engine refusal that names what is missing. Today it is the database's.
## Changeset
`.changeset/24583-tenant-column-not-null-ddl.md`: `patch` for
driver-sql, driver-turso and spec (`Metadata-protocol: none`, under
main's level rule from #25114). It keeps the BREAKING banner, the
ADR-0087 disposition and the remedy. Since review 6317931247 (B4), it
also says that on a database that has not taken the ceremony, a table
this release creates is NOT NULL beside nullable siblings.
1 parent 03966ce commit 5e9b100
367 files changed
Lines changed: 3271 additions & 81 deletions
File tree
- .changeset
- examples
- app-crm/test
- app-showcase/test
- app-todo/test
- packages
- client/src
- cli/src
- commands
- db
- migrate
- secret
- utils
- drivers
- driver-sql/src
- driver-turso/src
- objectql/src
- plugins
- plugin-approvals/src
- plugin-audit/src
- plugin-auth/src
- plugin-security/src
- objects
- plugin-sharing/src
- plugin-webhooks/src
- qa
- dogfood/test
- http-conformance/src
- rest/src
- runtime/src
- domains
- services
- service-analytics/src/__tests__
- service-automation/src
- builtin
- service-messaging/src
- objects
- service-storage/src
- spec/src
- data
- system/constants
- triggers/trigger-record-change/src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
| |||
96 | 97 | | |
97 | 98 | | |
98 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
99 | 103 | | |
100 | 104 | | |
101 | 105 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
61 | 62 | | |
62 | 63 | | |
63 | 64 | | |
| |||
128 | 129 | | |
129 | 130 | | |
130 | 131 | | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
131 | 135 | | |
132 | 136 | | |
133 | 137 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
| |||
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
90 | 94 | | |
91 | 95 | | |
92 | 96 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| |||
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
85 | 89 | | |
86 | 90 | | |
87 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| |||
61 | 62 | | |
62 | 63 | | |
63 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
64 | 68 | | |
65 | 69 | | |
66 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
68 | 72 | | |
69 | 73 | | |
70 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
0 commit comments