fix(objectql,driver-mongodb): declare the tenant index in indexes[], so a registry-backed object stops reporting itself invalid (#6810) - #6812
Merged
Conversation
…#6810) `applySystemFields` provisioned the injected `organization_id` column with `indexed: opts.multiTenant`. `indexed` is not a `FieldSchema` key — #2377 / ADR-0049 removed it because a field-level index flag built no index — and `FieldSchema` is a `strictObject`, so a field carrying it is rejected by name. `registerObject` runs `applySystemFields` before storing and `getItem('object', …)` serves that post-injection document, so the key reached `/meta`, where `decorateMetadataItem` re-parsed the served body and stamped `_diagnostics: { valid: false, errors: [{ path: 'fields.organization_id', code: 'unrecognized_keys' }] }` on every registry-backed object — both tenancy modes, both read exits. That is the channel Studio renders invalid-metadata banners from and an AI author reads to judge its own document, so the platform was reporting a defect on its own column and drowning real authoring errors. The tenant index is now declared in the object's `indexes[]`, where every other index in this system is declared: `{ fields: ['organization_id'] }` on a multi-tenant stack, nothing at all on a single-tenant one (absence is what `indexed: false` meant). `driver-mongodb` — the sole reader of the retired flag — reads declared indexes instead, generating the same index name it used to, so a re-synced collection finds its existing `idx_organization_id`. `driver-sql` already materialized `indexes[]`, so this is the first time the intent is enforced there at all. No `FieldSchema` change: re-declaring `indexed` would restore exactly the declared-but-unenforced key #2377 removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JE8Bbwb8qhau3yNtP3ftLJ
#6562 (PR #6811) landed while this branch was in flight and factored the tenant column into `TENANT_SCOPE_FIELD_DEF`. Conflict resolved onto that shared table: the definition is now spread VERBATIM, with the `indexed` key gone and the tenant index declared in `indexes[]`. `protocol-meta-effective-schema.test.ts` carries the three-line tripwire #6562 pinned in both directions so this fix could not be forgotten. All three flip to the post-fix truth, annotated with #6810 and with the old expectation quoted in place so the reversal reads as a record, not a rewrite: divergences(...) ['organization_id.indexed'] → [] registryBacked._diagnostics valid: false → { valid: true } organization_id.indexed === multiTenant → undefined, read off `indexes[]` One residual is recorded rather than left to be rediscovered: the DECLARATION does not converge the way the field set does — the overlay-backed answer is rebuilt from the stored body, which declares no indexes. Inert on that surface (drivers materialize from the REGISTERED schema, never a served document) and both answers parse green either way. `metadata-core`'s `injected-system-columns.ts` header described the stamped key as live; corrected to describe it as closed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JE8Bbwb8qhau3yNtP3ftLJ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 17 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…tch guards (#6810) `check:engine-double-contract` pins every engine double to ObjectQL's own `delete`/`update` dispatch predicates — a fake looser than the producer is how #4434 shipped a dead REST route with its suite green. The fake in `registry-tenant-index-declaration.test.ts` now routes both verbs through `assertEngineDeleteDispatch` / `assertEngineUpdateDispatch` from `@objectstack/metadata-core`, matching the pinned fake in `protocol-meta-effective-schema.test.ts` next to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JE8Bbwb8qhau3yNtP3ftLJ
…anization-id-indexed-key
os-zhuang
marked this pull request as ready for review
August 8, 2026 22:25
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6810
applySystemFieldsprovisioned the injectedorganization_idwithindexed: opts.multiTenant.indexedis not aFieldSchemakey — #2377 /ADR-0049 removed it because a field-level index flag built no index — and
FieldSchemais astrictObject, so a field carrying it is rejected by name.registerObjectrunsapplySystemFieldsbefore storing andgetItem('object', …)serves that post-injection document, so the key reached/meta, wheredecorateMetadataItemre-parsed the served body and stamped theverdict on it.
Direction A per the PM ruling on the issue: stop stamping a key the schema
rejects, declare the tenant index where every other index in this system is
declared (the object's
indexes[]), and teach the single consumer to read itthere. B (strip-before-serve) and C (re-declare
indexedonFieldSchema) were not needed — the premise held, see the table below.Premise verification (the falsifiable part, checked first)
indexes[]can express whatindexed: opts.multiTenantmeantIndexSchemais{ name?, fields[], unique? }(packages/spec/src/data/object.zod.ts).multiTenant: true→{ fields: ['organization_id'] };uniquedefaults tofalse, so a plain lookup index needs no key at all.multiTenant: false→ declare nothing, which is what the PM predicted: nothing filters by organization on an unwalled stack, so an index declared false and an index not declared are the same fact, andindexes[]spells it as absence.indexes[]produces the same DDL outcomefield.indexed→createIndex({organization_id: 1}, { name: 'idx_organization_id' }). New: the declaration generatesidx_<fields.join('_')>, i.e. the same name and same key spec, so a re-synced collection finds its existing index rather than building a second one. Pinned inmongodb-schema-declared-indexes.test.ts("byte-identical to what the retired flag built").nameis deliberately not set in the declaration: SQL index names are schema-global and must be table-qualified (buildIndexName→idx_<table>_organization_id), Mongo's are per-collection — one hardcoded name cannot be right for both, so each driver derives its own.field.indexedexistsdriver-mongodb/src/mongodb-schema.ts:87, as filed. Three objectql test files asserted the flag (registry.test.ts,registry-tenancy-posture.test.ts,injected-system-columns-parity.test.ts) and driver-mongodb's README +mongodb-driver.test.tsused it in a fixture (email: { type: 'email', indexed: true }) — that fixture was silently relying on a key the spec rejects, so it is rewritten toindexes: [{ fields: ['email'] }], producing the sameidx_email.A note the premise check turned up, since it changes what "same DDL outcome"
means at the fleet level:
driver-sql— which every walled deployment runs —never read the flag at all. It only ever materialized
indexes[]. So thetenant index has not existed on any SQL deployment regardless of what
indexed: truesaid, and this PR is the first time the intent is actuallyenforced there. Expect it to appear as ordinary index drift on existing tables
(
idx_<table>_organization_id), created byos migrate applyor theautoMigrate: 'safe'path in dev, like any other declared index. Called out inthe changeset.
What changed
packages/objectql/src/registry.ts—additions.organization_idis now{ ...TENANT_SCOPE_FIELD_DEF }, spread verbatim with nothing on top. Whenopts.multiTenant,{ fields: ['organization_id'] }is appended to theobject's
indexes[](an author's own entries keep their position), guarded bydeclaresTenantIndexso an author who hand-wrote the same single-column indexdoes not get a duplicate beside it — the array append is the one part of this
injection that is not naturally idempotent.
packages/drivers/driver-mongodb/src/mongodb-schema.ts— the} else if (field.indexed) {branch is replaced by a loop over declaredindexes[]. Kept to the minimum that makes the index land (defect-fix rider;the [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 drivers freeze covers investment). Every
uniquescope materializesits columns verbatim,
'organization'included — the same callFieldDef.uniquealready documents in that file, because the driverimplements no row-level tenancy and refuses to boot multi-tenant (driver-mongodb 完全没有行级租户隔离:读不加谓词、写不打戳,多租户下跨租户可读写 #3724).
packages/metadata-core/src/injected-system-columns.ts— comment only. Itsheader (landed hours ago with meta: an overlay-backed object read OMITS the injected system columns a registry-backed read includes — the same endpoint answers two different field sets #6562) described the stamped key as live; it now
describes it as closed. Correcting it was not optional: it is the file that
tells the next author why the table does not carry
indexed.@objectstack/objectql+@objectstack/driver-mongodb.No
packages/spec/FieldSchemachange — direction C would restore exactlythe declared-but-unenforced key #2377 removed.
The #6562 tripwire, flipped
#6562 (PR #6811) merged into
mainwhile this branch was in flight — itstripwire file did not exist at my base commit, so
origin/mainwas merged in(one conflict, in
registry.ts, resolved onto #6562's new sharedTENANT_SCOPE_FIELD_DEF) and all three pinned lines flipped to the post-fixtruth. The old expectations are quoted in place in the test comments, so the
reversal reads as a record rather than a rewrite (PD #13). Nothing was deleted.
One new residual is recorded there rather than left to be rediscovered: the
declaration does not converge the way the field set does — the overlay-backed
answer is rebuilt from the stored body, which declares no indexes. It is inert on
that surface (a driver materializes from the REGISTERED schema, never from a
served document — the same reasoning #6562 used to leave the flag at the
injection site) and both answers parse green either way. Asserted explicitly, so
the day a served-document consumer of
indexes[]appears, there is a line thatsays so.
Acceptance pins
Driven through the real
SchemaRegistry+ the realObjectStackProtocolImplementation— the filer's exact measurement, inverted:_diagnostics: { valid: true }at both/metaexits, in both tenancy modesregistry-tenant-index-declaration.test.tsindexed— asserted across every injected column, not just the tenant one[{ fields: ['organization_id'] }]), not merely the key removedmultiTenant=falsedeclares no tenant index and still parses greencreateIndex({organization_id: 1}, { name: 'idx_organization_id' })mongodb-schema-declared-indexes.test.tsThe Mongo DDL assertion is driven against a fake
Dbon purpose: this package'smongodb-memory-serversuite is opt-in (it downloads a server binary, #5517), soa DDL pin parked there would not run on any ordinary CI lane — which is exactly
the lane that has to notice a regression. Its fake engine opens both write verbs
with
assertEngineDeleteDispatch/assertEngineUpdateDispatch, ascheck:engine-double-contractrequires.Reverse verification — predicted, then run
Prediction written before reverting; both source files (
registry.ts,mongodb-schema.ts) restored toorigin/mainwith the tests kept at HEAD.objectql/registry-tenant-index-declaration.test.tsobjectql/registry.test.tsobjectql/registry-tenancy-posture.test.tsobjectql/injected-system-columns-parity.test.tsdriver-mongodb/mongodb-schema-declared-indexes.test.tsSignature-level, the reverted run reproduces the filer's measurement verbatim at
both exits and in both tenancy modes:
The three surviving-green cases are the ones that should survive:
multiTenant=falsedeclares no index either way, an author's own tenant index is not duplicated either
way, and a
systemFields: falseobject is untouched either way.Fix restored, working tree verified clean against HEAD afterwards.
Gates — enumerated fresh from
.github/workflows/lint.yml, run one by oneAll green locally, on a full build first.
pnpm lint✅slot-lookup·query-options-erasure·verify-stand-in·nul-bytes·doc-authoring·docs-audit-scope·role-word·quick-reference-counts·adr-anchors·org-identifier·authz-resolver·service-providers·route-envelope·error-code-casing·wildcard-fallthrough·meta-type-normalized·init-service-contract·durability-log-level·startup-registry-verdict·objectui-changeset·release-notes·release-body·node-version·workflow-status-functions·shard-attestation·published-files·engine-double-contract·kernel-hook-pairs·resume-authority-declared·driver-memory-census·merge-driver·spec-parsed-alias— all ✅check:type-check-coverage✅ ·check:driver-conformance✅ ·check:stall-guard✅ ·spec tsc --noEmit✅ ·spec check:generated --reconcile-only✅ ·spec check:skill-docs✅ ·spec check:spec-changes✅ ·spec check:upgrade-guide✅ ·spec check:authorable-surface✅ ·spec check:docs✅ ·spec check:skill-refs✅ ·check:skill-frame-sync✅ ·check:skill-compatibility✅ ·spec check:react-blocks✅ ·turbo typecheck(120/120) ✅ ·check:type-check-debt✅ (informational"can be lowered" lines only; nothing above its recorded number)
turbo run test— 135/135 tasks, exit 0check:engine-double-contractwas the one gate that went red on the first pass —the new fake engine's
delete/updatewere not routed through the producer'sdispatch predicates. Fixed in
915359e, re-run green.origin/mainwas merged twice during this work (5e247fdfor #6562, then6de592cfor #6809/driver-sql). The second merge was clean with no overlapagainst this diff; the overlap packages (
objectql,driver-sql,driver-mongodb,metadata-protocol,metadata-core) were rebuilt and re-testedafter it — 19/19 ✅.
packages/specdid not move on either side, so no generatedartifact is in play.
Generated by Claude Code