Skip to content

fix(objectql): strip the hidden __search companion from every record body (#7642) - #7868

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-7642-search-companion-projection-strip
Aug 12, 2026
Merged

fix(objectql): strip the hidden __search companion from every record body (#7642)#7868
huangyiirene merged 1 commit into
mainfrom
claude/issue-7642-search-companion-projection-strip

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#7642

What was broken

The __search search-normalization companion (#2486) is declared invisible to clients — hidden + readonly + system + searchable: false — and every one of those flags does something real: the column stays out of auto-views, out of the $search auto-default, and a $searchFields override naming it is refused with a 400 ("is hidden"). None of them is a PROJECTION rule. A query that names no fields reaches the driver with ast.fields undefined, drivers answer that with SELECT *, and the column rode back in the four record bodies the QA run measured (#7629): query results, GET by id, /search hits, and the 201 create body.

Premise re-confirmed on origin/main (1a19e9d) before the first edit: search-companion.ts:174-177 still provisions the flags, and SEARCH_COMPANION_FIELD still appears in packages/objectql/src only in registry.ts (provisioning), search-filter.ts (the recall OR-in) and the two barrel re-exports — no read or projection path.

The fix site

packages/objectql/src/engine.ts — the default-projection read path, at the same seam as maskSecretFields. The engine is the producer all four surfaces share: /search hits are engine.find rows verbatim (MetadataProtocol.searchAllhit.record), the 201 body is engine.insert's return verbatim (createDatarecord), GET by id is findOne (getData). Fixing consumers one at a time is how three of the four would have stayed broken.

DoorSiteCovered
query resultsfindengine.ts:6963
GET by idfindOneengine.ts:7124
/search hitssearchAllengine.find✅ via find
201 create bodyinsertengine.ts:7727
update responseupdateengine.ts:8666 — see below
nested expand recordsexpandRelatedRecordsthis.find✅ via find
delete responsedeleteData returns {object, id, success}n/a — no record body

The update response is not one of the four reported surfaces. It is the same column in the same response shape, and leaving it out would make POST and PATCH on one object disagree about whether a client-invisible column is visible, so it is covered. A predicate update resolves to an affected-row count (#4639) and is unaffected.

Two details the fix is shaped around

It is not gated on the schema declaring the column. The report's sharpest detail is that the symptom survived a restart with OS_SEARCH_PINYIN_ENABLED=false. That is not a stale process: with the switch off the registry stops declaring the field, but the physical column and its values remain (ADR-0045 migrations are additive) and SELECT * keeps returning them. A strip that asked schema.fields.__search first would be silent in exactly the deployment that filed the bug — so the key on the row is the signal. Both states are pinned.

One caller keeps its read.plugin-pinyin-search's backfill/reconcile walk projects ['id', ...sources, '__search'] under { isSystem: true } and compares the stored blob against a recomputed one; stripping that unconditionally would make it rewrite every row of every object on every pass — write amplification traded for a disclosure the issue rates low. So a system caller that names the column in fields still gets it. A non-system caller does not, even by name: assertProjectionFieldsExist only gates on whether a field is known, so ?select=__search would otherwise be a documented spelling straight through the strip. The projection is read from the caller's originalfields, captured before planFormulaProjection — that pass widens an explicit projection to every stored column when a formula is in play, companion included.

Scope fence

This one column only. The issue observes that hidden system columns come back generally, "as organization_id does". That is an observation, not a licence: stripping every hidden/system column from the default projection would remove organization_id and its siblings from every response, which is a contract decision and not a defect fix. Nothing here touches the $searchFields-override half either — that half already answers 400 correctly.

Tests

packages/objectql/src/search-companion-read-projection-conformance.test.ts — a matrix, not a test for the door that happened to be fixed. One contract with five independently-breakable doors is the shape that rots one door at a time, so every door runs the same assertion from the same table, in both provisioning states (declared / flag-off-with-stored-values). Also pinned: the fixture is non-vacuous (the stored row really carries the column), the strip does not write through to the backing store, the formula-widened projection still strips, and both halves of the system-caller exemption.

Reverse-checked by reverting engine.ts alone: 17 of the 24 cases fail on the unfixed engine (the 7 that pass are the fixture guards, the no-write-through pins, and the "system caller keeps it" case, which holds by construction pre-fix).

Gates

GateResult
pnpm check:query-options-erasure✅ exit 0 — 67 non-test sites, none new; test surface 242, unchanged
pnpm check:type-check-debt✅ exit 0 — 33 entries re-measured, none above record (objectql held at 355 after typing the new file; it was +3 on first draft and the three errors were fixed, not ledgered)
pnpm test (whole repo, --concurrency=1)✅ 135/135 tasks, incl. the dogfood suite
packages/objectql vitest✅ 187 files / 3312 tests
pnpm --filter @objectstack/{rest,metadata-protocol,plugin-pinyin-search} test✅ 1471 / 1066 / 14
pnpm --filter @objectstack/objectql typecheck + build
check:adr-anchors, check:durability-log-level, check:engine-double-contract, check:stack-collection-maps, check-engine-split-ratio (via scripts/pm/dispatch-gates.mjs)
eslint --no-inline-config on all changed files✅ clean

A first pnpm test at default concurrency reported 7 package failures; every one was a vitest worker EPIPE/crash under parallelism, and all 7 pass individually — the --concurrency=1 run above is the real number.

Region fence

buildDriverOptions (~engine.ts:2549-2560, PR #7833) is untouched, and no comment at engine.ts:8996 was edited (#7707's region).


Generated by Claude Code

…d body (#7642)
The `__search` search-normalization companion (#2486) is declared invisible to
clients — `hidden` + `readonly` + `system` + `searchable: false` — and every one
of those flags does something real: the column stays out of auto-views, out of
the `$search` auto-default, and a `$searchFields` override naming it is refused
with a 400 ("is hidden"). None of them is a PROJECTION rule. A query that names
no `fields` reaches the driver with `ast.fields` undefined, drivers answer that
with `SELECT *`, and the column rode back in the four record bodies QA measured
(#7629): query results, GET by id, `/search` hits, and the 201 create body.
The strip runs at the engine, the producer all four surfaces share — `/search`
hits are `engine.find` rows verbatim, the create body is `engine.insert`'s
return verbatim, so fixing consumers one at a time would have left three of the
four broken. Covered: `find`, `findOne`, the nested records `expand` produces,
the create response and the update response. The update response is not one of
the four reported surfaces but is the same column in the same response shape;
leaving it out would make POST and PATCH on one object disagree about whether a
client-invisible column is visible. A predicate update resolves to a count and
is unaffected.
Two shaping details, both from the report:
- Not gated on the schema declaring the column. The symptom survived a restart
with `OS_SEARCH_PINYIN_ENABLED=false`: with the switch off the registry stops
DECLARING the field, but the physical column and its values remain (ADR-0045
migrations are additive) and `SELECT *` keeps returning them. A strip that
asked `schema.fields.__search` first would be silent in exactly the deployment
that filed the bug.
- One caller keeps its read. `plugin-pinyin-search`'s backfill projects
`['id', ...sources, '__search']` under a system context and compares the
stored blob against a recomputed one; stripping it unconditionally would make
the walk rewrite every row of every object on every pass. A SYSTEM caller that
names the column still gets it — a non-system caller does not, even by name,
since `select` only gates on whether a field is KNOWN and `?select=__search`
would otherwise be a documented way straight through the strip.
Scope is this one column. Hidden system columns do come back generally
(`organization_id` and its siblings), but they are load-bearing in client
payloads today; removing them is a contract decision, not a defect fix.
The new suite is a MATRIX over every record-returning door rather than a test
for the door that was fixed, in both provisioning states — one contract, five
places that can break it independently is the shape that rots one door at a
time. Reverse-checked: 17 of its 24 cases fail on the unfixed engine.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 12:40am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx(via packages/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx(via packages/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx(via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review August 12, 2026 01:08
@huangyiirene
huangyiirene added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 8b90d68Aug 12, 2026
26 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-7642-search-companion-projection-strip branch August 12, 2026 01:22
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The __search companion column, declared client-invisible, is echoed in every record body

2 participants

@huangyiirene@claude