Skip to content

fix(metadata,objectql): drop the redundant object key from driver queries and the casts carrying it (#6231) - #7182

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-6231-driver-query-redundant-object
Aug 10, 2026
Merged

fix(metadata,objectql): drop the redundant object key from driver queries and the casts carrying it (#6231)#7182
os-zhuang merged 2 commits into
mainfrom
claude/issue-6231-driver-query-redundant-object

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #6231

What this is

DriverQuery (Omit< QueryAST, 'object' >) has existed since #6076 and five drivers followed in #6075, but five call sites stayed as they were — because they sat behind a cast the compiler could not see through.

The redundant key was never the expensive half. git grep 'query\.object' -- 'packages/drivers/*/src' is still zero on main: no driver reads it, so the key is inert and nothing disagrees at runtime. The cast is the cost. as any on a query argument does not suppress one key — it switches off checking for where / orderBy / fields in the same literal. That is the account #5181's changeset opened (cloud#1053 measured 20 such sites; cloud#1030's $like, an operator the filter dialect does not have, survived compilation and reached the runtime through exactly this hole). DatabaseLoader is the metadata main read path, so it was the worst place to be running unchecked.

The five sites, as found

Located by content, not by line — engine.ts had drifted ~800 lines since the card was filed. All five were exactly as the drivers seat's handoff described them.

Site As found on origin/main @ 3e8e669c0
packages/metadata/src/loaders/database-loader.ts:232 this.driver!.find(table, { object: table, ...query } as any)
packages/metadata/src/loaders/database-loader.ts:239 this.driver!.findOne(table, { object: table, ...query } as any)
packages/metadata/src/loaders/database-loader.ts:246 this.driver!.count(table, { object: table, ...query } as any)
packages/objectql/src/engine.ts:4135 secretDriver.find('sys_secret', { object: 'sys_secret', where: { id } } as QueryAST)
packages/objectql/src/lifecycle/lifecycle-service.ts:791 driver.count(obj.name, { object: obj.name })

File-by-file, including the declared cross-surface touches

packages/metadata/src/loaders/database-loader.ts (this card's own surface) — the three read helpers _find / _findOne / _count declared query: Record< string, unknown >, which is why a cast was needed at all. They now declare query: DriverQuery and hand it to the driver unchanged and uncast. That re-checks where / orderBy / fields at all nine of their call sites, not just the three edited lines.

packages/objectql/src/engine.tsdeclared cross-surface touch (domain:engine-core), authorized by the PM on this card. One line: the sys_secret read in resolveSecret. The as QueryAST existed only to satisfy the AST's then-required object; with the key gone the cast is gone too. #5574, the in-flight engine-core claim on this file, closed completed on 2026-08-08.

packages/objectql/src/lifecycle/lifecycle-service.tsdeclared cross-surface touch (domain:engine-core). This site carried no cast; the redundant key was admitted by a hand-written driver shape whose query was Record< string, unknown > — which would equally have admitted a where the dialect does not have. Following the file's existing idiom (ReclaimCapableDriver, RotationCapableDriver), that inline shape becomes a named CountCapableDriver typed with DriverQuery, and the call passes argument one only.

Tests — a pin at each of the three sites (details below).

scripts/query-options-erasure-baseline.jsonnot in the declared surface; declaring it here. Removing the three driver-branch casts lowered that file's erasure count 6 → 3, and the #4918 ratchet requires the baseline committed (ratchet DOWN: run --update and commit). One line changed, mine only. Caught locally; it would otherwise have been a red ESLint job.

.changeset/driver-query-redundant-object-callers.md — patch/patch.

Not touched, as instructed: packages/metadata/src/plugin.ts, repository.ts (#7000), packages/metadata-protocol/src/protocol.ts (#6992), packages/spec (#6298), content/docs/releases/.

Pattern or hand-edits?

Both, in that order — the pattern as a finder, hand-edits as the fix.

The card's suggested back-reference pattern works and I used it, but only to enumerate and to prove equivalence. It is not usable as a rewrite here because the five sites are not uniform: three spread a variable ({ object: table, ...query }), one is a literal with a where, and one has no cast at all and needed an interface changed rather than a call. Five careful edits, one pattern to prove the set was complete:

git grep -nP "\.(find|findOne|count|updateMany|deleteMany|explain)\(\s*('[^']*'|\"[^\"]*\"|[A-Za-z_\$][\w.\$!]*)\s*,\s*\{\s*object:\s*\2\s*[,}]" -- packages apps

The back-reference is what makes equivalence a property of the pattern rather than of my reading: it matches only when the value is character-for-character the first argument.

Landmine and protected keys — verified intact, and verified by the pattern. packages/objectql/src/engine-unknown-option.test.ts:183 still reads engine.find('task', { object: 'person' } as any) — untouched. The pattern does not match it, because 'person' is not 'task'; that is precisely the reason to use a back-reference instead of grepping for object:. Both other keys are equally unmatched and untouched: object inside an expand entry (:191, :198) names the related object, and syncSchemasBatch([{ object, schema }])'s object is genuinely read.

The pattern also found that the card's measurement was source-only. Ten more sites of the identical shape live in driver test files (driver-mongodb ×9, driver-sql ×1) — one of them spelling the cast as never, which an as any grep misses. Those are the drivers seat's surface and not in this card's authorized file list, so they are filed, not fixed: #7177.

What the removed casts exposed (the interesting part)

Assumption 3 of the dispatch was right, and it landed somewhere more useful than expected.

The driver calls — the five sites — compile with no cast at all, and exposed zero new errors. Record< string, unknown > from baseFilter satisfies FilterCondition, and every call shape (where, where+fields, where+orderBy+limit+offset) satisfies DriverQuery. So the query bag at those nine call sites was genuinely well-typed all along; the cast was buying nothing and hiding everything.

I also tried to remove the three engine-branch casts in the same helpers (this.engine.find(table, query as any)). Those are not among the five sites and were pre-existing on main. They do not compile, and the reason is a real pre-existing spec divergence:

src/loaders/database-loader.ts(230,38): error TS2345: Argument of type 'DriverQuery' is not
assignable to parameter of type '{ ... }'.
  Types of property 'search' are incompatible.

BaseQuerySchema.search is z.union([ z.string(), FullTextSearchSchema ]), and its doc says the bare string is the canonical Tier-1 contract per ADR-0061 D1. Its sibling EngineQueryOptionsSchema.search is FullTextSearchSchema.optional() — structured form only. So DriverQuery is not assignable to EngineQueryOptionsParsed, purely because of search. The runtime serves the string, and objectql's own tests prove both halves — engine.findOne('crm_account', { search: 'Two' } as any) appears five times in engine-findone-contract.test.ts, canonical spelling, cast to compile.

Fixing that means editing packages/spec, which this card's dispatch declared a STOP boundary. So per the instruction not to paper over a surfaced error with a narrower cast, the three engine-branch casts are left byte-identical to main, with a comment naming the cause and the tracking issue. Filed as #7178.

Tests

A pin at each of the three sites, asserting the shape the driver is actually handed — not that the source text lacks a key:

  • packages/metadata/src/loaders/database-loader.test.ts — wraps the mock driver's find/findOne/count, drives every read path the loader owns (load, loadMany, exists, list, stat, save), asserts the object name arrives as argument one and the AST has no object.
  • packages/objectql/src/lifecycle/lifecycle-service.test.ts — the governance counter is called with argument one only.
  • packages/objectql/src/secret-fields.test.tsresolveSecret's sys_secret read carries where and no object. (buildEngine now also returns its stub driver so the AST can be observed.)

I did not add type-level pins for DriverQuery itself: packages/spec/src/contracts/data-driver.test.ts:203 already carries the @ts-expect-error that a redundant object is rejected (#5181/#6076), inside a package tsc really compiles. Duplicating it here would add a second, weaker copy.

Reverse verification

Direction predicted before running: reverting each source file to my pinned base restores both the key and the cast, so all three pins should go red. Reverted via a saved patch and git checkout --, never git stash. Base is my own pinned 3e8e669c0, not a moving origin/main.

Predicted red, went red — all three, each with the predicted cause:

× never restates the object name inside the query AST
  AssertionError: expected { object: 'sys_metadata', …(1) } to not have property "object"
× counts by argument one only — the query never restates the object name
  AssertionError: expected { object: 'sys_job_run' } to not have property "object"
× resolveSecret reads sys_secret by argument one — the AST never restates the object name
  AssertionError: expected { object: 'sys_secret', …(1) } to not have property "object"

Second prediction — is the fix self-defending, or only test-pinned? Re-added object: 'sys_secret' to the engine.ts call without a cast. Predicted a compile error; got exactly one:

src/engine.ts(4135,59): error TS2353: Object literal may only specify known properties,
and 'object' does not exist in type 'DriverQuery'.

So a re-add is caught by the CI-gated TypeScript Type Check job, not merely by my tests. Only a deliberate new cast could re-open the hole, and the three pins catch that.

Guards, not evidence (green in both directions, reported as such): the metadata suite (589) and full objectql suite (2851) pass on both sides of the revert. They cannot go red on this change, because the removed key was inert on every path — which is the card's own premise. Their value here is confirming no behaviour moved, and that is all I claim for them.

Left unmeasured, deliberately: the ratcheted @objectstack/metadata DEBT count could not be re-measured through check:type-check-debt, which refuses to run without the whole workspace closure built (#6376: measuring from a partial closure "would silently measure a DIFFERENT WORLD"). I measured the package directly instead, identically on both sides: 89 before, 89 after — debt-neutral, and below its recorded 92. So this change restores checking at nine call sites without adding a single type error.

Verification

Check Result
pnpm --filter @objectstack/objectql typecheck clean (tsc --noEmit, no output)
pnpm --filter @objectstack/objectql test 165 files, 2851 passed
pnpm --filter @objectstack/metadata test 28 files, 589 passed
pnpm --filter '@objectstack/metadata' --filter '@objectstack/objectql' build Build success (dts included)
pnpm lint clean
pnpm check:query-options-erasure passes after the baseline ratchet-down (6 → 3)
pnpm check:slot-lookup holds, none new
pnpm check:verify-stand-in OK
pnpm check:nul-bytes OK, 6593 files

@objectstack/metadata has no typecheck script (DEBT ledger), so its type errors are not gated by the TypeScript Type Check job — but it is gated by the build: tsup's dts step compiles it, and it is a dependency of objectql, so the first version of this change failed the build rather than sailing through. That is how the search divergence above was found.

Out of scope, filed not fixed

🤖 Generated with Claude Code

https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw


Generated by Claude Code

claude added 2 commits August 10, 2026 01:54
…ueries and the casts carrying it (#6231)

`DriverQuery` (`Omit<QueryAST, 'object'>`) exists since #6076 and five drivers
followed in #6075, but five call sites stayed as they were because they sat
behind a cast the compiler could not see through.

The key itself is inert -- `git grep 'query\.object' -- 'packages/drivers/*/src'`
is zero, so no driver reads it. The cast was the cost: `as any` on a query
argument switches off checking for `where` / `orderBy` / `fields` too, the
account #5181's changeset opened (cloud#1030's `$like` reached runtime through
exactly this hole).

- metadata `DatabaseLoader._find/._findOne/._count`: declare `query: DriverQuery`
  and pass it to the driver unchanged and uncast (9 call sites re-checked).
  The ENGINE branch keeps its `as any` and says why in a comment: it is blocked
  by a spec-level divergence, not vestigial.
- objectql `resolveSecret`: the `sys_secret` read loses both the key and the
  `as QueryAST` that existed only to satisfy it.
- objectql `LifecycleService` governance counter: passes argument one only, and
  its hand-written driver shape becomes the named `CountCapableDriver` typed
  with `DriverQuery` instead of `Record<string, unknown>`.

Pins added at all three sites assert the shape the driver is actually handed;
re-adding the key without a cast is now `TS2353`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6bLax4KMrSfnE1ydFU8Dw
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 1:58am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata, @objectstack/objectql.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata, @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/cluster.mdx (via packages/metadata)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata, @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/metadata, @objectstack/objectql)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

3 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/v12.mdx (via @objectstack/metadata)
  • content/docs/releases/v9.mdx (via @objectstack/metadata)

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.

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM step-7 review (domain:metadata seat, session session_01W6bLax4KMrSfnE1ydFU8Dw). Marking ready + auto-merge (SQUASH).

CI on 16859b84c: 25 check runs, 0 pending, 0 non-green — ESLint and TypeScript Type Check both completed: success, plus Build Core, Test Core 1-3/3, Dogfood gates, Temporal Conformance, Check Changeset; 2 skipped by path filter.

The fix is deeper than the card asked for, in the right direction. The card reads as "delete a redundant key at five sites". What actually made the casts unnecessary was retyping the three read helpers in database-loader.ts from Record<string, unknown> to DriverQuery — which re-enables where/orderBy/fields checking at all nine of their call sites, not just the three edited lines. That is the account #5181 opened being closed properly rather than at the three places someone happened to look.

My assumption 1 was handled exactly as intended. I offered the back-reference pattern (PR #6210's method) as a starting point and said five careful edits beat one clever regex if the sites are not uniform. They are not — one site had no cast at all and needed an interface changed — so the pattern was used as a finder and equivalence-prover, never as a rewriter, and the five edits are by hand. Using the clever tool for the part it is good at and not for the part it is not is the judgement I wanted.

The self-defence check is the best thing in this report. Beyond "does it go red without the fix" (3/3, with the exact predicted causes), the dev ran a second prediction: re-add the key without a cast and predict a compile error. Measured TS2353: Object literal may only specify known properties, and 'object' does not exist in type 'DriverQuery'. So a future re-introduction is caught by the CI TypeScript job, not merely by these tests. That is the difference between fixing today's instances and closing the shape.

The STOP boundary held under pressure, and that matters more than the extra cleanup would have. Three adjacent engine-branch casts (not among the five sites) turned out to be blocked by a real pre-existing divergence: EngineQueryOptionsSchema.search accepts only the structured form while QueryAST.search, ADR-0061 D1, and the runtime all accept the canonical bare string. Removing those casts requires touching packages/spec, which was this card's declared STOP. They were left byte-identical to main with a comment naming the cause, and the divergence was filed as #7178 rather than fixed. Leaving three casts visibly in place with a reason is worth more than a PR that quietly widened past its boundary — and #7178 is the same #5181 account one layer up, so it is a real card, not a consolation.

Two more results worth having:

Honest non-measurements: the ratcheted @objectstack/metadata DEBT count via check:type-check-debt was left unmeasured because that gate refuses to run without the full workspace closure built (#6376) — measured the package directly on both sides instead (89 before, 89 after, against a recorded 92: debt-neutral). And both full suites are reported as guards, not evidence: they are green in both directions and cannot go red on this change, because the key is inert on every path — which is the card's own premise.

Landmine and forbidden keys confirmed intact. The engine-unknown-option.test.ts deliberately-unequal rejection site survives, as do expand's object and syncSchemasBatch's object.

Cross-seat note: #7178 lands in packages/spec and is being flagged to the spec-surface seat rather than acted on here.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 02:13
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 55da611 Aug 10, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6231-driver-query-redundant-object branch August 10, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants