diff --git a/.changeset/24583-tenant-column-not-null-ddl.md b/.changeset/24583-tenant-column-not-null-ddl.md
new file mode 100644
index 00000000000..ad64e6dcf72
--- /dev/null
+++ b/.changeset/24583-tenant-column-not-null-ddl.md
@@ -0,0 +1,21 @@
+---
+'@objectstack/driver-sql': patch
+'@objectstack/driver-turso': patch
+'@objectstack/spec': patch
+---
+
+feat(driver-sql,driver-turso)!: a table the driver creates carries its tenant column NOT NULL, so a write that names no organization is refused at the database
+
+Clause-②: yes (narrowing)
+Metadata-protocol: none
+
+
+
+**BREAKING** accept-set narrowing at the database (ADR-0131 D1: "There is no nullable tenant column anywhere"). It changes what a data writer can store, not what a metadata author can write, so it ships as `patch` with this banner.
+
+- **Before.** The SQL driver created a table's tenant column (`organization_id`, or a declared `tenancy.tenantField`) nullable, as the injected field's declaration says (`required: false`, no `storage`). A write that reached the driver without an organization landed a row no organization owns.
+- **Now.** Every table the driver CREATES takes its tenant column `NOT NULL`, with no metadata change: the plain managed table (`initObjects`), every newly rotated shard (asked of the base table's declaration), and a table the Turso remote face creates (`syncSchema`, `syncSchemasBatch`, `initObjects`). A write with no organization on such a table is refused by the database (`NOT NULL constraint failed:
.organization_id`, `violates not-null constraint`, or `cannot be null`). That includes an `upsert` whose payload omits the organization: the database checks the proposed row before it resolves the conflict.
+- **Unchanged.** A tenant column ADDED to a table that already exists stays nullable, and so does an existing shard or an existing remote table: a table that already holds rows is the organization-ownership ceremony's to tighten (`os migrate organization-ownership`), which reports a shard and the Turso remote face as unable to. The schema differ reports no drift over the tenant column's nullability either way. On a database that has not taken the ceremony, a table this release creates (a newly rotated shard, or a cold table a newly configured lifecycle tier creates) is NOT NULL beside nullable siblings: an organization-less row the lifecycle mover would carry into it is refused there, and stays where it is until the ceremony attributes it. An object with no tenant column (`tenancy: { enabled: false }`, `systemFields: { tenant: false }`) is untouched, even when it carries a field named `organization_id`.
+- **Who sees it.** A composed `os serve` / `os dev` / `os start` with auth stamps every write with its organization, so nothing changes there. A production `os serve` whose `AuthPlugin` did not compose (no `OS_AUTH_SECRET`) and a lean embedding (`@objectstack/objectql/core` or `createStandaloneStack` over a SQL driver, with no organization object) used to write those rows with a NULL organization; now each such write fails, loudly.
+
+**The remedy.** Carry the organization on the write: `ql.insert(object, { ...row, organization_id: orgId })`, or `{ context: { tenantId: orgId } }`. Compose authentication (set `OS_AUTH_SECRET`) so the platform derives it. If the object's rows legitimately belong to no organization, declare it so and it has no tenant column: `systemFields: { tenant: false }` or `tenancy: { enabled: false }`.
diff --git a/examples/app-crm/test/default-organization.testkit.ts b/examples/app-crm/test/default-organization.testkit.ts
new file mode 100644
index 00000000000..e2c6f2c3722
--- /dev/null
+++ b/examples/app-crm/test/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/example-crm',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/examples/app-crm/test/opportunity-stage-hook.test.ts b/examples/app-crm/test/opportunity-stage-hook.test.ts
index 7970cf69ea8..e509ad8839f 100644
--- a/examples/app-crm/test/opportunity-stage-hook.test.ts
+++ b/examples/app-crm/test/opportunity-stage-hook.test.ts
@@ -56,6 +56,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { Account, Opportunity, OpportunityLineItem } from '../src/objects/index.js';
import { allHooks } from '../src/hooks/index.js';
import { OpportunityStageHook } from '../src/hooks/opportunity.hook.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The app id `AppPlugin` derives its `packageId` from. */
const PACKAGE_ID = 'app:com.example.crm';
@@ -96,6 +97,9 @@ async function bootCrm(hooks: unknown[] = allHooks): Promise {
engine.registry.registerObject(def as never, PACKAGE_ID, 'crm');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Exactly the AppPlugin call, minus the sandbox body runner this app has no
// use for (its single hook is an inline code handler).
diff --git a/examples/app-showcase/test/approval-resume-relation-expand.test.ts b/examples/app-showcase/test/approval-resume-relation-expand.test.ts
index 349e41e2088..36dcd7cb85d 100644
--- a/examples/app-showcase/test/approval-resume-relation-expand.test.ts
+++ b/examples/app-showcase/test/approval-resume-relation-expand.test.ts
@@ -58,6 +58,7 @@ import {
TaskDoneNotifyOwnerFlow,
NotifyOwnerSubflow,
} from '../src/automation/flows/index.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** Everything the approvals service treats as an unconditional actor. */
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as any;
@@ -128,6 +129,9 @@ async function bootShowcaseApprovals(
objectql.registry.registerObject(obj, 'showcase', 'showcase');
}
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
for (const flow of flows) automation.registerFlow(flow.name, flow);
diff --git a/examples/app-showcase/test/default-organization.testkit.ts b/examples/app-showcase/test/default-organization.testkit.ts
new file mode 100644
index 00000000000..9b19e1b8999
--- /dev/null
+++ b/examples/app-showcase/test/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/example-showcase',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/examples/app-showcase/test/hook-body-persisted-writes.test.ts b/examples/app-showcase/test/hook-body-persisted-writes.test.ts
index 5116c3bce62..24c4bec744e 100644
--- a/examples/app-showcase/test/hook-body-persisted-writes.test.ts
+++ b/examples/app-showcase/test/hook-body-persisted-writes.test.ts
@@ -49,6 +49,7 @@ import { QuickJSScriptRunner, hookBodyRunnerFactory } from '@objectstack/runtime
import { Account, Project, Task, Inquiry } from '../src/data/objects/index.js';
import { allHooks } from '../src/data/hooks/index.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The app id `AppPlugin` derives its `packageId` from. */
const APP_ID = 'com.objectstack.showcase';
@@ -87,6 +88,9 @@ async function bootShowcase(hooks: unknown[] = allHooks): Promise {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engine.bindHooks(hooks as never[], {
packageId: PACKAGE_ID,
diff --git a/examples/app-showcase/test/new-project-wizard-initial-status.test.ts b/examples/app-showcase/test/new-project-wizard-initial-status.test.ts
index 7e43d075526..95d3ac23d7b 100644
--- a/examples/app-showcase/test/new-project-wizard-initial-status.test.ts
+++ b/examples/app-showcase/test/new-project-wizard-initial-status.test.ts
@@ -37,6 +37,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import stack from '../objectstack.config.js';
import { Account, Project } from '../src/data/objects/index.js';
import { NewProjectWizardPage } from '../src/ui/pages/new-project-wizard.page.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
type Rule = {
type?: string;
@@ -82,6 +83,9 @@ async function bootShowcase(): Promise {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return engine;
}
diff --git a/examples/app-showcase/test/paid-invoice-line-lock.test.ts b/examples/app-showcase/test/paid-invoice-line-lock.test.ts
index db43677304f..94ab53e55e2 100644
--- a/examples/app-showcase/test/paid-invoice-line-lock.test.ts
+++ b/examples/app-showcase/test/paid-invoice-line-lock.test.ts
@@ -34,6 +34,7 @@ import {
PAID_INVOICE_LINE_REMOVE_REFUSAL,
PAID_INVOICE_LINE_MOVE_REFUSAL,
} from '../src/data/hooks/index.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const APP_ID = 'com.objectstack.showcase';
const PACKAGE_ID = `app:${APP_ID}`;
@@ -61,6 +62,9 @@ async function bootShowcase(hooks: unknown[] = allHooks): Promise {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engine.bindHooks(hooks as never[], {
packageId: PACKAGE_ID,
bodyRunner: hookBodyRunnerFactory(new QuickJSScriptRunner(), { ql: engine, appId: APP_ID }),
diff --git a/examples/app-showcase/test/task-reopen-hook-refusal.test.ts b/examples/app-showcase/test/task-reopen-hook-refusal.test.ts
index 24cd29553b0..88a351b8c5a 100644
--- a/examples/app-showcase/test/task-reopen-hook-refusal.test.ts
+++ b/examples/app-showcase/test/task-reopen-hook-refusal.test.ts
@@ -38,6 +38,7 @@ import {
import { Account, Project, Task } from '../src/data/objects/index.js';
import { allHooks, TASK_REOPEN_REFUSAL } from '../src/data/hooks/index.js';
import { ReopenTaskAction } from '../src/ui/actions/index.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const APP_ID = 'com.objectstack.showcase';
const PACKAGE_ID = `app:${APP_ID}`;
@@ -65,6 +66,9 @@ async function bootShowcase(hooks: unknown[] = allHooks): Promise {
engine.registry.registerObject(def as never, PACKAGE_ID, 'showcase');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engine.bindHooks(hooks as never[], {
packageId: PACKAGE_ID,
bodyRunner: hookBodyRunnerFactory(new QuickJSScriptRunner(), { ql: engine, appId: APP_ID }),
diff --git a/examples/app-todo/test/default-organization.testkit.ts b/examples/app-todo/test/default-organization.testkit.ts
new file mode 100644
index 00000000000..c48a5fcb116
--- /dev/null
+++ b/examples/app-todo/test/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/example-todo',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/examples/app-todo/test/derived-flag-removal.test.ts b/examples/app-todo/test/derived-flag-removal.test.ts
index 846f36f1741..f912eba3efb 100644
--- a/examples/app-todo/test/derived-flag-removal.test.ts
+++ b/examples/app-todo/test/derived-flag-removal.test.ts
@@ -65,6 +65,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { Task } from '../src/objects/task.object.js';
import taskHook from '../src/objects/task.hook.js';
import TodoApp from '../objectstack.config.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The two fields this card retired. */
const REMOVED_FIELDS = ['is_completed', 'is_overdue'] as const;
@@ -94,6 +95,9 @@ async function bootEngine(objectDef: unknown = Task) {
openDrivers.push(driver);
objectql.registry.registerObject(objectDef, 'todo', 'todo');
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
objectql.bindHooks([taskHook], { packageId: 'app:com.example.todo' });
return objectql;
}
diff --git a/examples/app-todo/test/loop-body-iteration.test.ts b/examples/app-todo/test/loop-body-iteration.test.ts
index 2556232fdd9..38cf4b32568 100644
--- a/examples/app-todo/test/loop-body-iteration.test.ts
+++ b/examples/app-todo/test/loop-body-iteration.test.ts
@@ -44,6 +44,7 @@ import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/ser
import { OverdueEscalationFlow, TaskReminderFlow } from '../src/flows/index.js';
import { Task } from '../src/objects/task.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** One captured `messaging.emit()` call — the notice as the service received it. */
interface CapturedEmit {
@@ -109,6 +110,9 @@ async function bootTodoKernel(): Promise<{
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
automation.registerFlow(OverdueEscalationFlow.name, OverdueEscalationFlow);
automation.registerFlow(TaskReminderFlow.name, TaskReminderFlow);
diff --git a/examples/app-todo/test/overdue-escalation-days-overdue.test.ts b/examples/app-todo/test/overdue-escalation-days-overdue.test.ts
index bdedcee3088..2657908ed3d 100644
--- a/examples/app-todo/test/overdue-escalation-days-overdue.test.ts
+++ b/examples/app-todo/test/overdue-escalation-days-overdue.test.ts
@@ -36,6 +36,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { OverdueEscalationFlow } from '../src/flows/index.js';
import { Task } from '../src/objects/task.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const openDrivers: Array<{ disconnect?: () => Promise }> = [];
afterEach(async () => {
@@ -106,6 +107,9 @@ async function bootTodoData(): Promise {
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
return kernel.getService('data');
}
diff --git a/examples/app-todo/test/task-completion-trigger.test.ts b/examples/app-todo/test/task-completion-trigger.test.ts
index dbafeebe6af..c7f53052ee9 100644
--- a/examples/app-todo/test/task-completion-trigger.test.ts
+++ b/examples/app-todo/test/task-completion-trigger.test.ts
@@ -45,6 +45,7 @@ import { allFlows, TaskCompletionFlow } from '../src/flows/index.js';
import { Task } from '../src/objects/task.object.js';
import taskHook from '../src/objects/task.hook.js';
import TodoApp from '../objectstack.config.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@@ -83,6 +84,9 @@ async function bootTodoKernel(): Promise<{
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
// [#7036] The app's lifecycle hook, bound the way `AppPlugin` binds it from
// `defineStack({ hooks })`. This kernel is assembled by hand (no AppPlugin),
@@ -359,6 +363,9 @@ describe('#7036 — completing a task is possible for a normal user', () => {
openDrivers.push(driver);
objectql.registry.registerObject(Task, 'todo', 'todo');
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
// NB: no `bindHooks` — that is the whole fixture.
const id = await newTask(data);
diff --git a/examples/app-todo/test/task-recurrence.test.ts b/examples/app-todo/test/task-recurrence.test.ts
index 677a9547ff9..382639f0004 100644
--- a/examples/app-todo/test/task-recurrence.test.ts
+++ b/examples/app-todo/test/task-recurrence.test.ts
@@ -53,6 +53,7 @@ import { allFlows, TaskCompletionFlow } from '../src/flows/index.js';
import { todoFunctions, computeNextTaskDueDate } from '../src/functions/index.js';
import { Task } from '../src/objects/task.object.js';
import taskHook from '../src/objects/task.hook.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@@ -106,6 +107,9 @@ async function bootTodoKernel(): Promise<{
objectql.registry.registerObject(Task, 'todo', 'todo');
objectql.bindHooks([taskHook], { packageId: 'app:com.example.todo' });
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
automation.setFunctionResolver(
(name: string) => (todoFunctions as Record)[name] as any,
diff --git a/packages/cli/src/commands/db/orphans.integration.test.ts b/packages/cli/src/commands/db/orphans.integration.test.ts
index bb4500f028a..c5ded2f7a7f 100644
--- a/packages/cli/src/commands/db/orphans.integration.test.ts
+++ b/packages/cli/src/commands/db/orphans.integration.test.ts
@@ -352,10 +352,11 @@ describe('[#24964] os db orphans --delete — the delete is the engine\'s own',
it('a restrict dependent refuses the orphan\'s delete, and nothing of its cascade is removed', async () => {
const c = prepareCase();
const exportPath = join(c.caseDir, 'export.json');
- // A row outside the cascade that holds the orphan through a `restrict` lookup.
+ // A row outside the cascade that holds the orphan through a `restrict` lookup,
+ // in the organization the seeded rows belong to (a raw write is not stamped).
const driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: c.dbFile }, useNullAsDefault: true });
try {
- await (driver as any).knex('od_hold').insert({ id: 'h_row', name: 'holds it', member: 'm_orphan' });
+ await (driver as any).knex('od_hold').insert({ id: 'h_row', name: 'holds it', member: 'm_orphan', ...ORG });
} finally {
await driver.disconnect();
}
diff --git a/packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts b/packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts
index 5ee50d83a60..d27f6974378 100644
--- a/packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts
+++ b/packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts
@@ -146,7 +146,9 @@ for (const plugin of stack.plugins) await kernel.use(plugin);
await kernel.use(new PlatformObjectsPlugin());
await runtime.start();
const ql = kernel.getService('objectql');
-const SYSTEM = { context: { isSystem: true } };
+// The fixture's rows name an organization: a fresh tenant column is NOT NULL
+// (ADR-0131 D1), and this stack composes no authentication to derive one.
+const SYSTEM = { context: { isSystem: true, tenantId: 'org_fixture' } };
await ql.insert('sys_metadata_history', {
id: 'h_21529', event_seq: 1, name: 'os21529_note', type: 'object', version: 1,
operation_type: 'create', recorded_by: 'system', recorded_at: new Date().toISOString(),
@@ -405,7 +407,9 @@ await kernel.use(new SettingsServicePlugin({
await kernel.use(new StorageServicePlugin({ registerRoutes: false }));
await runtime.start();
const ql = kernel.getService('objectql');
-const SYSTEM = { context: { isSystem: true } };
+// The fixture's rows name an organization: a fresh tenant column is NOT NULL
+// (ADR-0131 D1), and this stack composes no authentication to derive one.
+const SYSTEM = { context: { isSystem: true, tenantId: 'org_fixture' } };
await ql.insert('sys_metadata', {
type: 'object', name: 'os21552_note', state: 'active',
metadata: JSON.stringify({ name: 'os21552_note', fields: { title: { type: 'text' } } }),
@@ -414,6 +418,8 @@ await kernel.shutdown();
const raw = new SqlDriver({ client: 'better-sqlite3', connection: { filename: process.env.FIXTURE_DB }, useNullAsDefault: true });
const k = raw.knex;
+// Rows planted below the writer name the fixture's organization: every table
+// this release created has a NOT NULL tenant column (ADR-0131 D1).
await k.raw('CREATE TABLE sys_account (id text primary key, provider_id text, account_id text, issuer text, user_id text)');
await k('sys_account').insert([
{ id: 'acc_1', provider_id: 'github', account_id: 'g-1', issuer: 'github', user_id: 'u1' },
@@ -429,7 +435,7 @@ await k('sys_audit_log').insert({
const shard = (await k.raw("select name from sqlite_master where type = 'table' and name glob 'sys_activity__r*'"))[0];
if (!shard) throw new Error('the control has no sys_activity shard: the rotation did not run');
await k(shard.name).insert({
- id: 'act_21552', timestamp: new Date().toISOString(), type: 'create', summary: 'created', object_name: 'sys_metadata', record_id: 'm_21552',
+ organization_id: 'org_fixture', id: 'act_21552', timestamp: new Date().toISOString(), type: 'create', summary: 'created', object_name: 'sys_metadata', record_id: 'm_21552',
metadata: JSON.stringify({
old: null,
new: {
@@ -446,23 +452,23 @@ await k('sys_audit_log').insert({
new_value: JSON.stringify({ id: 'em_24693', subject: 's', body_text: 'mail-body-sentinel-24693', body_html: null }),
});
await k(shard.name).insert({
- id: 'act_24693', timestamp: new Date().toISOString(), type: 'create', summary: 'created', object_name: 'sys_email', record_id: 'em_24693',
+ organization_id: 'org_fixture', id: 'act_24693', timestamp: new Date().toISOString(), type: 'create', summary: 'created', object_name: 'sys_email', record_id: 'em_24693',
changed_fields: '[]',
metadata: JSON.stringify({ old: null, new: { id: 'em_24693', subject: 's', body_text: 'mail-body-sentinel-24693', body_html: null } }),
});
await k('sys_comment').insert({
- id: 'cmt_23857', thread_id: 'sys_metadata:m_21552', body: 'planted below the writer', author_id: 'u1',
+ organization_id: 'org_fixture', id: 'cmt_23857', thread_id: 'sys_metadata:m_21552', body: 'planted below the writer', author_id: 'u1',
created_at: new Date().toISOString(),
});
await k('sys_secret').insert({
id: 'sec_21552', namespace: 'os21552', key: 'orphan', kms_key_id: 'local', alg: 'aes-256-gcm', version: 1, ciphertext: 'not-a-real-ciphertext',
});
await k('sys_file').insert({
- id: 'file_21552', key: 'attachments/os21552.txt', name: 'os21552.txt', size: 12, scope: 'attachments', status: 'committed',
+ organization_id: 'org_fixture', id: 'file_21552', key: 'attachments/os21552.txt', name: 'os21552.txt', size: 12, scope: 'attachments', status: 'committed',
});
// A retired field's column: in the table, in no metadata, holding a value.
await k.raw('ALTER TABLE os21529_contact ADD COLUMN legacy_note text');
-await k('os21529_contact').insert({ id: 'con_21573', name: 'Ann', legacy_note: 'kept-21573' });
+await k('os21529_contact').insert({ id: 'con_21573', name: 'Ann', legacy_note: 'kept-21573', organization_id: 'org_fixture' });
await raw.disconnect();
process.stderr.write('[fixture] seeded\\n');
process.exit(0);
diff --git a/packages/cli/src/commands/migrate/object-scope.integration.test.ts b/packages/cli/src/commands/migrate/object-scope.integration.test.ts
index 57784735318..6bf613f5bb3 100644
--- a/packages/cli/src/commands/migrate/object-scope.integration.test.ts
+++ b/packages/cli/src/commands/migrate/object-scope.integration.test.ts
@@ -109,7 +109,9 @@ for (const plugin of stack.plugins) await kernel.use(plugin);
await kernel.use(new PlatformObjectsPlugin());
await runtime.start();
const ql = kernel.getService('objectql');
-const SYSTEM = { context: { isSystem: true } };
+// The fixture's rows name an organization: a fresh tenant column is NOT NULL
+// (ADR-0131 D1), and this stack composes no authentication to derive one.
+const SYSTEM = { context: { isSystem: true, tenantId: 'org_fixture' } };
await ql.insert('os21644_account', { id: 'a1', name: 'Acme' }, SYSTEM);
await ql.insert('os21644_site', { id: 's1', name: 'HQ', geo: { lat: 1, lng: 2 } }, SYSTEM);
await ql.insert('os21644_product', { id: 'p1', name: 'Widget' }, SYSTEM);
diff --git a/packages/cli/src/commands/migrate/organization-ownership.declared-rule-mirrors.integration.test.ts b/packages/cli/src/commands/migrate/organization-ownership.declared-rule-mirrors.integration.test.ts
index e5d14c95370..c05cdb363d6 100644
--- a/packages/cli/src/commands/migrate/organization-ownership.declared-rule-mirrors.integration.test.ts
+++ b/packages/cli/src/commands/migrate/organization-ownership.declared-rule-mirrors.integration.test.ts
@@ -56,6 +56,7 @@ import { DECLARED_SHARING_RULE_SEEDER_RUNS } from '../../utils/organization-owne
// through dynamic `import()`s at MODULE LOAD, not inside a clocked case.
import '@objectstack/objectql';
import '@objectstack/metadata-protocol';
+import { relaxTenantColumnsOnFile } from '../../utils/pre-18-store.testkit.js';
type Command = (typeof import('./organization-ownership.js'))['default'];
let MigrateOrganizationOwnership: Command;
@@ -173,6 +174,10 @@ describe('os migrate organization-ownership on a database whose packages declare
await stack.stop();
}
seeded = sql('SELECT name, managed_by, customized, organization_id FROM sys_sharing_rule ORDER BY name');
+ // The rows planted below are an earlier release's, organization-less: its
+ // tenant columns were nullable, which a table this release creates never is
+ // (ADR-0131 D1). The file is relaxed to that release's shape first.
+ await relaxTenantColumnsOnFile(file);
// What a 17.x runtime left beside it.
const [{ id: organization }] = sql(`SELECT id FROM sys_organization WHERE slug = 'default'`) as Array<{ id: string }>;
diff --git a/packages/cli/src/commands/migrate/organization-ownership.no-organization.integration.test.ts b/packages/cli/src/commands/migrate/organization-ownership.no-organization.integration.test.ts
index 661896a2e69..251c20fcc14 100644
--- a/packages/cli/src/commands/migrate/organization-ownership.no-organization.integration.test.ts
+++ b/packages/cli/src/commands/migrate/organization-ownership.no-organization.integration.test.ts
@@ -60,6 +60,7 @@ import { isExitSignal } from '../../utils/format.js';
import '@objectstack/objectql';
import '@objectstack/metadata-protocol';
import '@objectstack/plugin-auth';
+import { relaxTenantColumnsOnFile } from '../../utils/pre-18-store.testkit.js';
type Command = (typeof import('./organization-ownership.js'))['default'];
let MigrateOrganizationOwnership: Command;
@@ -168,6 +169,10 @@ describe('os migrate organization-ownership on a single-posture database that ho
const stack = await bootStack(stackConfig(), { databaseFile: file });
await stack.stop();
+ // The rows planted below are an earlier release's, organization-less: its
+ // tenant columns were nullable, which a table this release creates never is
+ // (ADR-0131 D1). The file is relaxed to that release's shape first.
+ await relaxTenantColumnsOnFile(file);
expect(sql('SELECT COUNT(*) AS n FROM crm_event')).toEqual([{ n: SUBJECTS.length }]);
// What a 17.x install whose seed ran before any organization existed left.
diff --git a/packages/cli/src/commands/migrate/preview-read-only.integration.test.ts b/packages/cli/src/commands/migrate/preview-read-only.integration.test.ts
index e96e99c6b44..06cc13b813d 100644
--- a/packages/cli/src/commands/migrate/preview-read-only.integration.test.ts
+++ b/packages/cli/src/commands/migrate/preview-read-only.integration.test.ts
@@ -63,6 +63,7 @@ import { isExitSignal } from '../../utils/format.js';
// MODULE LOAD: the fixture's served boot reaches it through a dynamic
// `import()` inside a clocked hook (`scripts/check-test-source-alias.mjs`).
import '@objectstack/runtime';
+import { withDefaultOrganization } from '../../utils/default-organization-plugin.testkit.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const CLI_ROOT = resolve(HERE, '..', '..', '..');
@@ -249,7 +250,9 @@ async function createFixture(cell: DialectCell): Promise {
const served = await createStandaloneStack({ projectRoot: dir, databaseUrl });
const runtime = new Runtime({ cluster: false });
const kernel = runtime.getKernel();
- for (const plugin of served.plugins) await kernel.use(plugin as any);
+ // A real served boot composes authentication, whose Default Organization
+ // stamps the seed rows (ADR-0131 D9); the fresh tenant column is NOT NULL (D1).
+ for (const plugin of withDefaultOrganization(served.plugins)) await kernel.use(plugin as any);
for (const plugin of await buildDataMigrationPlugins({ automation: true, audit: true })) await kernel.use(plugin as any);
await runtime.start();
try {
diff --git a/packages/cli/src/commands/migrate/unmapped-columns.integration.test.ts b/packages/cli/src/commands/migrate/unmapped-columns.integration.test.ts
index e9d465a9cd8..daaeea2a09a 100644
--- a/packages/cli/src/commands/migrate/unmapped-columns.integration.test.ts
+++ b/packages/cli/src/commands/migrate/unmapped-columns.integration.test.ts
@@ -122,7 +122,9 @@ const kernel = runtime.getKernel();
for (const plugin of stack.plugins) await kernel.use(plugin);
await runtime.start();
const ql = kernel.getService('objectql');
-const SYSTEM = { context: { isSystem: true } };
+// The fixture's rows name an organization: a fresh tenant column is NOT NULL
+// (ADR-0131 D1), and this stack composes no authentication to derive one.
+const SYSTEM = { context: { isSystem: true, tenantId: 'org_fixture' } };
await ql.insert('um_contact', { id: 'c1', name: 'Ann', mailing_street: '1 Retired Way', mailing_city: 'Oldtown' }, SYSTEM);
await ql.insert('um_contact', { id: 'c2', name: 'Bob', mailing_street: '2 Retired Way', mailing_city: 'Newtown' }, SYSTEM);
await ql.insert('um_contact', { id: 'c3', name: 'Cy' }, SYSTEM);
diff --git a/packages/cli/src/commands/secret/orphans.driver-contract.test.ts b/packages/cli/src/commands/secret/orphans.driver-contract.test.ts
index c49a23405fe..57f5041d42c 100644
--- a/packages/cli/src/commands/secret/orphans.driver-contract.test.ts
+++ b/packages/cli/src/commands/secret/orphans.driver-contract.test.ts
@@ -151,7 +151,8 @@ describe('os secret orphans — the concrete driver behind both reads (#14843)',
}
await secretDriver.create('sys_secret', { ...SEEDED_SECRET });
- await settingDriver.create('sys_setting', { ...SEEDED_SETTING });
+ // The row names an organization: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ await settingDriver.create('sys_setting', { ...SEEDED_SETTING, organization_id: 'org_fixture' });
}, 180_000);
afterAll(async () => {
diff --git a/packages/cli/src/commands/secret/rewrap.driver-contract.test.ts b/packages/cli/src/commands/secret/rewrap.driver-contract.test.ts
index 17b4a1817ea..153389ab76b 100644
--- a/packages/cli/src/commands/secret/rewrap.driver-contract.test.ts
+++ b/packages/cli/src/commands/secret/rewrap.driver-contract.test.ts
@@ -117,7 +117,8 @@ describe('os secret rewrap — the concrete driver and the command, end to end (
for (const row of [SETTINGS_ROW, DATASOURCE_ROW, ORPHAN_ROW, PROBE_ROW]) {
await secretDriver.create('sys_secret', { ...row });
}
- await settingDriver.create('sys_setting', { namespace: 'smtp', key: 'password', value_enc: SETTINGS_ROW.id });
+ // The row names an organization: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ await settingDriver.create('sys_setting', { namespace: 'smtp', key: 'password', value_enc: SETTINGS_ROW.id, organization_id: 'org_fixture' });
writeFileSync(declaredFile, JSON.stringify([
{ name: 'warehouse', external: { credentialsRef: `sys_secret:${String(DATASOURCE_ROW.id)}` } },
]));
diff --git a/packages/cli/src/utils/default-organization-plugin.testkit.ts b/packages/cli/src/utils/default-organization-plugin.testkit.ts
new file mode 100644
index 00000000000..a5c49c3f376
--- /dev/null
+++ b/packages/cli/src/utils/default-organization-plugin.testkit.ts
@@ -0,0 +1,52 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * A fixture plugin that gives a served test boot what every real `single`
+ * install has before its inline seed runs: an organization object holding the
+ * Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. A real `os dev` / `os serve` composes authentication, which provisions
+ * the Default Organization, and the engine stamps every organization-less seed
+ * row with it (D9). `createStandaloneStack` alone composes no authentication,
+ * so a suite that serves an artifact's seeds through it would have them refused
+ * at the database. Spliced into the plugin list ahead of the app plugin (see
+ * {@link withDefaultOrganization}), it starts after the default datasource and
+ * before the app's `start()` seeds.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own; its row is seeded through the driver, as data at
+ * rest.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export function defaultOrganizationPlugin() {
+ return {
+ name: 'test.default-organization',
+ version: '0.0.0',
+ type: 'standard',
+ dependencies: ['com.objectstack.runtime.default-datasource'],
+ async init() {},
+ async start(ctx: { getService(name: string): any }) {
+ const ql = ctx.getService('objectql');
+ if (!ql.getSchema('sys_organization')) {
+ ql.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ 'test.default-organization',
+ );
+ }
+ await ql.syncObjectSchema('sys_organization');
+ const driver = ql.getDriverForObject('sys_organization');
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+ },
+ };
+}
+
+/** The served plugin list with {@link defaultOrganizationPlugin} ahead of every app plugin. */
+export function withDefaultOrganization(plugins: readonly T[]): T[] {
+ const out = [...plugins];
+ const datasource = out.findIndex((p: any) => p?.name === 'com.objectstack.runtime.default-datasource');
+ out.splice(datasource + 1, 0, defaultOrganizationPlugin() as T);
+ return out;
+}
diff --git a/packages/cli/src/utils/organization-ownership-not-null.test.ts b/packages/cli/src/utils/organization-ownership-not-null.test.ts
index 39770a71e11..14d0612313d 100644
--- a/packages/cli/src/utils/organization-ownership-not-null.test.ts
+++ b/packages/cli/src/utils/organization-ownership-not-null.test.ts
@@ -33,6 +33,16 @@ beforeEach(async () => {
await driver.initObjects([
{ name: 'crm_deal', fields: { name: { type: 'text' }, organization_id: { type: 'lookup', reference: 'sys_organization', required: false } } },
] as never);
+ // The ceremony tightens a table an earlier release built: its tenant column
+ // is nullable, which a table this release creates never is (ADR-0131 D1).
+ // Relaxed through the driver's own drift apply door, the shape of that store.
+ const { applied } = await driver.applyMigrationEntries([
+ {
+ kind: 'nullability_mismatch', remoteName: 'crm_deal', table: 'crm_deal', column: 'organization_id', category: 'needs_confirm',
+ op: { type: 'relax_not_null', table: 'crm_deal', column: 'organization_id' },
+ } as never,
+ ]);
+ if (applied.length !== 1) throw new Error('crm_deal was not returned to its pre-18 shape');
await knex('crm_deal').insert([
{ id: 'deal_1', name: 'Acme', organization_id: 'org_default' },
{ id: 'deal_2', name: 'Globex', organization_id: 'org_default' },
diff --git a/packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts b/packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts
index 1e8723c05ff..f0fadf2a0e7 100644
--- a/packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts
+++ b/packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts
@@ -73,6 +73,7 @@ import MigrateOrganizationOwnership from '../commands/migrate/organization-owner
import '@objectstack/runtime';
import '@objectstack/objectql';
import { PlatformObjectsPlugin } from '@objectstack/platform-objects/plugin';
+import { relaxTenantColumnsOnFile } from './pre-18-store.testkit.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const CLI_ROOT = resolve(HERE, '..', '..');
@@ -368,6 +369,9 @@ beforeAll(async () => {
templateDb = join(dir, 'template.db');
await bootV17(templateDb);
+ // A v17 database's tenant columns are nullable, which a table this release
+ // creates never is (ADR-0131 D1): the template is relaxed to that shape first.
+ await relaxTenantColumnsOnFile(templateDb);
const db = new Database(templateDb);
try {
for (const table of HOST_OBJECTS) {
diff --git a/packages/cli/src/utils/pre-18-store.testkit.ts b/packages/cli/src/utils/pre-18-store.testkit.ts
new file mode 100644
index 00000000000..4792f6f42a9
--- /dev/null
+++ b/packages/cli/src/utils/pre-18-store.testkit.ts
@@ -0,0 +1,38 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Return a SQLite file this release built to the shape an EARLIER release's
+ * database has: every tenant column (`organization_id`) nullable.
+ *
+ * ADR-0131 D1: since 18.x the SQL driver creates a table's tenant column NOT
+ * NULL. A v17 or 18.0 database still holds the nullable column, and the
+ * organization-ownership ceremony is what tightens it. A suite that boots this
+ * release over a file and then plants that earlier release's organization-less
+ * rows calls this between the two, so the file is the store the ceremony
+ * meets. Each column is relaxed through the driver's own drift apply door
+ * (`relax_not_null`, the SQLite table rebuild), never a hand `ALTER`.
+ */
+export async function relaxTenantColumnsOnFile(file: string, column = 'organization_id'): Promise {
+ // A dynamic import: this file lives under `src/`, where a static driver import
+ // is refused (`schema-migrate.lazy-driver-import.test.ts`).
+ const { SqlDriver } = await import('@objectstack/driver-sql');
+ const driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: file }, useNullAsDefault: true });
+ const relaxed: string[] = [];
+ try {
+ const knex = (driver as any).knex;
+ const tables = (await knex.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`)) as Array<{ name: string }>;
+ for (const { name } of tables) {
+ const info = (await knex.raw(`SELECT name, "notnull" AS nn FROM pragma_table_info(?)`, [name])) as Array<{ name: string; nn: number }>;
+ if (!info.some((c) => c.name === column && Number(c.nn) === 1)) continue;
+ const op = { type: 'relax_not_null', table: name, column };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: name, table: name, column, category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumnsOnFile: ${name}.${column} was not relaxed`);
+ relaxed.push(name);
+ }
+ } finally {
+ await driver.disconnect();
+ }
+ return relaxed;
+}
diff --git a/packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts b/packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts
index 917c47e9ff7..a8bc2c02b4b 100644
--- a/packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts
+++ b/packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts
@@ -87,6 +87,7 @@ import { PlatformObjectsPlugin } from '@objectstack/platform-objects/plugin';
import { SettingsServicePlugin } from '@objectstack/service-settings';
import '@objectstack/service-storage';
import '@objectstack/plugin-audit';
+import { withDefaultOrganization } from './default-organization-plugin.testkit.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = resolve(HERE, '..');
@@ -414,7 +415,9 @@ async function bootServedStack(dbFile: string): Promise {
const stack = await createStandaloneStack({ projectRoot: dir, databaseUrl: `file:${dbFile}` });
const runtime = new Runtime({ cluster: false });
const kernel = runtime.getKernel();
- for (const plugin of stack.plugins) await kernel.use(plugin as any);
+ // A real served boot composes authentication, whose Default Organization
+ // stamps the seed rows (ADR-0131 D9); the fresh tenant column is NOT NULL (D1).
+ for (const plugin of withDefaultOrganization(stack.plugins)) await kernel.use(plugin as any);
for (const plugin of await buildDataMigrationPlugins({ storage: true, automation: true, audit: true })) {
await kernel.use(plugin as any);
}
@@ -621,7 +624,9 @@ describe('[#21391] a one-shot boot arms no lifecycle sweep', () => {
const stack = await createStandaloneStack({ projectRoot: dir, databaseUrl: `file:${c.dbFile}`, skipSeedData: true });
const runtime = new Runtime({ cluster: false });
const kernel = runtime.getKernel();
- for (const plugin of stack.plugins) await kernel.use(plugin as any);
+ // A real served boot composes authentication, whose Default Organization
+ // stamps the seed rows (ADR-0131 D9); the fresh tenant column is NOT NULL (D1).
+ for (const plugin of withDefaultOrganization(stack.plugins)) await kernel.use(plugin as any);
await runtime.start();
try {
expect(armed(kernel)).toBe(true);
diff --git a/packages/client/src/client.batch-transaction.test.ts b/packages/client/src/client.batch-transaction.test.ts
index e72ccbb8cd7..c131568d34f 100644
--- a/packages/client/src/client.batch-transaction.test.ts
+++ b/packages/client/src/client.batch-transaction.test.ts
@@ -35,6 +35,7 @@ import { createRestApiPlugin } from '@objectstack/runtime';
import { ObjectStackClient } from './index';
import type { IHttpServer } from '@objectstack/spec/contracts';
import type { ServiceObject } from '@objectstack/spec/data';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* ⚠️ [#18070] The authz objects every authenticated request in this file
@@ -210,6 +211,9 @@ describe('data.batchTransaction (live Hono, #1604)', () => {
ql.registerObject(o.def, o.owner);
await ql.syncObjectSchema(o.def.name);
}
+ // A real `single` install's Default Organization: the writes below derive
+ // it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const httpServer = kernel.getService('http.server');
baseUrl = `http://localhost:${httpServer.getPort!()}`;
diff --git a/packages/client/src/client.hono.test.ts b/packages/client/src/client.hono.test.ts
index 48302233def..29e58ddd028 100644
--- a/packages/client/src/client.hono.test.ts
+++ b/packages/client/src/client.hono.test.ts
@@ -7,6 +7,7 @@ import { createRestApiPlugin } from '@objectstack/runtime';
import { ObjectStackClient } from './index';
import type { IHttpServer } from '@objectstack/spec/contracts';
import type { ServiceObject } from '@objectstack/spec/data';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* ⚠️ [#18070] The authz objects every authenticated request in this file
@@ -233,6 +234,9 @@ describe('ObjectStackClient (with Hono Server)', () => {
ql.registerObject(o.def, o.owner);
await ql.syncObjectSchema(o.def.name);
}
+ // A real `single` install's Default Organization: the writes below derive
+ // it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
// 5. Get Port from Service
const httpServer = kernel.getService('http.server');
diff --git a/packages/client/src/default-organization.testkit.ts b/packages/client/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..ad601fd4969
--- /dev/null
+++ b/packages/client/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/client',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/drivers/driver-sql/src/adr0120-three-posture-conformance.test.ts b/packages/drivers/driver-sql/src/adr0120-three-posture-conformance.test.ts
index 0fa6f81ace3..152ad99f753 100644
--- a/packages/drivers/driver-sql/src/adr0120-three-posture-conformance.test.ts
+++ b/packages/drivers/driver-sql/src/adr0120-three-posture-conformance.test.ts
@@ -5,6 +5,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { SqlDriver, classifyIndexKeyPart, parseIndexDdl } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* ADR-0120 §Acceptance tests — posture portability (S13/S14).
@@ -284,6 +285,9 @@ describe('ADR-0120 — one app package, three tenancy postures (S13/S14 acceptan
// (SQL UNIQUE is NULL-distinct), so every field-level unique was a
// silent no-op there. It must now be enforced under `single` exactly as
// under `isolated` — the posture is not what makes a constraint real.
+ // The NULL bucket exists only on a database built before 18.x (ADR-0131
+ // D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'crm_contact');
await driver.create('crm_contact', { email: 'platform@b.com' });
await expect(driver.create('crm_contact', { email: 'platform@b.com' }))
.rejects.toThrow(REJECTED);
@@ -305,6 +309,9 @@ describe('ADR-0120 — one app package, three tenancy postures (S13/S14 acceptan
await expect(driver.create('crm_case', { organization_id: 'org_a', department: 'ops', code: 'C1' }))
.rejects.toThrow(REJECTED);
// …and the NULL bucket is constrained too (D3 reaches declared indexes).
+ // The NULL bucket exists only on a database built before 18.x (ADR-0131
+ // D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'crm_case');
await driver.create('crm_case', { department: 'ops', code: 'C9' });
await expect(driver.create('crm_case', { department: 'ops', code: 'C9' }))
.rejects.toThrow(REJECTED);
@@ -314,6 +321,9 @@ describe('ADR-0120 — one app package, three tenancy postures (S13/S14 acceptan
// The nine engine idempotency keys live or die on this row: they are
// written by sudo with a NULL organization, so a composite would have
// enforced nothing on exactly the rows that matter.
+ // The NULL bucket exists only on a database built before 18.x (ADR-0131
+ // D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'crm_delivery');
await driver.create('crm_delivery', { source: 'smtp', dedup_key: 'K1' });
await expect(driver.create('crm_delivery', { source: 'smtp', dedup_key: 'K1' }))
.rejects.toThrow(REJECTED);
@@ -332,6 +342,9 @@ describe('ADR-0120 — one app package, three tenancy postures (S13/S14 acceptan
.resolves.toBeTruthy();
// The un-closed NULL hole, pinned as the honest status quo (D5c nudges
// toward the respelling that closes it; nothing forces it).
+ // The NULL bucket exists only on a database built before 18.x (ADR-0131
+ // D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'crm_team');
await driver.create('crm_team', { name: 'Platform' });
await expect(driver.create('crm_team', { name: 'Platform' })).resolves.toBeTruthy();
});
diff --git a/packages/drivers/driver-sql/src/pre-18-tenant-column.testkit.ts b/packages/drivers/driver-sql/src/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..dbfde756f2b
--- /dev/null
+++ b/packages/drivers/driver-sql/src/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,43 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { SqlDriver } from './sql-driver.js';
+
+/**
+ * Return a table the driver just created to the shape a database built BEFORE
+ * 18.x has: its tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x the driver creates a table's tenant column NOT NULL
+ * (`createColumn`'s `tenantColumn`). A database built by an earlier release
+ * still holds the nullable column — and, where the organization-ownership
+ * ceremony reported a table rather than clearing it (D10 fate 4), real rows
+ * with no organization in it. A suite whose subject is how the driver treats
+ * THOSE rows (the NULL-safe unique key part, the legacy composites, the
+ * respelling migrations) builds its table with `initObjects` and then calls
+ * this, so it keeps measuring the shape it was written for.
+ *
+ * Done through the driver's own drift apply door (`relax_not_null`: the SQLite
+ * table rebuild, or `ALTER COLUMN … DROP NOT NULL`), never a hand `ALTER`, so
+ * every index the table carries survives exactly as the driver built it.
+ */
+export async function asPre18TenantColumn(
+ driver: SqlDriver,
+ table: string,
+ column: string = 'organization_id',
+): Promise {
+ const entry = {
+ kind: 'nullability_mismatch',
+ remoteName: table,
+ table,
+ column,
+ expected: 'NULL',
+ actual: 'NOT NULL',
+ severity: 'warning',
+ category: 'needs_confirm',
+ op: { type: 'relax_not_null', table, column },
+ message: `${table}.${column}: the pre-18 nullable tenant column (test fixture).`,
+ } as unknown as Parameters[0][number];
+ const { applied } = await driver.applyMigrationEntries([entry]);
+ if (applied.length !== 1) {
+ throw new Error(`asPre18TenantColumn: the drift apply door did not relax ${table}.${column}`);
+ }
+}
diff --git a/packages/drivers/driver-sql/src/sql-driver-21227-create-answers-stored-row.test.ts b/packages/drivers/driver-sql/src/sql-driver-21227-create-answers-stored-row.test.ts
index ccf0a4050a9..afe6f76bb88 100644
--- a/packages/drivers/driver-sql/src/sql-driver-21227-create-answers-stored-row.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-21227-create-answers-stored-row.test.ts
@@ -47,10 +47,18 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { SqlDriver, type SqlDriverConfig } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
import { DIALECT_CELLS, declareDialectCell, type DialectCell } from './live-dialect-matrix.testkit.js';
const TABLE = 'os21227_stored_row';
+/**
+ * The organization every row in this file names: a fresh table's tenant column
+ * is NOT NULL (ADR-0131 D1), and this file's subject is the stored row, not
+ * tenancy — the two tenanted cases below say their own organization.
+ */
+const ORG = 'org_a';
+
const OBJECT = {
name: TABLE,
fields: {
@@ -117,7 +125,7 @@ function suite(face: Face) {
});
it('create with a generated id answers the stored row', async () => {
- const answer = await driver.create(TABLE, { title: 'generated', qty: 1, meta: { k: [1, 2] } });
+ const answer = await driver.create(TABLE, { organization_id: ORG, title: 'generated', qty: 1, meta: { k: [1, 2] } });
expect(typeof answer.id).toBe('string');
expect(answer.id).not.toBe('');
@@ -127,7 +135,7 @@ function suite(face: Face) {
});
it('create with a supplied id answers that row, in one statement or two by path', async () => {
- const answer = await driver.create(TABLE, { id: 'given-1', title: 'supplied', qty: 2 });
+ const answer = await driver.create(TABLE, { id: 'given-1', organization_id: ORG, title: 'supplied', qty: 2 });
expect(answer).toEqual(await stored('given-1'));
expect(answer.title).toBe('supplied');
@@ -139,9 +147,9 @@ function suite(face: Face) {
it('bulkCreate answers one stored row per written row, in the written order', async () => {
const answer = await driver.bulkCreate(TABLE, [
- { title: 'b1' },
- { id: 'given-b2', title: 'b2', qty: 2 },
- { title: 'b3', qty: 3 },
+ { organization_id: ORG, title: 'b1' },
+ { id: 'given-b2', organization_id: ORG, title: 'b2', qty: 2 },
+ { organization_id: ORG, title: 'b3', qty: 3 },
]);
expect(answer).toHaveLength(3);
@@ -156,7 +164,7 @@ function suite(face: Face) {
});
it('bulkCreate of one row answers one stored row', async () => {
- const answer = await driver.bulkCreate(TABLE, [{ id: 'only', title: 'single' }]);
+ const answer = await driver.bulkCreate(TABLE, [{ id: 'only', organization_id: ORG, title: 'single' }]);
expect(answer).toHaveLength(1);
expect(answer[0]).toEqual(await stored('only'));
@@ -200,6 +208,9 @@ function suite(face: Face) {
// 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.
+ // The unstamped row lands only on a table built before 18.x (ADR-0131 D1
+ // makes a fresh tenant column NOT NULL): this table is that store's.
+ await asPre18TenantColumn(driver, TABLE);
const batch = await driver.bulkCreate(TABLE, [
{ id: 't-m1', title: 'named', organization_id: 'org_b' },
{ id: 't-m2', title: 'unstamped' },
@@ -216,8 +227,11 @@ function suite(face: Face) {
let single!: Record;
let batch!: Record[];
try {
- single = await driver.create(TABLE, { id: 'tx-1', title: 'in tx' }, { transaction: trx });
- batch = await driver.bulkCreate(TABLE, [{ id: 'tx-2', title: 'in tx' }, { id: 'tx-3', title: 'in tx' }], {
+ single = await driver.create(TABLE, { id: 'tx-1', organization_id: ORG, title: 'in tx' }, { transaction: trx });
+ batch = await driver.bulkCreate(TABLE, [
+ { id: 'tx-2', organization_id: ORG, title: 'in tx' },
+ { id: 'tx-3', organization_id: ORG, title: 'in tx' },
+ ], {
transaction: trx,
});
} finally {
@@ -236,7 +250,7 @@ function suite(face: Face) {
);
statements = [];
- const err = await driver.create(TABLE, { id: 'gone', title: 'vanishes' }).then(
+ const err = await driver.create(TABLE, { id: 'gone', organization_id: ORG, title: 'vanishes' }).then(
() => null,
(e: unknown) => e as { code?: string; status?: number },
);
diff --git a/packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts b/packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts
index fbd8a911723..449ce0fcc1a 100644
--- a/packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
import { SqlDriver, classifyIndexKeyPart, parseIndexDdl, legacyUniqueReplacements } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* #8577 — the FIFTH act of the #8323 class: two more tenant-scoped objects
@@ -375,6 +376,9 @@ describe('#8577 — two more tenant-scoped declared unique indexes become per-or
// `suggested-audience-bindings-install-path.test.ts` §3.
const d = makeDriver();
await d.initObjects(app(sub, 'fixed') as any);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(d, sub.table);
expect((await createAsApi(d, sub.table, row(sub, 'seed1', undefined, sub.key))).status).toBe(201);
expect(await createAsApi(d, sub.table, row(sub, 'seed2', undefined, sub.key))).toMatchObject(
@@ -427,6 +431,9 @@ describe('#8577 — two more tenant-scoped declared unique indexes become per-or
*/
const seedDeployed = async (d: SqlDriver) => {
await d.initObjects(app(sub, 'pre') as any);
+ // Every deployment in the field was built before 18.x: its tenant
+ // column is nullable, and `r3` below is a row with no organization.
+ await asPre18TenantColumn(d, sub.table);
await d.create(sub.table, row(sub, 'r1', 'org_jia', sub.key) as any);
await d.create(sub.table, row(sub, 'r2', 'org_jia', sub.control) as any);
await d.create(sub.table, row(sub, 'r3', undefined, sub.platformKey) as any);
diff --git a/packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts b/packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts
index f7f472036af..1b5f6878fa5 100644
--- a/packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts
@@ -2,6 +2,7 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
import {
SqlDriver,
classifyIndexKeyPart,
@@ -381,6 +382,9 @@ describe('#8323 — declared unique indexes on the platform’s tenant-scoped ob
// so every platform-seeded capability could be duplicated at will.
const d = makeDriver();
await d.initObjects(FIXED_APP as any);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(d, 'sys_capability');
expect((await createAsApi(d, 'sys_capability', { id: 'p1', name: 'manage_metadata' })).status).toBe(201);
const duplicateSeed = await createAsApi(d, 'sys_capability', { id: 'p2', name: 'manage_metadata' });
diff --git a/packages/drivers/driver-sql/src/sql-driver-legacy-tenant-composite-null-distinct.test.ts b/packages/drivers/driver-sql/src/sql-driver-legacy-tenant-composite-null-distinct.test.ts
index e03cdeedbd2..b609f3f6ae1 100644
--- a/packages/drivers/driver-sql/src/sql-driver-legacy-tenant-composite-null-distinct.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-legacy-tenant-composite-null-distinct.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
import { SqlDriver, classifyIndexKeyPart, parseIndexDdl } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* #23603 row 7 — the OLD hand-written table composite
@@ -83,6 +84,9 @@ describe('row 7 — the old table composite is NULL-distinct, field-level unique
});
it('refuses a duplicate on an UNTENANTED install too', async () => {
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'kase');
await driver.create('kase', { case_number: 'CASE-2' });
const err = await refusal(driver.create('kase', { case_number: 'CASE-2' }));
expect(isUniqueViolationError(err)).toBe(true);
@@ -116,6 +120,9 @@ describe('row 7 — the old table composite is NULL-distinct, field-level unique
});
it('ACCEPTS a duplicate untenanted number — SQL UNIQUE is NULL-distinct', async () => {
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'kase');
const first = await driver.create('kase', { case_number: 'CASE-11' });
const second = await driver.create('kase', { case_number: 'CASE-11' });
expect(second.id).not.toBe(first.id);
diff --git a/packages/drivers/driver-sql/src/sql-driver-organizationless-counter-refusal.test.ts b/packages/drivers/driver-sql/src/sql-driver-organizationless-counter-refusal.test.ts
index c68724e0e43..51a71331b35 100644
--- a/packages/drivers/driver-sql/src/sql-driver-organizationless-counter-refusal.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-organizationless-counter-refusal.test.ts
@@ -18,6 +18,7 @@
import { afterEach, describe, expect, it } from 'vitest';
import { SqlDriver } from './index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
import {
ORGANIZATIONLESS_COUNTER_REFUSAL_CODE,
ORGANIZATIONLESS_COUNTER_REFUSAL_STATUS,
@@ -121,6 +122,11 @@ describe('[ADR-0131 D9, 8d] the counter refuses an organization-less row on a te
it('a composition with no organization object (a lean embedding) keeps `__global__` on a tenant table', async () => {
const driver = await bootDriver([CASE_OBJECT]);
+ // The organization-less row itself lands only where the tenant column is
+ // nullable — a table built before 18.x (ADR-0131 D1 makes a fresh one NOT
+ // NULL, so a lean embedding's fresh table refuses it at the database). The
+ // counter's bucket is this case's subject, so the table is that store's.
+ await asPre18TenantColumn(driver, 'crm_case');
const first = await driver.create('crm_case', { subject: 'lean embedding' });
expect(first.case_number).toBe('CASE-0001');
diff --git a/packages/drivers/driver-sql/src/sql-driver-sys-position-organization-unique.test.ts b/packages/drivers/driver-sql/src/sql-driver-sys-position-organization-unique.test.ts
index ea6087bdb23..cdb04dc46ff 100644
--- a/packages/drivers/driver-sql/src/sql-driver-sys-position-organization-unique.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-sys-position-organization-unique.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
import { SqlDriver, classifyIndexKeyPart, parseIndexDdl, legacyUniqueReplacements } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* #8468 — `sys_position.name`, the THIRD instance of the #8323 class.
@@ -316,6 +317,9 @@ describe('#8468 — sys_position.name is unique per organization, not per instal
// on it.
const d = makeDriver();
await d.initObjects(FIXED_APP as any);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(d, 'sys_position');
expect(
(await createAsApi(d, 'sys_position', {
@@ -349,6 +353,9 @@ describe('#8468 — sys_position.name is unique per organization, not per instal
*/
const seedDeployed = async (d: SqlDriver) => {
await d.initObjects(PRE_FIX_APP as any);
+ // Every deployment in the field was built before 18.x: its tenant column
+ // is nullable, and it holds rows with no organization.
+ await asPre18TenantColumn(d, 'sys_position');
await d.create('sys_position', {
id: 'pos1', organization_id: 'org_jia', name: 'sales_manager', label: 'Sales Manager', managed_by: 'admin',
} as any);
diff --git a/packages/drivers/driver-sql/src/sql-driver-sys-setting-organization-unique.test.ts b/packages/drivers/driver-sql/src/sql-driver-sys-setting-organization-unique.test.ts
index ba2ebd0925f..742e1bd11d5 100644
--- a/packages/drivers/driver-sql/src/sql-driver-sys-setting-organization-unique.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-sys-setting-organization-unique.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
import { SqlDriver, classifyIndexKeyPart, parseIndexDdl, legacyUniqueReplacements } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* #8555 — `sys_setting`, the SIXTH instance of the #8323 class, and the first
@@ -315,6 +316,9 @@ describe('#8555 — sys_setting: the declared unique index becomes per-organizat
// key at its own layer.
const d = makeDriver();
await d.initObjects(app('fixed') as any);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(d, TABLE);
const platform = { scope: 'global', namespace: 'branding', key: 'logo_url' };
expect((await createAsApi(d, row('seed1', undefined, platform))).status).toBe(201);
@@ -358,6 +362,9 @@ describe('#8555 — sys_setting: the declared unique index becomes per-organizat
*/
const seedDeployed = async (d: SqlDriver) => {
await d.initObjects(app('pre') as any);
+ // Every deployment in the field was built before 18.x: its tenant column
+ // is nullable, and it holds rows with no organization.
+ await asPre18TenantColumn(d, TABLE);
await d.create(TABLE, row('r1', 'org_jia', KEY) as any);
await d.create(TABLE, row('r2', 'org_jia', CONTROL) as any);
await d.create(TABLE, row('r3', undefined, { scope: 'global', key: 'platform_default' }) as any);
@@ -586,6 +593,9 @@ describe('#8555 — sys_setting: the declared unique index becomes per-organizat
it('AFTER the migration: two platform defaults on the `scope=global` layer are refused', async () => {
const d = makeDriver();
await d.initObjects(app('fixed') as any);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(d, TABLE);
await runIdentityMigration(d);
expect((await createAsApi(d, row('a', undefined, globalRow))).status).toBe(201);
diff --git a/packages/drivers/driver-sql/src/sql-driver-tenant-column-not-null.test.ts b/packages/drivers/driver-sql/src/sql-driver-tenant-column-not-null.test.ts
new file mode 100644
index 00000000000..d7edf442ea1
--- /dev/null
+++ b/packages/drivers/driver-sql/src/sql-driver-tenant-column-not-null.test.ts
@@ -0,0 +1,158 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * ADR-0131 D1 at the DDL: a table the driver CREATES carries its tenant column
+ * NOT NULL, with no declaration saying so (execution record 6285122667 on the
+ * v18 step 7 card; ruling record 6269955919, ② A: no metadata change).
+ *
+ * ## What is pinned
+ *
+ * 1. A fresh managed table refuses a NULL tenant value AT THE DATABASE — a raw
+ * insert through the driver's own Knex, so no engine or driver stamping can
+ * be what refuses it — on every provisioned dialect cell.
+ * 2. The same for a rotation shard the driver creates (SQLite: the one dialect
+ * `supportsRotation` answers for), asked of the base's declaration.
+ * 3. The differ reports no drift on either: the declaration says nullable
+ * (`required: false`, no `storage`) and the column says NOT NULL, and that
+ * difference is the ceremony's (`ceremonyOwnedNullability`), never drift.
+ * 4. The boundaries, each the other direction of the same predicate:
+ * - an ordinary field next to it stays nullable (the tenant column is the
+ * only column made NOT NULL without a declaration);
+ * - an object opted out of tenancy (`tenancy.enabled: false`) has no tenant
+ * column, so a field it happens to name `organization_id` stays nullable;
+ * - a tenant column ADDED to a table that already exists stays nullable:
+ * a populated table is the organization-ownership ceremony's to tighten,
+ * and so is a shard that already existed.
+ */
+
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { SqlDriver } from './sql-driver.js';
+import { DIALECT_CELLS, declareDialectCell, type DialectCell } from './live-dialect-matrix.testkit.js';
+
+const MATRIX = 'tenant column NOT NULL at CREATE TABLE (ADR-0131 D1)';
+
+/** The kernel's injected tenant field, as the driver receives it: nullable by declaration. */
+const ORG = { type: 'lookup', reference: 'sys_organization', required: false };
+
+const OWNED = 'tnn_owned';
+const GLOBAL = 'tnn_global';
+const GROWN = 'tnn_grown';
+const TABLES = [OWNED, GLOBAL, GROWN];
+
+/** Every NOT NULL refusal spelling: SQLite, PostgreSQL, MySQL. */
+const NOT_NULL_REFUSAL = /NOT NULL constraint failed|violates not-null constraint|cannot be null/i;
+
+const knexOf = (driver: SqlDriver): any => (driver as any).knex;
+
+async function nullable(driver: SqlDriver, table: string, column: string): Promise {
+ const info = await knexOf(driver)(table).columnInfo();
+ return info[column].nullable;
+}
+
+function declareTenantColumnSuite(cell: DialectCell): void {
+ describe(`${MATRIX} on ${cell.label}`, () => {
+ let driver: SqlDriver;
+
+ beforeEach(async () => {
+ driver = new SqlDriver(cell.config());
+ for (const t of TABLES) await knexOf(driver).schema.dropTableIfExists(t);
+ });
+
+ afterEach(async () => {
+ for (const t of TABLES) await knexOf(driver).schema.dropTableIfExists(t).catch(() => {});
+ await driver.disconnect();
+ });
+
+ it('a fresh table refuses a NULL tenant value at the database, and reports no drift', async () => {
+ await driver.initObjects([{ name: OWNED, fields: { organization_id: ORG, note: { type: 'text' } } }]);
+
+ expect(await nullable(driver, OWNED, 'organization_id')).toBe(false);
+ expect(await nullable(driver, OWNED, 'note')).toBe(true);
+
+ await expect(
+ knexOf(driver)(OWNED).insert({ id: 'r1', organization_id: null, note: 'no owner' }),
+ ).rejects.toThrow(NOT_NULL_REFUSAL);
+ await knexOf(driver)(OWNED).insert({ id: 'r2', organization_id: 'org_a', note: null });
+ expect((await knexOf(driver)(OWNED).select('id')).map((r: { id: string }) => r.id)).toEqual(['r2']);
+
+ const drift = (await driver.detectManagedDrift()).filter((d) => d.table === OWNED);
+ expect(drift).toEqual([]);
+ });
+
+ it('a tenancy-opted-out object has no tenant column: its organization_id stays nullable', async () => {
+ await driver.initObjects([{ name: GLOBAL, tenancy: { enabled: false }, fields: { organization_id: ORG } }]);
+
+ expect(await nullable(driver, GLOBAL, 'organization_id')).toBe(true);
+ expect((await driver.detectManagedDrift()).filter((d) => d.table === GLOBAL)).toEqual([]);
+ });
+
+ it('a tenant column ADDED to an existing table stays nullable: that table is the ceremony\'s', async () => {
+ await driver.initObjects([{ name: GROWN, fields: { note: { type: 'text' } } }]);
+ await knexOf(driver)(GROWN).insert({ id: 'before', note: 'pre-existing row' });
+
+ await driver.initObjects([{ name: GROWN, fields: { note: { type: 'text' }, organization_id: ORG } }]);
+
+ expect(await nullable(driver, GROWN, 'organization_id')).toBe(true);
+ expect((await driver.detectManagedDrift()).filter((d) => d.table === GROWN)).toEqual([]);
+ });
+ });
+}
+
+for (const cell of DIALECT_CELLS) {
+ declareDialectCell(cell, MATRIX, declareTenantColumnSuite);
+}
+
+describe(`${MATRIX}: rotation shards (SQLite)`, () => {
+ const T0 = Date.parse('2036-08-01T12:00:00.000Z');
+ const DAY = 86_400_000;
+ const ROTATED = {
+ name: 'tnn_activity',
+ fields: { organization_id: ORG, verb: { type: 'text' } },
+ lifecycle: { class: 'telemetry', storage: { strategy: 'rotation' as const, shards: 3, unit: 'day' as const } },
+ };
+
+ let driver: SqlDriver;
+ beforeEach(() => {
+ driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
+ });
+ afterEach(async () => {
+ await driver.disconnect();
+ });
+
+ it('a newly rotated shard refuses a NULL tenant value at the database, and reports no drift', async () => {
+ await driver.initObjects([ROTATED as any]);
+ // The sweep-time entry point, carrying no `tenancy` block: today's shard,
+ // then the next day's.
+ await driver.rotateShards(ROTATED as any, T0);
+ const { current, shards } = await driver.rotateShards(ROTATED as any, T0 + DAY);
+ expect(shards.length).toBeGreaterThan(1);
+
+ for (const shard of shards) {
+ expect(await nullable(driver, shard, 'organization_id')).toBe(false);
+ expect(await nullable(driver, shard, 'verb')).toBe(true);
+ }
+ await expect(knexOf(driver)(current).insert({ id: 'a1', organization_id: null, verb: 'x' })).rejects.toThrow(
+ NOT_NULL_REFUSAL,
+ );
+
+ // The shards are not managed tables of their own (the base name is the
+ // read view), so each is diffed explicitly, against the base's declaration.
+ const drift = await driver.detectManagedDrift(shards.map((name) => ({ name, fields: ROTATED.fields })));
+ expect(drift).toEqual([]);
+ });
+
+ it('a shard that already existed keeps the nullability it has: the ceremony reports it, nothing rebuilds it', async () => {
+ const legacy = `${ROTATED.name}__r20360801`;
+ await knexOf(driver).schema.createTable(legacy, (t: any) => {
+ t.string('id').primary();
+ t.string('created_at');
+ t.string('updated_at');
+ t.string('organization_id');
+ t.text('verb');
+ });
+
+ await driver.rotateShards(ROTATED as any, T0);
+
+ expect(await nullable(driver, legacy, 'organization_id')).toBe(true);
+ });
+});
diff --git a/packages/drivers/driver-sql/src/sql-driver-tenant-scope-read-doors.test.ts b/packages/drivers/driver-sql/src/sql-driver-tenant-scope-read-doors.test.ts
index 1b57c25168c..669d5c726fb 100644
--- a/packages/drivers/driver-sql/src/sql-driver-tenant-scope-read-doors.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-tenant-scope-read-doors.test.ts
@@ -70,6 +70,7 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { SqlDriver } from './index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
const TABLE = 'os6792_account';
const UNSCOPED_TABLE = 'os6792_ledger';
@@ -139,7 +140,9 @@ describe('driver-sql — every read door routes through applyTenantScope (#6792)
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);
// An organization-less row: no tenant's, so no scoped read reaches it
- // (ADR-0131 D1); only an unscoped one does.
+ // (ADR-0131 D1); only an unscoped one does. Such a row exists only on a
+ // database built before 18.x, whose tenant column is nullable.
+ await asPre18TenantColumn(driver, TABLE);
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);
diff --git a/packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts b/packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts
index 7dc50a5498c..049f31eb785 100644
--- a/packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, beforeEach, afterEach, assert } from 'vitest';
import { ObjectSchema } from '@objectstack/spec/data';
import { SqlDriver } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* Tenant-scope (organization_id) isolation tests.
@@ -140,6 +141,9 @@ describe('SqlDriver tenant scope (organization_id)', () => {
// therefore no tenant's: only an unscoped read reaches it.
describe('NULL tenant column = no tenant\'s row (ADR-0131 D1)', () => {
beforeEach(async () => {
+ // An organization-less row exists only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'account');
await driver.create('account', { id: 'g1', name: 'Orphan', tier: 'none' });
});
@@ -478,6 +482,8 @@ describe('SqlDriver tenant scope (organization_id)', () => {
});
it('the union path does not reach an organization-less row either (ADR-0131 D1)', async () => {
+ // A pre-18 database's row with no organization.
+ await asPre18TenantColumn(driver, 'account');
await driver.create('account', { id: 'g1', name: 'ORPHAN' });
const rows = await driver.find(
'account',
diff --git a/packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts b/packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts
index 3fb9824742f..6242d96eff2 100644
--- a/packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-tenant-scoped-declared-unique.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
import { SqlDriver, classifyIndexKeyPart, parseIndexDdl, legacyUniqueReplacements } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* #8554 — the FOURTH act of the #8323 class: five more tenant-scoped objects
@@ -416,6 +417,9 @@ describe('#8554 — five tenant-scoped declared unique indexes become per-organi
// is what prevents that, and seed upsert-by-name relies on it.
const d = makeDriver();
await d.initObjects(app(sub, 'fixed') as any);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(d, sub.table);
expect((await createAsApi(d, sub.table, row(sub, 'seed1', undefined, sub.key))).status).toBe(201);
expect(await createAsApi(d, sub.table, row(sub, 'seed2', undefined, sub.key))).toMatchObject(
@@ -455,6 +459,9 @@ describe('#8554 — five tenant-scoped declared unique indexes become per-organi
*/
const seedDeployed = async (d: SqlDriver) => {
await d.initObjects(app(sub, 'pre') as any);
+ // Every deployment in the field was built before 18.x: its tenant
+ // column is nullable, and `r3` below is a row with no organization.
+ await asPre18TenantColumn(d, sub.table);
await d.create(sub.table, row(sub, 'r1', 'org_jia', sub.key) as any);
await d.create(sub.table, row(sub, 'r2', 'org_jia', sub.control) as any);
await d.create(sub.table, row(sub, 'r3', undefined, sub.platformKey) as any);
diff --git a/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts b/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts
index 503d7fcf3a2..e43351665a5 100644
--- a/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts
@@ -3,6 +3,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { applyConversionsToStoredItem } from '@objectstack/spec';
import { SqlDriver, classifyIndexKeyPart, expectedIndexes, parseIndexDdl } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* Unique-scope materialization: tenancy composites (#3696) + the explicit
@@ -575,6 +576,9 @@ describe('SqlDriver unique × tenancy (#3696)', () => {
},
]);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'account');
const first = await driver.create('account', { email: 'dup@example.com' });
expect(first.organization_id ?? null).toBeNull();
await expect(driver.create('account', { email: 'dup@example.com' })).rejects.toThrow(
@@ -594,6 +598,9 @@ describe('SqlDriver unique × tenancy (#3696)', () => {
]);
// Platform (NULL-organization) row takes the value…
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'template');
await driver.create('template', { key: 'welcome' });
// …an organization may still hold the same value (per-org scope)…
const org = await driver.create('template', { organization_id: 'org_a', key: 'welcome' });
@@ -658,6 +665,9 @@ describe('SqlDriver unique × tenancy (#3696)', () => {
await expect(
driver.create('case_record', { organization_id: 'org_a', department: 'sales', code: 'C-1' }),
).rejects.toThrow(/UNIQUE constraint failed|duplicate key value/);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'case_record');
await driver.create('case_record', { department: 'sales', code: 'C-2' });
await expect(
driver.create('case_record', { department: 'sales', code: 'C-2' }),
@@ -696,6 +706,9 @@ describe('SqlDriver unique × tenancy (#3696)', () => {
'name',
]);
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, 'team');
await driver.create('team', { name: 'Core' });
await expect(driver.create('team', { name: 'Core' })).rejects.toThrow(
/UNIQUE constraint failed|duplicate key value/,
diff --git a/packages/drivers/driver-sql/src/sql-driver-upsert-cross-organization-refusal.test.ts b/packages/drivers/driver-sql/src/sql-driver-upsert-cross-organization-refusal.test.ts
index c32b1ff2e2c..dba201ec8a8 100644
--- a/packages/drivers/driver-sql/src/sql-driver-upsert-cross-organization-refusal.test.ts
+++ b/packages/drivers/driver-sql/src/sql-driver-upsert-cross-organization-refusal.test.ts
@@ -49,6 +49,7 @@
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
import { isUniqueViolationError } from '@objectstack/types';
import { SqlDriver } from '../src/index.js';
+import { asPre18TenantColumn } from './pre-18-tenant-column.testkit.js';
import { DIALECT_CELLS, declareDialectCell, type DialectCell } from './live-dialect-matrix.testkit.js';
interface WireBearingError extends Error {
@@ -261,6 +262,9 @@ function declareCrossOrganizationRefusal(cell: DialectCell): void {
* organization.
*/
it('refuses a conflict on a row that carries no organization, and leaves it untouched', async () => {
+ // Rows with no organization exist only on a database built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL).
+ await asPre18TenantColumn(driver, ACCOUNT.name);
await knexInstance(ACCOUNT.name).insert({ id: 'os21185_platform_row', email: 'platform@os21185.test', title: 'platform' });
const before = await snapshot();
diff --git a/packages/drivers/driver-sql/src/sql-driver.ts b/packages/drivers/driver-sql/src/sql-driver.ts
index 778b08f36ba..af50e57ea66 100644
--- a/packages/drivers/driver-sql/src/sql-driver.ts
+++ b/packages/drivers/driver-sql/src/sql-driver.ts
@@ -12141,6 +12141,13 @@ export class SqlDriver implements IDataDriver {
// ahead of either.
this.warnBuiltinColumnCollisions(shardName, obj.fields, builtinColumns, 'shard');
const exists = await this.knex.schema.hasTable(shardName);
+ // ADR-0131 D1: a shard is created from the BASE's declaration, so it takes
+ // the base's tenant column NOT NULL — asked of the base through the
+ // recording resolver, the same answer the declared-index sync below uses
+ // (the shard has no bookkeeping of its own until `aliasShardBookkeeping`).
+ // A shard that already exists keeps the nullability it has: the
+ // organization-ownership ceremony reports such a shard, it never rebuilds it.
+ const baseTenantField = this.computeAndRecordTenantField(baseTable, obj);
// Commit d0e3a885b: a shard carries the base table's declared indexes (below), so its
// columns need the same keyable-text decision the managed path makes.
const keyedColumns = indexedKeyColumns({
@@ -12156,7 +12163,7 @@ export class SqlDriver implements IDataDriver {
this.createAuditTimestampColumn(table, 'updated_at');
for (const [name, field] of Object.entries(obj.fields ?? {})) {
if (builtinColumns.has(name)) continue;
- this.createColumn(table, name, field, keyedColumns.get(name));
+ this.createColumn(table, name, field, keyedColumns.get(name), name === baseTenantField);
}
});
} else {
@@ -12199,7 +12206,7 @@ export class SqlDriver implements IDataDriver {
shardName,
perShard,
new Set(Object.keys(colInfo)),
- this.computeAndRecordTenantField(baseTable, obj),
+ baseTenantField,
obj.fields ?? {},
);
}
@@ -12742,7 +12749,8 @@ export class SqlDriver implements IDataDriver {
if (obj.fields) {
for (const [name, field] of Object.entries(obj.fields)) {
if (builtinColumns.has(name)) continue;
- this.createColumn(table, name, field, keyedColumns.get(name));
+ // ADR-0131 D1: a fresh table's tenant column is NOT NULL.
+ this.createColumn(table, name, field, keyedColumns.get(name), name === tenantField);
}
}
});
@@ -19743,6 +19751,7 @@ export class SqlDriver implements IDataDriver {
name: string,
field: any,
keyed?: { unique: boolean },
+ tenantColumn: boolean = false,
) {
// [#11567] Stated BEFORE the `multiple` short-circuit and before the type
// switch, because the spec's refusal is gated on neither: `FieldSchema`
@@ -19793,7 +19802,7 @@ export class SqlDriver implements IDataDriver {
// producers already agree.
if (isMultiValuedColumn(String(field.type ?? ''), field)) {
const jsonCol = this.jsonColumn(table, name);
- if (declaresColumnNotNull(field)) jsonCol.notNullable();
+ if (declaresColumnNotNull(field) || tenantColumn) jsonCol.notNullable();
return;
}
@@ -20204,7 +20213,18 @@ export class SqlDriver implements IDataDriver {
// [#17231] Asked through {@link declaresColumnNotNull} — the same
// question the multi-value short-circuit above now asks, stated once so
// this method's two exits cannot answer it differently again.
- if (declaresColumnNotNull(field)) col.notNullable();
+ //
+ // ADR-0131 D1 (execution record 6285122667 on the v18 step 7 card): the
+ // table's tenant column is the one column NOT NULL WITHOUT a declaration
+ // — "there is no nullable tenant column anywhere". `tenantColumn` is
+ // passed only by the two CREATE TABLE sites ({@link initObjects}' create
+ // branch, {@link ensureShardTable}'s), never by an ADD COLUMN: a column
+ // added to a table that already holds rows is the organization-ownership
+ // ceremony's to tighten (ruling ② A, 6269955919), and no metadata says
+ // it — the injected tenant field stays `required: false` with no
+ // `storage`, and the differ's `ceremonyOwnedNullability` reads the
+ // difference as no drift.
+ if (declaresColumnNotNull(field) || tenantColumn) col.notNullable();
this.applyDeclaredColumnDefault(col, field, type);
}
}
diff --git a/packages/drivers/driver-turso/src/pre-18-tenant-column.testkit.ts b/packages/drivers/driver-turso/src/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..8ba7b5e8125
--- /dev/null
+++ b/packages/drivers/driver-turso/src/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,59 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { TursoDriver } from './turso-driver.js';
+
+type ObjectDef = { name: string; fields?: Record; tenancy?: any; indexes?: any[] };
+
+/**
+ * Build an object's table on either `TursoDriver` face in the shape a database
+ * built BEFORE 18.x has: its tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x both faces create a table's tenant column NOT NULL.
+ * A store built by an earlier release still holds the nullable column, and
+ * rows with no organization where the organization-ownership ceremony reported
+ * a table rather than clearing it. A suite whose subject is how a face treats
+ * THOSE rows (the chokepoint's NULL-organization arm, the NULL-safe unique key
+ * part) builds its table through this, so it keeps measuring that shape.
+ *
+ * - **local face:** `initObjects`, then the driver's own drift apply door
+ * (`relax_not_null`, the SQLite table rebuild), never a hand `ALTER`.
+ * - **remote face:** that face has no door that relaxes a column (its
+ * `driftApplyLimit`), so the table is created the way a pre-18 remote sync
+ * created it — the transport's own `CREATE TABLE` with the tenant column's
+ * `NOT NULL` left off — and `initObjects` then runs over the existing table
+ * (its additive sync and declared indexes) as it would on an upgraded store.
+ */
+export async function initObjectsPre18(driver: TursoDriver, objectDef: ObjectDef): Promise {
+ const d = driver as any;
+ const column: string | null = d.computeTenantField(objectDef);
+ if (column === null) throw new Error(`initObjectsPre18: ${objectDef.name} has no tenant column`);
+
+ if (driver.transportMode !== 'remote') {
+ await driver.initObjects([objectDef] as never);
+ const entry = {
+ kind: 'nullability_mismatch',
+ remoteName: objectDef.name,
+ table: objectDef.name,
+ column,
+ expected: 'NULL',
+ actual: 'NOT NULL',
+ severity: 'warning',
+ category: 'needs_confirm',
+ op: { type: 'relax_not_null', table: objectDef.name, column },
+ message: `${objectDef.name}.${column}: the pre-18 nullable tenant column (test fixture).`,
+ };
+ const { applied } = await driver.applyMigrationEntries([entry as never]);
+ if (applied.length !== 1) throw new Error(`initObjectsPre18: ${objectDef.name}.${column} was not relaxed`);
+ return;
+ }
+
+ const transport = d.remoteTransport;
+ const created: string = transport.buildCreateTableSQL(objectDef.name, objectDef);
+ const constrained = new RegExp(`("${column}" [A-Z]+) NOT NULL`);
+ if (!constrained.test(created)) {
+ throw new Error(`initObjectsPre18: the remote CREATE TABLE for ${objectDef.name} does not constrain ${column}`);
+ }
+ await transport.ensureConnected();
+ await transport.client.execute(created.replace(constrained, '$1'));
+ await driver.initObjects([objectDef] as never);
+}
diff --git a/packages/drivers/driver-turso/src/remote-transport.ts b/packages/drivers/driver-turso/src/remote-transport.ts
index 5a6c9470346..97cc60b2329 100644
--- a/packages/drivers/driver-turso/src/remote-transport.ts
+++ b/packages/drivers/driver-turso/src/remote-transport.ts
@@ -2426,16 +2426,24 @@ export class RemoteTransport {
* Build a CREATE TABLE SQL string for the given object definition.
* Shared by syncSchema() and syncSchemasBatch() to avoid duplication.
*/
- private buildCreateTableSQL(tableName: string, objectDef: { fields?: Record }): string {
+ private buildCreateTableSQL(tableName: string, objectDef: { fields?: Record; tenancy?: any }): string {
let sql = `CREATE TABLE "${tableName}" ("id" TEXT PRIMARY KEY, "created_at" TEXT DEFAULT (datetime('now')), "updated_at" TEXT DEFAULT (datetime('now'))`;
+ // ADR-0131 D1, the local face's `createColumn` twin: a fresh table's
+ // tenant column is NOT NULL with no declaration saying so. Asked through
+ // the driver's schema resolver ({@link tenantFieldResolver}) — the by-table
+ // registry is still empty here. CREATE only: an `ADD COLUMN` below and in
+ // {@link syncSchemasBatch} stays nullable, and a remote store this face
+ // created before keeps what it has (the organization-ownership ceremony
+ // reports the remote face as unable to tighten it).
+ const tenantField = this.tenantFieldResolver ? this.tenantFieldResolver(objectDef) : null;
if (objectDef.fields) {
for (const [name, field] of Object.entries(objectDef.fields)) {
if (BUILTIN_COLUMNS.has(name)) continue;
const type = (field as any).type || 'string';
if (type === 'formula') continue; // Virtual — no column
this.assertSafeIdentifier(name);
- sql += `, "${name}" ${this.columnDeclarationSQL(field)}`;
+ sql += `, "${name}" ${this.columnDeclarationSQL(field, name === tenantField)}`;
}
}
@@ -2492,9 +2500,9 @@ export class RemoteTransport {
* asymmetry; closing it means deciding whether, and from which shared source,
* this face emits declared defaults.
*/
- private columnDeclarationSQL(field: any): string {
+ private columnDeclarationSQL(field: any, tenantColumn: boolean = false): string {
const colType = this.mapFieldTypeToSQL(field);
- return declaresColumnNotNull(field) ? `${colType} NOT NULL` : colType;
+ return declaresColumnNotNull(field) || tenantColumn ? `${colType} NOT NULL` : colType;
}
/**
diff --git a/packages/drivers/driver-turso/src/turso-local-remote-tenant-scope-parity.test.ts b/packages/drivers/driver-turso/src/turso-local-remote-tenant-scope-parity.test.ts
index 28f31ace84b..33130605c06 100644
--- a/packages/drivers/driver-turso/src/turso-local-remote-tenant-scope-parity.test.ts
+++ b/packages/drivers/driver-turso/src/turso-local-remote-tenant-scope-parity.test.ts
@@ -44,6 +44,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { DriverOptions } from '@objectstack/spec/data';
import { TursoDriver } from './turso-driver.js';
import { asLibsqlClient, makeLibsqlSqliteStub, type LibsqlSqliteStub } from './libsql-sqlite-stub.testkit.js';
+import { initObjectsPre18 } from './pre-18-tenant-column.testkit.js';
interface WireBearingError extends Error {
code?: string;
@@ -129,7 +130,9 @@ describe('a tenant-scoped call on BOTH TursoDriver faces answers one row set', (
drivers = { local, remote };
for (const driver of Object.values(drivers)) {
- await driver.initObjects([{ ...LEDGER, fields: { ...LEDGER.fields } }]);
+ // The seed's platform row carries no organization, which only a store
+ // built before 18.x can hold (ADR-0131 D1).
+ await initObjectsPre18(driver, { ...LEDGER, fields: { ...LEDGER.fields } });
for (const row of SEED) await driver.create(LEDGER.name, { ...row }, UNSCOPED);
}
});
diff --git a/packages/drivers/driver-turso/src/turso-local-remote-upsert-cross-organization-parity.test.ts b/packages/drivers/driver-turso/src/turso-local-remote-upsert-cross-organization-parity.test.ts
index d34b2126019..6001d366e3b 100644
--- a/packages/drivers/driver-turso/src/turso-local-remote-upsert-cross-organization-parity.test.ts
+++ b/packages/drivers/driver-turso/src/turso-local-remote-upsert-cross-organization-parity.test.ts
@@ -247,9 +247,13 @@ describe("an upsert whose conflict lands on another organization's row, on BOTH
const refused = await captureError(() =>
driver.upsert(ACCOUNT.name, { email: OTHER_EMAIL, title: 'overwritten?' }, ['email'], { tenantId: CALLER_ORG }),
);
+ // No tenant context, as before; the payload names an organization because
+ // a fresh tenant column is NOT NULL (ADR-0131 D1) and the database checks
+ // the proposed row before it resolves the conflict. It names the CALLER's,
+ // so the merge keeping OTHER_ORG below still proves it was not overwritten.
const merged = await driver.upsert(
ACCOUNT.name,
- { email: OTHER_EMAIL, title: 'merged without context' },
+ { email: OTHER_EMAIL, organization_id: CALLER_ORG, title: 'merged without context' },
['email'],
{ bypassTenantAudit: true },
);
diff --git a/packages/drivers/driver-turso/src/turso-remote-drift-detection.test.ts b/packages/drivers/driver-turso/src/turso-remote-drift-detection.test.ts
index fe13d49705a..81d10c3c9dc 100644
--- a/packages/drivers/driver-turso/src/turso-remote-drift-detection.test.ts
+++ b/packages/drivers/driver-turso/src/turso-remote-drift-detection.test.ts
@@ -51,6 +51,7 @@ import { createClient, type Client } from '@libsql/client';
import { TursoDriver } from './turso-driver.js';
import { makeLibsqlSqliteStub } from './libsql-sqlite-stub.testkit.js';
import { replicaFiles } from './replica-file.testkit.js';
+import { initObjectsPre18 } from './pre-18-tenant-column.testkit.js';
const scratch = mkdtempSync(join(tmpdir(), 'turso-remote-drift-'));
afterAll(() => rmSync(scratch, { recursive: true, force: true }));
@@ -147,7 +148,10 @@ type Drift = Awaited>;
describe('each drifted shape reads alike on the remote and the local face', () => {
/** The physical drift each case puts on disk after a clean remote sync, and the finding it must produce. */
- const CASES: Array<[string, string[], (drift: Drift) => void]> = [
+ // The fourth element: the case's rows carry no organization, which only a
+ // store built before 18.x can hold (ADR-0131 D1), so `tenant_t` is built in
+ // that shape first (`initObjectsPre18`).
+ const CASES: Array<[string, string[], (drift: Drift) => void, boolean?]> = [
[
'an extra physical column the declaration omits',
['ALTER TABLE "t" ADD COLUMN "legacy" TEXT', 'CREATE INDEX "idx_t_legacy" ON "t" ("legacy")'],
@@ -182,12 +186,14 @@ describe('each drifted shape reads alike on the remote and the local face', () =
(drift) => expect(drift).toContainEqual(expect.objectContaining({
table: 'tenant_t', category: 'destructive', op: expect.objectContaining({ type: 'recreate_index' }),
})),
+ true,
],
];
- it.each(CASES)('%s', async (_label, statements, finding) => {
+ it.each(CASES)('%s', async (_label, statements, finding, pre18Tenant) => {
const file = nextFile();
const { driver: remote } = await remoteOver(file);
+ if (pre18Tenant) await initObjectsPre18(remote, TENANT);
await SYNC.syncSchemasBatch(remote, ALL);
for (const sql of statements) await onDisk(file, sql);
const local = await localOver(file, ALL);
diff --git a/packages/drivers/driver-turso/src/turso-remote-inherited-members.test.ts b/packages/drivers/driver-turso/src/turso-remote-inherited-members.test.ts
index e81077ed4d3..0fa5665c094 100644
--- a/packages/drivers/driver-turso/src/turso-remote-inherited-members.test.ts
+++ b/packages/drivers/driver-turso/src/turso-remote-inherited-members.test.ts
@@ -46,6 +46,7 @@ import { afterAll, afterEach, describe, expect, it } from 'vitest';
import { createClient, type Client } from '@libsql/client';
import { SqlDriver } from '@objectstack/driver-sql';
import { REMOTE_FACE_ANSWERS, TursoDriver } from './turso-driver.js';
+import { initObjectsPre18 } from './pre-18-tenant-column.testkit.js';
interface WireBearingError extends Error {
code?: string;
@@ -100,8 +101,16 @@ const nextFile = () => join(scratch, `db-${++seq}.db`);
type Face = 'remote' | 'local';
-/** A driver of the given face over a fresh SQLite file, synced and seeded. */
-async function seeded(face: Face, file = nextFile()): Promise {
+/**
+ * A driver of the given face over a fresh SQLite file, synced and seeded.
+ *
+ * `t3` carries no organization, which only a store built before 18.x can hold
+ * (ADR-0131 D1), so `probe_org` is built in that shape (`initObjectsPre18`).
+ * `pre18Tenant: false` builds it as 18.x does, through the schema doors alone,
+ * and seeds only the rows that name an organization — for a case whose subject
+ * is what those doors count.
+ */
+async function seeded(face: Face, file = nextFile(), pre18Tenant = true): Promise {
const client = face === 'remote' ? createClient({ url: `file:${file}` }) : null;
const driver =
face === 'remote'
@@ -109,9 +118,15 @@ async function seeded(face: Face, file = nextFile()): Promise {
: new TursoDriver({ url: `file:${file}` });
open.push({ driver, client });
await driver.connect();
- await driver.initObjects([PROBE, TENANT]);
+ if (pre18Tenant) {
+ await driver.initObjects([PROBE]);
+ await initObjectsPre18(driver, TENANT);
+ } else {
+ await driver.initObjects([PROBE, TENANT]);
+ }
await driver.bulkCreate('probe_t', PROBE_ROWS.map((row) => ({ ...row })));
- await driver.bulkCreate('probe_org', TENANT_ROWS.map((row) => ({ ...row })));
+ const tenantRows = pre18Tenant ? TENANT_ROWS : TENANT_ROWS.filter((row) => row.organization_id !== null);
+ await driver.bulkCreate('probe_org', tenantRows.map((row) => ({ ...row })));
return driver;
}
@@ -405,7 +420,7 @@ describe('members whose remote answer is a declaration', () => {
describe('getSchemaSyncStats on the remote face: what its schema doors counted', () => {
it('a fresh remote database is observed as created from empty; the same database reached again is found; the local control counts too', async () => {
const file = nextFile();
- expect((await seeded('remote', file)).getSchemaSyncStats()).toEqual({ created: 2, existing: 0 });
+ expect((await seeded('remote', file, false)).getSchemaSyncStats()).toEqual({ created: 2, existing: 0 });
// A second process over the same remote database: every table is found.
const again = new TursoDriver({ url: 'libsql://probe.example.turso.io', client: createClient({ url: `file:${file}` }) });
open.push({ driver: again, client: null });
@@ -415,7 +430,7 @@ describe('getSchemaSyncStats on the remote face: what its schema doors counted',
// A re-sync of the tables this process created is not a find.
await again.syncSchemasBatch([{ object: 'probe_t', schema: PROBE }]);
expect(again.getSchemaSyncStats()).toEqual({ created: 0, existing: 2 });
- expect((await seeded('local')).getSchemaSyncStats()).toEqual({ created: 2, existing: 0 });
+ expect((await seeded('local', nextFile(), false)).getSchemaSyncStats()).toEqual({ created: 2, existing: 0 });
});
it("a remote boot that ran no schema door says nothing: both counts at zero, the contract's \"cannot say\"", () => {
diff --git a/packages/drivers/driver-turso/src/turso-remote-migration-entries.test.ts b/packages/drivers/driver-turso/src/turso-remote-migration-entries.test.ts
index 01a99bb828c..00d55e68e42 100644
--- a/packages/drivers/driver-turso/src/turso-remote-migration-entries.test.ts
+++ b/packages/drivers/driver-turso/src/turso-remote-migration-entries.test.ts
@@ -41,6 +41,7 @@ import { join } from 'node:path';
import { afterAll, afterEach, describe, expect, it } from 'vitest';
import { createClient, type Client } from '@libsql/client';
import { TursoDriver } from './turso-driver.js';
+import { initObjectsPre18 } from './pre-18-tenant-column.testkit.js';
const scratch = mkdtempSync(join(tmpdir(), 'turso-remote-migrate-'));
afterAll(() => rmSync(scratch, { recursive: true, force: true }));
@@ -196,6 +197,9 @@ describe('the index entries', () => {
async function bareTenantKey(duplicates: boolean) {
const file = nextFile();
const remote = await remoteOver(file);
+ // Duplicates the bare key admitted carry no organization, which only a
+ // store built before 18.x can hold (ADR-0131 D1).
+ if (duplicates) await initObjectsPre18(remote, TENANT);
await remote.syncSchemasBatch([{ object: 'tenant_t', schema: TENANT }]);
const synced = await indexesOf(file, 'tenant_t');
await onDisk(
diff --git a/packages/drivers/driver-turso/src/turso-remote-tenant-column-not-null.test.ts b/packages/drivers/driver-turso/src/turso-remote-tenant-column-not-null.test.ts
new file mode 100644
index 00000000000..536d9f0376a
--- /dev/null
+++ b/packages/drivers/driver-turso/src/turso-remote-tenant-column-not-null.test.ts
@@ -0,0 +1,161 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * ADR-0131 D1 on the Turso REMOTE face: a table this transport CREATES carries
+ * its tenant column NOT NULL, exactly as the local face's `createColumn`
+ * builds it (execution record 6285122667 on the v18 step 7 card; ruling record
+ * 6269955919, ② A: no declaration says so).
+ *
+ * ## The instrument
+ *
+ * As `turso-remote-not-null.test.ts`: the remote face is a `TursoDriver` in
+ * remote mode over a real `@libsql/client` `file:` client, so its DDL runs on
+ * a real libSQL connection; the local (Knex) face over a file of its own is the
+ * reference each remote answer is held equal to. No hosted remote is reached.
+ *
+ * ## What is pinned
+ *
+ * 1. Through each of the three remote schema doors, a fresh table's tenant
+ * column is NOT NULL — the local face's answer — while an ordinary field
+ * beside it stays nullable.
+ * 2. The database refuses a NULL tenant value: a raw statement on the remote
+ * store's own connection, so nothing above the database stamps the row.
+ * 3. Neither face reports drift over it: the declaration says nullable, the
+ * column says NOT NULL, and that difference is the ceremony's.
+ * 4. The boundaries: an object opted out of tenancy has no tenant column, so a
+ * field named `organization_id` stays nullable; a tenant column ADDED to a
+ * remote table that already exists stays nullable — an upgraded remote
+ * store keeps what it has (the ceremony reports this face as unable to
+ * tighten it).
+ */
+
+import { mkdtempSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { afterAll, afterEach, describe, expect, it } from 'vitest';
+import { createClient, type Client } from '@libsql/client';
+import { TursoDriver } from './turso-driver.js';
+
+const scratch = mkdtempSync(join(tmpdir(), 'turso-remote-tenant-not-null-'));
+afterAll(() => rmSync(scratch, { recursive: true, force: true }));
+let seq = 0;
+const nextFile = () => join(scratch, `db-${++seq}.db`);
+
+const open: Array<{ driver: TursoDriver; client: Client | null }> = [];
+afterEach(async () => {
+ while (open.length) {
+ const { driver, client } = open.pop()!;
+ await driver.disconnect().catch(() => {});
+ client?.close();
+ }
+});
+
+type Objects = Parameters[0] & object;
+
+async function remoteOver(file: string): Promise {
+ const client = createClient({ url: `file:${file}` });
+ const driver = new TursoDriver({ url: 'libsql://tenant-not-null.example.turso.io', client });
+ open.push({ driver, client });
+ await driver.connect();
+ expect(driver.transportMode).toBe('remote');
+ return driver;
+}
+
+async function localOver(file: string): Promise {
+ const driver = new TursoDriver({ url: `file:${file}` });
+ open.push({ driver, client: null });
+ await driver.connect();
+ expect(driver.transportMode).toBe('local');
+ return driver;
+}
+
+/** Each column's `notnull` flag as the database reports it, through a connection of its own. */
+async function notNullFlags(file: string, table: string): Promise> {
+ const c = createClient({ url: `file:${file}` });
+ try {
+ const rows = (await c.execute(`PRAGMA table_info("${table}")`)).rows;
+ return Object.fromEntries(rows.map((r) => [String(r.name), Number(r.notnull)]));
+ } finally {
+ c.close();
+ }
+}
+
+/** The kernel's injected tenant field, as the driver receives it: nullable by declaration. */
+const ORG = { type: 'lookup', reference: 'sys_organization', required: false };
+
+const OWNED = { name: 'tnn_owned', fields: { organization_id: ORG, note: { type: 'text' } } };
+const GLOBAL = { name: 'tnn_global', tenancy: { enabled: false }, fields: { organization_id: ORG } };
+const GROWN_BEFORE = { name: 'tnn_grown', fields: { note: { type: 'text' } } };
+const GROWN_AFTER = { name: 'tnn_grown', fields: { note: { type: 'text' }, organization_id: ORG } };
+
+type Door = 'syncSchemasBatch' | 'syncSchema' | 'initObjects';
+const SYNC: Record Promise> = {
+ syncSchemasBatch: (d, objects) => d.syncSchemasBatch(objects.map((o) => ({ object: o.name, schema: o }))),
+ syncSchema: async (d, objects) => {
+ for (const o of objects) await d.syncSchema(o.name, o);
+ },
+ initObjects: (d, objects) => d.initObjects(objects as never),
+};
+
+describe('CREATE TABLE: a fresh remote table carries its tenant column NOT NULL', () => {
+ it.each(['syncSchemasBatch', 'syncSchema', 'initObjects'])('synced through %s', async (door) => {
+ const referenceFile = nextFile();
+ await (await localOver(referenceFile)).initObjects([OWNED] as never);
+ const reference = await notNullFlags(referenceFile, OWNED.name);
+ expect({ organization_id: reference.organization_id, note: reference.note }).toEqual({ organization_id: 1, note: 0 });
+
+ const file = nextFile();
+ const remote = await remoteOver(file);
+ await SYNC[door](remote, [OWNED]);
+
+ expect(await notNullFlags(file, OWNED.name)).toEqual(reference);
+ expect(await remote.detectManagedDrift([OWNED])).toEqual([]);
+ const local = await localOver(file);
+ local.registerObjectMetadata([OWNED] as never);
+ expect(await local.detectManagedDrift([OWNED])).toEqual([]);
+ });
+
+ it('the remote database refuses a NULL tenant value', async () => {
+ const file = nextFile();
+ const remote = await remoteOver(file);
+ await remote.syncSchema(OWNED.name, OWNED);
+
+ const c = createClient({ url: `file:${file}` });
+ try {
+ await expect(
+ c.execute({ sql: 'INSERT INTO tnn_owned (id, organization_id, note) VALUES (?, NULL, ?)', args: ['r1', 'x'] }),
+ ).rejects.toThrow(/NOT NULL constraint failed: tnn_owned\.organization_id/);
+ await c.execute({ sql: 'INSERT INTO tnn_owned (id, organization_id, note) VALUES (?, ?, NULL)', args: ['r2', 'org_a'] });
+ expect((await c.execute('SELECT id FROM tnn_owned')).rows.map((r) => r.id)).toEqual(['r2']);
+ } finally {
+ c.close();
+ }
+ });
+});
+
+describe('the boundaries: no tenant column, or a table that already exists', () => {
+ it('a tenancy-opted-out object keeps organization_id nullable on both faces', async () => {
+ const remoteFile = nextFile();
+ await (await remoteOver(remoteFile)).syncSchema(GLOBAL.name, GLOBAL);
+ const localFile = nextFile();
+ await (await localOver(localFile)).initObjects([GLOBAL] as never);
+
+ expect((await notNullFlags(remoteFile, GLOBAL.name)).organization_id).toBe(0);
+ expect((await notNullFlags(localFile, GLOBAL.name)).organization_id).toBe(0);
+ });
+
+ it.each(['syncSchemasBatch', 'syncSchema'])(
+ 'a tenant column added to an existing remote table stays nullable (%s)',
+ async (door) => {
+ const file = nextFile();
+ const remote = await remoteOver(file);
+ await remote.syncSchema(GROWN_BEFORE.name, GROWN_BEFORE);
+ await remote.create(GROWN_BEFORE.name, { id: 'before', note: 'pre-existing row' });
+
+ await SYNC[door](remote, [GROWN_AFTER]);
+
+ expect((await notNullFlags(file, GROWN_AFTER.name)).organization_id).toBe(0);
+ expect(await remote.detectManagedDrift([GROWN_AFTER])).toEqual([]);
+ },
+ );
+});
diff --git a/packages/drivers/driver-turso/src/turso-remote-transaction-refusal.test.ts b/packages/drivers/driver-turso/src/turso-remote-transaction-refusal.test.ts
index 767b386b1fd..aa8452c5b22 100644
--- a/packages/drivers/driver-turso/src/turso-remote-transaction-refusal.test.ts
+++ b/packages/drivers/driver-turso/src/turso-remote-transaction-refusal.test.ts
@@ -308,14 +308,14 @@ describe('[#18616] the refusal fires on the HANDLE, not on remote mode', () => {
it('an explicitly undefined handle is not a handle', async () => {
const { driver, stub } = await makeRemote();
- await driver.create('crm_note', { id: 'n2', title: 'also kept' }, { transaction: undefined });
+ await driver.create('crm_note', { id: 'n2', organization_id: 'orgA', title: 'also kept' }, { transaction: undefined });
expect(rowCount(stub)).toBe(1);
});
it('LOCAL: rollback still undoes the write — the pin this card mirrors', async () => {
const driver = await makeLocal();
const trx = await driver.beginTransaction();
- await driver.create('crm_note', { id: 'l1', title: 'RollbackUser' }, { transaction: trx });
+ await driver.create('crm_note', { id: 'l1', organization_id: 'orgA', title: 'RollbackUser' }, { transaction: trx });
await driver.rollback(trx);
expect(await driver.find('crm_note', { where: { id: 'l1' } })).toHaveLength(0);
});
@@ -323,7 +323,7 @@ describe('[#18616] the refusal fires on the HANDLE, not on remote mode', () => {
it('LOCAL: commit still keeps the write', async () => {
const driver = await makeLocal();
const trx = await driver.beginTransaction();
- await driver.create('crm_note', { id: 'l2', title: 'TrxUser' }, { transaction: trx });
+ await driver.create('crm_note', { id: 'l2', organization_id: 'orgA', title: 'TrxUser' }, { transaction: trx });
await driver.commit(trx);
expect(await driver.find('crm_note', { where: { id: 'l2' } })).toHaveLength(1);
});
@@ -331,7 +331,7 @@ describe('[#18616] the refusal fires on the HANDLE, not on remote mode', () => {
it('REPLICA: the third face still opens and rolls back a transaction', async () => {
const driver = await makeReplica();
const trx = await driver.beginTransaction();
- await driver.create('crm_note', { id: 'r1', title: 'ReplicaUser' }, { transaction: trx });
+ await driver.create('crm_note', { id: 'r1', organization_id: 'orgA', title: 'ReplicaUser' }, { transaction: trx });
await driver.rollback(trx);
expect(await driver.find('crm_note', { where: { id: 'r1' } })).toHaveLength(0);
});
diff --git a/packages/objectql/src/registry-tenant-index-declaration.test.ts b/packages/objectql/src/registry-tenant-index-declaration.test.ts
index 4a331a2a427..8ac0acf0584 100644
--- a/packages/objectql/src/registry-tenant-index-declaration.test.ts
+++ b/packages/objectql/src/registry-tenant-index-declaration.test.ts
@@ -167,7 +167,7 @@ describe('#6810 — a registry-backed object reads back valid at both /meta exit
readonly: true,
system: true,
description:
- 'Tenant scope (auto-populated by org-scoping on insert; NULL on single-tenant stacks).',
+ 'Tenant scope (stamped on insert by org-scoping; never NULL — a write that names no organization is refused).',
});
});
diff --git a/packages/objectql/src/registry.ts b/packages/objectql/src/registry.ts
index e0d8667e214..ac37fff7fff 100644
--- a/packages/objectql/src/registry.ts
+++ b/packages/objectql/src/registry.ts
@@ -597,10 +597,11 @@ export interface SchemaRegistryOptions {
* Currently injects:
* - `organization_id` — always provisioned (unless the object opts out via
* `systemFields`/`tenancy.enabled:false` or is `better-auth` managed) so
- * the column never depends on the global multi-tenant flag. Required-false;
- * org-scoping populates it on insert in multi-tenant mode, and it stays
- * NULL on single-tenant stacks. Only the column's INDEX is gated on
- * `multiTenant` (no per-tenant filtering exists single-tenant).
+ * the column never depends on the global multi-tenant flag. Required-false
+ * in the declaration; org-scoping stamps it on insert (under `single` with
+ * the Default Organization), and it is never NULL (ADR-0131 D1). Only the
+ * column's INDEX is gated on `multiTenant` (no per-tenant filtering exists
+ * single-tenant).
* - `created_at` / `created_by` / `updated_at` / `updated_by` — audit
* fields. Marked `system: true, readonly: true` so detail views can
* surface them in a dedicated "System Information" section while
diff --git a/packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts b/packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts
index 5bdfbdadf75..9862dd726b4 100644
--- a/packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts
@@ -36,6 +36,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import type { ApprovalService } from './approval-service.js';
import { renderResultPage } from './action-link-pages.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const SUBMITTER = { userId: 'u_submitter', positions: [], permissions: [] } as any;
@@ -128,6 +129,9 @@ async function bootStack(options: { rawApp: boolean }) {
(objectql as any).registerDriver(driver, true);
objectql.registry.registerObject(leaveRequest as any, 'approvals-test', 'approvals-test');
await (objectql as any).syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql as any);
const svc = kernel.getService('approvals');
let n = 0;
diff --git a/packages/plugins/plugin-approvals/src/action-slot-backfill.integration.test.ts b/packages/plugins/plugin-approvals/src/action-slot-backfill.integration.test.ts
index 1a5e27bbea4..8569bee1717 100644
--- a/packages/plugins/plugin-approvals/src/action-slot-backfill.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/action-slot-backfill.integration.test.ts
@@ -36,6 +36,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const SUBMITTER = { userId: 'u_submitter', positions: [], permissions: [] } as any;
@@ -85,6 +86,9 @@ describe('the boot-time action-slot backfill (#21411)', () => {
// Real DDL — including the `acted_as` column, so a write of it the schema
// does not carry fails here rather than passing against a permissive fake.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
svc = new ApprovalService({ engine: engine as any, logger: { warn: () => {} } as any });
await engine.insert('crm_deal', { id: 'D1', title: 'Deal' }, { context: SYSTEM } as any);
});
diff --git a/packages/plugins/plugin-approvals/src/actor-lookups-hold-ids.integration.test.ts b/packages/plugins/plugin-approvals/src/actor-lookups-hold-ids.integration.test.ts
index edf4be063ce..47e86114010 100644
--- a/packages/plugins/plugin-approvals/src/actor-lookups-hold-ids.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/actor-lookups-hold-ids.integration.test.ts
@@ -46,6 +46,7 @@ import { APPROVAL_REVISE_NODE_TYPE } from '@objectstack/spec/automation';
import type { EngineQueryOptions } from '@objectstack/spec/data';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string, positions: string[] = []) =>
@@ -156,6 +157,9 @@ describe('ADR-0118 D1 — every sys_user lookup approvals writes holds an id or
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
svc = new ApprovalService({
diff --git a/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts b/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts
index b5faf949ce2..5beefef10c5 100644
--- a/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts
@@ -45,6 +45,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { bindSecurityCatalogReader, type SecurityCatalogEntry, type SecurityCatalogReader } from '@objectstack/core';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
@@ -185,6 +186,10 @@ describe('an empty admin_rescue slate opens on the organization\'s administrator
];
for (const def of defs) engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
await engine.syncSchemas();
+ // The organization-less rows below (positions, holdings) exist only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
bindSecurityCatalogReader(engine, catalogServing([
'organization_admin', 'organization_admin_no_bypass', 'admin_full_access',
]));
diff --git a/packages/plugins/plugin-approvals/src/approval-decision-outcome-notification.integration.test.ts b/packages/plugins/plugin-approvals/src/approval-decision-outcome-notification.integration.test.ts
index ee3523329da..fb365507a43 100644
--- a/packages/plugins/plugin-approvals/src/approval-decision-outcome-notification.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/approval-decision-outcome-notification.integration.test.ts
@@ -44,6 +44,7 @@ import type { EngineQueryOptions } from '@objectstack/spec/data';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { registerApprovalNode } from './approval-node.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -188,6 +189,9 @@ describe('the decision that ends an approval tells its submitter (approval.appro
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
automation = new AutomationEngine(silent);
diff --git a/packages/plugins/plugin-approvals/src/approval-list-display-labels.integration.test.ts b/packages/plugins/plugin-approvals/src/approval-list-display-labels.integration.test.ts
index 7057baeeb5b..e715bdf7254 100644
--- a/packages/plugins/plugin-approvals/src/approval-list-display-labels.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/approval-list-display-labels.integration.test.ts
@@ -54,6 +54,7 @@ import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string, positions: string[] = []) =>
@@ -136,6 +137,9 @@ describe('#23889 — the approval lists read engine-written display columns', ()
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
svc = new ApprovalService({
diff --git a/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts b/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts
index 486625ca21c..96a1eb655dd 100644
--- a/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts
@@ -76,6 +76,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { APPROVAL_REVISE_NODE_TYPE } from '@objectstack/spec/automation';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string, positions: string[] = []) =>
@@ -213,6 +214,10 @@ describe('every approval notification carries its text in body', () => {
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // The organization-less rows below (positions, holdings) exist only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
svc = new ApprovalService({
diff --git a/packages/plugins/plugin-approvals/src/approval-notification-keyed-text.integration.test.ts b/packages/plugins/plugin-approvals/src/approval-notification-keyed-text.integration.test.ts
index f2c57b5c96a..0f08bf82f5f 100644
--- a/packages/plugins/plugin-approvals/src/approval-notification-keyed-text.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/approval-notification-keyed-text.integration.test.ts
@@ -50,6 +50,7 @@ import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { ApprovalsTranslations } from './translations/index.js';
import { approvalMessages, enMessages } from './translations/messages.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -124,6 +125,9 @@ async function bootEngine(users: Array>): Promise ({ userId, positions: [], permissions: [] }) as any;
@@ -161,6 +162,9 @@ describe('opening an approval step tells each resolved approver, once (approval.
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
// `legal` is staffed by two people; `treasury` by nobody.
for (const [id, user] of [['up_1', 'u_carol'], ['up_2', 'u_dan']]) {
diff --git a/packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts b/packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts
index 0e8d2cf4961..6480b289735 100644
--- a/packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts
@@ -22,6 +22,7 @@ import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
import { SysApprovalToken } from './sys-approval-token.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const SUBMITTER = { userId: 'submitter', positions: [], permissions: [] } as any;
@@ -51,6 +52,9 @@ describe('[#21197] the approval token digest is internal, and the actionable lin
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
svc = new ApprovalService({
engine: engine as any,
automation: {
diff --git a/packages/plugins/plugin-approvals/src/default-organization.testkit.ts b/packages/plugins/plugin-approvals/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..fc514f66ebe
--- /dev/null
+++ b/packages/plugins/plugin-approvals/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/plugin-approvals',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/plugins/plugin-approvals/src/display-label-backfill.integration.test.ts b/packages/plugins/plugin-approvals/src/display-label-backfill.integration.test.ts
index 3ebd47128db..9cb8daca10c 100644
--- a/packages/plugins/plugin-approvals/src/display-label-backfill.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/display-label-backfill.integration.test.ts
@@ -47,6 +47,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const STAMP = '2026-09-01T09:00:00.000Z';
@@ -128,6 +129,9 @@ describe('#23889 — the boot-time display-label backfill', () => {
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of [
{ id: 'u_sub', name: 'Sam Submitter', email: 'sam@example.com' },
{ id: 'u_alice', name: 'Alice', email: 'alice@example.com' },
diff --git a/packages/plugins/plugin-approvals/src/later-step-original-submitter.integration.test.ts b/packages/plugins/plugin-approvals/src/later-step-original-submitter.integration.test.ts
index 88811cad9cc..d3c8daf8e16 100644
--- a/packages/plugins/plugin-approvals/src/later-step-original-submitter.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/later-step-original-submitter.integration.test.ts
@@ -51,6 +51,7 @@ import type { EngineQueryOptions } from '@objectstack/spec/data';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { registerApprovalNode } from './approval-node.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -250,6 +251,9 @@ describe('every step of an approval run names the run\'s original submitter (#24
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
automation = new AutomationEngine(silent);
diff --git a/packages/plugins/plugin-approvals/src/lookup-target-title-reader-fls.integration.test.ts b/packages/plugins/plugin-approvals/src/lookup-target-title-reader-fls.integration.test.ts
index 41bb909ee4e..ab144bfab6c 100644
--- a/packages/plugins/plugin-approvals/src/lookup-target-title-reader-fls.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/lookup-target-title-reader-fls.integration.test.ts
@@ -47,6 +47,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import type { FieldVisibilitySource } from './payload-redaction.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -200,6 +201,9 @@ describe('[#23618] a lookup target\'s title is served from what its reader may r
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const id of [RESTRICTED, MASKED, READER, SUBMITTER]) {
await engine.insert('sys_user', { id, name: id, email: `${id}@example.com` }, { context: SYSTEM } as any);
}
diff --git a/packages/plugins/plugin-approvals/src/lookup-target-title-row-access.integration.test.ts b/packages/plugins/plugin-approvals/src/lookup-target-title-row-access.integration.test.ts
index ed64b4ffe79..c7446cac029 100644
--- a/packages/plugins/plugin-approvals/src/lookup-target-title-row-access.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/lookup-target-title-row-access.integration.test.ts
@@ -65,6 +65,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ApprovalService, type ObjectReadAdmission } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -190,6 +191,9 @@ async function bootEngine(reads: CallerRead[]): Promise {
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
guardReads(engine, reads);
for (const id of [RESTRICTED, READER, SUBMITTER]) {
await engine.insert('sys_user', { id, name: id, email: `${id}@example.com` }, { context: SYSTEM } as any);
diff --git a/packages/plugins/plugin-approvals/src/payload-predicate-guard.test.ts b/packages/plugins/plugin-approvals/src/payload-predicate-guard.test.ts
index 0d6e260408a..451d2a4c04b 100644
--- a/packages/plugins/plugin-approvals/src/payload-predicate-guard.test.ts
+++ b/packages/plugins/plugin-approvals/src/payload-predicate-guard.test.ts
@@ -33,6 +33,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { namedSnapshotColumn, pinnedSubjectObject } from './payload-predicate-guard.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const REQUEST_OBJECT = 'sys_approval_request';
const SUBJECT = 'ppg_subject';
@@ -138,6 +139,9 @@ describe('[#21154] a query over the approval snapshot by a reader withheld a sub
engine.registry.registerObject(o as any, HARNESS_PACKAGE);
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const snapshot = (subject: string, row: Record) => ({
object_name: subject, record_id: String(row.id), status: 'pending',
diff --git a/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts b/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..b0cbb5efa38
--- /dev/null
+++ b/packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Return engine-synced tables to the shape a store built BEFORE 18.x has: the
+ * tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x the SQL driver creates a table's tenant column NOT
+ * NULL. A store an earlier release built still holds the nullable column and
+ * its organization-less rows — the holdings, grants and seeds this package's
+ * NULL readers still answer for until the organization-ownership ceremony has
+ * attributed them. A suite whose subject is those rows calls this after
+ * `syncSchemas()` and before seeding them. Done through each table's driver
+ * and its own drift apply door (`relax_not_null`), never a hand `ALTER`.
+ */
+export async function relaxTenantColumns(
+ engine: { getDriverForObject(object: string): unknown },
+ tables: readonly string[],
+ column = 'organization_id',
+): Promise {
+ for (const table of tables) {
+ const driver = engine.getDriverForObject(table) as {
+ applyMigrationEntries(entries: never[]): Promise<{ applied: unknown[] }>;
+ };
+ const op = { type: 'relax_not_null', table, column };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column, category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
+ }
+}
+
+/**
+ * {@link relaxTenantColumns} over every object of an app that carries a tenant
+ * column as the engine registered it — the shape a whole pre-18 store has.
+ */
+export async function relaxEveryTenantColumn(
+ engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown },
+ objects: readonly { name: string }[],
+): Promise {
+ const tenant = objects
+ .map((o) => o.name)
+ .filter((name) => {
+ const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined;
+ return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false;
+ });
+ await relaxTenantColumns(engine, tenant);
+}
+
+/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */
+export async function relaxEveryRegisteredTenantColumn(engine: {
+ getDriverForObject(object: string): unknown;
+ getSchema(object: string): unknown;
+ registry: { getAllObjects(): readonly { name: string }[] };
+}): Promise {
+ await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
+}
diff --git a/packages/plugins/plugin-approvals/src/reader-locale-display-labels.integration.test.ts b/packages/plugins/plugin-approvals/src/reader-locale-display-labels.integration.test.ts
index 2583c61b70a..f5a7c2f8ebf 100644
--- a/packages/plugins/plugin-approvals/src/reader-locale-display-labels.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/reader-locale-display-labels.integration.test.ts
@@ -57,6 +57,7 @@ import { TranslationDataSchema } from '@objectstack/spec/system';
import type { EngineQueryOptions } from '@objectstack/spec/data';
import type { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const reader = (userId: string, locale?: string) =>
@@ -187,6 +188,9 @@ describe('[#24863] the approvals door serves a request\'s labels in the reader\'
engine.registry.registerObject(def as any, 'approvals-reader-locale-test', 'approvals-reader-locale-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const [id, name] of [[SUBMITTER, 'Sam Submitter'], [MANAGER, 'Mona Manager'], [DIRECTOR, 'Dina Director']]) {
await engine.insert('sys_user', { id, name, email: `${id}@example.com` }, { context: SYSTEM } as any);
}
diff --git a/packages/plugins/plugin-approvals/src/record-delete-cancel.integration.test.ts b/packages/plugins/plugin-approvals/src/record-delete-cancel.integration.test.ts
index b0d33c626a6..71890400f8f 100644
--- a/packages/plugins/plugin-approvals/src/record-delete-cancel.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-delete-cancel.integration.test.ts
@@ -58,6 +58,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const SUBMITTER = { userId: 'submitter', positions: [], permissions: [] } as any;
@@ -157,6 +158,9 @@ describe('a deleted record auto-cancels its pending approvals (#13568)', () => {
// this card adds, so a write of it that the schema does not carry fails
// here rather than passing against a permissive fake.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
warnings = [];
automation = makeAutomationSpy();
@@ -349,6 +353,9 @@ describe('#13568 control — without the linkage the same delete strands the req
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
svc = new ApprovalService({ engine: engine as any });
await engine.insert('crm_leave_request', { id: 'LR6', title: 'Leave' }, { context: SYSTEM } as any);
// ⛔ deliberately NOT bound — that is the whole case.
diff --git a/packages/plugins/plugin-approvals/src/record-lock-batch-row-status.integration.test.ts b/packages/plugins/plugin-approvals/src/record-lock-batch-row-status.integration.test.ts
index b4d795dd5c0..68d75b5b38c 100644
--- a/packages/plugins/plugin-approvals/src/record-lock-batch-row-status.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-lock-batch-row-status.integration.test.ts
@@ -50,6 +50,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { resolveThrownHttpError } from '@objectstack/types';
import { bindApprovalLockHook } from './lifecycle-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const opportunity = {
name: 'opportunity',
@@ -105,6 +106,9 @@ describe('[#8570] a locked record\'s batch row carries the 409 the hook declared
}
// Real DDL through the real path.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
lockedId = String((await engine.insert('opportunity', { name: 'Deal', amount: 100 })).id);
freeId = String((await engine.insert('opportunity', { name: 'Other', amount: 100 })).id);
diff --git a/packages/plugins/plugin-approvals/src/record-lock-multi-update.integration.test.ts b/packages/plugins/plugin-approvals/src/record-lock-multi-update.integration.test.ts
index 19241ee3767..e6779521dba 100644
--- a/packages/plugins/plugin-approvals/src/record-lock-multi-update.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-lock-multi-update.integration.test.ts
@@ -34,6 +34,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { bindApprovalLockHook } from './lifecycle-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const opportunity = {
name: 'opportunity',
@@ -112,6 +113,9 @@ describe('approvals record lock — predicate (multi) updates (#4778)', () => {
// Real DDL through the real path — the tables every write below hits are
// created by the driver, not conjured by a store on first write.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
lockedId = String((await engine.insert('opportunity', { name: 'Deal', amount: 100 })).id);
freeId = String((await engine.insert('opportunity', { name: 'Other', amount: 100 })).id);
diff --git a/packages/plugins/plugin-approvals/src/record-lock-refusal-writer-fls.integration.test.ts b/packages/plugins/plugin-approvals/src/record-lock-refusal-writer-fls.integration.test.ts
index f158a652a8b..cb071aaceaf 100644
--- a/packages/plugins/plugin-approvals/src/record-lock-refusal-writer-fls.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-lock-refusal-writer-fls.integration.test.ts
@@ -41,6 +41,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { bindApprovalLockHook } from './lifecycle-hooks.js';
import type { FieldVisibilitySource } from './payload-redaction.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
/** The writer's envelope carries `permissions`, which the hook session does not. */
@@ -132,6 +133,9 @@ describe('[#23618] the RECORD_LOCKED refusal names the record from what its writ
engine.registry.registerObject(o as any, 'com.objectstack.test.23618');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
lockedId = String((await engine.insert('opportunity', { name: TITLE, amount: 100 }, { context: SYSTEM } as any)).id);
freeId = String((await engine.insert('opportunity', { name: 'Unlocked deal', amount: 5 }, { context: SYSTEM } as any)).id);
diff --git a/packages/plugins/plugin-approvals/src/record-lock-schedule-run.integration.test.ts b/packages/plugins/plugin-approvals/src/record-lock-schedule-run.integration.test.ts
index 75dbf2446ec..54b193594f8 100644
--- a/packages/plugins/plugin-approvals/src/record-lock-schedule-run.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-lock-schedule-run.integration.test.ts
@@ -35,6 +35,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { resolveRunDataContext } from '@objectstack/service-automation';
import { bindApprovalLockHook } from './lifecycle-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const opportunity = {
name: 'opportunity',
@@ -99,6 +100,9 @@ describe('an owning run and the approvals record lock (#3703 / #3712 / #3760)',
await engine.init();
for (const o of [opportunity, approvalRequest]) engine.registry.registerObject(o as any);
await engine.syncSchemas(); // real DDL for both tables
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const opp = await engine.insert('opportunity', { name: 'Deal', amount: 100 });
oppId = String(opp.id);
diff --git a/packages/plugins/plugin-approvals/src/record-lock-user-facing-message.test.ts b/packages/plugins/plugin-approvals/src/record-lock-user-facing-message.test.ts
index 4aa7062c87e..8a6bc2bb900 100644
--- a/packages/plugins/plugin-approvals/src/record-lock-user-facing-message.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-lock-user-facing-message.test.ts
@@ -43,6 +43,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { bindApprovalLockHook, APPROVALS_HOOK_PACKAGE } from './lifecycle-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OPPORTUNITY = {
name: 'opportunity',
@@ -105,6 +106,9 @@ describe('[#18153] the per-row refusal is user-facing copy', () => {
engine.registry.registerObject(o as any, 'com.objectstack.test.18153');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
opportunityId = String((await engine.insert('opportunity', { name: 'Acme renewal', amount: 100 })).id);
ledgerId = String((await engine.insert('ledger_entry', { amount: 100 })).id);
diff --git a/packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts b/packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts
index 42036862a32..7733ce6aa2b 100644
--- a/packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/record-reader-opt-in.integration.test.ts
@@ -47,6 +47,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const REQUEST = 'sys_approval_request';
const ACTION = 'sys_approval_action';
@@ -163,6 +164,9 @@ async function boot(options: ApprovalsPluginOptions = {}): Promise {
engine.registry.registerObject(def as any, PKG, PKG);
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const object of [DECLARED, UNDECLARED]) guardReads(engine, object);
const services: Record = { objectql: engine };
diff --git a/packages/plugins/plugin-approvals/src/remind-notified-per-person.integration.test.ts b/packages/plugins/plugin-approvals/src/remind-notified-per-person.integration.test.ts
index 3dc9f2789ce..d9e189edbb7 100644
--- a/packages/plugins/plugin-approvals/src/remind-notified-per-person.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/remind-notified-per-person.integration.test.ts
@@ -46,6 +46,7 @@ import type { EngineQueryOptions } from '@objectstack/spec/data';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { registerApprovalNode } from './approval-node.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -180,6 +181,9 @@ describe('a reminder counts the reminders it hands over, one per distinct approv
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
automation = new AutomationEngine(silent);
diff --git a/packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts b/packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts
index e4bd6997642..f7571b0c6dd 100644
--- a/packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts
@@ -53,6 +53,7 @@ import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
import { SysApprovalToken } from './sys-approval-token.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'linkrest_invoice';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
@@ -150,6 +151,9 @@ async function boot(): Promise {
engine.registry.registerObject(def as any, 'approvals-link-at-rest-test', 'approvals-link-at-rest-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const [id, name] of [[SUBMITTER, 'Sam Submitter'], [APPROVER, 'Ada Approver'], [ORG_ADMIN.userId, 'Olga Admin']]) {
await engine.insert('sys_user', { id, name, email: `${id}@example.com` }, { context: SYSTEM } as any);
}
diff --git a/packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts b/packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts
index 0cea7c52325..330bcd71cc2 100644
--- a/packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/request-child-read-gate.integration.test.ts
@@ -56,6 +56,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTION = 'sys_approval_action';
const APPROVER_INDEX = 'sys_approval_approver';
@@ -121,6 +122,9 @@ async function boot(
engine.registry.registerObject(def as any, 'approvals-child-read-gate-test', 'approvals-child-read-gate-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const recordA = 'sheet_a';
const recordB = 'sheet_b';
diff --git a/packages/plugins/plugin-approvals/src/request-read-gate.integration.test.ts b/packages/plugins/plugin-approvals/src/request-read-gate.integration.test.ts
index 08cf2c58924..d9472d43c9a 100644
--- a/packages/plugins/plugin-approvals/src/request-read-gate.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/request-read-gate.integration.test.ts
@@ -58,6 +58,7 @@ import { SysApprovalRequest } from './sys-approval-request.object.js';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const REQUEST = 'sys_approval_request';
const OBJECT = 'exam_sheet';
@@ -122,6 +123,9 @@ async function boot(
engine.registry.registerObject(def as any, 'approvals-read-gate-test', 'approvals-read-gate-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const recordA = 'sheet_a';
const recordB = 'sheet_b';
diff --git a/packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts b/packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts
index 6a2f65312ce..f8eb1ec1a3b 100644
--- a/packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/subject-title-reader-fls.integration.test.ts
@@ -50,6 +50,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import type { FieldVisibilitySource } from './payload-redaction.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -181,6 +182,9 @@ describe('[#23594] the subject title is served from what its reader may read', (
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const id of [RESTRICTED, READER, MASKED, SUBMITTER]) {
await engine.insert('sys_user', { id, name: id, email: `${id}@example.com` }, { context: SYSTEM } as any);
}
diff --git a/packages/plugins/plugin-approvals/src/sys-approval-delegation-delegator-picker.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-delegation-delegator-picker.test.ts
index 8b295845603..d531e0c86d4 100644
--- a/packages/plugins/plugin-approvals/src/sys-approval-delegation-delegator-picker.test.ts
+++ b/packages/plugins/plugin-approvals/src/sys-approval-delegation-delegator-picker.test.ts
@@ -33,6 +33,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { EngineCountOptions, EngineQueryOptions } from '@objectstack/spec/data';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
import { bindDelegationWriteGuard } from './lifecycle-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'sys_approval_delegation';
const SYSTEM = { context: { isSystem: true } } as any;
@@ -96,6 +97,9 @@ describe('sys_approval_delegation — the Delegator picker offers only what the
engine.registry.registerObject(sysUser as any, 'com.objectstack.test.delegator-picker');
engine.registry.registerObject(SysApprovalDelegation as any, 'com.objectstack.test.delegator-picker');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
bindDelegationWriteGuard(engine as any);
for (const u of USERS) await engine.insert('sys_user', u, SYSTEM);
});
diff --git a/packages/plugins/plugin-approvals/src/sys-approval-delegation-display-title.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-delegation-display-title.test.ts
index 0c6cfe49cd1..ecc8cad4da6 100644
--- a/packages/plugins/plugin-approvals/src/sys-approval-delegation-display-title.test.ts
+++ b/packages/plugins/plugin-approvals/src/sys-approval-delegation-display-title.test.ts
@@ -40,6 +40,7 @@ import {
import { SqlDriver } from '@objectstack/driver-sql';
import { resolveDisplayField } from '@objectstack/spec/data';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
const OBJECT = 'sys_approval_delegation';
@@ -94,6 +95,9 @@ describe('[#20044] sys_approval_delegation resolves a real record title under AD
await engine.init();
engine.registry.registerObject(SysApprovalDelegation as any, 'com.objectstack.test.20044');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterAll(async () => {
diff --git a/packages/plugins/plugin-approvals/src/sys-approval-display-title.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-display-title.test.ts
index f762b93c39a..d02051df63e 100644
--- a/packages/plugins/plugin-approvals/src/sys-approval-display-title.test.ts
+++ b/packages/plugins/plugin-approvals/src/sys-approval-display-title.test.ts
@@ -42,6 +42,7 @@ import { resolveDisplayField } from '@objectstack/spec/data';
import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalRequest } from './sys-approval-request.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
@@ -86,6 +87,9 @@ describe('[#20015] approval objects resolve a real record title under ADR-0079 o
engine.registry.registerObject(o as any, 'com.objectstack.test.20015');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterAll(async () => {
diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request-title.integration.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-request-title.integration.test.ts
index a5ad0f6d2c4..7ade70c0214 100644
--- a/packages/plugins/plugin-approvals/src/sys-approval-request-title.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/sys-approval-request-title.integration.test.ts
@@ -38,6 +38,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { SysApprovalRequest } from './sys-approval-request.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
const asUser = (userId: string) => ({ userId, positions: [], permissions: [] }) as any;
@@ -132,6 +133,9 @@ describe('#24665 — the approval request title is the flow label the approvals
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const u of [{ id: 'u_sub', name: 'Sam' }, { id: APPROVER, name: 'Ada' }]) {
await engine.insert('sys_user', u, { context: SYSTEM } as any);
}
diff --git a/packages/plugins/plugin-approvals/src/sys-approval-request-viewer.conformance.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-request-viewer.conformance.test.ts
index c3dbd865494..b514e99d669 100644
--- a/packages/plugins/plugin-approvals/src/sys-approval-request-viewer.conformance.test.ts
+++ b/packages/plugins/plugin-approvals/src/sys-approval-request-viewer.conformance.test.ts
@@ -36,6 +36,7 @@ import { ADMIN_FULL_ACCESS } from '@objectstack/spec/identity';
import { ApprovalService } from './approval-service.js';
import { ApprovalsServicePlugin } from './approvals-plugin.js';
import { SysApprovalRequest } from './sys-approval-request.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The value types a read-attachment leaf may declare (the spec's closed set). */
type LeafType = NonNullable[string][string];
@@ -133,6 +134,9 @@ describe('sys_approval_request — the served viewer block conforms to attachedO
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const id of ['u_sub', 'u_app', 'u_root']) {
await engine.insert('sys_user', { id, name: id, email: `${id}@example.com` }, { context: SYSTEM } as any);
}
diff --git a/packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts b/packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts
index 71552a094a9..e9ea358a995 100644
--- a/packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts
+++ b/packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts
@@ -66,6 +66,7 @@ import { SysApprovalAction } from './sys-approval-action.object.js';
import { SysApprovalApprover } from './sys-approval-approver.object.js';
import { SysApprovalDelegation } from './sys-approval-delegation.object.js';
import { SysApprovalToken } from './sys-approval-token.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const TOKEN = 'sys_approval_token';
const OBJECT = 'tokdoor_sheet';
@@ -143,6 +144,9 @@ async function boot(): Promise {
engine.registry.registerObject(def as any, 'approvals-token-door-test', 'approvals-token-door-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, { id: 'sheet_1', title: 'tokdoor ledger sheet' }, { context: SYSTEM } as any);
const services: Record = { objectql: engine };
diff --git a/packages/plugins/plugin-audit/src/activity-changed-fields-backfill.integration.test.ts b/packages/plugins/plugin-audit/src/activity-changed-fields-backfill.integration.test.ts
index 7c3e33023ab..ece3ec454e9 100644
--- a/packages/plugins/plugin-audit/src/activity-changed-fields-backfill.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/activity-changed-fields-backfill.integration.test.ts
@@ -24,6 +24,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { backfillActivityChangedFields } from './activity-changed-fields-backfill.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const ITEM = 'acb_item';
@@ -71,6 +72,9 @@ describe('[ADR-0139 E6] backfillActivityChangedFields', () => {
engine.registerDriver(driver, true);
engine.registry.registerObject(itemObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const id = (await engine.insert(ITEM, { name: 'x', stage: 'open', f_a: '1', f_b: '1' }, { context: SYS })).id;
const update = (patch: Row) => engine.update(ITEM, patch, { where: { id }, context: SYS });
diff --git a/packages/plugins/plugin-audit/src/activity-field-redaction.test.ts b/packages/plugins/plugin-audit/src/activity-field-redaction.test.ts
index 6540d261fd5..ea9c893f57f 100644
--- a/packages/plugins/plugin-audit/src/activity-field-redaction.test.ts
+++ b/packages/plugins/plugin-audit/src/activity-field-redaction.test.ts
@@ -41,6 +41,7 @@ import type { EngineQueryOptions } from '@objectstack/spec/data';
import { AuditPlugin } from './audit-plugin.js';
import { resolveServedFields } from './activity-field-redaction.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const ITEM = 'afr_item';
@@ -159,6 +160,9 @@ describe('[#21081] sys_activity value-bearing columns are served through the sec
engine.registry.registerObject(itemObject as any, HARNESS_PACKAGE);
engine.registry.registerObject(labelledObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Every row below is written by the real audit writer's CRUD mirror.
ids.item = (await engine.insert(
diff --git a/packages/plugins/plugin-audit/src/activity-milestone-kind.integration.test.ts b/packages/plugins/plugin-audit/src/activity-milestone-kind.integration.test.ts
index 91844faf349..620d5d5dbbf 100644
--- a/packages/plugins/plugin-audit/src/activity-milestone-kind.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/activity-milestone-kind.integration.test.ts
@@ -62,6 +62,7 @@ import { AuditPlugin } from './audit-plugin.js';
import { keptRowsOfParent } from './activity-field-redaction.js';
import { declaredMilestoneWatchedFields, milestoneKeyOf, planActivityStamp } from './activity-changed-fields.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const ITEM = 'amk_item';
@@ -166,6 +167,9 @@ describe('[#22771] a fired milestone is served with metadata.kind, judged like t
engine.registerDriver(driver, true);
engine.registry.registerObject(itemObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Every mirror row below is written by the real audit writer's CRUD mirror.
const update = (patch: Row) => engine.update(ITEM, patch, { where: { id: itemId }, context: SYS });
diff --git a/packages/plugins/plugin-audit/src/activity-predicate-guard.test.ts b/packages/plugins/plugin-audit/src/activity-predicate-guard.test.ts
index 493a46cd97a..361986ec82d 100644
--- a/packages/plugins/plugin-audit/src/activity-predicate-guard.test.ts
+++ b/packages/plugins/plugin-audit/src/activity-predicate-guard.test.ts
@@ -39,6 +39,7 @@ import { AuditPlugin } from './audit-plugin.js';
import { redactActivityRows } from './activity-field-redaction.js';
import { ACTIVITY_QUERY_GUARD, AUDIT_LOG_QUERY_GUARD, namedValueBearingColumns, pinnedParentObject } from './parent-field-query-guard.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const ITEM = 'apg_item';
@@ -216,6 +217,9 @@ describe('[#21154] a query over the activity text by a reader withheld a parent
engine.registry.registerObject(object as any, HARNESS_PACKAGE);
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Every row is written by the real audit writer's CRUD mirror.
ids.item = (await engine.insert(
diff --git a/packages/plugins/plugin-audit/src/activity-read-visibility.integration.test.ts b/packages/plugins/plugin-audit/src/activity-read-visibility.integration.test.ts
index fb84a1ce94f..a43f150e3f3 100644
--- a/packages/plugins/plugin-audit/src/activity-read-visibility.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/activity-read-visibility.integration.test.ts
@@ -38,6 +38,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
/** A parent object whose rows each caller reads only when it owns them. */
@@ -94,6 +95,9 @@ describe('sys_activity read visibility — the plugin read path', () => {
engine.registry.registerObject(ownedObject as any, HARNESS_PACKAGE);
engine.registry.registerObject(openObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Parent readability: a non-admin caller reads the ledger rows it owns.
engine.registerMiddleware(async (op: any, next: () => Promise) => {
diff --git a/packages/plugins/plugin-audit/src/activity-withheld-update.integration.test.ts b/packages/plugins/plugin-audit/src/activity-withheld-update.integration.test.ts
index fc14952edcb..38ad7a9f0f4 100644
--- a/packages/plugins/plugin-audit/src/activity-withheld-update.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/activity-withheld-update.integration.test.ts
@@ -49,6 +49,7 @@ import { AuditPlugin } from './audit-plugin.js';
import { computeWithheldUpdateFilter, keptRowsOfParent } from './activity-field-redaction.js';
import { planActivityStamp } from './activity-changed-fields.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
/** A record the member reads with two fields withheld. */
@@ -171,6 +172,9 @@ describe('[#21388] an update whose every recorded change is withheld from the re
engine.registry.registerObject(sealedObject as any, HARNESS_PACKAGE);
engine.registry.registerObject(prefixObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Every row below is written by the real audit writer's CRUD mirror.
const update = (object: string, id: string, patch: Row) =>
diff --git a/packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts b/packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts
index 82c74850aea..f7a0d234b5d 100644
--- a/packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-log-field-redaction.test.ts
@@ -48,6 +48,7 @@ import type { EngineQueryOptions } from '@objectstack/spec/data';
import { AuditPlugin } from './audit-plugin.js';
import { redactAuditLogRows } from './audit-log-field-redaction.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const LEDGER = 'sys_audit_log';
const ITEM = 'alr_item';
@@ -155,6 +156,9 @@ describe('[#21155] sys_audit_log before/after snapshots are served through the s
engine.registerDriver(driver, true);
engine.registry.registerObject(itemObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Every record-write row below is written by the real audit writer's CRUD mirror.
ids.live = (await engine.insert(
diff --git a/packages/plugins/plugin-audit/src/audit-log-predicate-guard.test.ts b/packages/plugins/plugin-audit/src/audit-log-predicate-guard.test.ts
index 145b7e3780f..1c336a8e268 100644
--- a/packages/plugins/plugin-audit/src/audit-log-predicate-guard.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-log-predicate-guard.test.ts
@@ -36,6 +36,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import type { EngineAggregateOptions, EngineQueryOptions } from '@objectstack/spec/data';
import { AuditPlugin } from './audit-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const LEDGER = 'sys_audit_log';
const ITEM = 'alg_item';
@@ -163,6 +164,9 @@ describe('[#21154] a query over the ledger snapshots by a reader withheld a pare
engine.registerDriver(driver, true);
for (const object of [itemObject, openObject]) engine.registry.registerObject(object as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Every ledger row is written by the real audit writer's CRUD mirror.
ids.item = (await engine.insert(ITEM, { name: 'item', f_masked: V.masked1, f_unserved: V.unserved1 }, { context: SYS })).id;
diff --git a/packages/plugins/plugin-audit/src/audit-log-read-visibility.integration.test.ts b/packages/plugins/plugin-audit/src/audit-log-read-visibility.integration.test.ts
index b914a29712e..9fca6247875 100644
--- a/packages/plugins/plugin-audit/src/audit-log-read-visibility.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-log-read-visibility.integration.test.ts
@@ -37,6 +37,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const LEDGER = 'sys_audit_log';
/** A parent object whose records each caller reads only when it owns them. */
@@ -117,6 +118,9 @@ describe('[#21175] sys_audit_log read visibility — the plugin read path', () =
engine.registry.registerObject(ownedObject as any, HARNESS_PACKAGE);
engine.registry.registerObject(openObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Parent readability: a non-admin caller reads the vault records it owns.
engine.registerMiddleware(async (op: any, next: () => Promise) => {
diff --git a/packages/plugins/plugin-audit/src/audit-record-label-display-field.test.ts b/packages/plugins/plugin-audit/src/audit-record-label-display-field.test.ts
index 88871c68cfc..2ddd93624a3 100644
--- a/packages/plugins/plugin-audit/src/audit-record-label-display-field.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-record-label-display-field.test.ts
@@ -39,6 +39,7 @@ import type { EngineQueryOptions } from '@objectstack/spec/data';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const HARNESS_PACKAGE = 'com.objectstack.audit.test.record-label-display-field';
@@ -163,6 +164,9 @@ describe('[#21878] the activity record label is the ADR-0079 title, with the id
engine.registerDriver(driver, true);
for (const o of objects) engine.registry.registerObject(o as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const insert = async (object: string, data: Row) => String((await engine.insert(object, data, { context: SYS })).id);
diff --git a/packages/plugins/plugin-audit/src/audit-write-locale-settings-seam.integration.test.ts b/packages/plugins/plugin-audit/src/audit-write-locale-settings-seam.integration.test.ts
index 10baf3d402d..eee4f9ece55 100644
--- a/packages/plugins/plugin-audit/src/audit-write-locale-settings-seam.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-write-locale-settings-seam.integration.test.ts
@@ -10,6 +10,7 @@ import { ObjectQL, ObjectQLPlugin } from '@objectstack/objectql';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* [#24235] A summary line is stored ONCE, at write time, so the writer's held
@@ -80,6 +81,9 @@ describe('audit writers — a localization change reaches the very next audited
engine.registry.registerObject(sysSetting as any, HARNESS_PACKAGE);
engine.registry.registerObject(task as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
sql = async (statement, bindings) => {
const result = await (driver as unknown as { execute(s: string, b: unknown[]): Promise }).execute(
diff --git a/packages/plugins/plugin-audit/src/audit-writers.definition-follows-metadata.integration.test.ts b/packages/plugins/plugin-audit/src/audit-writers.definition-follows-metadata.integration.test.ts
index 42d36e85f82..2f1ccb43c32 100644
--- a/packages/plugins/plugin-audit/src/audit-writers.definition-follows-metadata.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-writers.definition-follows-metadata.integration.test.ts
@@ -12,6 +12,7 @@ import { SECRET_MASK } from '@objectstack/objectql/core';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* [#24565] A definition changed through the metadata door takes effect in the
@@ -95,6 +96,9 @@ describe('audit writers answer from the current definition after a metadata-door
HARNESS_PACKAGE,
);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
protocol = kernel.getService('protocol') as typeof protocol;
}, 120_000);
diff --git a/packages/plugins/plugin-audit/src/audit-writers.own-ui-state.integration.test.ts b/packages/plugins/plugin-audit/src/audit-writers.own-ui-state.integration.test.ts
index db1206ce1fd..e47102ddb20 100644
--- a/packages/plugins/plugin-audit/src/audit-writers.own-ui-state.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/audit-writers.own-ui-state.integration.test.ts
@@ -12,6 +12,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { SysUserPreference } from '@objectstack/platform-objects/identity';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* [#23804, ruling record 6212163130] A user's own write to their own per-user
@@ -94,6 +95,9 @@ describe('audit writers — a user\'s own UI-state write leaves no ledger row, o
engine.registry.registerObject(SysUserPreference as any, HARNESS_PACKAGE);
engine.registry.registerObject(sharingRule as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
sql = async (statement, bindings) => {
const result = await (driver as unknown as { execute(s: string, b: unknown[]): Promise }).execute(
diff --git a/packages/plugins/plugin-audit/src/comment-edit-stamp.integration.test.ts b/packages/plugins/plugin-audit/src/comment-edit-stamp.integration.test.ts
index a6368622a8e..48895c799e9 100644
--- a/packages/plugins/plugin-audit/src/comment-edit-stamp.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/comment-edit-stamp.integration.test.ts
@@ -41,6 +41,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { mapDataError } from '@objectstack/types';
import { AuditPlugin } from './audit-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const COMMENT = 'sys_comment';
/** A parent object every caller reads. */
@@ -85,6 +86,9 @@ describe('sys_comment — the platform stamps an edit of the body (#23124)', ()
engine.registerDriver(driver, true);
engine.registry.registerObject(boardObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
}, 120_000);
afterAll(async () => {
diff --git a/packages/plugins/plugin-audit/src/comment-parent-pair.integration.test.ts b/packages/plugins/plugin-audit/src/comment-parent-pair.integration.test.ts
index f556480e1e5..fac39863728 100644
--- a/packages/plugins/plugin-audit/src/comment-parent-pair.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/comment-parent-pair.integration.test.ts
@@ -31,6 +31,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { parseCommentThreadId } from './comment-access-hooks.js';
import { backfillCommentParentPair } from './comment-parent-pair-backfill.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const COMMENT = 'sys_comment';
const REACTION = 'sys_comment_reaction';
@@ -179,6 +180,9 @@ describe('[ADR-0139 E7] the comment-thread read gate over the stamped parent pai
engine.registry.registerObject(objectOf(GATED) as any, HARNESS_PACKAGE);
engine.registry.registerObject(objectOf(FAR, { datasource: 'warehouse' }) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
installSecurityMiddleware(engine);
engine.registerMiddleware(async (op: any, next: () => Promise) => {
if (READ_OPS.has(op.operation) && op.context && !op.context.isSystem && !isAdmin(op.context) && op.ast) {
@@ -421,6 +425,9 @@ describe('[ADR-0139 E7] backfillCommentParentPair', () => {
({ kernel, engine } = await bootKernel(driver));
engine.registry.registerObject(objectOf(DOC) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (let i = 0; i < 5; i++) {
const doc = String((await engine.insert(DOC, { name: `d${i}` }, { context: SYS })).id);
rowIds.push(String((await engine.insert(COMMENT, commentOn(DOC, doc, `c${i}`), { context: SYS })).id));
@@ -526,6 +533,9 @@ describe('[ADR-0139 D3] on a driver that refuses the leaf, a comment read fails
({ kernel, engine } = await bootKernel(new RefusingDriver({ filename: ':memory:' })));
engine.registry.registerObject(objectOf(DOC) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const doc = String((await engine.insert(DOC, { name: 'one', owner: MEMBER_A.userId }, { context: SYS })).id);
await engine.insert(COMMENT, commentOn(DOC, doc, 'one'), { context: SYS });
}, 120_000);
diff --git a/packages/plugins/plugin-audit/src/comment-reaction-access.integration.test.ts b/packages/plugins/plugin-audit/src/comment-reaction-access.integration.test.ts
index 46592fc9cf1..654072f748c 100644
--- a/packages/plugins/plugin-audit/src/comment-reaction-access.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/comment-reaction-access.integration.test.ts
@@ -52,6 +52,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { mapDataError } from '@objectstack/types';
import { AuditPlugin } from './audit-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const COMMENT = 'sys_comment';
const REACTION = 'sys_comment_reaction';
@@ -126,6 +127,9 @@ describe('sys_comment_reaction — the plugin read and write paths', () => {
engine.registry.registerObject(ownedObject as any, HARNESS_PACKAGE);
engine.registry.registerObject(openObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Parent readability: a non-admin caller reads the ledger rows it owns.
engine.registerMiddleware(async (op: any, next: () => Promise) => {
diff --git a/packages/plugins/plugin-audit/src/default-organization.testkit.ts b/packages/plugins/plugin-audit/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..5ca7ea9f37d
--- /dev/null
+++ b/packages/plugins/plugin-audit/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/plugin-audit',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/plugins/plugin-audit/src/parent-record-read-admission.integration.test.ts b/packages/plugins/plugin-audit/src/parent-record-read-admission.integration.test.ts
index cb0d7f7fde1..8b857497f73 100644
--- a/packages/plugins/plugin-audit/src/parent-record-read-admission.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/parent-record-read-admission.integration.test.ts
@@ -47,6 +47,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { AuditPlugin } from './audit-plugin.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const LEDGER = 'sys_audit_log';
@@ -144,6 +145,9 @@ describe('parent-record read gates: the object-level read admission', () => {
engine.registry.registerObject(objectOf(VAULT, 'Vault') as any, HARNESS_PACKAGE);
engine.registry.registerObject(objectOf(OPEN, 'Board') as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// plugin-security's refusal of a vault read, for anyone but the admin.
engine.registerMiddleware(async (op: any, next: () => Promise) => {
diff --git a/packages/plugins/plugin-audit/src/parent-record-read-leaf.integration.test.ts b/packages/plugins/plugin-audit/src/parent-record-read-leaf.integration.test.ts
index ec4e6d88237..2b78169020e 100644
--- a/packages/plugins/plugin-audit/src/parent-record-read-leaf.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/parent-record-read-leaf.integration.test.ts
@@ -51,6 +51,7 @@ import { AuditPlugin } from './audit-plugin.js';
import { LEDGER_AUDIT_CAPABILITY } from './audit-log-read-visibility.js';
import { PARENT_GATE_SCAN_LIMIT } from './parent-record-read-gate.js';
import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACTIVITY = 'sys_activity';
const LEDGER = 'sys_audit_log';
@@ -207,6 +208,9 @@ describe('[ADR-0139 E5] the parent-record read gates on the leaf path', () => {
engine.registry.registerObject(objectOf(GATED) as any, HARNESS_PACKAGE);
engine.registry.registerObject(objectOf(FAR, { datasource: 'warehouse' }) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
installSecurityMiddleware(engine);
// D5's second class: an object-scoped read middleware narrows the gated parent.
engine.registerMiddleware(async (op: any, next: () => Promise) => {
@@ -438,6 +442,9 @@ describe('[ADR-0139 D3] on a driver that refuses the leaf, the read fails and sa
({ kernel, engine } = await bootKernel(new RefusingDriver({ filename: ':memory:' })));
engine.registry.registerObject(objectOf(DOC) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(DOC, { name: 'one', owner: MEMBER_A.userId }, { context: SYS });
}, 120_000);
diff --git a/packages/plugins/plugin-audit/src/read-audit-view-instant-preservation.integration.test.ts b/packages/plugins/plugin-audit/src/read-audit-view-instant-preservation.integration.test.ts
index 3a9eb5e5c78..67643eeae35 100644
--- a/packages/plugins/plugin-audit/src/read-audit-view-instant-preservation.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/read-audit-view-instant-preservation.integration.test.ts
@@ -74,6 +74,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { installReadAuditWriter, READ_AUDIT_ACTION } from './read-audit.js';
import { SysAuditLog } from './objects/index.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The ledger under test — the REAL shipped object definition, not a stand-in. */
const LEDGER = 'sys_audit_log';
@@ -147,6 +148,9 @@ describe('[#16829] the record-view ledger keeps the VIEW instant through the rea
engine.registry.registerObject(SysAuditLog as any, HARNESS_PACKAGE);
// Real DDL for both tables, including the builtin audit timestamp columns.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(
AUDITED_OBJECT,
diff --git a/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts b/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts
index bb5a7cb0627..980f8bde8f0 100644
--- a/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts
+++ b/packages/plugins/plugin-audit/src/sys-comment-reactions-door.integration.test.ts
@@ -28,6 +28,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { mapDataError } from '@objectstack/types';
import { AuditPlugin } from './audit-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const COMMENT = 'sys_comment';
/** A parent object every caller reads. */
@@ -74,6 +75,9 @@ describe('sys_comment — a write naming the retired reactions column is refused
engine.registerDriver(driver, true);
engine.registry.registerObject(openObject as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const board = await engine.insert(OPEN, { name: 'board' }, { context: SYS });
threadId = `${OPEN}:${board.id}`;
diff --git a/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts b/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts
index c37085455a5..2624c7eec6d 100644
--- a/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts
+++ b/packages/plugins/plugin-auth/src/account-identity-preflight.test.ts
@@ -46,6 +46,7 @@ import {
SYS_ACCOUNT_OBJECT,
} from './account-identity-preflight.js';
import { authIdentityObjects } from './manifest.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM = { context: { isSystem: true } } as never;
@@ -105,6 +106,10 @@ async function bootEngine(objects: unknown[]): Promise {
engine.registry.registerObject(object as never, '@objectstack/plugin-auth');
}
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
return engine;
}
diff --git a/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts b/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts
index 5c8d48ce2b5..8706c212e60 100644
--- a/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts
+++ b/packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts
@@ -47,6 +47,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AuthManager } from './auth-manager.js';
import { authIdentityObjects } from './manifest.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
/** The caller's value. Synthetic; asserted ABSENT from every carrier. */
const S = 'SENTINEL-21274-BOUND-VALUE';
@@ -252,6 +253,10 @@ for (const cell of CELLS) {
engine.registry.registerObject(sysPermissionSet as never, '@objectstack/plugin-security');
engine.registry.registerObject(sysUserPermissionSet as never, '@objectstack/plugin-security');
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// The seeded holder of the sentinel, then the constraint the two writes
// below will meet in the DATABASE — not in any check of ours.
diff --git a/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts b/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts
index d14080f2608..3e9e5c3d9f8 100644
--- a/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts
+++ b/packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts
@@ -42,6 +42,7 @@ import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-
import { ensureDefaultOrganization } from './ensure-default-organization.js';
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
import { withLegacyPermissionSetTable } from './__tests__/legacy-catalog-objects.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -134,6 +135,10 @@ async function standingByReader(posture: Posture): Promise undefined);
registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, {
diff --git a/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts b/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts
index e2384898862..1e3ce369050 100644
--- a/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts
+++ b/packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts
@@ -36,6 +36,7 @@ import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-
import { ensureDefaultOrganization } from './ensure-default-organization.js';
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
import { withLegacyPermissionSetTable } from './__tests__/legacy-catalog-objects.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as any;
const GUARD_PACKAGE = 'test.grant-readers-unnamed';
@@ -113,6 +114,10 @@ async function soleGrantAnchoredAdmin(): Promise {
]),
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D5] The Default Organization a `single` boot creates before
// `kernel:ready` (AuthPlugin's invariant); it owns the promotion's grant.
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, SYS);
diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.activation-ledger.test.ts b/packages/plugins/plugin-auth/src/last-admin-guard.activation-ledger.test.ts
index 46210f01378..e5e87ffeebd 100644
--- a/packages/plugins/plugin-auth/src/last-admin-guard.activation-ledger.test.ts
+++ b/packages/plugins/plugin-auth/src/last-admin-guard.activation-ledger.test.ts
@@ -21,6 +21,7 @@ import { resetPlatformAdminEmailMemo } from '@objectstack/core';
import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-guard.js';
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM = { context: { isSystem: true } } as const;
const text = { type: 'text' as const };
@@ -116,6 +117,10 @@ async function boot(opts: Boot = { permissions: [ADMIN_FULL_ACCESS] }): Promise<
await engine.init();
for (const o of objects) engine.registry.registerObject(o as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const guardEngine = opts.readThrough?.(engine) ?? (engine as unknown as LastAdminGuardEngine);
if (opts.permissions) {
const catalog = { permissions: opts.permissions.map((name) => ({ name })) };
diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts b/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts
index 6c8c990da5c..b849e651846 100644
--- a/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts
+++ b/packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts
@@ -52,6 +52,7 @@ import { resetPlatformAdminEmailMemo } from '@objectstack/core';
import { registerLastAdminGuard, USER_STANDING_KEYS, type LastAdminGuardEngine } from './last-admin-guard.js';
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const ENV = 'OS_PLATFORM_OWNER_EMAIL';
/**
@@ -205,6 +206,10 @@ async function boot(opts: { unguarded?: boolean } = {}): Promise {
engine.registry.registerObject(o as never);
}
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D4] The set a grant names is the catalog's definition.
bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] });
if (!opts.unguarded) {
diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.re-pricing.test.ts b/packages/plugins/plugin-auth/src/last-admin-guard.re-pricing.test.ts
index f79c3f47a70..4fa2036206f 100644
--- a/packages/plugins/plugin-auth/src/last-admin-guard.re-pricing.test.ts
+++ b/packages/plugins/plugin-auth/src/last-admin-guard.re-pricing.test.ts
@@ -40,6 +40,7 @@ import { resetPlatformAdminEmailMemo } from '@objectstack/core';
import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-guard.js';
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
/** [ADR-0131 D3, ADR-0126 §4] The activation ledger a permission set is switched off in. */
const sysMetadataActivation = {
@@ -172,6 +173,10 @@ async function boot(): Promise {
engine.registry.registerObject(o as never);
}
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D4] The set a grant names is the catalog's definition.
bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] });
registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, { packageId: 'test.last-admin-guard-re-pricing' });
diff --git a/packages/plugins/plugin-auth/src/last-admin-guard.test.ts b/packages/plugins/plugin-auth/src/last-admin-guard.test.ts
index 283db61973f..14789fee1fb 100644
--- a/packages/plugins/plugin-auth/src/last-admin-guard.test.ts
+++ b/packages/plugins/plugin-auth/src/last-admin-guard.test.ts
@@ -66,6 +66,7 @@ import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js
import { sqliteEnginePool, type BootedSqliteEngine } from './__tests__/sqlite-engine-pool.testkit.js';
import { admin } from 'better-auth/plugins/admin';
import { organization } from 'better-auth/plugins/organization';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
// ---------------------------------------------------------------------------
// Fixtures
@@ -290,6 +291,10 @@ async function bootEngine(opts: BootOptions, track?: ObjectQL[]): Promise();
catalogs.set(engine, names);
const catalog = () => ({ permissions: [...names].map((name) => ({ name })) });
diff --git a/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts b/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..b0cbb5efa38
--- /dev/null
+++ b/packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Return engine-synced tables to the shape a store built BEFORE 18.x has: the
+ * tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x the SQL driver creates a table's tenant column NOT
+ * NULL. A store an earlier release built still holds the nullable column and
+ * its organization-less rows — the holdings, grants and seeds this package's
+ * NULL readers still answer for until the organization-ownership ceremony has
+ * attributed them. A suite whose subject is those rows calls this after
+ * `syncSchemas()` and before seeding them. Done through each table's driver
+ * and its own drift apply door (`relax_not_null`), never a hand `ALTER`.
+ */
+export async function relaxTenantColumns(
+ engine: { getDriverForObject(object: string): unknown },
+ tables: readonly string[],
+ column = 'organization_id',
+): Promise {
+ for (const table of tables) {
+ const driver = engine.getDriverForObject(table) as {
+ applyMigrationEntries(entries: never[]): Promise<{ applied: unknown[] }>;
+ };
+ const op = { type: 'relax_not_null', table, column };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column, category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
+ }
+}
+
+/**
+ * {@link relaxTenantColumns} over every object of an app that carries a tenant
+ * column as the engine registered it — the shape a whole pre-18 store has.
+ */
+export async function relaxEveryTenantColumn(
+ engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown },
+ objects: readonly { name: string }[],
+): Promise {
+ const tenant = objects
+ .map((o) => o.name)
+ .filter((name) => {
+ const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined;
+ return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false;
+ });
+ await relaxTenantColumns(engine, tenant);
+}
+
+/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */
+export async function relaxEveryRegisteredTenantColumn(engine: {
+ getDriverForObject(object: string): unknown;
+ getSchema(object: string): unknown;
+ registry: { getAllObjects(): readonly { name: string }[] };
+}): Promise {
+ await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
+}
diff --git a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts
index 2c163359999..4f09ab5bb2a 100644
--- a/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts
+++ b/packages/plugins/plugin-auth/src/scim-deactivation-reconcile-user.test.ts
@@ -99,6 +99,7 @@ import '@better-auth/core/context';
import '@better-auth/core/error';
import '@better-auth/oauth-provider';
import '@better-auth/scim';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
/**
* [#23184] Every sign-in below verifies the password the case attached, which
@@ -241,6 +242,10 @@ async function bootEngine(): Promise {
engine.registry.registerObject(object as never, '@objectstack/plugin-auth');
}
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D4] `admin_full_access` is a catalog definition — what the
// break-glass guard, like the resolver, resolves a grant's set name to.
bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] });
diff --git a/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts b/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts
index 514040ce296..58a891f704f 100644
--- a/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts
+++ b/packages/plugins/plugin-auth/src/signup-existing-address-refusal.test.ts
@@ -69,6 +69,7 @@ import { AuthManager } from './auth-manager.js';
import { authIdentityObjects } from './manifest.js';
import { SELF_REGISTRATION_CLOSED } from './audience-posture.js';
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const BASE = 'http://localhost:3000';
const AUTH = `${BASE}/api/v1/auth`;
@@ -155,6 +156,10 @@ async function bootEngine(): Promise {
engine.registry.registerObject(sysPermissionSet as never, '@objectstack/plugin-security');
engine.registry.registerObject(sysUserPermissionSet as never, '@objectstack/plugin-security');
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D4] The declared self-registration set is a catalog definition
// — what the admission asks before it admits.
return bindTestSecurityCatalog(engine, { permissions: [{ name: 'member_default' }] });
diff --git a/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts b/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts
index a38d8535d0c..d18b30b9ff7 100644
--- a/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts
+++ b/packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts
@@ -88,6 +88,7 @@ import type { PluginContext } from '@objectstack/core';
import { inviteForAudienceGate } from './audience-gate-test-support';
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit';
import { ADMIN_FULL_ACCESS_CAPABILITIES } from '@objectstack/spec';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const BASE = 'http://localhost:3000';
const AUTH = `${BASE}/api/v1/auth`;
@@ -187,6 +188,10 @@ async function bootEngine(): Promise {
engine.registry.registerObject(sysPosition as never, '@objectstack/plugin-auth');
engine.registry.registerObject(sysUserPosition as never, '@objectstack/plugin-auth');
await engine.syncSchemas();
+ // The organization-less rows below (global sets, grants, identities) exist
+ // only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
+ // NOT NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D4] The platform's `admin_full_access` definition, which a
// grant of it resolves through (no security plugin binds one here).
return bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS, ...ADMIN_FULL_ACCESS_CAPABILITIES }] });
diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts
index b7046c0ca9d..7718259ab8f 100644
--- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts
+++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts
@@ -54,6 +54,7 @@ import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testki
import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM_CTX = { isSystem: true };
@@ -110,6 +111,10 @@ async function boot(): Promise {
],
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D5] The Default Organization a `single` boot creates before any
// `kernel:ready` hook runs; it owns the promotion's grant.
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, { context: { isSystem: true } });
diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts
index c8db8e5b254..09693b40b30 100644
--- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts
+++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-existing-holder-scan.test.ts
@@ -86,6 +86,7 @@ import {
import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM_CTX = { isSystem: true };
const OWNER_ENV = 'OS_PLATFORM_OWNER_EMAIL';
@@ -137,6 +138,10 @@ async function boot(): Promise {
objects: [SysPermissionSet, SysUserPermissionSet, SysUser, SysAccount, SysOrganization],
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D5] The Default Organization a `single` boot creates before any
// `kernel:ready` hook runs; it owns the promotion's grant.
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, { context: { isSystem: true } });
diff --git a/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts b/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts
index 8aa6ec10b77..55b45d567f1 100644
--- a/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts
+++ b/packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts
@@ -74,6 +74,7 @@ import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testki
import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYSTEM_CTX = { isSystem: true };
const OWNER_ENV = 'OS_PLATFORM_OWNER_EMAIL';
@@ -128,6 +129,10 @@ async function boot(): Promise {
objects: [SysPermissionSet, SysUserPermissionSet, SysUser, SysAccount, SysOrganization],
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// [ADR-0131 D3/D5] The Default Organization a `single` boot creates before any
// `kernel:ready` hook runs; it owns the promotion's grant.
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, { context: { isSystem: true } });
diff --git a/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts b/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts
index 24c2d6cbbfd..535d57c2e00 100644
--- a/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts
+++ b/packages/plugins/plugin-security/src/builtin-positions.boot.test.ts
@@ -70,6 +70,7 @@ import { securityObjects, SECURITY_PLUGIN_ID } from './manifest.js';
import { securityBuiltinPositions } from './builtin-positions.js';
import { withLegacyCatalogTables } from './__tests__/legacy-catalog-objects.testkit.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -231,6 +232,10 @@ async function boot(scenario: Scenario): Promise {
objects: withLegacyCatalogTables([SysUser, SysAccount, SysMember, SysOrganization, ...securityObjects]),
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
if (scenario.authoredPosition) {
// How a metadata author's saved definition is hydrated: no package.
(engine as any).registry.registerItem('position', { name: 'door_authored', label: 'Door Authored' }, 'name');
diff --git a/packages/plugins/plugin-security/src/by-id-write-unreadable-not-found.test.ts b/packages/plugins/plugin-security/src/by-id-write-unreadable-not-found.test.ts
index 9f24cd14ae2..31915c2bde0 100644
--- a/packages/plugins/plugin-security/src/by-id-write-unreadable-not-found.test.ts
+++ b/packages/plugins/plugin-security/src/by-id-write-unreadable-not-found.test.ts
@@ -34,6 +34,7 @@ import { recordNotFoundError } from '@objectstack/core';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -98,6 +99,9 @@ async function boot(): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(PARENT, [
{ id: 'p_plain', name: 'plain' },
{ id: 'p_cascade', name: 'has a hidden child' },
diff --git a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts
index f5ddf50b64f..3242f2b9f99 100644
--- a/packages/plugins/plugin-security/src/catalog-reference-report.test.ts
+++ b/packages/plugins/plugin-security/src/catalog-reference-report.test.ts
@@ -53,6 +53,7 @@ import {
type CatalogReferenceReport,
type CatalogReferenceReportEngine,
} from './catalog-reference-report.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -132,6 +133,10 @@ async function boot(opts: { posture?: 'single' | 'isolated' } = {}): Promise p.name === 'member_default')!;
@@ -225,6 +226,10 @@ beforeAll(async () => {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
// The two delegators exist as users, so the on-behalf-of link resolves rather
// than failing closed. Each holds the manager's set through a stored position
diff --git a/packages/plugins/plugin-security/src/controlled-by-parent-every-master-edit.test.ts b/packages/plugins/plugin-security/src/controlled-by-parent-every-master-edit.test.ts
index ac0017fb0dc..d978a2bb9ab 100644
--- a/packages/plugins/plugin-security/src/controlled-by-parent-every-master-edit.test.ts
+++ b/packages/plugins/plugin-security/src/controlled-by-parent-every-master-edit.test.ts
@@ -44,6 +44,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ORG = 'org_cbp_eme';
@@ -152,6 +153,10 @@ async function boot() {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
diff --git a/packages/plugins/plugin-security/src/controlled-by-parent-every-row-master.test.ts b/packages/plugins/plugin-security/src/controlled-by-parent-every-row-master.test.ts
index db6ae60217f..6fc3f2f8467 100644
--- a/packages/plugins/plugin-security/src/controlled-by-parent-every-row-master.test.ts
+++ b/packages/plugins/plugin-security/src/controlled-by-parent-every-row-master.test.ts
@@ -61,6 +61,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ORG = 'org_cbp_erm';
@@ -164,6 +165,10 @@ async function boot() {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
diff --git a/packages/plugins/plugin-security/src/controlled-by-parent-master-object-read.test.ts b/packages/plugins/plugin-security/src/controlled-by-parent-master-object-read.test.ts
index d8d77f076b1..bf6a3ae9d24 100644
--- a/packages/plugins/plugin-security/src/controlled-by-parent-master-object-read.test.ts
+++ b/packages/plugins/plugin-security/src/controlled-by-parent-master-object-read.test.ts
@@ -44,6 +44,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ORG = 'org_cbp_mor';
@@ -154,6 +155,10 @@ async function boot() {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
diff --git a/packages/plugins/plugin-security/src/controlled-by-parent-master-object-write.test.ts b/packages/plugins/plugin-security/src/controlled-by-parent-master-object-write.test.ts
index 274b723de70..96a980ff414 100644
--- a/packages/plugins/plugin-security/src/controlled-by-parent-master-object-write.test.ts
+++ b/packages/plugins/plugin-security/src/controlled-by-parent-master-object-write.test.ts
@@ -217,10 +217,14 @@ async function boot() {
let n = 0;
return {
engine,
- /** A stored detail under `master`, written as SYSTEM (seed replay skips the reference check). */
+ /**
+ * A stored detail under `master`, written as SYSTEM (seed replay skips the
+ * reference check). A driver write is not stamped, so it names the rig's
+ * organization itself.
+ */
seedDetail: async (detail: string, master: string) => {
const id = `${detail}_seed_${n++}`;
- await e.getDriver(detail).create(detail, { id, note: 'seeded', master });
+ await e.getDriver(detail).create(detail, { id, note: 'seeded', master, organization_id: ORG });
return id;
},
stored: async (object: string, id: string) =>
diff --git a/packages/plugins/plugin-security/src/controlled-by-parent-write-shape-master-edit.test.ts b/packages/plugins/plugin-security/src/controlled-by-parent-write-shape-master-edit.test.ts
index 0f1869bb5b9..cb17c0d56c7 100644
--- a/packages/plugins/plugin-security/src/controlled-by-parent-write-shape-master-edit.test.ts
+++ b/packages/plugins/plugin-security/src/controlled-by-parent-write-shape-master-edit.test.ts
@@ -44,6 +44,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ORG = 'org_cbp_wsme';
@@ -148,6 +149,10 @@ async function boot() {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
diff --git a/packages/plugins/plugin-security/src/default-organization.testkit.ts b/packages/plugins/plugin-security/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..67b123c1ad9
--- /dev/null
+++ b/packages/plugins/plugin-security/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/plugin-security',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts b/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts
index 5a76dc67d38..684da9673dc 100644
--- a/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts
+++ b/packages/plugins/plugin-security/src/delegated-admin-gate-holding-organization.test.ts
@@ -34,6 +34,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { DelegatedAdminGate } from './delegated-admin-gate.js';
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
+import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js';
const ORG_A = 'org_a_acme';
/** Sorts BEFORE every `org_a` / `*_a_*` id, like the sibling suites. */
@@ -87,6 +88,9 @@ async function bootEngine() {
objects: OBJECTS,
} as any);
await engine.syncSchemas();
+ // The organization-less holdings and rows below exist only on a store built
+ // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it.
+ await relaxEveryTenantColumn(engine, OBJECTS);
engines.push(engine);
return engine;
}
diff --git a/packages/plugins/plugin-security/src/delegated-organization-membership.test.ts b/packages/plugins/plugin-security/src/delegated-organization-membership.test.ts
index f7670f3f8cf..2e6864b974e 100644
--- a/packages/plugins/plugin-security/src/delegated-organization-membership.test.ts
+++ b/packages/plugins/plugin-security/src/delegated-organization-membership.test.ts
@@ -50,6 +50,7 @@ import { SysOrganization, SysUser, SysMember } from '@objectstack/platform-objec
import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const ORG_A = 'org_a';
const ORG_C = 'org_c';
@@ -134,6 +135,10 @@ async function boot(posture: Posture) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
engines.push(engine);
for (const id of [ORG_A, ORG_C, ORG_D]) {
diff --git a/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts b/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts
index d87113708fc..4a9330a9643 100644
--- a/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts
+++ b/packages/plugins/plugin-security/src/delegated-write-never-exceeds-delegator.test.ts
@@ -45,6 +45,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ALPHA = 'org_alpha';
@@ -124,6 +125,10 @@ async function boot(posture: TenancyPosture) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
diff --git a/packages/plugins/plugin-security/src/explain-asker-gate.test.ts b/packages/plugins/plugin-security/src/explain-asker-gate.test.ts
index a1fc8b712f8..703574a3274 100644
--- a/packages/plugins/plugin-security/src/explain-asker-gate.test.ts
+++ b/packages/plugins/plugin-security/src/explain-asker-gate.test.ts
@@ -66,6 +66,7 @@ import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
import { PermissionEvaluator } from './permission-evaluator.js';
import { explainAccess, explainAccessForAsker, type ExplainEngineDeps, type ExplainInput } from './explain-engine.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -172,6 +173,10 @@ beforeAll(async () => {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const e = engine as unknown as { insert: (o: string, row: unknown, opts: never) => Promise };
for (const id of [MEMBER, ADMIN.userId, BLIND_ADMIN.userId, DELEGATE.userId, REFUSED_ADMIN.userId, REFUSED_DELEGATE.userId, EDIT_ONLY_USER]) {
diff --git a/packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts b/packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts
index f1341a53456..77d1baf81a7 100644
--- a/packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts
+++ b/packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts
@@ -44,6 +44,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import type { ExplainDecision } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -109,6 +110,9 @@ async function boot(makeDriver: () => Driver, predicate: string) {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
booted.push({ engine, driver, table: OBJ });
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts b/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts
index 549dc1d756b..4af40d1f223 100644
--- a/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts
+++ b/packages/plugins/plugin-security/src/explain-enforce-parity.test.ts
@@ -90,6 +90,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -326,6 +327,10 @@ async function bootRls(predicate: string, opts: { grantCrud?: boolean } = {}) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
await engine.insert(OBJ, [
{ id: 'r1', status: 'open', title: 'x', amount: 5 },
{ id: 'r2', status: 'open', title: 'open', amount: 7 },
@@ -471,6 +476,10 @@ async function bootPrincipal(source: PostureSource) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise };
for (const id of [ALPHA, BETA]) await e.insert('sys_organization', { id, name: id, slug: id }, SYS);
@@ -651,6 +660,10 @@ async function bootSharing(opts: { faultReadFilter?: boolean; authored?: boolean
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
await engine.insert(LINE, [
{ id: 'l_other', name: 'owned by someone else', owner_id: 'u_somebody' },
{ id: 'l_own', name: 'owned by the control', owner_id: 'u_own' },
@@ -823,6 +836,10 @@ async function bootDelegated(posture: TenancyPosture) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise };
for (const id of [ALPHA, BETA]) await e.insert('sys_organization', { id, name: id, slug: id }, SYS);
diff --git a/packages/plugins/plugin-security/src/explain-json-column-refusal.test.ts b/packages/plugins/plugin-security/src/explain-json-column-refusal.test.ts
index 4876471bad6..6ff17e0cf67 100644
--- a/packages/plugins/plugin-security/src/explain-json-column-refusal.test.ts
+++ b/packages/plugins/plugin-security/src/explain-json-column-refusal.test.ts
@@ -57,6 +57,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import type { ExplainDecision } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -122,6 +123,9 @@ async function boot(makeDriver: () => Driver, predicates: Record
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
booted.push({ engine, driver, table: OBJ });
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/explain-principal-person.test.ts b/packages/plugins/plugin-security/src/explain-principal-person.test.ts
index ec632f1a1be..420cef4b436 100644
--- a/packages/plugins/plugin-security/src/explain-principal-person.test.ts
+++ b/packages/plugins/plugin-security/src/explain-principal-person.test.ts
@@ -37,6 +37,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -122,6 +123,10 @@ beforeAll(async () => {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const e = engine as unknown as { insert: (o: string, row: unknown, opts: never) => Promise };
await e.insert('sys_user', [
diff --git a/packages/plugins/plugin-security/src/explain-read-absent-every-write-verb.test.ts b/packages/plugins/plugin-security/src/explain-read-absent-every-write-verb.test.ts
index 06078162097..861bfeb741b 100644
--- a/packages/plugins/plugin-security/src/explain-read-absent-every-write-verb.test.ts
+++ b/packages/plugins/plugin-security/src/explain-read-absent-every-write-verb.test.ts
@@ -46,6 +46,7 @@ import type { ExplainDecision, ExplainOperation } from '@objectstack/spec/securi
import { SecurityPlugin } from './security-plugin.js';
import { rlsOperationForVerb } from './lifecycle-verb-rls-operation.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -161,6 +162,9 @@ beforeAll(async () => {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// One unreadable and one readable row per (axis, verb), so a delete or a
// transfer that lands never changes another cell's row.
for (const { set } of AXES) {
diff --git a/packages/plugins/plugin-security/src/explain-read-absent-record-story.test.ts b/packages/plugins/plugin-security/src/explain-read-absent-record-story.test.ts
index b0f85c1eab4..73985629001 100644
--- a/packages/plugins/plugin-security/src/explain-read-absent-record-story.test.ts
+++ b/packages/plugins/plugin-security/src/explain-read-absent-record-story.test.ts
@@ -48,6 +48,7 @@ import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
import { explainAccess, type ExplainEngineDeps } from './explain-engine.js';
import { PermissionEvaluator } from './permission-evaluator.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -128,6 +129,9 @@ beforeAll(async () => {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Both masters belong to another user; only the second is shared to the
// caller, at `read`, which lets the caller read it and edit neither.
await engine.insert(MASTER, [
diff --git a/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts b/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts
index 7a62538a884..6540a058b42 100644
--- a/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts
+++ b/packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts
@@ -81,6 +81,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -134,6 +135,10 @@ async function boot(makeDriver: () => Driver, posture: TenancyPosture) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
const e = engine as unknown as { insert: (o: string, row: Record, opts: never) => Promise };
for (const id of [ALPHA, BETA]) await e.insert('sys_organization', { id, name: id, slug: id }, SYS);
diff --git a/packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts b/packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts
index d16e63fb4f4..6c0cb7efcb3 100644
--- a/packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts
+++ b/packages/plugins/plugin-security/src/file-field-hydration-refused.test.ts
@@ -26,6 +26,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from './security-plugin';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DOC = 'sec_file_refusal_doc';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
@@ -90,6 +91,9 @@ describe('a file field the security layer refused reads as refused, not as "no f
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts b/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts
index d450b652659..6f4c2f78590 100644
--- a/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts
+++ b/packages/plugins/plugin-security/src/grant-holder-membership-refusal.test.ts
@@ -39,6 +39,7 @@ import {
GRANT_HOLDER_MEMBERSHIP_HOOK_PACKAGE,
registerGrantHolderMembershipRefusal,
} from './grant-holder-membership-refusal.js';
+import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js';
// ---------------------------------------------------------------------------
// Fixture
@@ -139,6 +140,9 @@ async function boot(opts: BootOptions = {}) {
],
} as any);
await engine.syncSchemas();
+ // The organization-less holdings and rows below exist only on a store built
+ // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it.
+ await relaxEveryTenantColumn(engine, [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet]);
// `sys_user` stays registered and unprovisioned, as in the sibling suites:
// the engine's own `user_id` existence probe then cannot run and stands
// down, so what is measured here is the membership predicate alone.
diff --git a/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts b/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts
index f31058574e1..7cacc318b33 100644
--- a/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts
+++ b/packages/plugins/plugin-security/src/grant-permission-set-name.equivalence.test.ts
@@ -52,6 +52,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'
import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -116,6 +117,10 @@ async function grantsByPrincipal(posture: Posture): Promise = {
diff --git a/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts b/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts
index 3da2f565814..cb0d4a6adfa 100644
--- a/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts
+++ b/packages/plugins/plugin-security/src/grant-permission-set-name.test.ts
@@ -41,6 +41,7 @@ import {
grantSetNotInCatalogMessage,
registerGrantPermissionSetNameHooks,
} from './grant-permission-set-name.js';
+import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js';
// ---------------------------------------------------------------------------
// Fixture
@@ -93,6 +94,9 @@ async function boot() {
objects: [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet, SysMember],
} as any);
await engine.syncSchemas();
+ // The organization-less holdings and rows below exist only on a store built
+ // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it.
+ await relaxEveryTenantColumn(engine, [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet, SysMember]);
// The authz resolver reads these; the auth plugin registers them in a deployment.
for (const o of [SysUser, SysMember]) {
if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set');
diff --git a/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts b/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts
index f9788eac1af..df771499430 100644
--- a/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts
+++ b/packages/plugins/plugin-security/src/grant-readers-by-name.golden.test.ts
@@ -52,6 +52,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'
import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
/** The environment-authored set the member holds a grant of, as the security catalog declares it. */
const LEGACY_REPORTS = { name: 'legacy_reports', label: 'Legacy reports' };
@@ -138,6 +139,10 @@ async function readersByPrincipal(posture: Posture): Promise = {
diff --git a/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts b/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts
index 3e7218d9aeb..a59827ffa40 100644
--- a/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts
+++ b/packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts
@@ -44,6 +44,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'
import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const CATALOG_SETS: Array<{ name: string; label?: string }> = [...defaultPermissionSets, { name: 'legacy_reports', label: 'Legacy' }];
@@ -81,6 +82,10 @@ async function boot(): Promise {
],
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
engines.push(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/plugins/plugin-security/src/insert-check-post-image.test.ts b/packages/plugins/plugin-security/src/insert-check-post-image.test.ts
index bc1fc946c92..a94a9516dcc 100644
--- a/packages/plugins/plugin-security/src/insert-check-post-image.test.ts
+++ b/packages/plugins/plugin-security/src/insert-check-post-image.test.ts
@@ -94,6 +94,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import type { PermissionSet } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// ── the app, transcribed from the card ─────────────────────────────────────
@@ -357,6 +358,9 @@ async function boot(makeDriver: () => unknown): Promise {
objects: OBJECTS,
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
// The app's stamp, on BOTH write events — the card's `runAs: 'system'` shape:
diff --git a/packages/plugins/plugin-security/src/insert-seam-judges-organization.test.ts b/packages/plugins/plugin-security/src/insert-seam-judges-organization.test.ts
index 133024cbd92..9023ebdb560 100644
--- a/packages/plugins/plugin-security/src/insert-seam-judges-organization.test.ts
+++ b/packages/plugins/plugin-security/src/insert-seam-judges-organization.test.ts
@@ -47,6 +47,7 @@ import { SysOrganization, SysUser, SysMember } from '@objectstack/platform-objec
import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const OWN_ORG = 'org_a';
const SISTER_ORG = 'org_c';
@@ -343,6 +344,10 @@ describe('[#24822 · ADR-0090 D10] a delegated insert by a crossing operator is
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
engines.push(engine);
for (const id of [ORG_A, ORG_C, ORG_D]) {
diff --git a/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts b/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts
index 7678e12a8d3..7776f519d19 100644
--- a/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts
+++ b/packages/plugins/plugin-security/src/objects/default-row-scope-declarations.test.ts
@@ -52,6 +52,7 @@ import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from '../__te
import { SysUserPosition } from './sys-user-position.object.js';
import { SysUserPermissionSet } from './sys-user-permission-set.object.js';
import { bindTestSecurityCatalog } from '../__tests__/security-catalog.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from '../pre-18-tenant-column.testkit.js';
type Policy = {
name: string;
@@ -404,6 +405,10 @@ async function bootBaseline(declared: boolean) {
objects: [...declarers, objects.get('sys_member')!, SysUserPermissionSet, SysUserPosition],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
for (const o of [SysPosition, SysPermissionSet, SysPositionPermissionSet]) {
if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set');
}
diff --git a/packages/plugins/plugin-security/src/objects/reserved-identity-names.test.ts b/packages/plugins/plugin-security/src/objects/reserved-identity-names.test.ts
index 14f576274a2..36f295026c5 100644
--- a/packages/plugins/plugin-security/src/objects/reserved-identity-names.test.ts
+++ b/packages/plugins/plugin-security/src/objects/reserved-identity-names.test.ts
@@ -43,6 +43,7 @@ import {
reservedIdentityNameMessage,
} from './reserved-identity-names.js';
import { DelegatedAdminGate } from '../delegated-admin-gate.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const engines: ObjectQL[] = [];
afterEach(async () => {
@@ -67,6 +68,9 @@ async function boot(): Promise {
objects: [SysUserPosition],
} as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
return engine;
}
diff --git a/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts b/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts
index e7edb8c0f35..5473ae07428 100644
--- a/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts
+++ b/packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts
@@ -48,6 +48,7 @@ import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from '../__te
import { SysUserPosition } from './sys-user-position.object.js';
import { SysUserPermissionSet } from './sys-user-permission-set.object.js';
import { bindTestSecurityCatalog } from '../__tests__/security-catalog.testkit.js';
+import { relaxEveryRegisteredTenantColumn } from '../pre-18-tenant-column.testkit.js';
type Policy = { name: string; object?: string; operation: string; using?: string; enabled?: boolean };
type ObjectDef = { name: string; fields?: Record; defaultRowScope?: Policy };
@@ -264,6 +265,10 @@ async function boot(shape: Shape, sets: PermissionSet[]) {
objects: [...schemas, SysUserPermissionSet, SysUserPosition],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
for (const o of [SysPosition, SysPermissionSet, SysPositionPermissionSet]) {
if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set');
}
diff --git a/packages/plugins/plugin-security/src/objects/sys-security-assignment-display-title.test.ts b/packages/plugins/plugin-security/src/objects/sys-security-assignment-display-title.test.ts
index 5be7c09c7d0..c20b5c0581d 100644
--- a/packages/plugins/plugin-security/src/objects/sys-security-assignment-display-title.test.ts
+++ b/packages/plugins/plugin-security/src/objects/sys-security-assignment-display-title.test.ts
@@ -49,6 +49,7 @@ import {
import { SqlDriver } from '@objectstack/driver-sql';
import { resolveDisplayField } from '@objectstack/spec/data';
import { SysUserPosition } from './sys-user-position.object.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
@@ -118,6 +119,9 @@ describe('[#20044] permission-assignment tables resolve a real record title unde
engine.registry.registerObject(schema, 'com.objectstack.test.20044');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterAll(async () => {
diff --git a/packages/plugins/plugin-security/src/objects/sys-user-permission-set-person-title.test.ts b/packages/plugins/plugin-security/src/objects/sys-user-permission-set-person-title.test.ts
index 02afb741012..58148f4452a 100644
--- a/packages/plugins/plugin-security/src/objects/sys-user-permission-set-person-title.test.ts
+++ b/packages/plugins/plugin-security/src/objects/sys-user-permission-set-person-title.test.ts
@@ -44,6 +44,7 @@ import { SysUser } from '@objectstack/platform-objects';
import { resolveDisplayField, resolveRecordDisplayName } from '@objectstack/spec/data';
import type { ExecutionContext } from '@objectstack/spec/kernel';
import { SysUserPermissionSet } from './sys-user-permission-set.object.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
const OBJECT = 'sys_user_permission_set';
@@ -83,6 +84,9 @@ describe('sys_user_permission_set is titled by the person, not the raw user id',
engine.registry.registerObject(SysUser as any, 'com.objectstack.test.person-title');
engine.registry.registerObject(SysUserPermissionSet as any, 'com.objectstack.test.person-title');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// Row-level security on `sys_user` for a reader that is not SYSTEM — the
// composition the security plugin makes (its own row scope is "own row or
// an organization member"; here, the ids in `readable`).
diff --git a/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts b/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts
index 586caade527..9c106f3df5d 100644
--- a/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts
+++ b/packages/plugins/plugin-security/src/orgless-position-name-fold.test.ts
@@ -63,6 +63,7 @@ import { SysOrganization, SysUser, SysMember } from '@objectstack/platform-objec
import { SecurityPlugin } from './security-plugin.js';
import { buildContextForUser } from './explain-engine.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as any;
/** The organization that authors the colliding sets. Nobody below is its member except `USER_OTHER`. */
@@ -145,6 +146,10 @@ async function boot(): Promise {
],
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
engines.push(engine);
const e = engine as any;
await e.insert('sys_organization', { id: ORG_OTHER, name: ORG_OTHER, slug: 'other' }, SYS);
diff --git a/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts b/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts
index 06bf5c2f0dd..369cdd7e6f0 100644
--- a/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts
+++ b/packages/plugins/plugin-security/src/position-catalog-refusal.test.ts
@@ -36,6 +36,7 @@ import { SysMetadataActivation } from '@objectstack/platform-objects/system';
import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { namesWithoutCatalogRow, positionNotInCatalogMessage } from './position-catalog-refusal.js';
+import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js';
// ---------------------------------------------------------------------------
// Fixture
@@ -130,6 +131,9 @@ async function boot(opts: { walled?: boolean } = {}) {
objects: [SysUserPosition, SysUserPermissionSet, SysMember, SysMetadataActivation, QA_INQUIRY],
} as any);
await engine.syncSchemas();
+ // The organization-less holdings and rows below exist only on a store built
+ // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it.
+ await relaxEveryTenantColumn(engine, [SysUserPosition, SysUserPermissionSet, SysMember, SysMetadataActivation, QA_INQUIRY]);
// [#21516] The authz resolver reads these on every grant resolution; in a
// deployment the auth and security plugins register them. This harness
// composes neither, so the ones its app does not declare are registered
diff --git a/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts b/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..b0cbb5efa38
--- /dev/null
+++ b/packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Return engine-synced tables to the shape a store built BEFORE 18.x has: the
+ * tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x the SQL driver creates a table's tenant column NOT
+ * NULL. A store an earlier release built still holds the nullable column and
+ * its organization-less rows — the holdings, grants and seeds this package's
+ * NULL readers still answer for until the organization-ownership ceremony has
+ * attributed them. A suite whose subject is those rows calls this after
+ * `syncSchemas()` and before seeding them. Done through each table's driver
+ * and its own drift apply door (`relax_not_null`), never a hand `ALTER`.
+ */
+export async function relaxTenantColumns(
+ engine: { getDriverForObject(object: string): unknown },
+ tables: readonly string[],
+ column = 'organization_id',
+): Promise {
+ for (const table of tables) {
+ const driver = engine.getDriverForObject(table) as {
+ applyMigrationEntries(entries: never[]): Promise<{ applied: unknown[] }>;
+ };
+ const op = { type: 'relax_not_null', table, column };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column, category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
+ }
+}
+
+/**
+ * {@link relaxTenantColumns} over every object of an app that carries a tenant
+ * column as the engine registered it — the shape a whole pre-18 store has.
+ */
+export async function relaxEveryTenantColumn(
+ engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown },
+ objects: readonly { name: string }[],
+): Promise {
+ const tenant = objects
+ .map((o) => o.name)
+ .filter((name) => {
+ const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined;
+ return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false;
+ });
+ await relaxTenantColumns(engine, tenant);
+}
+
+/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */
+export async function relaxEveryRegisteredTenantColumn(engine: {
+ getDriverForObject(object: string): unknown;
+ getSchema(object: string): unknown;
+ registry: { getAllObjects(): readonly { name: string }[] };
+}): Promise {
+ await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
+}
diff --git a/packages/plugins/plugin-security/src/predicate-write-unreadable-not-matched.test.ts b/packages/plugins/plugin-security/src/predicate-write-unreadable-not-matched.test.ts
index d778b4ae7c9..ae146f0e0a5 100644
--- a/packages/plugins/plugin-security/src/predicate-write-unreadable-not-matched.test.ts
+++ b/packages/plugins/plugin-security/src/predicate-write-unreadable-not-matched.test.ts
@@ -42,6 +42,7 @@ import { MAX_BULK_PER_ROW_HOOK_ROWS } from '@objectstack/spec/data';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -118,6 +119,9 @@ async function boot(opts: { bulkRows?: number } = {}): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(PARENT, [{ id: 'p_hook', name: 'hooked' }], SYS);
await engine.insert(CHILD, [
// readable (own) and writable
diff --git a/packages/plugins/plugin-security/src/readable-ids-delegated-compose.test.ts b/packages/plugins/plugin-security/src/readable-ids-delegated-compose.test.ts
index 042c9a6325d..531babfbff9 100644
--- a/packages/plugins/plugin-security/src/readable-ids-delegated-compose.test.ts
+++ b/packages/plugins/plugin-security/src/readable-ids-delegated-compose.test.ts
@@ -46,6 +46,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ORG = 'org_rid_del';
@@ -151,6 +152,10 @@ async function boot() {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
diff --git a/packages/plugins/plugin-security/src/readable-ids-nested-compose.test.ts b/packages/plugins/plugin-security/src/readable-ids-nested-compose.test.ts
index cce5a12e950..5ebdc57d1fd 100644
--- a/packages/plugins/plugin-security/src/readable-ids-nested-compose.test.ts
+++ b/packages/plugins/plugin-security/src/readable-ids-nested-compose.test.ts
@@ -51,6 +51,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { context: { isSystem: true } } as never;
const ORG = 'org_rid_nest';
@@ -194,6 +195,10 @@ async function boot() {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set catalogue row below exists only on a
+ // store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): that table is this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set']);
const e = engine as unknown as {
insert: (o: string, row: Record, opts: never) => Promise;
@@ -240,12 +245,13 @@ async function boot() {
}, SYS);
// The parents, one per grandparent, plus one with a dangling and one with a
// null master pointer (written through the driver: the required master is
- // what the authoring door refuses, and these rows exist to be excluded).
+ // what the authoring door refuses, and these rows exist to be excluded). A
+ // driver write is not stamped, so it names the rig's organization itself.
for (const [id, grand] of [
['p_both', 'g_both'], ['p_del_only', 'g_del_only'], ['p_caller_only', 'g_caller_only'],
['p_neither', 'g_neither'], ['p_shared', 'g_shared'], ['p_dangling', 'g_missing'], ['p_null', null],
] as const) {
- await e.getDriver(PARENT).create(PARENT, { id, name: id, grand });
+ await e.getDriver(PARENT).create(PARENT, { id, name: id, grand, organization_id: ORG });
}
// The same children in both tables, so the two paths read the same rows.
for (const child of [CHILD_CBP, CHILD_LEAF]) {
@@ -254,7 +260,7 @@ async function boot() {
['c_neither', 'p_neither'], ['c_shared', 'p_shared'], ['c_via_dangling', 'p_dangling'], ['c_via_null', 'p_null'],
['c_dangling', 'p_missing'], ['c_null', null],
] as const) {
- await e.getDriver(child).create(child, { id, note: '', parent });
+ await e.getDriver(child).create(child, { id, note: '', parent, organization_id: ORG });
}
}
diff --git a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts
index ba69e021ba0..0c8568f07ee 100644
--- a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts
+++ b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.golden.test.ts
@@ -46,6 +46,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'
import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -113,6 +114,10 @@ async function resolverByPrincipal(posture: Posture): Promise = {
diff --git a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts
index 3617b4ff735..af6a6380779 100644
--- a/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts
+++ b/packages/plugins/plugin-security/src/resolve-authz-grant-set-by-name.test.ts
@@ -35,6 +35,7 @@ import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js'
import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true } as const;
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -75,6 +76,10 @@ async function newEngine(): Promise {
],
} as any);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
engines.push(engine);
vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
return engine;
diff --git a/packages/plugins/plugin-security/src/rls-accessible-org-ids-plumbing.test.ts b/packages/plugins/plugin-security/src/rls-accessible-org-ids-plumbing.test.ts
index dafbdde98c7..dbdd7441e57 100644
--- a/packages/plugins/plugin-security/src/rls-accessible-org-ids-plumbing.test.ts
+++ b/packages/plugins/plugin-security/src/rls-accessible-org-ids-plumbing.test.ts
@@ -53,6 +53,7 @@ import type { PermissionSet } from '@objectstack/spec/security';
import { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// ── the fixture: six rows across THREE organizations ───────────────────────
//
@@ -153,6 +154,9 @@ async function boot(
objects: OBJECTS,
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/plugins/plugin-security/src/rls-boolean-comparand-door.test.ts b/packages/plugins/plugin-security/src/rls-boolean-comparand-door.test.ts
index 7e11069617a..0acc7032b5f 100644
--- a/packages/plugins/plugin-security/src/rls-boolean-comparand-door.test.ts
+++ b/packages/plugins/plugin-security/src/rls-boolean-comparand-door.test.ts
@@ -47,6 +47,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** Every compiled policy filter `compileCelToFilter` handed the seam, deep-frozen, by predicate. */
const compiled = new Map();
@@ -127,6 +128,9 @@ async function boot(config: Record, predicate: string) {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
cleanups.push(async () => {
if (config.client === 'pg') await (driver as unknown as { execute(sql: string): Promise }).execute(`drop table if exists ${OBJ}`);
await engine.destroy();
diff --git a/packages/plugins/plugin-security/src/rls-check-by-id-update-post-hook.test.ts b/packages/plugins/plugin-security/src/rls-check-by-id-update-post-hook.test.ts
index 76b54355898..bb702ecb565 100644
--- a/packages/plugins/plugin-security/src/rls-check-by-id-update-post-hook.test.ts
+++ b/packages/plugins/plugin-security/src/rls-check-by-id-update-post-hook.test.ts
@@ -48,6 +48,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import type { PermissionSet } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The organization the caller holds. */
const OWN_ORG = 'org_a';
@@ -187,6 +188,9 @@ async function boot(makeDriver: () => unknown): Promise {
objects: OBJECTS,
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
// The organization shape's stamp: it reads the parent OUTSIDE RLS and
diff --git a/packages/plugins/plugin-security/src/rls-check-contains-membership.test.ts b/packages/plugins/plugin-security/src/rls-check-contains-membership.test.ts
index 8732e695b54..efdf811b3c4 100644
--- a/packages/plugins/plugin-security/src/rls-check-contains-membership.test.ts
+++ b/packages/plugins/plugin-security/src/rls-check-contains-membership.test.ts
@@ -33,6 +33,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -78,6 +79,9 @@ async function boot(makeDriver: () => Driver, clause: 'using' | 'check') {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
booted.push(engine);
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/rls-check-cross-class-field-refused.test.ts b/packages/plugins/plugin-security/src/rls-check-cross-class-field-refused.test.ts
index 21cd16e5120..5cdd6693f9f 100644
--- a/packages/plugins/plugin-security/src/rls-check-cross-class-field-refused.test.ts
+++ b/packages/plugins/plugin-security/src/rls-check-cross-class-field-refused.test.ts
@@ -44,6 +44,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -106,6 +107,9 @@ async function boot(makeDriver: () => Driver, clause: 'using' | 'check', predica
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
booted.push({ engine, driver, table: OBJ });
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/rls-check-defaults-to-using.test.ts b/packages/plugins/plugin-security/src/rls-check-defaults-to-using.test.ts
index 1dc2877baec..43d5337a0eb 100644
--- a/packages/plugins/plugin-security/src/rls-check-defaults-to-using.test.ts
+++ b/packages/plugins/plugin-security/src/rls-check-defaults-to-using.test.ts
@@ -45,6 +45,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import type { PermissionSet } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OWN_ORG = 'org_a';
const OTHER_ORG = 'org_b';
@@ -124,6 +125,9 @@ async function boot(makeDriver: () => unknown, ps: PermissionSet): Promise = {
manifest: { register: vi.fn() },
diff --git a/packages/plugins/plugin-security/src/rls-check-multi-row-writes.test.ts b/packages/plugins/plugin-security/src/rls-check-multi-row-writes.test.ts
index 389b4bd2f68..cace4a04cde 100644
--- a/packages/plugins/plugin-security/src/rls-check-multi-row-writes.test.ts
+++ b/packages/plugins/plugin-security/src/rls-check-multi-row-writes.test.ts
@@ -53,6 +53,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import type { PermissionSet } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECTS = [
{
@@ -138,6 +139,9 @@ async function boot(makeDriver: () => unknown, ps: PermissionSet): Promise = {
manifest: { register: vi.fn() },
diff --git a/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts b/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts
index b244ff4a834..69f5193bd3f 100644
--- a/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts
+++ b/packages/plugins/plugin-security/src/rls-check-stored-form.test.ts
@@ -78,6 +78,7 @@ import {
storedFormCheckJudge,
storedFormImage,
} from './rls-check-stored-form.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** A plain object — a filter node or an operator map. */
function isPlain(value: unknown): value is Record {
@@ -172,6 +173,9 @@ async function boot(makeDriver: () => Driver, predicate: string, using: string =
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
booted.push(engine);
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/rls-list-literal-comparand-fails-closed.test.ts b/packages/plugins/plugin-security/src/rls-list-literal-comparand-fails-closed.test.ts
index 82bd2c6716e..cf3f8e62c36 100644
--- a/packages/plugins/plugin-security/src/rls-list-literal-comparand-fails-closed.test.ts
+++ b/packages/plugins/plugin-security/src/rls-list-literal-comparand-fails-closed.test.ts
@@ -26,6 +26,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJ = 'qa_ticket';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
@@ -63,6 +64,9 @@ async function boot(clause: 'using' | 'check', predicate: string) {
}],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/rls-null-comparand-fails-closed.test.ts b/packages/plugins/plugin-security/src/rls-null-comparand-fails-closed.test.ts
index 508520afc7b..26b68085b90 100644
--- a/packages/plugins/plugin-security/src/rls-null-comparand-fails-closed.test.ts
+++ b/packages/plugins/plugin-security/src/rls-null-comparand-fails-closed.test.ts
@@ -48,6 +48,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJ = 'qa_ticket';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
@@ -120,6 +121,9 @@ async function boot(makeDriver: () => unknown, clause: 'using' | 'check', predic
}],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/rls-number-comparand-door.test.ts b/packages/plugins/plugin-security/src/rls-number-comparand-door.test.ts
index d846da268b5..156d5e24f8f 100644
--- a/packages/plugins/plugin-security/src/rls-number-comparand-door.test.ts
+++ b/packages/plugins/plugin-security/src/rls-number-comparand-door.test.ts
@@ -35,6 +35,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
const MEMBER_DEFAULT = defaultPermissionSets.find((p) => p.name === 'member_default')!;
@@ -76,6 +77,9 @@ async function boot(predicate: string) {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
booted.push(engine);
const set = PermissionSetSchema.parse({
diff --git a/packages/plugins/plugin-security/src/rls-one-value-comparand-fails-closed.test.ts b/packages/plugins/plugin-security/src/rls-one-value-comparand-fails-closed.test.ts
index aaf582dd534..4fc68de5556 100644
--- a/packages/plugins/plugin-security/src/rls-one-value-comparand-fails-closed.test.ts
+++ b/packages/plugins/plugin-security/src/rls-one-value-comparand-fails-closed.test.ts
@@ -36,6 +36,7 @@ import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { RLS_DENY_FILTER } from './rls-compiler.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJ = 'qa_ticket';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
@@ -86,6 +87,9 @@ async function boot(makeDriver: () => unknown, clause: 'using' | 'check', predic
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
await engine.insert('qa_person', [{ id: 'p1', name: 'P1' }, { id: 'p2', name: 'P2' }], { context: SYS_CTX } as never);
diff --git a/packages/plugins/plugin-security/src/rls-phantom-column-negation.test.ts b/packages/plugins/plugin-security/src/rls-phantom-column-negation.test.ts
index c342dd6fc84..4ccfd76ccb9 100644
--- a/packages/plugins/plugin-security/src/rls-phantom-column-negation.test.ts
+++ b/packages/plugins/plugin-security/src/rls-phantom-column-negation.test.ts
@@ -76,6 +76,7 @@ import type { PermissionSet } from '@objectstack/spec/security';
import { RLSCompiler, RLS_DENY_FILTER } from './rls-compiler.js';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// ── the fixture, transcribed from the card: three rows, one of them public ──
@@ -246,6 +247,9 @@ async function boot(makeDriver: () => unknown, ps: PermissionSet): Promise = {
manifest: { register: vi.fn() },
diff --git a/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts b/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts
index ce822eff5d2..6a461a6522b 100644
--- a/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts
+++ b/packages/plugins/plugin-security/src/rls-stored-list-ordering-fails-closed.test.ts
@@ -57,6 +57,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { PermissionSetSchema } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJ = 'qa_ticket';
const SYS_CTX = { isSystem: true, userId: 'usr_system' };
@@ -109,6 +110,9 @@ async function boot(makeDriver: () => unknown, clause: 'using' | 'check', predic
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engines.push(engine);
await engine.insert('qa_person', [{ id: 'p1', name: 'P1' }, { id: 'p2', name: 'P2' }], { context: SYS_CTX } as never);
diff --git a/packages/plugins/plugin-security/src/rls-variable-root-comparand-fails-closed.test.ts b/packages/plugins/plugin-security/src/rls-variable-root-comparand-fails-closed.test.ts
index b474bc2b944..792003816e4 100644
--- a/packages/plugins/plugin-security/src/rls-variable-root-comparand-fails-closed.test.ts
+++ b/packages/plugins/plugin-security/src/rls-variable-root-comparand-fails-closed.test.ts
@@ -35,6 +35,7 @@ import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.tes
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
import { RLS_DENY_FILTER } from './rls-compiler.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJ = 'qa_ticket';
const GUARD_SET = 'qa_ticket_guard';
@@ -84,6 +85,9 @@ async function boot(policies: Policy[]) {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// [#21516] The authz resolver reads these on every grant resolution; in a
// deployment the auth and security plugins register them. This harness
// composes neither, so the ones its app does not declare are registered
diff --git a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts
index a8ff3270d4f..dae76c50ba2 100644
--- a/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts
+++ b/packages/plugins/plugin-security/src/sys-audit-log-row-scope.test.ts
@@ -32,6 +32,7 @@ import type { PermissionSet } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { defaultPermissionSets } from './objects/default-permission-sets.js';
import { isPlatformTenantPolicy } from './platform-tenant-policies.js';
+import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
const LEDGER = 'sys_audit_log';
const POLICY = 'sys_audit_log_org';
@@ -121,6 +122,10 @@ async function boot(opts: { posture: Posture; sets?: PermissionSet[]; ledger?: R
],
} as never);
await engine.syncSchemas();
+ // The organization-less rows below (global sets, positions, grants) exist only
+ // on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
+ // NULL): this is that store.
+ await relaxEveryRegisteredTenantColumn(engine);
await engine.insert('sys_organization', [{ id: 'org_a', name: 'A' }, ...(single ? [] : [{ id: 'org_b', name: 'B' }])], SYS);
const before = opts.ledger === LEDGER_BEFORE;
const ledgerRows = [
diff --git a/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts b/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts
index dd39d862bc5..f3870413f39 100644
--- a/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts
+++ b/packages/plugins/plugin-security/src/sys-user-permission-set-window-order.test.ts
@@ -43,6 +43,7 @@ import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
+import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js';
const MEMBER_DEFAULT = { name: 'member_default', label: 'Member', objects: {} } as unknown as PermissionSet;
const QA_ADMIN = {
@@ -86,6 +87,9 @@ async function boot() {
objects: [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet, SysMember],
} as any);
await engine.syncSchemas();
+ // The organization-less holdings and rows below exist only on a store built
+ // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it.
+ await relaxEveryTenantColumn(engine, [SysPosition, SysUserPosition, SysPermissionSet, SysPositionPermissionSet, SysUserPermissionSet, SysMember]);
for (const o of [SysUser, SysMember]) {
if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as never, 'qa.authz-read-set');
}
diff --git a/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts b/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts
index 3d858cfbc5b..6d51688a328 100644
--- a/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts
+++ b/packages/plugins/plugin-security/src/tenant-wall-organization-removal.test.ts
@@ -56,6 +56,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import type { PermissionSet } from '@objectstack/spec/security';
import { SecurityPlugin } from './security-plugin.js';
import { SysUserPosition, SysUserPermissionSet } from './objects/index.js';
+import { relaxEveryTenantColumn } from './pre-18-tenant-column.testkit.js';
/** The organization the caller holds (its active organization). */
const OWN_ORG = 'org_a';
@@ -148,6 +149,9 @@ async function boot(makeDriver: () => unknown, posture: Posture = 'isolated'): P
objects: OBJECTS,
} as never);
await engine.syncSchemas();
+ // The organization-less holdings and rows below exist only on a store built
+ // before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is it.
+ await relaxEveryTenantColumn(engine, OBJECTS);
engines.push(engine);
// The stored-row half: a hook that clears the column when asked to by name.
diff --git a/packages/plugins/plugin-security/src/update-seam-judges-organization.test.ts b/packages/plugins/plugin-security/src/update-seam-judges-organization.test.ts
index 15e8cc9b476..6ebf5a37d58 100644
--- a/packages/plugins/plugin-security/src/update-seam-judges-organization.test.ts
+++ b/packages/plugins/plugin-security/src/update-seam-judges-organization.test.ts
@@ -50,6 +50,7 @@ import { SysOrganization, SysUser, SysMember } from '@objectstack/platform-objec
import { SysPosition, SysPermissionSet, SysPositionPermissionSet } from './__tests__/legacy-catalog-objects.testkit.js';
import { SysUserPosition } from './objects/sys-user-position.object.js';
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const OWN_ORG = 'org_a';
const SISTER_ORG = 'org_c';
@@ -145,6 +146,10 @@ async function boot(posture: Posture) {
],
} as never);
await engine.syncSchemas();
+ // The organization-less permission-set and position catalogue rows below
+ // exist only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant
+ // column NOT NULL): those tables are this store's.
+ await relaxTenantColumns(engine as never, ['sys_permission_set', 'sys_position']);
engines.push(engine);
for (const id of [OWN_ORG, SISTER_ORG, FOREIGN_ORG]) {
diff --git a/packages/plugins/plugin-sharing/src/reconcile-refused-grant-continues.test.ts b/packages/plugins/plugin-sharing/src/reconcile-refused-grant-continues.test.ts
index 26d77bb9313..d13d96d6c09 100644
--- a/packages/plugins/plugin-sharing/src/reconcile-refused-grant-continues.test.ts
+++ b/packages/plugins/plugin-sharing/src/reconcile-refused-grant-continues.test.ts
@@ -60,6 +60,22 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
import { SharingService } from './sharing-service.js';
import { SharingRuleService } from './sharing-rule-service.js';
+/**
+ * Return driver-created tables to a pre-18 store's shape, tenant column
+ * nullable, through the driver's own drift apply door (`relax_not_null`).
+ * The organization-less rows this file seeds exist only on such a store:
+ * ADR-0131 D1 makes a fresh tenant column NOT NULL.
+ */
+async function relaxTenantColumns(driver: SqlDriver, tables: readonly string[]): Promise {
+ for (const table of tables) {
+ const op = { type: 'relax_not_null', table, column: 'organization_id' };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column: 'organization_id', category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table} was not relaxed`);
+ }
+}
+
const OBJECT = 'os14754_deal';
const ORG_A = 'org_a';
const ORG_B = 'org_b';
@@ -163,6 +179,7 @@ async function boot(posture: 'single' | 'isolated' | 'group' = 'isolated') {
{ name: 'sys_record_share', fields: SHARE_FIELDS } as never,
{ name: 'sys_sharing_rule', fields: RULE_FIELDS } as never,
]);
+ await relaxTenantColumns(driver, [OBJECT, 'sys_record_share', 'sys_sharing_rule']);
await driver.create(OBJECT, { id: 'rec_first', stage: 'won', owner_id: 'u_a', organization_id: ORG_A } as never);
await driver.create(OBJECT, { id: 'rec_orgless', stage: 'won', owner_id: 'u_p' } as never);
diff --git a/packages/plugins/plugin-sharing/src/record-share-guest-recipient-retired.test.ts b/packages/plugins/plugin-sharing/src/record-share-guest-recipient-retired.test.ts
index 253249d71a9..9520cfad90d 100644
--- a/packages/plugins/plugin-sharing/src/record-share-guest-recipient-retired.test.ts
+++ b/packages/plugins/plugin-sharing/src/record-share-guest-recipient-retired.test.ts
@@ -23,7 +23,9 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SysRecordShare } from './objects/sys-record-share.object.js';
-const SYS = { context: { isSystem: true } } as never;
+// The write names its organization: a fresh tenant column is NOT NULL (ADR-0131
+// D1), and this lean engine registers no organization object to derive one from.
+const SYS = { context: { isSystem: true, tenantId: 'org_os_qa' } } as never;
/** The four values a row may carry, in the select's declared order. */
const ROW_RECIPIENTS = ['user', 'group', 'position', 'unit_and_subordinates'];
diff --git a/packages/plugins/plugin-sharing/src/record-share-organization-stamp.test.ts b/packages/plugins/plugin-sharing/src/record-share-organization-stamp.test.ts
index d5660786159..452785dff19 100644
--- a/packages/plugins/plugin-sharing/src/record-share-organization-stamp.test.ts
+++ b/packages/plugins/plugin-sharing/src/record-share-organization-stamp.test.ts
@@ -92,6 +92,20 @@ const ORG_FIELDS: Record> = {
name: { type: 'text', name: 'name', label: 'Name' },
};
+/**
+ * Return driver-created tables to a pre-18 store's shape, tenant column
+ * nullable, through the driver's own drift apply door (`relax_not_null`).
+ */
+async function relaxTenantColumns(driver: SqlDriver, tables: readonly string[]): Promise {
+ for (const table of tables) {
+ const op = { type: 'relax_not_null', table, column: 'organization_id' };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column: 'organization_id', category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table} was not relaxed`);
+ }
+}
+
const ORG_A = 'org_a';
const ORG_B = 'org_b';
@@ -151,7 +165,8 @@ async function boot(posture?: 'single' | 'isolated' | 'group', organizations: re
// `no-organization-yet` reading the earlier blocks rely on — and provisioned
// only for the cases that need the install to hold several.
if (organizations.length > 0) {
- ql.registerObject({ name: 'sys_organization', label: 'Organization', isSystem: true, fields: ORG_FIELDS } as never);
+ // The real `sys_organization` carries no tenant column of its own.
+ ql.registerObject({ name: 'sys_organization', label: 'Organization', isSystem: true, systemFields: { tenant: false }, fields: ORG_FIELDS } as never);
}
await driver.initObjects([
{ name: OBJECT, fields: DEAL_FIELDS } as never,
@@ -159,6 +174,10 @@ async function boot(posture?: 'single' | 'isolated' | 'group', organizations: re
{ name: 'sys_sharing_rule', fields: RULE_FIELDS } as never,
...(organizations.length > 0 ? [{ name: 'sys_organization', fields: ORG_FIELDS } as never] : []),
]);
+ // `deal_p1` and its organization-less grants exist only on a store built
+ // before 18.x, whose tenant columns are nullable (ADR-0131 D1 makes a fresh
+ // one NOT NULL): the fixture is that store.
+ await relaxTenantColumns(driver, [OBJECT, 'sys_record_share', 'sys_sharing_rule']);
for (const id of organizations) await driver.create('sys_organization', { id, name: id } as never);
// Seeded through the driver: the fixture is the DATA at rest.
diff --git a/packages/plugins/plugin-sharing/src/rule-criteria-org-scope.test.ts b/packages/plugins/plugin-sharing/src/rule-criteria-org-scope.test.ts
index f48d3dc6333..71624ea211b 100644
--- a/packages/plugins/plugin-sharing/src/rule-criteria-org-scope.test.ts
+++ b/packages/plugins/plugin-sharing/src/rule-criteria-org-scope.test.ts
@@ -77,6 +77,22 @@ import { SharingService } from './sharing-service.js';
import { SharingRuleService } from './sharing-rule-service.js';
import type { DeclaredSharingRule } from './declared-sharing-rules.js';
+/**
+ * Return driver-created tables to a pre-18 store's shape, tenant column
+ * nullable, through the driver's own drift apply door (`relax_not_null`).
+ * The organization-less rows this file seeds exist only on such a store:
+ * ADR-0131 D1 makes a fresh tenant column NOT NULL.
+ */
+async function relaxTenantColumns(driver: SqlDriver, tables: readonly string[]): Promise {
+ for (const table of tables) {
+ const op = { type: 'relax_not_null', table, column: 'organization_id' };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column: 'organization_id', category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table} was not relaxed`);
+ }
+}
+
const OBJECT = 'os10119_deal';
const DEAL_FIELDS: Record> = {
@@ -227,6 +243,7 @@ async function boot(posture?: 'single' | 'isolated' | 'group', opts: BootOptions
{ name: 'sys_record_share', fields: SHARE_FIELDS } as never,
{ name: 'sys_sharing_rule', fields: RULE_FIELDS } as never,
]);
+ await relaxTenantColumns(driver, [OBJECT, 'sys_record_share', 'sys_sharing_rule']);
// Seeded through the driver, not the engine: the fixture is the DATA at
// rest, and routing it through the engine's write path would drag the
diff --git a/packages/plugins/plugin-sharing/src/share-link-internal-columns.test.ts b/packages/plugins/plugin-sharing/src/share-link-internal-columns.test.ts
index dcfe71845b9..7b101ff972c 100644
--- a/packages/plugins/plugin-sharing/src/share-link-internal-columns.test.ts
+++ b/packages/plugins/plugin-sharing/src/share-link-internal-columns.test.ts
@@ -110,7 +110,9 @@ async function boot() {
await engine.init();
for (const o of [SysShareLink, TARGET]) engine.registry.registerObject(o as never, '@objectstack/plugin-sharing');
await engine.syncSchemas();
- await (engine as any).insert('pin_doc', { id: 'doc_1', title: 'Shared doc' }, { context: { isSystem: true } });
+ // Seeded under one organization: a fresh tenant column is NOT NULL (ADR-0131
+ // D1), and this lean engine registers no organization object to derive one from.
+ await (engine as any).insert('pin_doc', { id: 'doc_1', title: 'Shared doc' }, { context: { isSystem: true, tenantId: 'org_pin' } });
const service = new ShareLinkService({ engine: engine as unknown as SharingEngine });
const http = new MockHttp();
registerShareLinkRoutes(http, service, engine as unknown as SharingEngine, {
diff --git a/packages/plugins/plugin-sharing/src/share-link-organization-stamp.test.ts b/packages/plugins/plugin-sharing/src/share-link-organization-stamp.test.ts
index b7cd9fef9da..7e8312e7e46 100644
--- a/packages/plugins/plugin-sharing/src/share-link-organization-stamp.test.ts
+++ b/packages/plugins/plugin-sharing/src/share-link-organization-stamp.test.ts
@@ -40,6 +40,22 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
import { ShareLinkService } from './share-link-service.js';
import { SysShareLink } from './objects/index.js';
+/**
+ * Return driver-created tables to a pre-18 store's shape, tenant column
+ * nullable, through the driver's own drift apply door (`relax_not_null`).
+ * The organization-less rows this file seeds exist only on such a store:
+ * ADR-0131 D1 makes a fresh tenant column NOT NULL.
+ */
+async function relaxTenantColumns(driver: SqlDriver, tables: readonly string[]): Promise {
+ for (const table of tables) {
+ const op = { type: 'relax_not_null', table, column: 'organization_id' };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column: 'organization_id', category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table} was not relaxed`);
+ }
+}
+
const OBJECT = 'os15212_brief';
const ORG_A = 'org_a';
const ORG_B = 'org_b';
@@ -81,10 +97,12 @@ async function boot(posture: 'single' | 'isolated', organizations: readonly stri
await ql.init();
ql.registerObject({ name: OBJECT, label: 'Brief', fields: BRIEF_FIELDS } as never);
ql.registerObject(SysShareLink as never);
- ql.registerObject({ name: 'sys_organization', label: 'Organization', isSystem: true, fields: ORG_FIELDS } as never);
+ // The real `sys_organization` carries no tenant column of its own.
+ ql.registerObject({ name: 'sys_organization', label: 'Organization', isSystem: true, systemFields: { tenant: false }, fields: ORG_FIELDS } as never);
await driver.initObjects(
[OBJECT, 'sys_share_link', 'sys_organization'].map((name) => ({ name, fields: (ql.getSchema(name) as any).fields }) as never),
);
+ await relaxTenantColumns(driver, [OBJECT, 'sys_share_link']);
for (const id of organizations) await driver.create('sys_organization', { id, name: id } as never);
await driver.create(OBJECT, { id: 'brief_a', name: 'A', organization_id: ORG_A } as never);
await driver.create(OBJECT, { id: 'brief_p', name: 'P' } as never);
diff --git a/packages/plugins/plugin-sharing/src/share-link-password.test.ts b/packages/plugins/plugin-sharing/src/share-link-password.test.ts
index c2536402e0b..294377712d6 100644
--- a/packages/plugins/plugin-sharing/src/share-link-password.test.ts
+++ b/packages/plugins/plugin-sharing/src/share-link-password.test.ts
@@ -190,6 +190,8 @@ async function drive(
const B = '/api/v1/share-links';
const CREATOR = { userId: 'usr_creator' };
+/** The organization the fixture's records belong to. */
+const SEED_ORG = 'org_pin';
const PASSWORD = 'correct horse 21839 $ battery';
/**
@@ -249,7 +251,10 @@ async function boot(
engine.registry.registerObject(o as never, '@objectstack/plugin-sharing');
}
await engine.syncSchemas();
- const sys = { context: { isSystem: true } };
+ // Seeded under one organization, as every row on a real install is: a fresh
+ // tenant column is NOT NULL (ADR-0131 D1), and this lean engine registers no
+ // organization object to derive one from. A minted link takes its record's.
+ const sys = { context: { isSystem: true, tenantId: SEED_ORG } };
await (engine as any).insert('pin_doc', { id: 'doc_1', title: 'Shared doc' }, sys);
await (engine as any).insert('ai_conversations', { id: 'conv_1', title: 'Chat' }, sys);
await (engine as any).insert(
diff --git a/packages/plugins/plugin-sharing/src/share-link-service.email-refusal-cost.pin.test.ts b/packages/plugins/plugin-sharing/src/share-link-service.email-refusal-cost.pin.test.ts
index 7f08b1f9e7a..1169940bed6 100644
--- a/packages/plugins/plugin-sharing/src/share-link-service.email-refusal-cost.pin.test.ts
+++ b/packages/plugins/plugin-sharing/src/share-link-service.email-refusal-cost.pin.test.ts
@@ -84,7 +84,9 @@ beforeAll(async () => {
await engine.init();
for (const o of [SysShareLink, TARGET]) engine.registry.registerObject(o as any, '@objectstack/plugin-sharing-test');
await engine.syncSchemas();
- await engine.insert(OBJECT, { id: RECORD, title: 'Shared doc' }, { context: { isSystem: true } } as any);
+ // Seeded under one organization: a fresh tenant column is NOT NULL (ADR-0131
+ // D1), and this lean engine registers no organization object to derive one from.
+ await engine.insert(OBJECT, { id: RECORD, title: 'Shared doc' }, { context: { isSystem: true, tenantId: 'org_pin' } } as any);
// Count the privileged hash read — the `password_hash` dereference the
// internal-column strip forces through the engine's accessor.
diff --git a/packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts b/packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts
index 1b87f57ac62..d6b73362986 100644
--- a/packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts
+++ b/packages/plugins/plugin-webhooks/src/auto-enqueuer-disabled-report.test.ts
@@ -44,6 +44,7 @@ import { MemoryHttpOutbox, MessagingService, type EnqueueHttpInput } from '@obje
import { DeclaredWebhookSource, declaredWebhookRefId } from './declared-webhooks.js';
import { SysWebhook } from './sys-webhook.object.js';
import { WebhookOutboxPlugin, type WebhookOutboxPluginOptions } from './webhook-outbox-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'my_app_note';
const HOOK = 'my_app_note_changed';
@@ -166,6 +167,9 @@ async function boot(options: BootOptions) {
});
engine.registry.registerObject(SysWebhook as any, 'test');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const realtime = options.realtime ? new FakeRealtime() : undefined;
if (realtime) engine.setRealtimeService(realtime);
diff --git a/packages/plugins/plugin-webhooks/src/default-organization.testkit.ts b/packages/plugins/plugin-webhooks/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..246835b9ba3
--- /dev/null
+++ b/packages/plugins/plugin-webhooks/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/plugin-webhooks',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/plugins/plugin-webhooks/src/webhook-delivery-real-sql-driver.test.ts b/packages/plugins/plugin-webhooks/src/webhook-delivery-real-sql-driver.test.ts
index 8f1b6b94fd7..81eb46dfaea 100644
--- a/packages/plugins/plugin-webhooks/src/webhook-delivery-real-sql-driver.test.ts
+++ b/packages/plugins/plugin-webhooks/src/webhook-delivery-real-sql-driver.test.ts
@@ -34,6 +34,7 @@ import { HttpDelivery, MessagingService, SqlHttpOutbox, type EnqueueHttpInput }
import { AutoEnqueuer } from './auto-enqueuer.js';
import { DeclaredWebhookSource, declaredWebhookRefId } from './declared-webhooks.js';
import { SysWebhook } from './sys-webhook.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYSTEM = { context: { isSystem: true } };
@@ -94,6 +95,9 @@ describe('#24835 — a webhook delivery is stored for an update on a real SqlDri
engine.registry.registerObject(HttpDelivery as any, 'test');
engine.registry.registerObject(SysWebhook as any, 'test');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const realtime = new FakeRealtime();
engine.setRealtimeService(realtime);
diff --git a/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts
index 1d072ec2a10..c30eff7de6f 100644
--- a/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts
+++ b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts
@@ -569,11 +569,15 @@ describe('attachments permission matrix (#2755)', () => {
// Hook-bypass re-reference: a DIRECT DRIVER insert (no engine hooks
// fire, so afterInsert cannot un-tombstone) + a backdated tombstone.
const driver = ql.getDriverForObject('sys_attachment');
+ // The row carries its parent's organization, as the engine would stamp
+ // it: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ const parent = await ql.findOne('att_case', { where: { id: caseAId }, context: SYS });
await driver.create('sys_attachment', {
id: `bypass-${fileId}`,
parent_object: 'att_case',
parent_id: caseAId,
file_id: fileId,
+ organization_id: parent.organization_id,
created_at: new Date(),
});
await ql.update(
diff --git a/packages/qa/dogfood/test/default-organization.testkit.ts b/packages/qa/dogfood/test/default-organization.testkit.ts
new file mode 100644
index 00000000000..6767dfb63bf
--- /dev/null
+++ b/packages/qa/dogfood/test/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/dogfood',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/qa/dogfood/test/email-template-boot-sweep.test.ts b/packages/qa/dogfood/test/email-template-boot-sweep.test.ts
index 2d507ae5b35..f6fc749255e 100644
--- a/packages/qa/dogfood/test/email-template-boot-sweep.test.ts
+++ b/packages/qa/dogfood/test/email-template-boot-sweep.test.ts
@@ -32,6 +32,7 @@ import { describe, it, expect, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { EmailServicePlugin } from '@objectstack/plugin-email';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYS = { isSystem: true, positions: [], permissions: [] };
const TABLE = 'sys_email_template';
@@ -100,6 +101,10 @@ const onTable = (statements: string[]) => statements.filter((s) => s.includes(`\
* every row carrying stale wording a send must never render.
*/
async function seedSharedSlot(engine: ObjectQL): Promise {
+ // `etpl_z` belongs to no organization, which only a store built before 18.x
+ // can hold (ADR-0131 D1 makes a fresh tenant column NOT NULL): the table is
+ // that store's.
+ await relaxTenantColumns(engine as any, [TABLE]);
const rows: Array<[string, string | null, string]> = [
['etpl_m', 'org_b', 'en-US'],
['etpl_z', null, 'en-US'],
diff --git a/packages/qa/dogfood/test/engine-where-shape-refusal.test.ts b/packages/qa/dogfood/test/engine-where-shape-refusal.test.ts
index a7b7b4892f3..83cce916c2d 100644
--- a/packages/qa/dogfood/test/engine-where-shape-refusal.test.ts
+++ b/packages/qa/dogfood/test/engine-where-shape-refusal.test.ts
@@ -22,6 +22,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true, userId: 'usr_system' } };
@@ -67,6 +68,9 @@ beforeEach(async () => {
objects: [PROBE_ORDER],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const [id, name, amount] of [['o1', 'a', 50], ['o2', 'b', 150], ['o3', 'c', 50], ['o4', 'd', 150]] as const) {
await engine.insert('probe_order', { id, name, amount }, SYS as never);
}
diff --git a/packages/qa/dogfood/test/pre-18-tenant-column.testkit.ts b/packages/qa/dogfood/test/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..b0cbb5efa38
--- /dev/null
+++ b/packages/qa/dogfood/test/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Return engine-synced tables to the shape a store built BEFORE 18.x has: the
+ * tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x the SQL driver creates a table's tenant column NOT
+ * NULL. A store an earlier release built still holds the nullable column and
+ * its organization-less rows — the holdings, grants and seeds this package's
+ * NULL readers still answer for until the organization-ownership ceremony has
+ * attributed them. A suite whose subject is those rows calls this after
+ * `syncSchemas()` and before seeding them. Done through each table's driver
+ * and its own drift apply door (`relax_not_null`), never a hand `ALTER`.
+ */
+export async function relaxTenantColumns(
+ engine: { getDriverForObject(object: string): unknown },
+ tables: readonly string[],
+ column = 'organization_id',
+): Promise {
+ for (const table of tables) {
+ const driver = engine.getDriverForObject(table) as {
+ applyMigrationEntries(entries: never[]): Promise<{ applied: unknown[] }>;
+ };
+ const op = { type: 'relax_not_null', table, column };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column, category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
+ }
+}
+
+/**
+ * {@link relaxTenantColumns} over every object of an app that carries a tenant
+ * column as the engine registered it — the shape a whole pre-18 store has.
+ */
+export async function relaxEveryTenantColumn(
+ engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown },
+ objects: readonly { name: string }[],
+): Promise {
+ const tenant = objects
+ .map((o) => o.name)
+ .filter((name) => {
+ const schema = engine.getSchema(name) as { fields?: Record; tenancy?: { enabled?: boolean } } | undefined;
+ return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false;
+ });
+ await relaxTenantColumns(engine, tenant);
+}
+
+/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */
+export async function relaxEveryRegisteredTenantColumn(engine: {
+ getDriverForObject(object: string): unknown;
+ getSchema(object: string): unknown;
+ registry: { getAllObjects(): readonly { name: string }[] };
+}): Promise {
+ await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
+}
diff --git a/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts b/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts
index 8c404eca10a..5bb0860d4bb 100644
--- a/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts
+++ b/packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts
@@ -72,6 +72,7 @@ import { type VerifyStack } from '@objectstack/verify';
import { bootShowcase } from './showcase-boot.js';
import { leaveOrganization } from './armed.js';
import { catalogPermissionSet } from './catalog-permission-set.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const RULES = '/sharing/rules';
const SYS = { isSystem: true } as const;
@@ -171,6 +172,9 @@ describe('#8158 — a manage_sharing holder with NO active organization cannot r
// an organization-less system insert (ADR-0131 D9), so the row is landed
// beneath the engine, as the residue an older release left. It applies
// nowhere now.
+ // That residue lives only on a store built before 18.x, whose tenant column is
+ // nullable (ADR-0131 D1 makes a fresh one NOT NULL): the table is that store's.
+ await relaxTenantColumns(ql as any, ['sys_user_permission_set']);
await ql.getDriver('sys_user_permission_set').create('sys_user_permission_set', {
id: 'ups_8158_org_less', user_id: orgLessUserId, permission_set: 'sharing_admin_8158', organization_id: null,
});
diff --git a/packages/qa/dogfood/test/storage-growth.dogfood.test.ts b/packages/qa/dogfood/test/storage-growth.dogfood.test.ts
index 97eccd4e820..e6f138b3703 100644
--- a/packages/qa/dogfood/test/storage-growth.dogfood.test.ts
+++ b/packages/qa/dogfood/test/storage-growth.dogfood.test.ts
@@ -65,6 +65,7 @@ describe('objectstack verify LIFECYCLE (ADR-0057): declared policies bound growt
let engine: EngineLike;
let lifecycle: LifecycleLike;
let driver: DriverLike;
+ let organizationId: string;
// Platform storage convention: `created_at` is registry-injected as a
// `datetime` field, stored as epoch-ms INTEGER on SQLite (a JS Date through
@@ -76,6 +77,11 @@ describe('objectstack verify LIFECYCLE (ADR-0057): declared policies bound growt
beforeAll(async () => {
stack = await bootShowcase();
engine = stack.kernel.getService('objectql') as unknown as EngineLike;
+ // The runaway writer's rows belong to the install's organization, as the
+ // engine would stamp them: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ organizationId = String(
+ ((await (engine as any).find('sys_organization', { context: { isSystem: true } })) as Array<{ id: string }>)[0].id,
+ );
lifecycle = stack.kernel.getService('lifecycle') as unknown as LifecycleLike;
expect(lifecycle?.sweep, 'the ObjectQLPlugin must register the ADR-0057 lifecycle service').toBeTruthy();
@@ -148,12 +154,14 @@ describe('objectstack verify LIFECYCLE (ADR-0057): declared policies bound growt
// any window (must never be touched).
for (let i = 0; i < 40; i++) {
await driver.create('growth_probe_event', {
+ organization_id: organizationId,
payload: `tick-${i}`,
created_at: backdated(5 + i * 5), // 5d … 200d
});
}
for (let i = 0; i < 5; i++) {
await driver.create('growth_probe_record', {
+ organization_id: organizationId,
payload: `business-${i}`,
created_at: backdated(400),
});
@@ -215,7 +223,7 @@ describe('objectstack verify LIFECYCLE (ADR-0057): declared policies bound growt
// Round-trip: write through the driver, read through the view, and a
// sweep applies the 'rotation' policy without touching the rows inside
// the window.
- await streamDriver.create('growth_probe_stream', { payload: 'tick' });
+ await streamDriver.create('growth_probe_stream', { organization_id: organizationId, payload: 'tick' });
expect(await streamDriver.count('growth_probe_stream', {})).toBe(1);
const report = await lifecycle.sweep();
@@ -227,6 +235,7 @@ describe('objectstack verify LIFECYCLE (ADR-0057): declared policies bound growt
it('ARCHIVE SAFETY: an audit ledger with a declared archive is never hot-deleted unarchived', async () => {
for (let i = 0; i < 3; i++) {
await driver.create('growth_probe_ledger', {
+ organization_id: organizationId,
payload: `ledger-${i}`,
created_at: backdated(365), // far past the 90d hot window
});
diff --git a/packages/qa/dogfood/test/undeclared-key-conformance.test.ts b/packages/qa/dogfood/test/undeclared-key-conformance.test.ts
index 878a1b97a7c..22459778948 100644
--- a/packages/qa/dogfood/test/undeclared-key-conformance.test.ts
+++ b/packages/qa/dogfood/test/undeclared-key-conformance.test.ts
@@ -41,6 +41,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DRIVERS = [
{
@@ -139,6 +140,9 @@ describe.each(DRIVERS)('undeclared-key conformance: $name', ({ make }) => {
await engine.init();
engine.registerApp(APP);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterEach(async () => {
diff --git a/packages/qa/http-conformance/src/conformance.integration.test.ts b/packages/qa/http-conformance/src/conformance.integration.test.ts
index ddcbbdb53e3..04eba5237da 100644
--- a/packages/qa/http-conformance/src/conformance.integration.test.ts
+++ b/packages/qa/http-conformance/src/conformance.integration.test.ts
@@ -23,6 +23,7 @@ import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
import { createDispatcherPlugin, createRestApiPlugin } from '@objectstack/runtime';
import { NodeServerPlugin } from './node-plugin.js';
import type { IHttpServer } from '@objectstack/spec/contracts';
+import { installDefaultOrganization } from './default-organization.testkit.js';
type AdapterCase = {
label: 'node' | 'hono';
@@ -174,6 +175,9 @@ async function bootStack(makePlugin: () => any, opts: { withAnalytics?: boolean
// a 404 OBJECT_NOT_FOUND — a routing-shaped symptom for a DDL-shaped cause.
await ql.syncObjectSchema('task');
for (const o of authzResolverObjects) ql.registry.registerObject(o.def as never, o.owner);
+ // A real `single` install's Default Organization: the writes below derive
+ // it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const httpServer = kernel.getService('http.server');
return { kernel, base: `http://127.0.0.1:${httpServer.getPort!()}` };
diff --git a/packages/qa/http-conformance/src/default-organization.testkit.ts b/packages/qa/http-conformance/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..6c7bcdc0855
--- /dev/null
+++ b/packages/qa/http-conformance/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/http-conformance',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts b/packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts
index b2420779686..f665daa36d1 100644
--- a/packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts
+++ b/packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts
@@ -40,6 +40,7 @@ import { ApprovalsServicePlugin } from '@objectstack/plugin-approvals';
import type { ApprovalService } from '@objectstack/plugin-approvals';
import { createHonoApp } from '@objectstack/hono';
import type { IHttpServer } from '@objectstack/spec/contracts';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PREFIX = '/api/v1';
const ACT_PATH = `${PREFIX}/approvals/act`;
@@ -105,6 +106,9 @@ async function bootKernel(opts: { selfHosted: boolean; approvals?: boolean }) {
(objectql as any).registerDriver(driver, true);
objectql.registry.registerObject(leaveRequest as any, 'approvals-act-test', 'approvals-act-test');
await (objectql as any).syncSchemas();
+ // A real `single` install's Default Organization: the writes below derive
+ // it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql as any);
let n = 0;
/** A pending request with live action tokens for {@link APPROVER}. */
diff --git a/packages/qa/http-conformance/src/hono-body-refusal.conformance.test.ts b/packages/qa/http-conformance/src/hono-body-refusal.conformance.test.ts
index fd22a7fe9a0..5e8e5b9907a 100644
--- a/packages/qa/http-conformance/src/hono-body-refusal.conformance.test.ts
+++ b/packages/qa/http-conformance/src/hono-body-refusal.conformance.test.ts
@@ -40,6 +40,7 @@ import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { HttpDispatcher } from '@objectstack/runtime';
import { HonoHttpServer } from '@objectstack/plugin-hono-server';
import { createHonoApp } from '@objectstack/hono';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PREFIX = '/api/v1';
const MEMBER = { userId: 'u_member', isSystem: false, systemPermissions: [] as string[] };
@@ -85,6 +86,9 @@ async function boot(auth?: Record) {
const ql = kernel.getService('objectql');
ql.registerObject(TASK as any);
await ql.syncObjectSchema(TASK.name);
+ // A real `single` install's Default Organization: the writes below derive
+ // it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql as any);
vi.spyOn(HttpDispatcher.prototype as any, 'timedResolveExecutionContext')
.mockImplementation(async () => ({ ...MEMBER }));
diff --git a/packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts b/packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts
index 4f024dbeaad..135dbbd3b9b 100644
--- a/packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts
+++ b/packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts
@@ -83,6 +83,7 @@ import type { ObjectQL } from '@objectstack/objectql';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { HttpDispatcher } from '@objectstack/runtime';
import { createHonoApp } from '@objectstack/hono';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PREFIX = '/api/v1';
const MEMBER = { userId: 'u_member', isSystem: false, systemPermissions: [] as string[] };
@@ -172,6 +173,9 @@ async function boot(auth?: Record) {
ql.registerObject(object as any);
await ql.syncObjectSchema(object.name);
}
+ // A real `single` install's Default Organization: the writes below derive
+ // it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql as any);
vi.spyOn(HttpDispatcher.prototype as any, 'timedResolveExecutionContext')
.mockImplementation(async () => ({ ...MEMBER }));
diff --git a/packages/rest/src/aggregation-filter-array-membership.test.ts b/packages/rest/src/aggregation-filter-array-membership.test.ts
index 8fe9fbfb347..38c7b17a397 100644
--- a/packages/rest/src/aggregation-filter-array-membership.test.ts
+++ b/packages/rest/src/aggregation-filter-array-membership.test.ts
@@ -47,6 +47,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_agg_filter_membership_20873';
@@ -147,6 +148,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(DOC as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
if (populated) for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const protocol = new ObjectStackProtocolImplementation(engine as any);
diff --git a/packages/rest/src/aggregation-filter-json-column-refusal.test.ts b/packages/rest/src/aggregation-filter-json-column-refusal.test.ts
index dc2488badd3..7fe40b2bb39 100644
--- a/packages/rest/src/aggregation-filter-json-column-refusal.test.ts
+++ b/packages/rest/src/aggregation-filter-json-column-refusal.test.ts
@@ -52,6 +52,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_agg_filter_json_col_21007';
@@ -215,6 +216,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(DOC as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
if (populated) for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
warn = vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
diff --git a/packages/rest/src/aggregation-filter-temporal-storage-rule.test.ts b/packages/rest/src/aggregation-filter-temporal-storage-rule.test.ts
index 8d5047f841e..17936ba474f 100644
--- a/packages/rest/src/aggregation-filter-temporal-storage-rule.test.ts
+++ b/packages/rest/src/aggregation-filter-temporal-storage-rule.test.ts
@@ -34,6 +34,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'ledger_order';
@@ -89,6 +90,9 @@ async function boot(populated: boolean) {
await engine.init();
engine.registry.registerObject(LEDGER_ORDER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
if (populated) await engine.insert(OBJECT, ROWS as any);
const protocol = new ObjectStackProtocolImplementation(engine as any);
diff --git a/packages/rest/src/aggregation-filter-where-doors.test.ts b/packages/rest/src/aggregation-filter-where-doors.test.ts
index a05b29d3d99..c0b798b5f49 100644
--- a/packages/rest/src/aggregation-filter-where-doors.test.ts
+++ b/packages/rest/src/aggregation-filter-where-doors.test.ts
@@ -46,6 +46,7 @@ import { ObjectQL, applyInMemoryAggregation } from '@objectstack/objectql';
import { SqlDriver, withheldFilterDiagnosticOf } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'ledger_order';
@@ -114,6 +115,9 @@ async function boot(
await engine.init();
engine.registry.registerObject(object as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
if (populated) await engine.insert(OBJECT, rows as any);
const calls = { aggregate: 0, find: 0 };
diff --git a/packages/rest/src/aggregation-flag-comparand-refusal.test.ts b/packages/rest/src/aggregation-flag-comparand-refusal.test.ts
index b88b01f965e..d6a53f481a3 100644
--- a/packages/rest/src/aggregation-flag-comparand-refusal.test.ts
+++ b/packages/rest/src/aggregation-flag-comparand-refusal.test.ts
@@ -55,6 +55,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver, withheldFilterDiagnosticOf } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_agg_flag_comparand_20981';
@@ -117,6 +118,9 @@ describe('[#20981] a non-boolean $exists / $null in a per-aggregation filter or
await engine.init();
engine.registry.registerObject(DOC as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
diff --git a/packages/rest/src/analytics-contains-membership-door.test.ts b/packages/rest/src/analytics-contains-membership-door.test.ts
index bd9fdec3e2d..da4ad6f4f9e 100644
--- a/packages/rest/src/analytics-contains-membership-door.test.ts
+++ b/packages/rest/src/analytics-contains-membership-door.test.ts
@@ -35,6 +35,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_contains_membership_item';
@@ -142,6 +143,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(ITEM as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// The plugin's own composition over the real engine: both auto-bridges,
diff --git a/packages/rest/src/analytics-dataset-aggregate-policies-door.test.ts b/packages/rest/src/analytics-dataset-aggregate-policies-door.test.ts
index db2346b126d..0dbc4d923a9 100644
--- a/packages/rest/src/analytics-dataset-aggregate-policies-door.test.ts
+++ b/packages/rest/src/analytics-dataset-aggregate-policies-door.test.ts
@@ -40,6 +40,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_dataset_policy_ledger';
@@ -192,6 +193,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/rest/src/analytics-dataset-json-dimension-door.test.ts b/packages/rest/src/analytics-dataset-json-dimension-door.test.ts
index ac9369f9253..990e1acc776 100644
--- a/packages/rest/src/analytics-dataset-json-dimension-door.test.ts
+++ b/packages/rest/src/analytics-dataset-json-dimension-door.test.ts
@@ -44,6 +44,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_dataset_json_dim_ledger';
/** The object the ledger's `account` lookup references — joined through the dataset's `include`. */
@@ -166,6 +167,9 @@ for (const cell of CELLS) {
engine.registry.registerObject(ACCOUNT_OBJECT as any);
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ACCOUNTS) await engine.insert(ACCOUNT, { ...row } as any);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
diff --git a/packages/rest/src/analytics-dataset-measure-number-door.test.ts b/packages/rest/src/analytics-dataset-measure-number-door.test.ts
index 775f163a1ea..7d1bc04ff70 100644
--- a/packages/rest/src/analytics-dataset-measure-number-door.test.ts
+++ b/packages/rest/src/analytics-dataset-measure-number-door.test.ts
@@ -45,6 +45,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_dataset_measure_number_ledger';
@@ -187,6 +188,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/rest/src/analytics-dataset-multi-value-door.test.ts b/packages/rest/src/analytics-dataset-multi-value-door.test.ts
index 0ebe3edcfd7..86f3cd26eb6 100644
--- a/packages/rest/src/analytics-dataset-multi-value-door.test.ts
+++ b/packages/rest/src/analytics-dataset-multi-value-door.test.ts
@@ -41,6 +41,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_dataset_multi_value_ledger';
const OPTIONS = [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }];
@@ -144,6 +145,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/rest/src/analytics-dataset-row-wildcard-door.test.ts b/packages/rest/src/analytics-dataset-row-wildcard-door.test.ts
index 95ec535503f..82909054c72 100644
--- a/packages/rest/src/analytics-dataset-row-wildcard-door.test.ts
+++ b/packages/rest/src/analytics-dataset-row-wildcard-door.test.ts
@@ -49,6 +49,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_dataset_row_wildcard_ledger';
@@ -145,6 +146,9 @@ for (const strategy of ['native', 'objectql'] as const) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/rest/src/analytics-field-permission-gate.test.ts b/packages/rest/src/analytics-field-permission-gate.test.ts
index 96c908f1353..93bf7d15bbe 100644
--- a/packages/rest/src/analytics-field-permission-gate.test.ts
+++ b/packages/rest/src/analytics-field-permission-gate.test.ts
@@ -44,6 +44,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_an_fls_ledger';
const OWNER = 'rest_an_fls_owner';
@@ -188,6 +189,9 @@ async function boot(strategy: 'native' | 'objectql'): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/analytics-hop-object-reference.test.ts b/packages/rest/src/analytics-hop-object-reference.test.ts
index 7dda9909152..19f88683eb7 100644
--- a/packages/rest/src/analytics-hop-object-reference.test.ts
+++ b/packages/rest/src/analytics-hop-object-reference.test.ts
@@ -39,6 +39,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const LEDGER = 'rest_an_hop_ledger';
/** The target of lookup `owner`: readable by the member. */
@@ -138,6 +139,9 @@ async function boot(strategy: 'native' | 'objectql'): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/analytics-masked-field-gate.test.ts b/packages/rest/src/analytics-masked-field-gate.test.ts
index 8f55d6c76f2..3998c6a9773 100644
--- a/packages/rest/src/analytics-masked-field-gate.test.ts
+++ b/packages/rest/src/analytics-masked-field-gate.test.ts
@@ -43,6 +43,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_an_mask_ledger';
const OWNER = 'rest_an_mask_owner';
@@ -189,6 +190,9 @@ async function boot(strategy: 'native' | 'objectql'): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/analytics-nested-relation-filter.test.ts b/packages/rest/src/analytics-nested-relation-filter.test.ts
index b1d449ace1d..86aeef47842 100644
--- a/packages/rest/src/analytics-nested-relation-filter.test.ts
+++ b/packages/rest/src/analytics-nested-relation-filter.test.ts
@@ -51,6 +51,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_an_nested_ledger';
const OWNER = 'rest_an_nested_owner';
@@ -194,6 +195,9 @@ async function boot(opts: { caps?: 'objectql'; getReadScope?: (object: string) =
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/analytics-not-multivalue-null-guard.test.ts b/packages/rest/src/analytics-not-multivalue-null-guard.test.ts
index 6fd92fd5e6e..ff77f4dde11 100644
--- a/packages/rest/src/analytics-not-multivalue-null-guard.test.ts
+++ b/packages/rest/src/analytics-not-multivalue-null-guard.test.ts
@@ -55,6 +55,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_an_notmv_ledger';
const OWNER = 'rest_an_notmv_owner';
@@ -179,6 +180,9 @@ async function boot(caps?: 'objectql'): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/analytics-relationship-path-admission.test.ts b/packages/rest/src/analytics-relationship-path-admission.test.ts
index 0b37bbc37a4..0be69e61fd8 100644
--- a/packages/rest/src/analytics-relationship-path-admission.test.ts
+++ b/packages/rest/src/analytics-relationship-path-admission.test.ts
@@ -39,6 +39,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const LEDGER = 'rest_an_rpa_ledger';
/** Reached through a relationship path; the member holds no read grant on it. */
@@ -222,6 +223,9 @@ async function boot(strategy: 'native' | 'objectql'): Promise {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts b/packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts
index 3c0a8720e33..463db27795d 100644
--- a/packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts
+++ b/packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts
@@ -41,6 +41,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The record matcher's remedy, as its message opens. */
const MATCHER_REMEDY =
@@ -114,6 +115,9 @@ async function boot(f: Fixture) {
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const set = PermissionSetSchema.parse({
name: 'qa_cross_class_remedy',
diff --git a/packages/rest/src/data-aggregate-field-type-door.test.ts b/packages/rest/src/data-aggregate-field-type-door.test.ts
index a0e0fbb234c..4ab219e8e9b 100644
--- a/packages/rest/src/data-aggregate-field-type-door.test.ts
+++ b/packages/rest/src/data-aggregate-field-type-door.test.ts
@@ -49,6 +49,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_aggregate_type_20914';
const TARGET = 'rest_aggregate_type_target_20914';
@@ -155,6 +156,9 @@ for (const cell of CELLS) {
engine.registry.registerObject(TARGET_OBJECT as any);
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const id of ['t1', 't2']) await engine.insert(TARGET, { id, name: id } as any);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
diff --git a/packages/rest/src/data-boolean-comparand-door.test.ts b/packages/rest/src/data-boolean-comparand-door.test.ts
index 6194e6782c5..f8358372402 100644
--- a/packages/rest/src/data-boolean-comparand-door.test.ts
+++ b/packages/rest/src/data-boolean-comparand-door.test.ts
@@ -43,6 +43,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_boolean_door_21382';
@@ -174,6 +175,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(TASKS as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Reads of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-date-read-year-below-100.test.ts b/packages/rest/src/data-date-read-year-below-100.test.ts
index 96446029f6e..0395db7d4b2 100644
--- a/packages/rest/src/data-date-read-year-below-100.test.ts
+++ b/packages/rest/src/data-date-read-year-below-100.test.ts
@@ -48,6 +48,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest20280_days';
@@ -136,6 +137,9 @@ function measure(cell: Cell, config: Record): void {
await engine.init();
engine.registry.registerObject(DAYS_OBJECT as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any);
const rest = new RestServer(createMockServer() as any, protocol as any, { api: { requireAuth: false } } as any);
diff --git a/packages/rest/src/data-date-write-iso-only.test.ts b/packages/rest/src/data-date-write-iso-only.test.ts
index f28ec5f17ea..43bfd07c18e 100644
--- a/packages/rest/src/data-date-write-iso-only.test.ts
+++ b/packages/rest/src/data-date-write-iso-only.test.ts
@@ -41,6 +41,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_date_20481';
@@ -125,6 +126,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, { id: 'o1', customer_id: 'c1', placed_on: '2026-01-10' } as any);
// Writes of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-field-comparand-permission.test.ts b/packages/rest/src/data-field-comparand-permission.test.ts
index 025bd82aca2..f83a8166628 100644
--- a/packages/rest/src/data-field-comparand-permission.test.ts
+++ b/packages/rest/src/data-field-comparand-permission.test.ts
@@ -30,6 +30,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_comparand_perm_ledger';
@@ -144,6 +145,9 @@ describe('a hidden field as a cross-field comparand answers the key form\'s refu
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/data-group-by-json-door.test.ts b/packages/rest/src/data-group-by-json-door.test.ts
index d97e6b141da..e900f537e33 100644
--- a/packages/rest/src/data-group-by-json-door.test.ts
+++ b/packages/rest/src/data-group-by-json-door.test.ts
@@ -40,6 +40,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_group_by_json_20783';
@@ -135,6 +136,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Reads of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-json-stored-group-distinct-door.test.ts b/packages/rest/src/data-json-stored-group-distinct-door.test.ts
index 993069cceb6..c167ecd9ba6 100644
--- a/packages/rest/src/data-json-stored-group-distinct-door.test.ts
+++ b/packages/rest/src/data-json-stored-group-distinct-door.test.ts
@@ -42,6 +42,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_json_stored_key_20808';
const TARGET = 'rest_json_stored_key_target_20808';
@@ -170,6 +171,9 @@ for (const cell of CELLS) {
engine.registry.registerObject(TARGET_OBJECT as any);
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const id of ['t1', 't2']) await engine.insert(TARGET, { id, name: id } as any);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
diff --git a/packages/rest/src/data-nested-object-door.test.ts b/packages/rest/src/data-nested-object-door.test.ts
index 551f106faf6..956ac5ee152 100644
--- a/packages/rest/src/data-nested-object-door.test.ts
+++ b/packages/rest/src/data-nested-object-door.test.ts
@@ -49,6 +49,7 @@ import { ObjectQL, RELATION_FILTER_ID_CAP } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_nested_obj_20745';
const OWNER = 'rest_nested_own_20745';
@@ -182,6 +183,9 @@ for (const cell of CELLS) {
engine.registry.registerObject(OWNER_OBJECT as any);
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of OWNERS) await engine.insert(OWNER, { ...row } as any);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
diff --git a/packages/rest/src/data-nested-relation-permission.test.ts b/packages/rest/src/data-nested-relation-permission.test.ts
index 747b5c3f16c..72de5ec4377 100644
--- a/packages/rest/src/data-nested-relation-permission.test.ts
+++ b/packages/rest/src/data-nested-relation-permission.test.ts
@@ -33,6 +33,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SecurityPlugin } from '@objectstack/plugin-security';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_nested_perm_ledger';
const OWNER = 'rest_nested_perm_owner';
@@ -120,6 +121,9 @@ describe('[#20802] the nested-relation form reads the related object as the call
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/rest/src/data-no-operator-object-door.test.ts b/packages/rest/src/data-no-operator-object-door.test.ts
index f715073679d..459a645f802 100644
--- a/packages/rest/src/data-no-operator-object-door.test.ts
+++ b/packages/rest/src/data-no-operator-object-door.test.ts
@@ -45,6 +45,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_no_op_object_20546';
const OWNER = 'rest_no_op_owner_20546';
@@ -157,6 +158,9 @@ for (const cell of CELLS) {
engine.registry.registerObject(OWNER_OBJECT as any);
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of OWNERS) await engine.insert(OWNER, { ...row } as any);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
diff --git a/packages/rest/src/data-number-comparand-door.test.ts b/packages/rest/src/data-number-comparand-door.test.ts
index 40708777f58..5636eb3d692 100644
--- a/packages/rest/src/data-number-comparand-door.test.ts
+++ b/packages/rest/src/data-number-comparand-door.test.ts
@@ -50,6 +50,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_number_door_20351';
@@ -204,6 +205,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Reads of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-query-calendar-day-last-day.test.ts b/packages/rest/src/data-query-calendar-day-last-day.test.ts
index bf1b7bb7b96..bed70473c1b 100644
--- a/packages/rest/src/data-query-calendar-day-last-day.test.ts
+++ b/packages/rest/src/data-query-calendar-day-last-day.test.ts
@@ -45,6 +45,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_calendar_day_20600';
const HOST_ZONE = 'America/New_York';
@@ -142,6 +143,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any);
const rest = new RestServer(createMockServer() as any, protocol as any, { api: { requireAuth: false } } as any);
diff --git a/packages/rest/src/data-query-calendar-day-year-below-100.test.ts b/packages/rest/src/data-query-calendar-day-year-below-100.test.ts
index 014dd3d6f6b..d8a042ba844 100644
--- a/packages/rest/src/data-query-calendar-day-year-below-100.test.ts
+++ b/packages/rest/src/data-query-calendar-day-year-below-100.test.ts
@@ -51,6 +51,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_calendar_day_20550';
const HOST_ZONE = 'America/New_York';
@@ -147,6 +148,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any);
const rest = new RestServer(createMockServer() as any, protocol as any, { api: { requireAuth: false } } as any);
diff --git a/packages/rest/src/data-query-date-year-range.test.ts b/packages/rest/src/data-query-date-year-range.test.ts
index 6dd199f5de5..274a9c4e99b 100644
--- a/packages/rest/src/data-query-date-year-range.test.ts
+++ b/packages/rest/src/data-query-date-year-range.test.ts
@@ -38,6 +38,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'ledger_year';
@@ -106,6 +107,9 @@ async function boot() {
await engine.init();
engine.registry.registerObject(LEDGER_YEAR as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, ROWS as any);
// Reads of THIS object — the protocol's own metadata reads are not the question.
diff --git a/packages/rest/src/data-query-epoch-ms-date-comparand.test.ts b/packages/rest/src/data-query-epoch-ms-date-comparand.test.ts
index e5137ba441c..aabf958cf0c 100644
--- a/packages/rest/src/data-query-epoch-ms-date-comparand.test.ts
+++ b/packages/rest/src/data-query-epoch-ms-date-comparand.test.ts
@@ -30,6 +30,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'ledger_order';
@@ -95,6 +96,9 @@ async function boot(populated: boolean): Promise {
await engine.init();
engine.registry.registerObject(LEDGER_ORDER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
if (populated) await engine.insert(OBJECT, ROWS as any);
const protocol = new ObjectStackProtocolImplementation(engine as any);
diff --git a/packages/rest/src/data-query-having-temporal-door.test.ts b/packages/rest/src/data-query-having-temporal-door.test.ts
index 57a00504bc1..5943e1dd46d 100644
--- a/packages/rest/src/data-query-having-temporal-door.test.ts
+++ b/packages/rest/src/data-query-having-temporal-door.test.ts
@@ -31,6 +31,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'ledger_having';
@@ -87,6 +88,9 @@ async function boot() {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, ROWS as any);
// Reads of THIS object — the protocol's own metadata reads are not the question.
diff --git a/packages/rest/src/data-query-unprojected-declared-fields.test.ts b/packages/rest/src/data-query-unprojected-declared-fields.test.ts
index ce1e5915b11..f2133e3c404 100644
--- a/packages/rest/src/data-query-unprojected-declared-fields.test.ts
+++ b/packages/rest/src/data-query-unprojected-declared-fields.test.ts
@@ -37,6 +37,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rq_contact';
const LADDER = 'rq_ladder';
@@ -122,6 +123,9 @@ async function bootOne(): Promise {
engine.registry.registerObject(CONTACT_V1 as any);
engine.registry.registerObject(LADDER_V1 as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, [
{ id: 'c1', name: 'Ada', email: 'ada@example.com', mailing_street: '1 Retired Way', mailing_city: 'Oldtown' },
{ id: 'c2', name: 'Bo', email: 'bo@example.com', mailing_street: '2 Retired Way', mailing_city: 'Oldtown' },
@@ -138,6 +142,9 @@ async function bootTwo() {
await engine.init();
engine.registry.registerObject(CONTACT_V2 as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
engine.registry.registerObject(LADDER_V2 as any);
const protocol = new ObjectStackProtocolImplementation(engine as any);
diff --git a/packages/rest/src/data-resolved-token-past-date-range.test.ts b/packages/rest/src/data-resolved-token-past-date-range.test.ts
index a620d9d25d4..c6cb22173eb 100644
--- a/packages/rest/src/data-resolved-token-past-date-range.test.ts
+++ b/packages/rest/src/data-resolved-token-past-date-range.test.ts
@@ -30,6 +30,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// Wed 2026-09-30 12:00 UTC: every resolved value below is read off this instant.
const PINNED_NOW = new Date('2026-09-30T12:00:00.000Z');
@@ -100,6 +101,9 @@ describe('[#21068] a date macro past the instants a Date holds, at the public do
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Reads of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-resolved-token-year-range.test.ts b/packages/rest/src/data-resolved-token-year-range.test.ts
index e32a09d2fba..e60c3e3e0f2 100644
--- a/packages/rest/src/data-resolved-token-year-range.test.ts
+++ b/packages/rest/src/data-resolved-token-year-range.test.ts
@@ -33,6 +33,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// Wed 2026-09-30 12:00 UTC: every resolved day below is read off this instant.
const PINNED_NOW = new Date('2026-09-30T12:00:00.000Z');
@@ -117,6 +118,9 @@ describe('[#20844] a relative-date placeholder resolved outside its field\'s yea
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Reads of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-search-multi-valued-membership.test.ts b/packages/rest/src/data-search-multi-valued-membership.test.ts
index 637a1449542..7a45a2daeeb 100644
--- a/packages/rest/src/data-search-multi-valued-membership.test.ts
+++ b/packages/rest/src/data-search-multi-valued-membership.test.ts
@@ -36,6 +36,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** `examples/app-todo`'s `todo_task.tags` shape: a `select` declared `multiple: true`, in the auto-default set. */
const TASK = {
@@ -154,6 +155,9 @@ for (const cell of CELLS) {
engine.registry.registerObject(TASK as any);
engine.registry.registerObject(NOTE as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of TASK_ROWS) await engine.insert(TASK.name, { ...row } as any);
for (const row of NOTE_ROWS) await engine.insert(NOTE.name, { ...row } as any);
vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
diff --git a/packages/rest/src/data-temporal-write-real-day-iso.test.ts b/packages/rest/src/data-temporal-write-real-day-iso.test.ts
index 2c0345de061..f9b145ec796 100644
--- a/packages/rest/src/data-temporal-write-real-day-iso.test.ts
+++ b/packages/rest/src/data-temporal-write-real-day-iso.test.ts
@@ -86,6 +86,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { renderValidationMessage } from '@objectstack/spec/system';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_temporal_20525';
const HOST_ZONE = 'America/New_York';
@@ -185,6 +186,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, { id: 'o1', customer_id: 'c1', placed_on: '2026-01-10', opened_at: '2026-01-10T09:00:00Z' } as any);
// Writes of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-temporal-year-range.test.ts b/packages/rest/src/data-temporal-year-range.test.ts
index 22b51886daa..11fd86a7474 100644
--- a/packages/rest/src/data-temporal-year-range.test.ts
+++ b/packages/rest/src/data-temporal-year-range.test.ts
@@ -47,6 +47,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_year_20264';
@@ -195,6 +196,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Reads and writes of THIS object — the protocol's own metadata traffic is not the question.
diff --git a/packages/rest/src/data-write-result-declared-fields.test.ts b/packages/rest/src/data-write-result-declared-fields.test.ts
index cd17c750d15..5cc74f00c63 100644
--- a/packages/rest/src/data-write-result-declared-fields.test.ts
+++ b/packages/rest/src/data-write-result-declared-fields.test.ts
@@ -35,6 +35,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rq_contact';
const RETIRED = ['mailing_street', 'mailing_city'] as const;
@@ -102,6 +103,9 @@ async function bootOne(): Promise {
await engine.init();
engine.registry.registerObject(CONTACT_V1 as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, SEEDED.map((id) => ({
id, name: id, email: `${id}@example.com`, api_token: `tok_${id}`,
mailing_street: `${id} Retired Way`, mailing_city: 'Oldtown',
@@ -120,6 +124,9 @@ async function bootTwo() {
await engine.init();
engine.registry.registerObject(CONTACT_V2 as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const events: Captured[] = [];
engine.setRealtimeService({
diff --git a/packages/rest/src/default-organization.testkit.ts b/packages/rest/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..0bafb98f970
--- /dev/null
+++ b/packages/rest/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/rest',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/rest/src/import-control-opt-out-gate.test.ts b/packages/rest/src/import-control-opt-out-gate.test.ts
index 3573f353c0c..8b34d02812a 100644
--- a/packages/rest/src/import-control-opt-out-gate.test.ts
+++ b/packages/rest/src/import-control-opt-out-gate.test.ts
@@ -36,6 +36,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco
import { SysMetadataAuditObject, SysMetadataCommitObject, SysMetadataHistoryObject, SysMetadataObject } from '@objectstack/metadata-core';
import { SysImportJob } from '@objectstack/platform-objects/audit';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
function makeSqliteDriver() {
return new SqlDriver({
@@ -109,6 +110,9 @@ async function boot() {
await engine.init();
for (const o of [DEAL, CASE, HIDDEN, SysImportJob]) engine.registry.registerObject(o as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const o of [SysMetadataObject, SysMetadataHistoryObject, SysMetadataAuditObject, SysMetadataCommitObject]) {
if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as any);
}
diff --git a/packages/rest/src/import-integration.test.ts b/packages/rest/src/import-integration.test.ts
index 57c0a636519..d1f81f112b1 100644
--- a/packages/rest/src/import-integration.test.ts
+++ b/packages/rest/src/import-integration.test.ts
@@ -30,6 +30,7 @@ import { SysMetadataAuditObject, SysMetadataCommitObject, SysMetadataHistoryObje
import { MetadataManager } from '@objectstack/metadata';
import { RestServer } from './rest-server';
import { loadExcelJs } from './xlsx-module.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// ---------------------------------------------------------------------------
// The real backend: better-sqlite3 `:memory:`, constructed the canonical way
@@ -158,6 +159,9 @@ async function boot(services?: Map) {
// Real DDL — the NOT NULL / column types the importer's dry run claims to
// predict are now physically there to be violated.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert('user', { id: 'u1', name: '张三', email: 'zhang@x.com' });
await engine.insert('user', { id: 'u2', name: '李四', email: 'li@x.com' });
@@ -651,6 +655,9 @@ describe('import route — a mapping target that names no field (#20150)', () =>
({ route, engine } = await boot());
engine.registry.registerObject(NOTE as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// A dotted path whose head names no field of the object is no target
// (`task` declares no `mailing_address`; #20149 made a declared PART of a
// compound field a target, pinned in the describe block below).
@@ -776,6 +783,9 @@ describe('import route — a declared part of a compound field (#20149)', () =>
({ route, engine } = await boot());
engine.registry.registerObject(CONTACT as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// The customer template's five columns, unchanged (hotcrm#1836).
register('contact_address_feed', [
{ source: 'ID', target: 'id' },
diff --git a/packages/rest/src/import-job-integration.test.ts b/packages/rest/src/import-job-integration.test.ts
index 33cf9b98588..cfa9745e7e7 100644
--- a/packages/rest/src/import-job-integration.test.ts
+++ b/packages/rest/src/import-job-integration.test.ts
@@ -30,6 +30,7 @@ import { SysImportJob } from '@objectstack/platform-objects/audit';
// from here so that moving it is observable at the enforcement point.
import { IMPORT_JOB_MAX_ROWS } from '@objectstack/spec/api';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// The real backend: better-sqlite3 `:memory:`, constructed the canonical way
// (`examples/app-crm`, `cli db clean`, PR #5715's `makeDefaultDriver()`).
@@ -102,6 +103,9 @@ async function boot(decorateDriver?: (driver: any) => void) {
engine.registry.registerObject(SYS_IMPORT_JOB as any);
// Real DDL for both tables before the first job row is written.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// [#21516] The protocol reads the stored-metadata family; the engine refuses a
// name its registry does not hold, so the harness registers the family as a boot
@@ -291,15 +295,17 @@ describe('async import job — real engine + protocol integration', () => {
// job is running — but the 3-row job has no mid-loop checkpoint, so the
// loop still completes every row before noticing.
const slowCtx = await boot((driver) => {
+ // Every argument is forwarded: the options carry the organization the
+ // engine stamps (ADR-0131 D9), which a NOT NULL tenant column needs (D1).
const bulkCreate = driver.bulkCreate.bind(driver);
- driver.bulkCreate = async (o: string, rows2: any[]) => {
+ driver.bulkCreate = async (o: string, rows2: any[], ...rest: any[]) => {
if (o === 'task') await new Promise((r) => setImmediate(r));
- return bulkCreate(o, rows2);
+ return bulkCreate(o, rows2, ...rest);
};
const create = driver.create.bind(driver);
- driver.create = async (o: string, data: any) => {
+ driver.create = async (o: string, data: any, ...rest: any[]) => {
if (o === 'task') await new Promise((r) => setImmediate(r));
- return create(o, data);
+ return create(o, data, ...rest);
};
});
const created = await callCreate(slowCtx.create, {
diff --git a/packages/rest/src/import-row-dropped-fields-20701.test.ts b/packages/rest/src/import-row-dropped-fields-20701.test.ts
index bfc8a671a56..5449d158895 100644
--- a/packages/rest/src/import-row-dropped-fields-20701.test.ts
+++ b/packages/rest/src/import-row-dropped-fields-20701.test.ts
@@ -41,6 +41,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { SysImportJob } from '@objectstack/platform-objects/audit';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'proj_drops_20701';
@@ -91,6 +92,9 @@ async function boot() {
engine.registry.registerObject(PROJ as any);
engine.registry.registerObject(SysImportJob as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, { id: 'e1', title: 'open one', n: 1, status: 'open' } as any);
// A record whose `readonlyWhen` is already true; seeded under `isSystem`.
await engine.insert(OBJECT, { id: 'e2', title: 'closed one', n: 1, status: 'closed', locked: 'L' } as any, { context: { isSystem: true } } as any);
diff --git a/packages/rest/src/import-row-schema-drift-20701.test.ts b/packages/rest/src/import-row-schema-drift-20701.test.ts
index 1ce9441a493..0ee17a0ba0f 100644
--- a/packages/rest/src/import-row-schema-drift-20701.test.ts
+++ b/packages/rest/src/import-row-schema-drift-20701.test.ts
@@ -46,6 +46,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { SysImportJob } from '@objectstack/platform-objects/audit';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'proj_drift_20701';
@@ -94,6 +95,9 @@ async function boot() {
engine.registry.registerObject(PROJ as any);
engine.registry.registerObject(SysImportJob as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, { id: 'e1', title: 'existing', code: 'X', must: 'm' } as any);
// The drift: `late` is declared after the DDL ran.
engine.registry.registerObject(PROJ_DRIFTED as any);
diff --git a/packages/rest/src/import-template-route.test.ts b/packages/rest/src/import-template-route.test.ts
index 4e24b1a8722..f592482f332 100644
--- a/packages/rest/src/import-template-route.test.ts
+++ b/packages/rest/src/import-template-route.test.ts
@@ -23,6 +23,7 @@ import { RestServer } from './rest-server';
import { parseXlsxToRows } from './import-prepare.js';
import { isTemplateRequired, templateInsertDefault } from './import-template.js';
import { loadXlsxWorkbook } from './xlsx-test-loader.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
function makeSqliteDriver() {
return new SqlDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
@@ -124,6 +125,9 @@ async function boot(opts: BootOptions = {}) {
await engine.init();
for (const o of [ACCOUNT, LINE, DEAL]) engine.registry.registerObject(o as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert('account', { id: 'a1', name: 'Acme' });
// [#21516] The protocol reads the stored-metadata family; the engine refuses a
// name its registry does not hold, so the harness registers the family as a boot
@@ -489,6 +493,9 @@ describe('?template=true — a required field the engine fills from its option d
const { get, importRoute, engine } = await boot();
engine.registry.registerObject(TICKET as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const bytes = (await get({ template: 'true' }, 'ticket')).body();
const wb = await loadXlsxWorkbook(bytes);
@@ -603,6 +610,9 @@ describe('the `*` agrees with the engine: starred exactly when the import door r
} as any);
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const disagreements: string[] = [];
for (const [key, { def: authored, blank, stored, served }] of Object.entries(PARITY)) {
@@ -752,6 +762,9 @@ async function bootExportFixture(security?: Record) {
},
} as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert('user', { id: 'u1', name: '张三' });
await engine.insert('user', { id: 'u2', name: '李四' });
await engine.insert('task', { id: '1', title: '写代码', done: true, priority: 'high', due: '2026-06-30T00:00:00.000Z', owner: 'u1' });
diff --git a/packages/rest/src/import-undo-controls.test.ts b/packages/rest/src/import-undo-controls.test.ts
index 6fdb890c8a5..7bbbf9980a8 100644
--- a/packages/rest/src/import-undo-controls.test.ts
+++ b/packages/rest/src/import-undo-controls.test.ts
@@ -38,6 +38,7 @@ import { SysMetadataAuditObject, SysMetadataCommitObject, SysMetadataHistoryObje
import { SysImportJob } from '@objectstack/platform-objects/audit';
import { UndoImportJobResponseSchema } from '@objectstack/spec/api';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
function makeSqliteDriver() {
return new SqlDriver({
@@ -130,6 +131,9 @@ async function boot() {
await engine.init();
for (const o of [DEAL, CASE, TICKET, SysImportJob]) engine.registry.registerObject(o as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const o of [SysMetadataObject, SysMetadataHistoryObject, SysMetadataAuditObject, SysMetadataCommitObject]) {
if (!engine.registry.getObject(o.name)) engine.registry.registerObject(o as any);
}
diff --git a/packages/rest/src/meta-delete-drop-storage-other-rows.test.ts b/packages/rest/src/meta-delete-drop-storage-other-rows.test.ts
index 507e6c9d2aa..8686ca6ee50 100644
--- a/packages/rest/src/meta-delete-drop-storage-other-rows.test.ts
+++ b/packages/rest/src/meta-delete-drop-storage-other-rows.test.ts
@@ -42,6 +42,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco
import { SysMetadata, SysMetadataHistoryObject, SysMetadataAuditObject } from '@objectstack/platform-objects/metadata';
import { hashSpec } from '@objectstack/metadata-core';
import { RestServer } from './rest-server.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** `registry.registerObject` requires a package id (see the sibling real-stack tests). */
const TEST_PACKAGE_ID = 'objectstack-test';
@@ -97,6 +98,9 @@ async function boot(environmentId?: string) {
engine.registry.registerObject(SysMetadataHistoryObject as any, TEST_PACKAGE_ID);
engine.registry.registerObject(SysMetadataAuditObject as any, TEST_PACKAGE_ID);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any, undefined, environmentId);
const config = { api: { requireAuth: false }, metadata: { enableCache: false } };
diff --git a/packages/rest/src/public-form-required.test.ts b/packages/rest/src/public-form-required.test.ts
index 28c59f1e98e..88b9fc40239 100644
--- a/packages/rest/src/public-form-required.test.ts
+++ b/packages/rest/src/public-form-required.test.ts
@@ -34,6 +34,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'pf_required_lead_24131';
const SLUG = 'pf-required-24131';
@@ -126,6 +127,9 @@ describe('[#24131] POST /forms/:slug/submit enforces the form\'s own `required`'
await engine.init();
engine.registry.registerObject(LEAD as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol: any = new ObjectStackProtocolImplementation(engine as any);
const realGetMetaItems = protocol.getMetaItems.bind(protocol);
diff --git a/packages/rest/src/rest-aggregate-compensated-sum.test.ts b/packages/rest/src/rest-aggregate-compensated-sum.test.ts
index eb34a866cbf..7a228ad0753 100644
--- a/packages/rest/src/rest-aggregate-compensated-sum.test.ts
+++ b/packages/rest/src/rest-aggregate-compensated-sum.test.ts
@@ -38,6 +38,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_agg_20489';
@@ -105,6 +106,9 @@ describe('[#20489] sum / avg — one double on both SQLite paths, engine and RES
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
let i = 0;
for (const [g, { values }] of Object.entries(GROUPS)) {
for (const w of values) await engine.insert(OBJECT, { id: `r${i++}`, g, w } as any);
diff --git a/packages/rest/src/rest-aggregate-numeric-having.test.ts b/packages/rest/src/rest-aggregate-numeric-having.test.ts
index d6af006929b..9bbe00eaa5b 100644
--- a/packages/rest/src/rest-aggregate-numeric-having.test.ts
+++ b/packages/rest/src/rest-aggregate-numeric-having.test.ts
@@ -39,6 +39,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'rest_agg_20335';
@@ -155,6 +156,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
// One row per call: MySQL answers no RETURNING for a batch insert.
for (const row of ROWS) await engine.insert(OBJECT, row as any);
diff --git a/packages/rest/src/rest-aggregate-positions.test.ts b/packages/rest/src/rest-aggregate-positions.test.ts
index 50d6f23b959..bea9649329b 100644
--- a/packages/rest/src/rest-aggregate-positions.test.ts
+++ b/packages/rest/src/rest-aggregate-positions.test.ts
@@ -31,6 +31,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'ledger_positions';
@@ -91,6 +92,9 @@ async function boot() {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, ROWS as any);
// Reads of THIS object — the protocol's own metadata reads are not the question.
diff --git a/packages/rest/src/rest-stored-expression-dropped-fields.test.ts b/packages/rest/src/rest-stored-expression-dropped-fields.test.ts
index d0eb5f35f08..8ccba098afd 100644
--- a/packages/rest/src/rest-stored-expression-dropped-fields.test.ts
+++ b/packages/rest/src/rest-stored-expression-dropped-fields.test.ts
@@ -24,6 +24,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { RestServer } from './rest-server';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'se_line';
const AMOUNT = { object: OBJECT, fields: ['amount'], reason: 'stored_expression' };
@@ -66,6 +67,9 @@ beforeAll(async () => {
},
} as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, [
{ id: 'l1', quantity: 3, unit_price: 10 },
{ id: 'l2', quantity: 2, unit_price: 7 },
diff --git a/packages/rest/src/validate-update-stored-row-representation.test.ts b/packages/rest/src/validate-update-stored-row-representation.test.ts
index 7e63b92c3fc..62aadcc578c 100644
--- a/packages/rest/src/validate-update-stored-row-representation.test.ts
+++ b/packages/rest/src/validate-update-stored-row-representation.test.ts
@@ -18,6 +18,7 @@
import { describe, it, expect, afterEach } from 'vitest';
import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'pv_repr';
@@ -53,6 +54,9 @@ async function boot() {
await engine.init();
engine.registry.registerObject({ name: OBJECT, label: 'Preview Representation', fields } as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const stored: Record = { title: 'stored' };
for (const [column, [, , value]] of Object.entries(COLUMNS)) {
stored[column] = value;
diff --git a/packages/runtime/src/action-engine-facade-find-envelope.test.ts b/packages/runtime/src/action-engine-facade-find-envelope.test.ts
index a6be5b7e434..140f8bcb268 100644
--- a/packages/runtime/src/action-engine-facade-find-envelope.test.ts
+++ b/packages/runtime/src/action-engine-facade-find-envelope.test.ts
@@ -46,6 +46,7 @@ import { assertEngineDeleteDispatch } from '@objectstack/metadata-core';
import { ObjectQL } from '@objectstack/objectql';
import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm';
import { buildActionEngineFacade, ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION } from './action-execution.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const deps: any = { resolveService: () => undefined, getObjectQL: async () => undefined };
@@ -201,6 +202,9 @@ async function makeRealEngine() {
await engine.init();
engine.registry.registerObject(PROBE_OBJECT, 'test');
await engine.syncSchemas?.();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const ctx = { isSystem: true } as any;
// t3 carries a non-null `deleted_at`, so a filter that is HONOURED returns
diff --git a/packages/runtime/src/analytics-cube-measure-field-type-door.test.ts b/packages/runtime/src/analytics-cube-measure-field-type-door.test.ts
index 9bbf0f4b665..54bc9c1aced 100644
--- a/packages/runtime/src/analytics-cube-measure-field-type-door.test.ts
+++ b/packages/runtime/src/analytics-cube-measure-field-type-door.test.ts
@@ -31,6 +31,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
import { createDispatcherPlugin } from './dispatcher-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21044_route_ledger';
@@ -152,6 +153,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
for (const [face, caps] of [
diff --git a/packages/runtime/src/analytics-json-dimension-door.test.ts b/packages/runtime/src/analytics-json-dimension-door.test.ts
index 3d387cf3574..3186bfc0782 100644
--- a/packages/runtime/src/analytics-json-dimension-door.test.ts
+++ b/packages/runtime/src/analytics-json-dimension-door.test.ts
@@ -47,6 +47,7 @@ import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/serv
import { DatasetSchema } from '@objectstack/spec/ui';
import { createDispatcherPlugin } from './dispatcher-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// [#21061] The analytics domain refuses an anonymous caller first (ADR-0056 D2),
// so this harness signs its caller in: an `auth` slot in the shape
@@ -167,6 +168,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Count what reaches the engine for THIS object, on both bridges.
diff --git a/packages/runtime/src/analytics-query-window-validity.test.ts b/packages/runtime/src/analytics-query-window-validity.test.ts
index e4725774767..33c1a26a369 100644
--- a/packages/runtime/src/analytics-query-window-validity.test.ts
+++ b/packages/runtime/src/analytics-query-window-validity.test.ts
@@ -42,6 +42,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { AnalyticsServicePlugin, type AnalyticsService } from '@objectstack/service-analytics';
import { createDispatcherPlugin } from './dispatcher-plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// [#21061] The analytics domain refuses an anonymous caller first (ADR-0056 D2),
// so this harness signs its caller in: an `auth` slot in the shape
@@ -159,6 +160,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(DEAL as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
// Count what reaches the engine for THIS object, on both bridges.
diff --git a/packages/runtime/src/datasource-mapping-before-schema-sync.test.ts b/packages/runtime/src/datasource-mapping-before-schema-sync.test.ts
index 290857cf7f5..2f261960bd2 100644
--- a/packages/runtime/src/datasource-mapping-before-schema-sync.test.ts
+++ b/packages/runtime/src/datasource-mapping-before-schema-sync.test.ts
@@ -104,7 +104,8 @@ function platformPlugin() {
});
},
start: async (ctx: any) => {
- await ctx.getService('objectql').insert(PLATFORM_OBJECT, { title: BOOT_ROW }, { context: { isSystem: true } });
+ // Written in an organization: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ await ctx.getService('objectql').insert(PLATFORM_OBJECT, { title: BOOT_ROW }, { context: { isSystem: true, tenantId: 'org_a' } });
},
};
}
diff --git a/packages/runtime/src/default-datasource-plugin.test.ts b/packages/runtime/src/default-datasource-plugin.test.ts
index 77d5553375a..b04bb0cbd09 100644
--- a/packages/runtime/src/default-datasource-plugin.test.ts
+++ b/packages/runtime/src/default-datasource-plugin.test.ts
@@ -193,7 +193,8 @@ describe('DefaultDatasourcePlugin — the default datasource as a declaration (#
expect(defaultName).toBeDefined();
// Identity, not equivalence: the engine's default driver IS the host's instance.
expect(engine.getDriverByName?.(defaultName!)).toBe(hostBuilt);
- await engine.insert('note', { title: 'through-the-adopted-default' });
+ // Written in an organization: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ await engine.insert('note', { title: 'through-the-adopted-default' }, { context: { tenantId: 'org_a' } } as never);
const rows = await engine.find('note');
expect(rows.map((r: any) => r.title)).toContain('through-the-adopted-default');
// [commit 13a6cb4ad] The capture is a PIN, not a mute: the probe's two log lines
diff --git a/packages/runtime/src/default-organization-statements.golden.test.ts b/packages/runtime/src/default-organization-statements.golden.test.ts
index 39155fc2c23..305679807a8 100644
--- a/packages/runtime/src/default-organization-statements.golden.test.ts
+++ b/packages/runtime/src/default-organization-statements.golden.test.ts
@@ -37,6 +37,7 @@ import { PlatformObjectsPlugin } from '@objectstack/platform-objects/plugin';
import { AppPlugin } from './app-plugin.js';
import { DefaultDatasourcePlugin } from './default-datasource-plugin.js';
import { createDispatcherPlugin } from './dispatcher-plugin.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const BOOT_TIMEOUT = 180_000;
const ORIGIN = 'http://localhost:3000';
@@ -172,6 +173,10 @@ describe('[#24523] statements per warm request: an organization-owned admin gran
});
it('CONTROL — the organization-less grant anchors nothing: on either slug, no standing, and no sys_organization statement is issued for it', async () => {
+ // An organization-less grant exists only on a store built before 18.x
+ // (ADR-0131 D1 makes a fresh tenant column NOT NULL): this table is that
+ // store's, relaxed through its driver's own drift apply door.
+ await relaxTenantColumns((engine as any).getDriver('sys_user_permission_set'), ['sys_user_permission_set']);
const grant = await adminGrant();
const [organization] = await find('sys_organization');
await engine.update('sys_user_permission_set', { id: grant.id, organization_id: null }, { context: SYS });
diff --git a/packages/runtime/src/default-organization.testkit.ts b/packages/runtime/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..6c95d77ab86
--- /dev/null
+++ b/packages/runtime/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/runtime',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/runtime/src/dispatcher-unique-violation-wire-code.test.ts b/packages/runtime/src/dispatcher-unique-violation-wire-code.test.ts
index 0f7ce2c29b6..52a78f2522b 100644
--- a/packages/runtime/src/dispatcher-unique-violation-wire-code.test.ts
+++ b/packages/runtime/src/dispatcher-unique-violation-wire-code.test.ts
@@ -45,6 +45,7 @@ import { SysMetadataActivation } from '@objectstack/platform-objects/system';
import { HttpDispatcher } from './http-dispatcher.js';
import type { HttpProtocolContext } from './http-dispatcher.js';
import { bindTestSecurityCatalog } from './security/security-catalog.testkit.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PREFIX = '/api/v1';
/** `registry.registerObject` requires a package id (see the sibling real-stack tests). */
@@ -116,6 +117,9 @@ async function bootEngine(objects: readonly object[]): Promise {
await engine.init();
for (const o of objects) engine.registry.registerObject(o as any, TEST_PACKAGE_ID);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return engine;
}
diff --git a/packages/runtime/src/domains/data-conflict-caller-visibility.test.ts b/packages/runtime/src/domains/data-conflict-caller-visibility.test.ts
index d86cd39c03e..be143198e4f 100644
--- a/packages/runtime/src/domains/data-conflict-caller-visibility.test.ts
+++ b/packages/runtime/src/domains/data-conflict-caller-visibility.test.ts
@@ -51,6 +51,7 @@ import { RestServer } from '@objectstack/rest';
import { declaredEnvelopeForThrow } from '@objectstack/plugin-hono-server';
import { resolveThrownHttpError } from '@objectstack/types';
import { HttpDispatcher } from '../http-dispatcher.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const PREFIX = '/api/v1';
const DATA = `${PREFIX}/data`;
@@ -228,6 +229,9 @@ describe('[#23586] a 409 CONCURRENT_UPDATE reports only what the caller may read
],
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const services: Record = {
manifest: { register: vi.fn() },
diff --git a/packages/runtime/src/domains/data-delete-expected-version.test.ts b/packages/runtime/src/domains/data-delete-expected-version.test.ts
index ee5a1a832e4..5c6316e99a6 100644
--- a/packages/runtime/src/domains/data-delete-expected-version.test.ts
+++ b/packages/runtime/src/domains/data-delete-expected-version.test.ts
@@ -50,6 +50,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco
import { RestServer, repeatedQueryParamMessage } from '@objectstack/rest';
import { resolveThrownHttpError } from '@objectstack/types';
import { HttpDispatcher } from '../http-dispatcher.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const PREFIX = '/api/v1';
const DATA = `${PREFIX}/data`;
@@ -90,6 +91,9 @@ async function bootStore() {
await engine.init();
engine.registry.registerObject(TASK as any, TEST_PACKAGE_ID);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any);
await protocol.createData({ object: 'task', data: { id: 'r1', name: 'first' } } as any);
const row: any = await engine.findOne('task', { where: { id: 'r1' } });
diff --git a/packages/runtime/src/domains/data-update-expected-version.test.ts b/packages/runtime/src/domains/data-update-expected-version.test.ts
index 26dd3571f2b..910db9134cb 100644
--- a/packages/runtime/src/domains/data-update-expected-version.test.ts
+++ b/packages/runtime/src/domains/data-update-expected-version.test.ts
@@ -52,6 +52,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco
import { RestServer } from '@objectstack/rest';
import { resolveThrownHttpError } from '@objectstack/types';
import { HttpDispatcher } from '../http-dispatcher.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const PREFIX = '/api/v1';
const DATA = `${PREFIX}/data`;
@@ -96,6 +97,9 @@ async function bootStore() {
await engine.init();
engine.registry.registerObject(TASK as any, TEST_PACKAGE_ID);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any);
await protocol.createData({ object: 'task', data: { id: 'r1', name: BEFORE } } as any);
const row: any = await engine.findOne('task', { where: { id: 'r1' } });
diff --git a/packages/runtime/src/domains/data-write-request-gate.test.ts b/packages/runtime/src/domains/data-write-request-gate.test.ts
index ecfc4395a40..fa033e7f3d4 100644
--- a/packages/runtime/src/domains/data-write-request-gate.test.ts
+++ b/packages/runtime/src/domains/data-write-request-gate.test.ts
@@ -82,6 +82,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco
import { RestServer } from '@objectstack/rest';
import { resolveThrownHttpError } from '@objectstack/types';
import { HttpDispatcher } from '../http-dispatcher.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const PREFIX = '/api/v1';
const DATA = `${PREFIX}/data`;
@@ -136,6 +137,9 @@ async function bootStore() {
await engine.init();
for (const object of OBJECTS) engine.registry.registerObject(object as any, TEST_PACKAGE_ID);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const protocol = new ObjectStackProtocolImplementation(engine as any);
for (const object of OBJECTS) {
await protocol.createData({ object: object.name, data: { id: 'r1', name: BEFORE } } as any);
diff --git a/packages/runtime/src/domains/share-links-internal-hash-probe.test.ts b/packages/runtime/src/domains/share-links-internal-hash-probe.test.ts
index c592b62cab1..e35e51acc29 100644
--- a/packages/runtime/src/domains/share-links-internal-hash-probe.test.ts
+++ b/packages/runtime/src/domains/share-links-internal-hash-probe.test.ts
@@ -32,6 +32,7 @@ import { HttpDispatcher } from '../http-dispatcher.js';
import type { DomainHandlerDeps } from '../domain-handler-registry.js';
import type { HttpProtocolContext } from '../http-dispatcher.js';
import { handleShareLinksRequest } from './share-links.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SHARED = 'pin_doc';
/** The system context, erased once here; the options bags that carry it stay typed. */
@@ -89,6 +90,9 @@ async function harness() {
await engine.init();
for (const o of [SysShareLink, TARGET]) engine.registry.registerObject(o as any, '@objectstack/runtime-test');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(SHARED, { id: RECORD, title: 'Shared doc' }, { context: { isSystem: true } } as any);
const svc = new ShareLinkService({ engine: engine as any });
const deps = makeDeps(engine, svc);
diff --git a/packages/runtime/src/domains/share-links-messages-audience.test.ts b/packages/runtime/src/domains/share-links-messages-audience.test.ts
index b5362f50280..749deeeff21 100644
--- a/packages/runtime/src/domains/share-links-messages-audience.test.ts
+++ b/packages/runtime/src/domains/share-links-messages-audience.test.ts
@@ -61,6 +61,7 @@ import { HttpDispatcher } from '../http-dispatcher.js';
import type { DomainHandlerDeps } from '../domain-handler-registry.js';
import type { HttpProtocolContext } from '../http-dispatcher.js';
import { handleShareLinksRequest } from './share-links.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const CONVERSATION = 'conv_23371';
const DOC = 'doc_23371';
@@ -192,6 +193,9 @@ beforeAll(async () => {
engine.registry.registerObject(o as any, '@objectstack/runtime-test');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const sys = { context: { isSystem: true } } as any;
await engine.insert('ai_conversations', { id: CONVERSATION, title: 'Shared chat' }, sys);
await engine.insert(
diff --git a/packages/runtime/src/domains/share-links-password-encoding.test.ts b/packages/runtime/src/domains/share-links-password-encoding.test.ts
index 2c32090d2de..1efa902856c 100644
--- a/packages/runtime/src/domains/share-links-password-encoding.test.ts
+++ b/packages/runtime/src/domains/share-links-password-encoding.test.ts
@@ -29,6 +29,7 @@ import { HttpDispatcher } from '../http-dispatcher.js';
import type { DomainHandlerDeps } from '../domain-handler-registry.js';
import type { HttpProtocolContext } from '../http-dispatcher.js';
import { handleShareLinksRequest } from './share-links.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const CJK = '分享密码二二零四九';
const EMOJI = 'open 🔐🦊 sesame';
@@ -100,6 +101,9 @@ async function harness(password: string) {
await engine.init();
for (const o of [SysShareLink, CONVERSATIONS, MESSAGES]) engine.registry.registerObject(o as any, '@objectstack/runtime-test');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const sys = { context: { isSystem: true } } as any;
await engine.insert('ai_conversations', { id: 'conv_1', title: 'Chat' }, sys);
await engine.insert(
diff --git a/packages/runtime/src/domains/share-links-public-cache-headers.test.ts b/packages/runtime/src/domains/share-links-public-cache-headers.test.ts
index 68d1bd9885f..08ddc01702a 100644
--- a/packages/runtime/src/domains/share-links-public-cache-headers.test.ts
+++ b/packages/runtime/src/domains/share-links-public-cache-headers.test.ts
@@ -23,6 +23,7 @@ import { HttpDispatcher } from '../http-dispatcher.js';
import type { DomainHandlerDeps } from '../domain-handler-registry.js';
import type { HttpProtocolContext } from '../http-dispatcher.js';
import { handleShareLinksRequest } from './share-links.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SHARED = 'pin_doc';
const RECORD = 'doc_1';
@@ -79,6 +80,9 @@ async function harness() {
await engine.init();
for (const o of [SysShareLink, TARGET]) engine.registry.registerObject(o as any, '@objectstack/runtime-test');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(SHARED, { id: RECORD, title: 'Shared doc' }, { context: { isSystem: true } } as any);
const svc = new ShareLinkService({ engine: engine as any });
const deps = makeDeps(engine, svc);
diff --git a/packages/runtime/src/domains/share-links-refusal-order.test.ts b/packages/runtime/src/domains/share-links-refusal-order.test.ts
index 7041251ea14..492301ae0c9 100644
--- a/packages/runtime/src/domains/share-links-refusal-order.test.ts
+++ b/packages/runtime/src/domains/share-links-refusal-order.test.ts
@@ -74,6 +74,7 @@ import { HttpDispatcher } from '../http-dispatcher.js';
import type { DomainHandlerDeps } from '../domain-handler-registry.js';
import type { HttpProtocolContext } from '../http-dispatcher.js';
import { handleShareLinksRequest } from './share-links.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const OBJECT = 'refusal_doc';
const RECORD = 'doc_1';
@@ -169,6 +170,9 @@ beforeAll(async () => {
await engine.init();
for (const o of [SysShareLink, TARGET]) engine.registry.registerObject(o as any, '@objectstack/runtime-test');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
await engine.insert(OBJECT, { id: RECORD, title: 'Shared doc' }, { context: { isSystem: true } } as any);
const svc = new ShareLinkService({ engine: engine as any });
diff --git a/packages/runtime/src/notification-schema-conformance.integration.test.ts b/packages/runtime/src/notification-schema-conformance.integration.test.ts
index 4e8bcb57ceb..deb12892ef6 100644
--- a/packages/runtime/src/notification-schema-conformance.integration.test.ts
+++ b/packages/runtime/src/notification-schema-conformance.integration.test.ts
@@ -50,6 +50,7 @@ import type { ServiceObject } from '@objectstack/spec/data';
import { createDispatcherPlugin } from './dispatcher-plugin.js';
import { DriverPlugin } from './driver-plugin.js';
import { captureExpectedReadRefusals } from './expected-read-refusal-noise.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// One inbox per concern. The mark-read routes MUTATE read-state, so a shared
// user would make these suites order-dependent — the unread fixture the gap
@@ -311,6 +312,9 @@ describe('[#5792] the notification wire bodies conform to the schemas the catalo
authzEngine.registerObject(o.def, o.owner);
await authzEngine.syncObjectSchema(o.def.name);
}
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(authzEngine);
const httpServer = kernel.getService('http.server');
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
diff --git a/packages/runtime/src/notifications.hono.integration.test.ts b/packages/runtime/src/notifications.hono.integration.test.ts
index 574e1a1629a..4acc535550b 100644
--- a/packages/runtime/src/notifications.hono.integration.test.ts
+++ b/packages/runtime/src/notifications.hono.integration.test.ts
@@ -13,6 +13,7 @@ import { DriverPlugin } from './driver-plugin.js';
import { captureExpectedReadRefusals } from './expected-read-refusal-noise.js';
import type { IHttpServer } from '@objectstack/spec/contracts';
import type { ServiceObject } from '@objectstack/spec/data';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* End-to-end regression for framework #3362 (`#3354 not effective on hono`).
@@ -242,6 +243,9 @@ describe('in-app notifications over a real hono server (integration, #3362)', ()
authzEngine.registerObject(o.def, o.owner);
await authzEngine.syncObjectSchema(o.def.name);
}
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(authzEngine);
const httpServer = kernel.getService('http.server');
baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
diff --git a/packages/runtime/src/package-uninstall-shared-object.integration.test.ts b/packages/runtime/src/package-uninstall-shared-object.integration.test.ts
index 4fb5969734c..1cc6aa72585 100644
--- a/packages/runtime/src/package-uninstall-shared-object.integration.test.ts
+++ b/packages/runtime/src/package-uninstall-shared-object.integration.test.ts
@@ -201,7 +201,8 @@ async function boot() {
async records(name: string) {
return ((await knex(name).select('title')) as Array<{ title: string }>).map((r) => r.title);
},
- insertRecord: (name: string, title: string) => engine.insert(name, { title }, { context: { isSystem: true } } as any),
+ // Written in an organization: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ insertRecord: (name: string, title: string) => engine.insert(name, { title }, { context: { isSystem: true, tenantId: 'org_a' } } as any),
};
}
diff --git a/packages/runtime/src/pre-18-tenant-column.testkit.ts b/packages/runtime/src/pre-18-tenant-column.testkit.ts
new file mode 100644
index 00000000000..e7648974357
--- /dev/null
+++ b/packages/runtime/src/pre-18-tenant-column.testkit.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Return driver-created tables to the shape a store built BEFORE 18.x has:
+ * the tenant column nullable.
+ *
+ * ADR-0131 D1: since 18.x the SQL driver creates a table's tenant column NOT
+ * NULL. A store an earlier release built still holds the nullable column and
+ * its organization-less legacy rows (until the organization-ownership
+ * ceremony attributes them, or reports them). A suite whose subject is those
+ * rows — a backfill over them, a sweep that must still reach them — builds
+ * its tables, then calls this before seeding them. Done through the driver's
+ * own drift apply door (`relax_not_null`), never a hand `ALTER`.
+ */
+export async function relaxTenantColumns(
+ driver: { applyMigrationEntries(entries: never[]): Promise<{ applied: unknown[] }> },
+ tables: readonly string[],
+ column = 'organization_id',
+): Promise {
+ for (const table of tables) {
+ const op = { type: 'relax_not_null', table, column };
+ const { applied } = await driver.applyMigrationEntries([
+ { kind: 'nullability_mismatch', remoteName: table, table, column, category: 'needs_confirm', op } as never,
+ ]);
+ if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
+ }
+}
diff --git a/packages/runtime/src/sandbox-relay-unique-violation-wire-code.test.ts b/packages/runtime/src/sandbox-relay-unique-violation-wire-code.test.ts
index 574eba8c7dc..7cb4bea6275 100644
--- a/packages/runtime/src/sandbox-relay-unique-violation-wire-code.test.ts
+++ b/packages/runtime/src/sandbox-relay-unique-violation-wire-code.test.ts
@@ -37,6 +37,7 @@ import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protoco
import { HttpDispatcher } from './http-dispatcher.js';
import { actionBodyRunnerFactory, hookBodyRunnerFactory } from './sandbox/body-runner.js';
import { QuickJSScriptRunner } from './sandbox/quickjs-runner.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PREFIX = '/api/v1';
/** `registry.registerObject` requires a package id (see the sibling real-stack tests). */
@@ -113,6 +114,9 @@ beforeAll(async () => {
await engine.init();
for (const o of [UDOC, HDOC]) engine.registry.registerObject(o as any, TEST_PACKAGE_ID);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const ql = engine as any;
const bindBody = actionBodyRunnerFactory(new QuickJSScriptRunner(), { ql: engine, appId: 'udoc_app' });
diff --git a/packages/runtime/src/seed-loader-null-organization-holder-real-driver.integration.test.ts b/packages/runtime/src/seed-loader-null-organization-holder-real-driver.integration.test.ts
index 554802486dc..734ac0020ba 100644
--- a/packages/runtime/src/seed-loader-null-organization-holder-real-driver.integration.test.ts
+++ b/packages/runtime/src/seed-loader-null-organization-holder-real-driver.integration.test.ts
@@ -37,6 +37,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SeedLoaderRequestSchema, type EngineQueryOptions } from '@objectstack/spec/data';
import type { PluginContext } from '@objectstack/core';
import { AppPlugin } from './app-plugin';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const ORG = 'org_twdefaultk3x9qfa2';
const OTHER_ORG = 'org_twotherorgp4w8m2';
@@ -120,6 +121,9 @@ async function boot() {
await engine.init();
for (const o of OBJECTS) engine.registry.registerObject(o as any, 'com.objectstack.test.24301');
await raw.initObjects(OBJECTS.map((o) => engine.registry.getObject(o.name) ?? o) as any);
+ // The 17.x residue this file plants lands only on a store built before 18.x,
+ // whose tenant columns are nullable (ADR-0131 D1 makes a fresh one NOT NULL).
+ await relaxTenantColumns(raw as any, OBJECTS.filter((o) => (engine.registry.getObject(o.name) as any)?.fields?.organization_id).map((o) => o.name));
cleanups.push(() => engine.destroy());
const systemRead: EngineQueryOptions = { context: { isSystem: true } };
diff --git a/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts b/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts
index 21467f13584..61d89e3aed2 100644
--- a/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts
+++ b/packages/runtime/src/seed-tenancy-autonumber-split.integration.test.ts
@@ -60,6 +60,7 @@ import {
resolveSeedTenancySeam,
GLOBAL_TENANT,
} from '@objectstack/metadata-protocol';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const ORG_ID = 'org_mssymr19xzd645gv';
const SEEDED_ROWS = 38;
@@ -113,6 +114,9 @@ async function bootInstall() {
engine.registry.registerObject(CASE_OBJECT, '#8686');
engine.registry.registerObject(ORG_OBJECT, '#8686');
await driver.initObjects([CASE_OBJECT, ORG_OBJECT]);
+ // The pre-ADR-0131 residue lands only on a store built before 18.x, whose
+ // tenant column is nullable (D1 makes a fresh one NOT NULL): this is that store.
+ await relaxTenantColumns(driver as any, [CASE_OBJECT.name]);
return { driver, engine };
}
@@ -507,6 +511,9 @@ describe('#8686 seed/API tenancy split — autonumber scope', () => {
engine.registry.registerObject(ticketObject, '#12394');
engine.registry.registerObject(ORG_OBJECT, '#12394');
await driver.initObjects([ticketObject, ORG_OBJECT]);
+ // The pre-ADR-0131 residue lands only on a store built before 18.x, whose
+ // tenant column is nullable (D1 makes a fresh one NOT NULL): this is that store.
+ await relaxTenantColumns(driver as any, [ticketObject.name]);
// Seed-shaped writes: untenanted, because no organization existed yet —
// the pre-ADR-0131 residue, landed through the driver call the engine made
@@ -577,6 +584,9 @@ describe('#8686 seed/API tenancy split — autonumber scope', () => {
engine.registry.registerObject(sysObject, '#8686');
engine.registry.registerObject(ORG_OBJECT, '#8686');
await driver.initObjects([sysObject, ORG_OBJECT]);
+ // The pre-ADR-0131 residue lands only on a store built before 18.x, whose
+ // tenant column is nullable (D1 makes a fresh one NOT NULL): this is that store.
+ await relaxTenantColumns(driver as any, [sysObject.name]);
// Land untenanted platform rows, then bring an organization into existence.
// [C8 stage 8b] Straight through the driver, as `landPreC1SeedResidue` does:
diff --git a/packages/runtime/src/standalone-stack-hydrate-metadata.test.ts b/packages/runtime/src/standalone-stack-hydrate-metadata.test.ts
index 38a07b897ea..f03700b7dd0 100644
--- a/packages/runtime/src/standalone-stack-hydrate-metadata.test.ts
+++ b/packages/runtime/src/standalone-stack-hydrate-metadata.test.ts
@@ -167,7 +167,8 @@ describe('[#20071] createStandaloneStack declares sys_metadata hydration instead
// is the only path that puts it there — which is what makes it a sharp
// second leg below.
expect(engine1.registry.getObject(OBJECT)?.name, 'boot 1 registers the object it just saved').toBe(OBJECT);
- await engine1.insert(OBJECT, { title: 'survives the restart' });
+ // Written in an organization: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ await engine1.insert(OBJECT, { title: 'survives the restart' }, { context: { tenantId: 'org_a' } } as never);
await first.shutdown();
kernels.splice(kernels.indexOf(first), 1);
diff --git a/packages/services/service-analytics/src/__tests__/analytics-faces-one-lowering.test.ts b/packages/services/service-analytics/src/__tests__/analytics-faces-one-lowering.test.ts
index 66dd08b68af..7cfbfb4d7ac 100644
--- a/packages/services/service-analytics/src/__tests__/analytics-faces-one-lowering.test.ts
+++ b/packages/services/service-analytics/src/__tests__/analytics-faces-one-lowering.test.ts
@@ -72,6 +72,7 @@ import { NativeSQLStrategy } from '../strategies/native-sql-strategy.js';
import { ObjectQLStrategy } from '../strategies/objectql-strategy.js';
import { NO_DATETIME_COLUMNS, normalizeAnalyticsFilterTree, type NormalizedFilterNode } from '../strategies/filter-normalizer.js';
import { compileScopedFilterToSql } from '../read-scope-sql.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = resolve(HERE, '..');
@@ -378,6 +379,9 @@ for (const cell of DB_CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as never);
const realExecute = (engine as any).execute.bind(engine);
(engine as any).execute = (sql: string, opts?: { object?: string }) => {
@@ -588,6 +592,9 @@ describe('[#5930 step 4] the temporal conformance matrix answers on every analyt
},
} as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const r of TEMPORAL_ROWS) await engine.insert(TEMPORAL, { id: r.id, [COLUMN.at]: r.at, [COLUMN.on]: r.on } as never);
faces = await composeFaces(engine, [temporalCube]);
});
diff --git a/packages/services/service-analytics/src/__tests__/api-exposure-door.test.ts b/packages/services/service-analytics/src/__tests__/api-exposure-door.test.ts
index 447ba7587f9..5cb45adceb9 100644
--- a/packages/services/service-analytics/src/__tests__/api-exposure-door.test.ts
+++ b/packages/services/service-analytics/src/__tests__/api-exposure-door.test.ts
@@ -41,6 +41,7 @@ import { DatasetSchema } from '@objectstack/spec/ui';
import type { ExecutionContext } from '@objectstack/spec/kernel';
import { AnalyticsServicePlugin } from '../plugin.js';
import { AnalyticsService } from '../analytics-service.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const HIDDEN = 'exposure_hidden';
const NO_LIST = 'exposure_nolist';
@@ -246,6 +247,9 @@ describe.each(STRATEGIES)('the analytics door honours the generic-exit declarati
engine.registry.registerObject(obj as any);
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const [object, rows] of Object.entries(ROWS)) {
for (const row of rows) await engine.insert(object, { ...row } as any);
}
diff --git a/packages/services/service-analytics/src/__tests__/caller-measure-no-field-door.test.ts b/packages/services/service-analytics/src/__tests__/caller-measure-no-field-door.test.ts
index dd5b1d09030..6556168a672 100644
--- a/packages/services/service-analytics/src/__tests__/caller-measure-no-field-door.test.ts
+++ b/packages/services/service-analytics/src/__tests__/caller-measure-no-field-door.test.ts
@@ -56,6 +56,7 @@ import {
inferredCallerMeasureSql,
} from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21437_deal';
@@ -149,6 +150,9 @@ describe('[#21437] a caller-named measure whose source names no field — at the
await engine.init();
engine.registry.registerObject(DEAL as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/cube-measure-field-type-door.test.ts b/packages/services/service-analytics/src/__tests__/cube-measure-field-type-door.test.ts
index 43325d23aa9..48065670d2e 100644
--- a/packages/services/service-analytics/src/__tests__/cube-measure-field-type-door.test.ts
+++ b/packages/services/service-analytics/src/__tests__/cube-measure-field-type-door.test.ts
@@ -53,6 +53,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21044_cube_measure_ledger';
@@ -180,6 +181,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/cube-measure-relationship-path-type.test.ts b/packages/services/service-analytics/src/__tests__/cube-measure-relationship-path-type.test.ts
index 4e4dc88a71e..7df90c6d198 100644
--- a/packages/services/service-analytics/src/__tests__/cube-measure-relationship-path-type.test.ts
+++ b/packages/services/service-analytics/src/__tests__/cube-measure-relationship-path-type.test.ts
@@ -62,6 +62,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ACCOUNT = 'os21129_account';
const PERSON = 'os21129_person';
@@ -226,6 +227,9 @@ for (const cell of CELLS) {
await engine.init();
for (const object of [ACCOUNT_OBJECT, PERSON_OBJECT, DEAL_OBJECT]) engine.registry.registerObject(object as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ACCOUNTS) await engine.insert(ACCOUNT, { ...row } as any);
for (const row of PEOPLE) await engine.insert(PERSON, { ...row } as any);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
diff --git a/packages/services/service-analytics/src/__tests__/default-organization.testkit.ts b/packages/services/service-analytics/src/__tests__/default-organization.testkit.ts
new file mode 100644
index 00000000000..34d061fd492
--- /dev/null
+++ b/packages/services/service-analytics/src/__tests__/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/service-analytics',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/services/service-analytics/src/__tests__/json-stored-door-live-drivers.test.ts b/packages/services/service-analytics/src/__tests__/json-stored-door-live-drivers.test.ts
index 8be4827860c..66242bbf4b2 100644
--- a/packages/services/service-analytics/src/__tests__/json-stored-door-live-drivers.test.ts
+++ b/packages/services/service-analytics/src/__tests__/json-stored-door-live-drivers.test.ts
@@ -41,6 +41,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { DatasetSchema } from '@objectstack/spec/ui';
import { AnalyticsServicePlugin } from '../plugin.js';
import type { AnalyticsService } from '../analytics-service.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'analytics_json_stored_door_ledger';
const OPTIONS = [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }];
@@ -141,6 +142,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/json-stored-door-undeclared-join.test.ts b/packages/services/service-analytics/src/__tests__/json-stored-door-undeclared-join.test.ts
index c225f754ae2..532e5b4d301 100644
--- a/packages/services/service-analytics/src/__tests__/json-stored-door-undeclared-join.test.ts
+++ b/packages/services/service-analytics/src/__tests__/json-stored-door-undeclared-join.test.ts
@@ -59,6 +59,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PERSON = 'os21232_person';
const ACCOUNT = 'os21232_account';
@@ -225,6 +226,9 @@ for (const cell of CELLS) {
await engine.init();
for (const object of [PERSON_OBJECT, ACCOUNT_OBJECT, DEAL_OBJECT]) engine.registry.registerObject(object as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of PEOPLE) await engine.insert(PERSON, { ...row } as any);
for (const row of ACCOUNTS) await engine.insert(ACCOUNT, { ...row } as any);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
diff --git a/packages/services/service-analytics/src/__tests__/list-at-scalar-operator-both-faces.test.ts b/packages/services/service-analytics/src/__tests__/list-at-scalar-operator-both-faces.test.ts
index 53acbfc92c4..4b6b7957b68 100644
--- a/packages/services/service-analytics/src/__tests__/list-at-scalar-operator-both-faces.test.ts
+++ b/packages/services/service-analytics/src/__tests__/list-at-scalar-operator-both-faces.test.ts
@@ -52,6 +52,7 @@ import type { Cube } from '@objectstack/spec/data';
import { DatasetSchema } from '@objectstack/spec/ui';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21448_list_ledger';
@@ -239,6 +240,9 @@ for (const cell of DRIVER_CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/native-sql-aggregate-policies.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-aggregate-policies.test.ts
index 2180f4f86fd..21fa74e42f1 100644
--- a/packages/services/service-analytics/src/__tests__/native-sql-aggregate-policies.test.ts
+++ b/packages/services/service-analytics/src/__tests__/native-sql-aggregate-policies.test.ts
@@ -60,6 +60,7 @@ import { ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21042_policy_ledger';
@@ -237,6 +238,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/native-sql-base-column-qualify.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-base-column-qualify.test.ts
index 830d9a3cb98..d05e364673c 100644
--- a/packages/services/service-analytics/src/__tests__/native-sql-base-column-qualify.test.ts
+++ b/packages/services/service-analytics/src/__tests__/native-sql-base-column-qualify.test.ts
@@ -55,6 +55,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PERSON = 'os21249_person';
const DEAL = 'os21249_deal';
@@ -204,6 +205,9 @@ for (const cell of CELLS) {
await engine.init();
for (const object of [PERSON_OBJECT, DEAL_OBJECT]) engine.registry.registerObject(object as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of PEOPLE) await engine.insert(PERSON, { ...row } as any);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
diff --git a/packages/services/service-analytics/src/__tests__/native-sql-boolean-comparand-door.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-boolean-comparand-door.test.ts
index 2afd67cafbe..e627bd3b100 100644
--- a/packages/services/service-analytics/src/__tests__/native-sql-boolean-comparand-door.test.ts
+++ b/packages/services/service-analytics/src/__tests__/native-sql-boolean-comparand-door.test.ts
@@ -51,6 +51,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21376_flag_ledger';
@@ -216,6 +217,9 @@ for (const cell of DRIVER_CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/native-sql-measure-number-presentation.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-measure-number-presentation.test.ts
index 155088944a0..193d07e2952 100644
--- a/packages/services/service-analytics/src/__tests__/native-sql-measure-number-presentation.test.ts
+++ b/packages/services/service-analytics/src/__tests__/native-sql-measure-number-presentation.test.ts
@@ -71,6 +71,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { DEFAULT_ORGANIZATION_ID, installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os20889_measure_ledger';
@@ -243,6 +244,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
@@ -357,7 +361,11 @@ for (const cell of CELLS) {
// exact-decimal column stores them exactly (SQLite's column rounds on write).
const EXACT = ['9007199254740993', '12345678901234567.123456789'];
for (const [i, literal] of EXACT.entries()) {
- await driver.execute(`insert into ${OBJECT} (id, category, amount) values ('p${i}', 'p${i}', ${literal})`);
+ // The raw row names the install's organization, as the engine's own
+ // writes do: a fresh tenant column is NOT NULL (ADR-0131 D1).
+ await driver.execute(
+ `insert into ${OBJECT} (id, category, amount, organization_id) values ('p${i}', 'p${i}', ${literal}, '${DEFAULT_ORGANIZATION_ID}')`,
+ );
}
const native = byCategory((await cubeRead('native', ['sum_amount', 'avg_amount', 'min_amount'], ['category'])).rows);
const objectql = byCategory((await cubeRead('objectql', ['sum_amount', 'avg_amount', 'min_amount'], ['category'])).rows);
diff --git a/packages/services/service-analytics/src/__tests__/native-sql-number-comparand-door.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-number-comparand-door.test.ts
index 819107f0f76..fb69f2c986e 100644
--- a/packages/services/service-analytics/src/__tests__/native-sql-number-comparand-door.test.ts
+++ b/packages/services/service-analytics/src/__tests__/native-sql-number-comparand-door.test.ts
@@ -73,6 +73,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os21426_amount_ledger';
const ACCOUNT = 'os21426_amount_account';
@@ -337,6 +338,9 @@ for (const cell of DRIVER_CELLS) {
engine.registry.registerObject(ACCOUNT_DEF as any);
engine.registry.registerObject(LEDGER as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ACCOUNTS) await engine.insert(ACCOUNT, { ...row } as any);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as any);
diff --git a/packages/services/service-analytics/src/__tests__/native-sql-offset-only-window.test.ts b/packages/services/service-analytics/src/__tests__/native-sql-offset-only-window.test.ts
index 8877b7eec8d..8d15d0a37dc 100644
--- a/packages/services/service-analytics/src/__tests__/native-sql-offset-only-window.test.ts
+++ b/packages/services/service-analytics/src/__tests__/native-sql-offset-only-window.test.ts
@@ -48,6 +48,7 @@ import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
import { NativeSQLStrategy, windowClauseSql } from '../strategies/native-sql-strategy.js';
import type { DatasetScopedStrategyContext } from '../strategies/types.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DEAL = 'os21365_deal';
@@ -181,6 +182,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(DEAL_OBJECT as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/objectql-echo-bucket-enumeration.test.ts b/packages/services/service-analytics/src/__tests__/objectql-echo-bucket-enumeration.test.ts
index 9c7149174d1..a4b9efb9a8a 100644
--- a/packages/services/service-analytics/src/__tests__/objectql-echo-bucket-enumeration.test.ts
+++ b/packages/services/service-analytics/src/__tests__/objectql-echo-bucket-enumeration.test.ts
@@ -118,6 +118,7 @@ import { BUILTIN_DRIVER_IDS, TimeUpdateInterval, type BuiltinDriverId, type Cube
import { declaredRefusalMessage } from '@objectstack/types';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DEAL = 'os21647_bucket_deal';
const DEAL_OBJECT = {
@@ -327,6 +328,9 @@ async function serve(face: DriverFace) {
engine.registry.registerObject(DEAL_OBJECT as any);
if (seeded) {
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const deal of EXCEPTION_DEALS) await engine.insert(DEAL, { ...deal } as any);
}
diff --git a/packages/services/service-analytics/src/__tests__/objectql-echo-date-bucket.test.ts b/packages/services/service-analytics/src/__tests__/objectql-echo-date-bucket.test.ts
index c6439755325..9aa387b39b4 100644
--- a/packages/services/service-analytics/src/__tests__/objectql-echo-date-bucket.test.ts
+++ b/packages/services/service-analytics/src/__tests__/objectql-echo-date-bucket.test.ts
@@ -64,6 +64,7 @@ import { AnalyticsServicePlugin } from '../plugin.js';
import { ObjectQLStrategy } from '../strategies/objectql-strategy.js';
import type { StrategyContext } from '../strategies/types.js';
import { declaredRefusalMessage } from '@objectstack/types';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DEAL = 'os21441_bucket_deal';
@@ -209,6 +210,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(DEAL_OBJECT as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
driver.knex.on('query', (q: { sql: string }) => { driverRan.push(q.sql); });
diff --git a/packages/services/service-analytics/src/__tests__/objectql-echo-offset-only-window.test.ts b/packages/services/service-analytics/src/__tests__/objectql-echo-offset-only-window.test.ts
index 74f046e9f99..211a6021265 100644
--- a/packages/services/service-analytics/src/__tests__/objectql-echo-offset-only-window.test.ts
+++ b/packages/services/service-analytics/src/__tests__/objectql-echo-offset-only-window.test.ts
@@ -50,6 +50,7 @@ import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
import { ObjectQLStrategy } from '../strategies/objectql-strategy.js';
import type { StrategyContext } from '../strategies/types.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DEAL = 'os21365_echo_deal';
@@ -163,6 +164,9 @@ for (const cell of CELLS) {
await engine.init();
engine.registry.registerObject(DEAL_OBJECT as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
const realExecute = (engine as any).execute.bind(engine);
diff --git a/packages/services/service-analytics/src/__tests__/objectql-face-order-limit.test.ts b/packages/services/service-analytics/src/__tests__/objectql-face-order-limit.test.ts
index ed8e64547b1..010850e8f66 100644
--- a/packages/services/service-analytics/src/__tests__/objectql-face-order-limit.test.ts
+++ b/packages/services/service-analytics/src/__tests__/objectql-face-order-limit.test.ts
@@ -51,6 +51,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PERSON = 'os21316_person';
const DEAL = 'os21316_deal';
@@ -192,6 +193,9 @@ for (const cell of CELLS) {
await engine.init();
for (const object of [PERSON_OBJECT, DEAL_OBJECT]) engine.registry.registerObject(object as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of PEOPLE) await engine.insert(PERSON, { ...row } as any);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
diff --git a/packages/services/service-analytics/src/__tests__/order-key-selected.test.ts b/packages/services/service-analytics/src/__tests__/order-key-selected.test.ts
index 940b0014ae7..a43ecf14af6 100644
--- a/packages/services/service-analytics/src/__tests__/order-key-selected.test.ts
+++ b/packages/services/service-analytics/src/__tests__/order-key-selected.test.ts
@@ -39,6 +39,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { Cube } from '@objectstack/spec/data';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const PERSON = 'os21267_person';
const DEAL = 'os21267_deal';
@@ -203,6 +204,9 @@ for (const cell of CELLS) {
await engine.init();
for (const object of [PERSON_OBJECT, DEAL_OBJECT]) engine.registry.registerObject(object as any);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of PEOPLE) await engine.insert(PERSON, { ...row } as any);
for (const row of DEALS) await engine.insert(DEAL, { ...row } as any);
diff --git a/packages/services/service-analytics/src/__tests__/picklist-dimension-label-i18n.test.ts b/packages/services/service-analytics/src/__tests__/picklist-dimension-label-i18n.test.ts
index c92c227fb24..13016c2f08a 100644
--- a/packages/services/service-analytics/src/__tests__/picklist-dimension-label-i18n.test.ts
+++ b/packages/services/service-analytics/src/__tests__/picklist-dimension-label-i18n.test.ts
@@ -54,6 +54,7 @@ import { DatasetSchema } from '@objectstack/spec/ui';
import { translateObject, type TranslationBundle } from '@objectstack/spec/system';
import type { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const OBJECT = 'os22178_account';
const PICKLIST = 'os22178_industry';
@@ -213,6 +214,9 @@ describe('[#22178] a picklist-bound select dimension renders its labels in the r
await engine.init();
engine.registerApp({ id: 'com.test.os22178', name: 'os22178', picklists: [INDUSTRY], objects: [ACCOUNT] } as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as never);
for (const [face, caps] of [
diff --git a/packages/services/service-analytics/src/__tests__/read-scope-temporal-coercion.test.ts b/packages/services/service-analytics/src/__tests__/read-scope-temporal-coercion.test.ts
index 94821df1885..bfba3de67bf 100644
--- a/packages/services/service-analytics/src/__tests__/read-scope-temporal-coercion.test.ts
+++ b/packages/services/service-analytics/src/__tests__/read-scope-temporal-coercion.test.ts
@@ -35,6 +35,7 @@ import { DatasetSchema } from '@objectstack/spec/ui';
import { AnalyticsService } from '../analytics-service.js';
import { AnalyticsServicePlugin } from '../plugin.js';
import { compileScopedFilterToSql, type ReadScopeCompileOptions } from '../read-scope-sql.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// ── Where the pair applies: after the lowering, on the value comparisons ─────
@@ -199,6 +200,9 @@ for (const cell of DB_CELLS) {
await engine.init();
engine.registry.registerObject(LEDGER as never);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
for (const row of ROWS) await engine.insert(OBJECT, { ...row } as never);
const realExecute = (engine as any).execute.bind(engine);
(engine as any).execute = (sql: string, opts?: { object?: string }) => {
diff --git a/packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts b/packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts
index 5ba3ad067df..9626fa2ea72 100644
--- a/packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts
+++ b/packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts
@@ -38,6 +38,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { AutomationServicePlugin } from '../plugin.js';
import { AutomationEngine } from '../engine.js';
import { registerCrudNodes } from './crud-nodes.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { isSystem: true } as const;
@@ -80,6 +81,9 @@ describe('get_record answers the row id as output.id whatever its fields project
ql.registerDriver(driver, true);
ql.registry.registerObject(OBJECT as any, 'pin-get-record-id', 'pin-get-record-id');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const row: any = await ql.insert(OBJECT.name, { title: 'alpha', status: 'open' }, { context: SYS });
rowId = row.id;
diff --git a/packages/services/service-automation/src/builtin/get-record-stored-metadata-family.integration.test.ts b/packages/services/service-automation/src/builtin/get-record-stored-metadata-family.integration.test.ts
index 9bb8396ff33..35263bca12b 100644
--- a/packages/services/service-automation/src/builtin/get-record-stored-metadata-family.integration.test.ts
+++ b/packages/services/service-automation/src/builtin/get-record-stored-metadata-family.integration.test.ts
@@ -48,6 +48,7 @@ import { hashSpec } from '@objectstack/metadata-core';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { AutomationServicePlugin } from '../plugin.js';
import type { AutomationEngine } from '../engine.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { isSystem: true } as const;
const KEYED = /^hmac-sha256:[0-9a-f]{64}$/;
@@ -148,6 +149,9 @@ describe('flow get_record serves the stored-metadata family the way the data doo
ql.registerDriver(driver, true);
ql.registry.registerObject(COPY_OBJECT as any, 'pin-21519', 'pin-21519');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const metadata = JSON.stringify(DS_BODY);
await ql.insert('sys_metadata', {
diff --git a/packages/services/service-automation/src/builtin/get-record-stored-metadata-filter-refusal.integration.test.ts b/packages/services/service-automation/src/builtin/get-record-stored-metadata-filter-refusal.integration.test.ts
index 5d8ee9859b5..5a439f7b29e 100644
--- a/packages/services/service-automation/src/builtin/get-record-stored-metadata-filter-refusal.integration.test.ts
+++ b/packages/services/service-automation/src/builtin/get-record-stored-metadata-filter-refusal.integration.test.ts
@@ -48,6 +48,7 @@ import { hashSpec } from '@objectstack/metadata-core';
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
import { AutomationServicePlugin } from '../plugin.js';
import type { AutomationEngine, NodeExecutor } from '../engine.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { isSystem: true } as const;
const KEYED = /^hmac-sha256:[0-9a-f]{64}$/;
@@ -152,6 +153,9 @@ describe("flow get_record refuses a filter that evaluates the stored-metadata fa
ql.registerDriver(driver, true);
ql.registry.registerObject(COPY_OBJECT as any, 'pin-21623', 'pin-21623');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const metadata = JSON.stringify(DS_BODY);
await ql.insert('sys_metadata', {
diff --git a/packages/services/service-automation/src/builtin/notify-zero-delivery-visibility.integration.test.ts b/packages/services/service-automation/src/builtin/notify-zero-delivery-visibility.integration.test.ts
index efc69d8b412..e7323f58154 100644
--- a/packages/services/service-automation/src/builtin/notify-zero-delivery-visibility.integration.test.ts
+++ b/packages/services/service-automation/src/builtin/notify-zero-delivery-visibility.integration.test.ts
@@ -116,6 +116,7 @@ import type { FlowRunSummary } from '@objectstack/spec/automation';
import { AutomationEngine } from '../engine.js';
import { registerNotifyNode } from './notify-node.js';
import { formatRunSummaryLine } from '../run-summary.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
/** The employer organization whose admin members are the intended recipients. */
const ORG_EMPLOYER = 'org_employer_alpha';
@@ -299,6 +300,9 @@ async function boot(kind: DriverKind) {
const PKG = '@objectstack/service-messaging';
for (const o of FIXTURES) ql.registry.registerObject(o as any, PKG, PKG);
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
data = ql as unknown as IDataEngine;
} else {
data = inProcessEngine();
diff --git a/packages/services/service-automation/src/connector-pull.integration.test.ts b/packages/services/service-automation/src/connector-pull.integration.test.ts
index 01bad9c8260..4f5414f450a 100644
--- a/packages/services/service-automation/src/connector-pull.integration.test.ts
+++ b/packages/services/service-automation/src/connector-pull.integration.test.ts
@@ -34,6 +34,7 @@ import { AutomationServicePlugin } from './plugin.js';
import type { EngineQueryOptions } from '@objectstack/spec/data';
import type { IAutomationService } from '@objectstack/spec/contracts';
import type { ConnectorPullProtocol } from './connector-pull.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const CONTACT = {
name: 'contact',
@@ -147,6 +148,9 @@ describe('[#20919] connector pull, end to end through a real rest connector', ()
await ql.init();
ql.registry.registerObject(CONTACT as any);
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const automation = new AutomationServicePlugin({ suspendedRunStore: 'memory' });
kernel = new LiteKernel({ logger: { level: 'silent' } } as never);
diff --git a/packages/services/service-automation/src/default-organization.testkit.ts b/packages/services/service-automation/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..ebbb9e4f8dd
--- /dev/null
+++ b/packages/services/service-automation/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/service-automation',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/services/service-automation/src/flow-field-expression-scale.integration.test.ts b/packages/services/service-automation/src/flow-field-expression-scale.integration.test.ts
index 266ddab51f3..c9fb8297010 100644
--- a/packages/services/service-automation/src/flow-field-expression-scale.integration.test.ts
+++ b/packages/services/service-automation/src/flow-field-expression-scale.integration.test.ts
@@ -44,6 +44,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { AutomationServicePlugin } from './plugin.js';
import type { AutomationEngine } from './engine.js';
import { VALUE_SLOT_TEMPLATE_REFUSAL } from '@objectstack/spec/automation';
+import { installDefaultOrganization } from './default-organization.testkit.js';
function makeSqliteDriver() {
return new SqlDriver({
@@ -114,6 +115,9 @@ describe('flow-computed money lands within its declared scale (#11060, oracle fo
ql.registerDriver(driver, true);
ql.registry.registerObject(quote as any, 'scale-test', 'scale-test');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
}
const quoteByTitle = (title: string) =>
diff --git a/packages/services/service-automation/src/flow-metadata-save-arming.integration.test.ts b/packages/services/service-automation/src/flow-metadata-save-arming.integration.test.ts
index 290ed9e226a..945ab28bd1c 100644
--- a/packages/services/service-automation/src/flow-metadata-save-arming.integration.test.ts
+++ b/packages/services/service-automation/src/flow-metadata-save-arming.integration.test.ts
@@ -36,6 +36,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import type { AutomationContext } from '@objectstack/spec/contracts';
import { AutomationServicePlugin } from './plugin.js';
import type { AutomationEngine, FlowTrigger, FlowTriggerBinding } from './engine.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The saved surface these tests drive: `ObjectStackProtocolImplementation`'s write doors. */
interface MetadataWriteDoors {
@@ -133,6 +134,9 @@ describe('a flow saved through the metadata API is armed on the running engine (
'qa-21725',
);
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const rec = objectqlRecordTrigger(ql);
automation.registerTrigger(rec.trigger);
diff --git a/packages/services/service-automation/src/paused-run-visibility.test.ts b/packages/services/service-automation/src/paused-run-visibility.test.ts
index c5870b58869..10eda9d33fc 100644
--- a/packages/services/service-automation/src/paused-run-visibility.test.ts
+++ b/packages/services/service-automation/src/paused-run-visibility.test.ts
@@ -80,6 +80,7 @@ import { AutomationEngine } from './engine.js';
import type { RunRecord, SuspendedRun, SuspendedRunStore } from './engine.js';
import { InMemorySuspendedRunStore } from './suspended-run-store.js';
import { AutomationServicePlugin } from './plugin.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const silent = { info() {}, warn() {}, error() {}, debug() {}, child() { return silent; } } as never;
const flush = () => new Promise((r) => setTimeout(r, 0));
@@ -547,6 +548,9 @@ describe('#8050 cold boot over the same sqlite file (full stack)', () => {
(ql as any).registry.registerObject(o.def as never, o.owner);
}
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const automation = kernel.getService('automation');
automation.registerNodeExecutor(holdExecutor);
diff --git a/packages/services/service-automation/src/resume-gone-suspension-settle.test.ts b/packages/services/service-automation/src/resume-gone-suspension-settle.test.ts
index c1e0aea5198..56576bb564c 100644
--- a/packages/services/service-automation/src/resume-gone-suspension-settle.test.ts
+++ b/packages/services/service-automation/src/resume-gone-suspension-settle.test.ts
@@ -53,6 +53,7 @@ import type { RunRecord, SuspendedRun, SuspendedRunStore, SuspensionClaimOutcome
import { InMemorySuspendedRunStore } from './suspended-run-store.js';
import { AutomationServicePlugin } from './plugin.js';
import { SysAutomationRun } from './sys-automation-run.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const silent = { info() {}, warn() {}, error() {}, debug() {}, child() { return silent; } } as never;
const SYSTEM = { isSystem: true } as never;
@@ -125,6 +126,9 @@ describe('a gone-for-good resume settles the run row (full stack, sqlite file)',
await driver.connect();
ql.registerDriver(driver, true);
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
const automation = kernel.getService('automation');
return { kernel, ql, automation };
@@ -435,7 +439,11 @@ describe('the boot re-arm runs before a late-bound flow arrives (plugin boot ord
} as never);
await kernel.use(new AutomationServicePlugin());
await kernel.bootstrap();
- return { kernel, ql: kernel.getService('objectql'), automation: kernel.getService('automation') };
+ const ql = kernel.getService('objectql');
+ // A real `single` install's Default Organization: the run rows below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
+ return { kernel, ql, automation: kernel.getService('automation') };
}
it('the re-armed resume finds the flow not bound yet, and the run stays parked and resumes once the boot completes', async () => {
diff --git a/packages/services/service-automation/src/runas-attribution-contract.test.ts b/packages/services/service-automation/src/runas-attribution-contract.test.ts
index f31b5ec148a..80cf75defc6 100644
--- a/packages/services/service-automation/src/runas-attribution-contract.test.ts
+++ b/packages/services/service-automation/src/runas-attribution-contract.test.ts
@@ -53,6 +53,7 @@ import { ObjectQLPlugin, type ObjectQL } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { AutomationServicePlugin } from './plugin.js';
import type { AutomationEngine } from './engine.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** Real backend: better-sqlite3 `:memory:` through driver-sql. */
function makeSqliteDriver() {
@@ -178,6 +179,9 @@ describe("runAs:'system' attribution contract — elevation decides authorizatio
ql.registerDriver(driver, true);
ql.registry.registerObject(crmTask as any, 'attribution-test', 'attribution-test');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
// Observe the SAME session envelope the audit stamp hooks and
// plugin-audit's `writeAudit` read. Registered at priority 100 so it runs
diff --git a/packages/services/service-automation/src/runas-system-stamping.integration.test.ts b/packages/services/service-automation/src/runas-system-stamping.integration.test.ts
index 2cd4c60b178..e4d51b72ad5 100644
--- a/packages/services/service-automation/src/runas-system-stamping.integration.test.ts
+++ b/packages/services/service-automation/src/runas-system-stamping.integration.test.ts
@@ -162,6 +162,16 @@ describe("runAs:'system' create_record stamps organization_id / owner_id / creat
it('a USER-LESS system run (schedule shape) stamps nothing — and that is the contract, not a gap', async () => {
await boot();
+ // The row this run writes names no organization, which only a store built
+ // before 18.x can hold: a fresh tenant column is NOT NULL (ADR-0131 D1), and
+ // this composition registers no organization object to derive one from.
+ const relaxed = await (ql.getDriverForObject('crm_task') as any).applyMigrationEntries([
+ {
+ kind: 'nullability_mismatch', remoteName: 'crm_task', table: 'crm_task', column: 'organization_id', category: 'needs_confirm',
+ op: { type: 'relax_not_null', table: 'crm_task', column: 'organization_id' },
+ },
+ ]);
+ expect(relaxed.applied).toHaveLength(1);
automation.registerFlow('night_sweep', sweepFlow('night_sweep', { title: 'renew C' }) as any);
// What ScheduleTrigger actually supplies: an event and params, NO user, NO
diff --git a/packages/services/service-automation/src/sys-automation-run-display-title.test.ts b/packages/services/service-automation/src/sys-automation-run-display-title.test.ts
index 663a33c2092..99cd6b87d41 100644
--- a/packages/services/service-automation/src/sys-automation-run-display-title.test.ts
+++ b/packages/services/service-automation/src/sys-automation-run-display-title.test.ts
@@ -26,6 +26,7 @@ import { ObjectQL, resolveRecordTitle } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { resolveDisplayField } from '@objectstack/spec/data';
import { SysAutomationRun } from './sys-automation-run.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
@@ -68,6 +69,9 @@ describe('[#20015] sys_automation_run resolves a real record title under ADR-007
await engine.init();
engine.registry.registerObject(SysAutomationRun as any, 'com.objectstack.test.20015');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterAll(async () => {
diff --git a/packages/services/service-messaging/src/actor-reference.pin.test.ts b/packages/services/service-messaging/src/actor-reference.pin.test.ts
index 0bc3e2077a6..3b5488f512d 100644
--- a/packages/services/service-messaging/src/actor-reference.pin.test.ts
+++ b/packages/services/service-messaging/src/actor-reference.pin.test.ts
@@ -22,6 +22,7 @@ import { InboxMessage } from './objects/inbox-message.object.js';
import { NotificationReceipt } from './objects/notification-receipt.object.js';
import { MessagingService } from './messaging-service.js';
import { createInboxChannel } from './inbox-channel.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as const;
const GHOST = 'usr_ghost_21913';
@@ -47,6 +48,9 @@ beforeEach(async () => {
engine.registry.registerObject(InboxMessage as any, '@objectstack/service-messaging');
engine.registry.registerObject(NotificationReceipt as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
const user = await engine.insert('sys_user', { name: 'Ada', email: 'ada@example.test' }, SYS);
userId = String((user as any).id);
});
diff --git a/packages/services/service-messaging/src/default-organization.testkit.ts b/packages/services/service-messaging/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..22a2407f50d
--- /dev/null
+++ b/packages/services/service-messaging/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise;
+ getDriverForObject(object: string):
+ | {
+ create(object: string, data: Record): Promise;
+ findOne(object: string, query: unknown): Promise;
+ }
+ | undefined;
+}): Promise {
+ // Idempotent: a suite that syncs one engine more than once may call it each time.
+ if (!engine.getSchema('sys_organization')) {
+ engine.registry.registerObject(
+ { name: 'sys_organization', label: 'Organization', managedBy: 'better-auth', fields: { name: { type: 'text', label: 'Name' } } },
+ '@objectstack/service-messaging',
+ );
+ }
+ await engine.syncSchemas();
+ // Seeded through the driver: the organization is data at rest, not a write
+ // the suite under test should observe (an audit or activity mirror would).
+ const driver = engine.getDriverForObject('sys_organization')!;
+ if (await driver.findOne('sys_organization', { where: { id: DEFAULT_ORGANIZATION_ID } })) return;
+ await driver.create('sys_organization', { id: DEFAULT_ORGANIZATION_ID, name: 'Default' });
+}
diff --git a/packages/services/service-messaging/src/delivery-headers-at-rest.integration.test.ts b/packages/services/service-messaging/src/delivery-headers-at-rest.integration.test.ts
index 45cd2ced109..8a3189d7cc3 100644
--- a/packages/services/service-messaging/src/delivery-headers-at-rest.integration.test.ts
+++ b/packages/services/service-messaging/src/delivery-headers-at-rest.integration.test.ts
@@ -54,6 +54,7 @@ import { SqlHttpOutbox } from './sql-http-outbox.js';
import { HttpDispatcher } from './http-dispatcher.js';
import { HttpDelivery, SYS_HTTP_DELIVERY } from './objects/http-delivery.object.js';
import type { FetchImpl } from './http-sender.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* [#10740] `IHttpOutbox.redeliver` now requires its caller to state the
@@ -108,6 +109,9 @@ describe('sys_http_delivery — authored headers vs the data API (#8118)', () =>
engine.registry.registerObject(HttpDelivery as any, '@objectstack/service-messaging');
// Real DDL through the real path — physical columns exactly as prod.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return { driver, engine: engine! };
}
diff --git a/packages/services/service-messaging/src/dispatcher-idle-cost.integration.test.ts b/packages/services/service-messaging/src/dispatcher-idle-cost.integration.test.ts
index d54d192165d..0100d688896 100644
--- a/packages/services/service-messaging/src/dispatcher-idle-cost.integration.test.ts
+++ b/packages/services/service-messaging/src/dispatcher-idle-cost.integration.test.ts
@@ -41,6 +41,7 @@ import { NotificationDelivery } from './objects/notification-delivery.object.js'
import { NotificationDispatcher } from './dispatcher.js';
import type { MessagingChannel } from './channel.js';
import type { INotificationOutbox } from './outbox.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The production default. */
const PARTITIONS = 8;
@@ -85,6 +86,9 @@ beforeEach(async () => {
await engine.init();
engine.registry.registerObject(NotificationDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
calls = { update: 0, find: 0, other: 0 };
sent = [];
diff --git a/packages/services/service-messaging/src/http-dispatcher-idle-cost.integration.test.ts b/packages/services/service-messaging/src/http-dispatcher-idle-cost.integration.test.ts
index af74f3364a4..628407e8269 100644
--- a/packages/services/service-messaging/src/http-dispatcher-idle-cost.integration.test.ts
+++ b/packages/services/service-messaging/src/http-dispatcher-idle-cost.integration.test.ts
@@ -42,6 +42,7 @@ import { HttpDispatcher } from './http-dispatcher.js';
import { hashPartition } from './backoff.js';
import type { FetchImpl } from './http-sender.js';
import type { EnqueueHttpInput, IHttpOutbox } from './http-outbox.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The production default. */
const PARTITIONS = 8;
@@ -86,6 +87,9 @@ beforeEach(async () => {
await engine.init();
engine.registry.registerObject(HttpDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
calls = { update: 0, find: 0, other: 0 };
posted = [];
diff --git a/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts b/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts
index f122a5009c6..8ac892bbe52 100644
--- a/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts
+++ b/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts
@@ -70,6 +70,7 @@ import type {
HttpDelivery as HttpDeliveryRow,
IHttpOutbox,
} from './http-outbox.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const TTL = 60_000;
/** Step-1 instant: node A's claim. */
@@ -119,6 +120,9 @@ function sqlBackend(): Backend {
await engine.init();
engine.registry.registerObject(HttpDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return new SqlHttpOutbox(engine as any, { partitionCount: 1 });
},
async destroy() {
diff --git a/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts b/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts
index d76fabaf05a..76cbcb923f2 100644
--- a/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts
+++ b/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts
@@ -54,6 +54,7 @@ import { HttpDispatcher } from './http-dispatcher.js';
import { HttpRedeliverError, type IHttpOutbox } from './http-outbox.js';
import { HttpDelivery, SYS_HTTP_DELIVERY } from './objects/http-delivery.object.js';
import type { FetchImpl } from './http-sender.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* [#10740] `IHttpOutbox.redeliver` now requires its caller to state the
@@ -106,6 +107,9 @@ describe('sys_http_delivery — parked drop records are not redeliverable (#8069
await engine.init();
engine.registry.registerObject(HttpDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return { driver, engine: engine! };
}
diff --git a/packages/services/service-messaging/src/http-signature-at-rest.integration.test.ts b/packages/services/service-messaging/src/http-signature-at-rest.integration.test.ts
index e2eeeb57c70..73e519b373e 100644
--- a/packages/services/service-messaging/src/http-signature-at-rest.integration.test.ts
+++ b/packages/services/service-messaging/src/http-signature-at-rest.integration.test.ts
@@ -44,6 +44,7 @@ import { SqlHttpOutbox } from './sql-http-outbox.js';
import { HttpDispatcher } from './http-dispatcher.js';
import { HttpDelivery, SYS_HTTP_DELIVERY } from './objects/http-delivery.object.js';
import type { FetchImpl } from './http-sender.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* [#10740] `IHttpOutbox.redeliver` now requires its caller to state the
@@ -109,6 +110,9 @@ describe('sys_http_delivery — signing secret at rest (#7722)', () => {
// driver from the object definition, so its physical columns are
// exactly the ones production would have.
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return { driver, engine: engine! };
}
diff --git a/packages/services/service-messaging/src/objects/http-delivery-display-title.test.ts b/packages/services/service-messaging/src/objects/http-delivery-display-title.test.ts
index db7c6b8a4f8..c54fd8aa5a9 100644
--- a/packages/services/service-messaging/src/objects/http-delivery-display-title.test.ts
+++ b/packages/services/service-messaging/src/objects/http-delivery-display-title.test.ts
@@ -22,6 +22,7 @@ import { ObjectQL, resolveRecordTitle } from '@objectstack/objectql';
import { SqlDriver } from '@objectstack/driver-sql';
import { resolveDisplayField } from '@objectstack/spec/data';
import { HttpDelivery } from './http-delivery.object.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
@@ -75,6 +76,9 @@ describe('[#20015] sys_http_delivery resolves a real record title under ADR-0079
await engine.init();
engine.registry.registerObject(HttpDelivery as any, 'com.objectstack.test.20015');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterAll(async () => {
diff --git a/packages/services/service-messaging/src/objects/notification-display-title.test.ts b/packages/services/service-messaging/src/objects/notification-display-title.test.ts
index 92a01ce0943..516d8e13e73 100644
--- a/packages/services/service-messaging/src/objects/notification-display-title.test.ts
+++ b/packages/services/service-messaging/src/objects/notification-display-title.test.ts
@@ -49,6 +49,7 @@ import { resolveDisplayField } from '@objectstack/spec/data';
import { NotificationDelivery } from './notification-delivery.object.js';
import { NotificationPreference } from './notification-preference.object.js';
import { NotificationReceipt } from './notification-receipt.object.js';
+import { installDefaultOrganization } from '../default-organization.testkit.js';
const SYS = { context: { isSystem: true } } as any;
const NOW = new Date('2026-09-25T00:00:00Z');
@@ -155,6 +156,9 @@ describe('[#20044] notification objects resolve a real record title under ADR-00
engine.registry.registerObject(schema, 'com.objectstack.test.20044');
}
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterAll(async () => {
diff --git a/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts b/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts
index f9183516d65..a9638df8430 100644
--- a/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts
+++ b/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts
@@ -52,6 +52,7 @@ import { MemoryNotificationOutbox } from './memory-outbox.js';
import { SqlNotificationOutbox } from './sql-outbox.js';
import { NotificationDelivery } from './objects/notification-delivery.object.js';
import type { INotificationOutbox, NotificationDeliveryRecord } from './outbox.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** The refusal identity — `name` + ADR-0112 `code`, never a bare throw. */
const REFUSAL = { name: 'NotificationAckError', code: 'DELIVERY_NOT_ELIGIBLE' };
@@ -97,6 +98,9 @@ function sqlBackend(): Backend {
await engine.init();
engine.registry.registerObject(NotificationDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return new SqlNotificationOutbox(engine as any, { partitionCount: 1 });
},
async destroy() {
diff --git a/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts b/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts
index 6a224c01b63..a0b4d4ec4c7 100644
--- a/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts
+++ b/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts
@@ -58,6 +58,7 @@ import { MemoryNotificationOutbox } from './memory-outbox.js';
import { SqlNotificationOutbox } from './sql-outbox.js';
import { NotificationDelivery } from './objects/notification-delivery.object.js';
import type { ClaimedDeliveryRecord, INotificationOutbox, NotificationDeliveryRecord } from './outbox.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/**
* The refusal identity both backends must produce. `DELIVERY_NOT_ELIGIBLE` is
@@ -96,6 +97,9 @@ function sqlBackend(): Backend {
await engine.init();
engine.registry.registerObject(NotificationDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return new SqlNotificationOutbox(engine as any, { partitionCount: 1 });
},
async destroy() {
diff --git a/packages/services/service-messaging/src/outbox-claim-credential-lossy-column.integration.test.ts b/packages/services/service-messaging/src/outbox-claim-credential-lossy-column.integration.test.ts
index 59b3464df64..0008d2817f1 100644
--- a/packages/services/service-messaging/src/outbox-claim-credential-lossy-column.integration.test.ts
+++ b/packages/services/service-messaging/src/outbox-claim-credential-lossy-column.integration.test.ts
@@ -62,6 +62,7 @@ import { NotificationDelivery } from './objects/notification-delivery.object.js'
import { HttpDelivery } from './objects/http-delivery.object.js';
import type { ClaimedDeliveryRecord, INotificationOutbox, NotificationDeliveryRecord } from './outbox.js';
import type { HttpDelivery as HttpDeliveryRow, IHttpOutbox } from './http-outbox.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
// ── The `real` column double ────────────────────────────────────────────────
@@ -175,6 +176,9 @@ async function sqliteEngine(...objects: unknown[]): Promise {
await engine.init();
for (const object of objects) engine.registry.registerObject(object as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
return engine;
}
diff --git a/packages/services/service-messaging/src/plugin-enqueue-wakes-dispatcher.test.ts b/packages/services/service-messaging/src/plugin-enqueue-wakes-dispatcher.test.ts
index 2cd804df0a6..f939def6bca 100644
--- a/packages/services/service-messaging/src/plugin-enqueue-wakes-dispatcher.test.ts
+++ b/packages/services/service-messaging/src/plugin-enqueue-wakes-dispatcher.test.ts
@@ -40,6 +40,7 @@ import type { MessagingService } from './messaging-service.js';
import { DELIVERY_OBJECT } from './sql-outbox.js';
import { SYS_HTTP_DELIVERY } from './objects/http-delivery.object.js';
import { hashPartition } from './backoff.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
/** No timer-driven tick inside the test's lifetime. */
const DISPATCH_INTERVAL_MS = 60_000;
@@ -93,6 +94,9 @@ async function bootMessagingKernel() {
objectql.registerDriver(driver, true);
openDrivers.push(driver);
await objectql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(objectql);
return {
engine: kernel.getService('data'),
diff --git a/packages/services/service-messaging/src/redeliver-concurrent-claim.integration.test.ts b/packages/services/service-messaging/src/redeliver-concurrent-claim.integration.test.ts
index 559e9f3deb6..5dad6b5f6e3 100644
--- a/packages/services/service-messaging/src/redeliver-concurrent-claim.integration.test.ts
+++ b/packages/services/service-messaging/src/redeliver-concurrent-claim.integration.test.ts
@@ -39,6 +39,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SqlHttpOutbox } from './sql-http-outbox.js';
import { MemoryHttpOutbox } from './memory-http-outbox.js';
import { HttpDelivery, SYS_HTTP_DELIVERY } from './objects/http-delivery.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
let engine: ObjectQL;
let driver: SqlDriver;
@@ -54,6 +55,9 @@ beforeEach(async () => {
await engine.init();
engine.registry.registerObject(HttpDelivery as any, '@objectstack/service-messaging');
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
});
afterEach(async () => {
diff --git a/packages/services/service-storage/src/attachment-read-leaf.integration.test.ts b/packages/services/service-storage/src/attachment-read-leaf.integration.test.ts
index 3f82990ea50..236f6d6e339 100644
--- a/packages/services/service-storage/src/attachment-read-leaf.integration.test.ts
+++ b/packages/services/service-storage/src/attachment-read-leaf.integration.test.ts
@@ -42,6 +42,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
import { SysAttachment } from '@objectstack/platform-objects';
import type { ISecurityService } from '@objectstack/spec/contracts';
import { installAttachmentReadVisibility } from './attachment-access-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const ATTACHMENT = 'sys_attachment';
/** Owner-scoped: a member reads its own records. */
@@ -204,6 +205,9 @@ describe('[ADR-0139 E7] the attachment read gate on the leaf path', () => {
engine.registry.registerObject(objectOf(GATED) as any, HARNESS_PACKAGE);
engine.registry.registerObject(objectOf(FAR, { datasource: 'warehouse' }) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
installSecurityMiddleware(engine);
// D5's second class: an object-scoped read middleware narrows the gated parent.
engine.registerMiddleware(async (op: any, next: () => Promise) => {
@@ -378,6 +382,9 @@ describe('[ADR-0139 D3] on a driver that refuses the leaf, the attachment read f
({ kernel, engine } = await bootKernel(new RefusingDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true })));
engine.registry.registerObject(objectOf(DOC) as any, HARNESS_PACKAGE);
await engine.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(engine);
installAttachmentReadVisibility(engine as any, silent, () => kernel.getService('security'));
const doc = await engine.insert(DOC, { name: 'one', owner: MEMBER_A.userId }, { context: SYS });
await engine.insert(ATTACHMENT, attachmentTo(DOC, doc.id), { context: SYS });
diff --git a/packages/services/service-storage/src/attachment-refused-attach-tombstone.test.ts b/packages/services/service-storage/src/attachment-refused-attach-tombstone.test.ts
index 730fe108007..bcfa1cdf2e1 100644
--- a/packages/services/service-storage/src/attachment-refused-attach-tombstone.test.ts
+++ b/packages/services/service-storage/src/attachment-refused-attach-tombstone.test.ts
@@ -47,6 +47,7 @@ import {
type AttachmentSecurityLike,
type AttachmentSharingLike,
} from './attachment-access-hooks.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const DAY_MS = 86_400_000;
const SYS = { context: { isSystem: true } } as const;
@@ -106,6 +107,9 @@ async function boot(opts: { verdict?: Verdict; master?: MasterOutcome; sharing?:
} as any, 'test-22466');
ql.registry.registerObject({ name: 'att_parent', fields: { name: { type: 'text' } } } as any, 'test-22466');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
installAttachmentLifecycleHooks(ql as any, logger);
const sharing: AttachmentSharingLike | null =
diff --git a/packages/services/service-storage/src/backfill-sys-file-public-scope.test.ts b/packages/services/service-storage/src/backfill-sys-file-public-scope.test.ts
index c0cd83e45ff..a88f0d03f95 100644
--- a/packages/services/service-storage/src/backfill-sys-file-public-scope.test.ts
+++ b/packages/services/service-storage/src/backfill-sys-file-public-scope.test.ts
@@ -50,6 +50,7 @@ import type {
SysFilePublicScopeBackfillOptions as EntryOptions,
SysFilePublicScopeBackfillReport as EntryReport,
} from './index.js';
+import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
const SYSTEM = { context: { isSystem: true } };
@@ -91,6 +92,9 @@ describe('sys_file public-scope backfill (#22443 ruling B) — a real ObjectQL o
fields: { title: { type: 'text' }, image: { type: 'image' } },
} as any, 'test-22443');
await engine.syncSchemas();
+ // The legacy organization-less rows below exist only on a store built before
+ // 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
+ await relaxTenantColumns(sql as any, ['sys_file']);
storage = {
upload: vi.fn(async () => {}),
@@ -120,8 +124,10 @@ describe('sys_file public-scope backfill (#22443 ruling B) — a real ObjectQL o
* condition that reaches `copyOwnedFile` (copy-on-claim), which re-inserts
* the source row's scope on the copy.
*/
+ // The claiming write acts in an organization: `product` is created by this
+ // release, whose tenant column is NOT NULL (ADR-0131 D1).
const copyBySecondReference = (recordId: string, fileId: string) =>
- engine.insert('product', { id: recordId, title: recordId, image: fileId });
+ engine.insert('product', { id: recordId, title: recordId, image: fileId }, { context: { tenantId: 'org_a' } } as any);
it('a deployment with no public rows runs clean: it counts zero and writes nothing', async () => {
await engine.insert('sys_file', { ...legacyPublicRow('f_user'), scope: 'user' }, SYSTEM as any);
diff --git a/packages/services/service-storage/src/chunk-part-record-concurrency.test.ts b/packages/services/service-storage/src/chunk-part-record-concurrency.test.ts
index 9ea667946d6..1b50a191504 100644
--- a/packages/services/service-storage/src/chunk-part-record-concurrency.test.ts
+++ b/packages/services/service-storage/src/chunk-part-record-concurrency.test.ts
@@ -34,6 +34,7 @@ import type { UploadSessionProgress, UploadSessionRecord } from './metadata-stor
import { registerStorageRoutes, CHUNK_RECORD_ATTEMPTS } from './storage-routes.js';
import { SystemFile } from './objects/system-file.object.js';
import { SystemUploadSession } from './objects/system-upload-session.object.js';
+import { installDefaultOrganization } from './default-organization.testkit.js';
const BASE = '/api/v1/storage';
const INIT = `${BASE}/upload/chunked`;
@@ -105,6 +106,9 @@ async function realEngine(): Promise {
ql.registry.registerObject(SystemFile as any, 'com.objectstack.storage');
ql.registry.registerObject(SystemUploadSession as any, 'com.objectstack.storage');
await ql.syncSchemas();
+ // A real `single` install's Default Organization: the system writes below
+ // derive it (ADR-0131 D9), as a fresh NOT NULL tenant column requires (D1).
+ await installDefaultOrganization(ql);
let writes = 0;
const realUpdate = ql.update.bind(ql);
(ql as any).update = (object: string, data: any, options: any) => {
diff --git a/packages/services/service-storage/src/default-organization.testkit.ts b/packages/services/service-storage/src/default-organization.testkit.ts
new file mode 100644
index 00000000000..92328ba84e7
--- /dev/null
+++ b/packages/services/service-storage/src/default-organization.testkit.ts
@@ -0,0 +1,45 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Give a lean test engine what every real `single` install has: an
+ * organization object holding its Default Organization.
+ *
+ * ADR-0131 D1: a table the SQL driver creates carries its tenant column NOT
+ * NULL. The engine stamps an organization-less system write with the derived
+ * organization under `single` (D9) — but only where the composition registers
+ * the organization object; a bare `ObjectQL` + `SqlDriver` registers none, so
+ * the write reaches the database with no organization and is refused there.
+ * A suite whose subject is not tenancy (claims, acks, idle cost, at-rest
+ * shapes) calls this once its own objects are registered, so its system
+ * writes land the way they do on a real install.
+ *
+ * The object mirrors the platform's: managed by better-auth, so it carries no
+ * tenant column of its own.
+ */
+export const DEFAULT_ORGANIZATION_ID = 'org_default';
+
+export async function installDefaultOrganization(engine: {
+ registry: { registerObject(def: unknown, owner: string): unknown };
+ getSchema(object: string): unknown;
+ syncSchemas(): Promise