Skip to content

An author-declared organization_id withholds the platform's tenant index — the wall's hottest predicate runs unindexed on a multi-tenant deployment #8459

Description

@os-zhuang

Filed by the #8375 dev agent, measured while converging the tenant-index stamp across the /meta read-exit seam. Unassigned. Out of #8375's scope — that card preserved this behaviour exactly rather than widening it, and this is the behaviour it preserved.

Symptom

applySystemFields (packages/objectql/src/registry.ts) declares the tenant-scope index only for an object whose organization_id column the PLATFORM provisioned. An author who declares the column themselves gets the column they wrote — which is correct and deliberate, pinned as "does NOT overwrite an author-declared organization_id" — but no index on it, on a multi-tenant deployment:

registry.getObject('x').fields.organization_id ==> the author's own declaration
registry.getObject('x').indexes ==> undefined

The column is still THE tenant isolation key. SecurityPlugin's RLS layer appends organization_id = current_user.organization_id to essentially every read on that object, so the deployment's hottest predicate runs unindexed — the exact condition #6810 landed the indexes[] declaration to fix, reached by a different route.

Why it looks incidental rather than decided

Before #8375 the index push sat physically nested inside the field-injection branch:

if (wantTenant && !schema.fields?.organization_id) {
additions.organization_id = { ...TENANT_SCOPE_FIELD_DEF };
if (opts.multiTenant && !declaresTenantIndex(schema)) { ...push the index... }
}

so "the author declared the column" and "the platform declares no index" were the same condition by nesting, not by an argued decision. Nothing in #6810's reasoning distinguishes the two cases: its argument is that on a walled deployment the tenant column is filtered on constantly and therefore wants an index, which is true regardless of who typed the column.

#8375 lifted the decision into provisionTenantScopeIndex and preserved this behaviour deliberately and exactly (via isInjectedColumnDefinition), because widening it was outside that card and would have changed what the registry answers. That makes this the right moment to decide it on purpose.

Reproduction

  1. new SchemaRegistry({ multiTenant: true }).
  2. Register an object declaring its own organization_id, e.g. { type: 'lookup', reference: 'sys_organization', label: 'Org' } — any shape not byte-identical to TENANT_SCOPE_FIELD_DEF.
  3. registry.getObject(name).indexes is undefined; an object that declared nothing gets [{ fields: ['organization_id'] }].

The decision this needs

Not obviously a straight bug fix — it is a question about whose declaration governs:

C is the narrowest reading of "is this actually the tenant key", but it is a third predicate where #8375 just finished reducing two to one, so it should be weighed against the cost of that.

Dedup

Searched applySystemFields organization_id index / tenant index / author-declared column. Nearest neighbours: #8323 (tenant-scoped UNIQUE indexes going global — driver DDL normalization, not whether the index is declared at all) and #6810 (closed; moved the tenant index from a rejected field-level indexed key into indexes[], and is the card whose intent this gap sits inside). Neither is a twin.

Related

#8375 (the read-exit convergence that surfaced this) · #6810 · #6562 · ADR-0049

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions