Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add opt-in deep exact query argument checking - #2720

Merged
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6
Jul 22, 2026
Merged

Add opt-in deep exact query argument checking#2720
ymc9 merged 4 commits into
zenstackhq:devfrom
olup:codex/fix-select-subset-ts6

Conversation

@olup

@olupolup commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes#2719 by adding an opt-in exactness check for inferred query arguments:

constdb=newZenStackClient(schema,{// ...typing: {exactQueryArgs: true},});

With the option enabled, unknown properties are rejected recursively in inline and hoisted arguments across read, create, update, delete, count, aggregate, groupBy, and exists operations. Covered structures include nested relations, arrays, where, select, include, orderBy, cursor, connect, connectOrCreate, and nested mutation branches.

Design

NoExtraProperties<T, Shape> walks only the keys present in the argument inferred from user code. Expected input unions are consulted property by property, so the checker does not eagerly rebuild every possible schema branch. There is no arbitrary recursion-depth cap.

The check preserves property-local diagnostics and treats runtime leaf/open-value types such as Date, Decimal, Uint8Array, functions, and JSON-style open records appropriately. The empty finite-shape case also preserves a valid {} while continuing to reject supplied unknown keys.

The option remains disabled by default because the type-checking cost depends on schema and query complexity. There are no runtime client changes.

Performance

Measured on Node 26.5 using the same final e2e corpus with exactness off and on. Values are medians of three isolated sequential runs per state.

CompilerMetricOverhead
TypeScript 6.0.3Memory+17,047K (+1.80%)
TypeScript 6.0.3Type instantiations+31,921 (+1.34%)
TypeScript 6.0.3Check time+0.53s (+10.19%)
TS7 native previewMemory+7,604K (+1.18%)
TS7 native previewType instantiations+25,445 (+0.81%)
TS7 native previewAllocations+63,382 (+0.61%)
TS7 native previewCheck time+0.215s (+27.42%)

The relative TS7 timing increase is larger than the structural deltas, but the absolute median check-time increase is 0.215 seconds on this corpus. The cost is workload-dependent, which is why the feature remains opt-in.

Validation

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck (TypeScript 6.0.3)
  • pnpm exec tsgo -p tests/e2e/tsconfig.json --noEmit --pretty false (TS7 native preview)
  • pnpm --filter @zenstackhq/orm lint
  • Prettier check on all changed files

Summary by CodeRabbit

  • New Features

    • Added an opt-in “exact query arguments” mode (typing.exactQueryArgs) for stricter TypeScript validation.
    • Query argument types now enforce exact shapes (rejecting extra/unknown fields), including deeply nested relation payloads.
    • Updated selection/subset typing to support an options parameter while preserving existing compile-time safeguards (e.g., select vs include/omit).
  • Tests

    • Expanded type-level checks to confirm correct acceptance/rejection across read/write and aggregate/mutation operations in exact mode.

@coderabbitai

coderabbitaiBot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in recursive exact query-argument typing through QueryOptions, Subset, and SelectSubset, propagates the option across CRUD method signatures, and expands compile-time tests for invalid nested arguments.

Changes

Exact query argument typing

Layer / File(s)Summary
Strict argument type helpers
packages/orm/src/client/options.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/utils/type-utils.ts
Adds typing.exactQueryArgs, recursive NoExtraProperties, and conditional enforcement through StrictArgs while preserving existing selection conflict errors.
CRUD contract option propagation
packages/orm/src/client/contract.ts
Passes Options into Subset and SelectSubset across query, mutation, deletion, aggregation, grouping, existence, and return methods.
Exact-mode typecheck coverage
tests/e2e/orm/schemas/typing/typecheck.ts
Adds strict-client negative assertions for invalid nested read and write arguments, relation payloads, aggregations, grouping, and extra subset properties.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers:ymc9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title is concise and accurately describes the main change: opt-in deep exact query-argument checking.
Linked Issues check✅ PassedThe changes add opt-in exactQueryArgs support and tests that reject invalid direct client query arguments as required by #2719.
Out of Scope Changes check✅ PassedThe edits stay focused on exact query-argument typing, tests, and supporting type utilities/options with no unrelated features.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ymc9

ymc9 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for making the fix! I'm not sure if it's by design, but I believe the improved EPC check only works for top-level select/omit/include. Adding recursion will probably incur significant type-checking cost, so I think it's a valid trade-off. Looking good to me.

ymc9
ymc9 previously approved these changes Jun 25, 2026
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Great ! I'll have a quick look this morning on recursion/performance.

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 3816813 to f5fa064CompareJune 25, 2026 07:46
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produces by codex under human supervision)

I updated the branch to address the recursion/performance concern with a different approach.

Instead of using a fixed recursion depth, the new version only applies strict checking along the user-provided selection tree, and only for select, include, and omit keys. When a nested relation argument is reached, it checks the keys at that FindArgs level, but it does not recursively exact-check arbitrary sub-objects like where, orderBy, or cursor; those continue to be handled by their existing types. So the recursion is bounded by the shape of the user query, not by an arbitrary depth constant, and it stays focused on selection/include/omit exactness.

I also compared this with Drizzle's model. Drizzle's KnownKeysOnly is shallow, while the recursive behavior mainly comes from DBQueryConfig recursively typing relation with configs. A purely shallow KnownKeysOnly-style helper was not enough for ZenStack's FindArgs shape, so this patch keeps a small targeted recursive helper around selection keys only.

Quick local typechecking perf check:

  • Command: cd tests/e2e && /usr/bin/time -p npx tsc --noEmit --extendedDiagnostics --pretty false
  • Ran 3 times on the current PR baseline and 3 times on this update.

Average e2e results:

MetricBaselineUpdatedDelta
Types300,193314,965+4.9%
Instantiations2,475,5232,582,921+4.3%
Memory used~998 MB~1009 MB+1.2%
Check time10.64s10.69sroughly unchanged
Wall time15.69s15.55sroughly unchanged/noise

I also checked packages/orm directly with tsc --extendedDiagnostics; the structural metrics were essentially flat there (Instantiations: 730,826 -> 730,833).

Validation run locally:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as draft June 25, 2026 08:18
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

Checking a couple of details on the where queries before reopening this one

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 31aa89e to 371687eCompareJune 25, 2026 08:25
@olup

olup commented Jun 25, 2026

Copy link
Copy Markdown
ContributorAuthor

(message produced by codex under human supervision)

Small follow-up: I also checked the where/orderBy concern explicitly.

The selection recursion helper intentionally does not recursively walk arbitrary sub-objects like scalar filter payloads or sort payloads. However, where, orderBy, and cursor should still be checked against the queried model's existing argument types at the level where they appear.

I added explicit type tests for:

  • invalid root where field
  • invalid root orderBy field
  • invalid nested relation where field
  • invalid nested relation orderBy field
  • unsupported field in orderBy

Implementation-wise, the patch now applies shallow exactness to where, orderBy, and cursor at each query-args level, while keeping the deeper recursive exactness focused on select, include, and omit. It still does not recursively exact-check arbitrary filter/operator internals beyond the existing WhereInput/OrderBy types.

Validation rerun:

  • pnpm --filter @zenstackhq/orm build
  • pnpm --filter e2e test:typecheck
  • npx -p typescript@5.9.3 tsc --noEmit -p tsconfig.json --pretty false from tests/e2e

@olup
olup marked this pull request as ready for review June 25, 2026 08:29
@ymc9

ymc9 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the update. I've been thinking about the issue and the trade-offs. So ideally, we should have a full recursive EPC for the entire query args. However, this can incur high extra type-checking costs due to the typing complexity of fields like where, orderBy, data, etc. Having a special deep visit into select/include/omit can be beneficial for that path with a reasonable extra cost, however, it can also cause confusion why some part is deeply checked, while others are not, in a query args ...

I'm wondering if it's overall better to just have a shallow, strict check for all keys. Implementation is simpler and easier to reason, behavior is consistent, at the cost of losing deep EPC. However, since everything is strictly checked at runtime, and also for IDE experience, the language server's field recommendation is still fully contextual, maybe it's a reasonable trade-off?

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

Hey - I have been away for a bit.

To state back the context of the issue : I really beleive that the interesting thing about a typed ORM is letting you rework your schema with the confidence of your type system catching any mistake. What happens now in ts 6+ is that the argument of the query are not properly typed - meaning a change in your schema can lead to an invalid query failing at run time. To me this feels completely impossible to accpet.

Now should the argument typing be only on top level? I underdtand the tradeoff but from a user perspective I would ask why ? If I use narrow selects on relationship, and a property is gone, I would 100% expect zenstack to error at compile time.

I think prisma succeed in this way by generating types in codegen, but Drizzle managed it with type only.

If there is any way to achieve a light recursive typing of the orm query arguments we should absolutely do it, don't you think ?

My present proposal might not be it, but we should explore this - especially since ts7 is so much more efficient.

@olup

olup commented Jul 14, 2026

Copy link
Copy Markdown
ContributorAuthor

So here are my explorations:

The key observation is that the full schema/query-argument graph does not need to be recursively expanded. Recursion can follow only the keys present in the user-provided input, making it naturally bounded by that input without an arbitrary depth limit.

here are some metrics taken from running the test suite before and after my ongiong explorations (not what's in the PR RN)

MetricBaselineRecursive prototypeDelta
Types470,297560,182+19.1%
Instantiations3,119,4013,723,645+19.4%
Memory used644 MB727 MB+12.8%
Memory allocations10.32 M11.82 M+14.6%
Check time0.79 s1.27 s+0.48 s
Total time1.01 s1.73 s+0.72 s

This is the most complete version I could come up with.

I think its necessary in terms of feature but if the cost is too large, I ma told there is also the possibility to make it an option on the client. User could activate this if they want complete typing of queries in ts7.

example:

const db = new ZenStackClient(schema, {
dialect,
typing: {
exactQueryArgs: true,
},
});

@olup

olup commented Jul 15, 2026

Copy link
Copy Markdown
ContributorAuthor

The activation flag is working. I think this could make a interesting design - complicated project can get a higher overhead, but gaining strict checking in the process. The tradeoff could be the user choice. I'll update the PR with this design. Would it be interesting @ymc9 ?

@olup
olupforce-pushed the codex/fix-select-subset-ts6 branch from 371687e to add2903CompareJuly 15, 2026 19:25
@olupolup changed the title Fix strict select subset typing under TS6Add opt-in deep exact query argument checkingJul 15, 2026
@coderabbitai

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/orm/src/utils/type-utils.ts (1)

99-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty Keys collapses the whole object to never, even when T is also empty.

When Shape has zero keys, _NoExtraObject returns a bare never for the whole type instead of a mapped object. This is stricter than necessary: the mapped branch already rejects every key of T when Keys is never (since K extends Keys is false for every K), but returning bare never also rejects the vacuous case where T itself has no keys (T = {}), because never intersected into Subset/SelectSubset collapses the entire args type to never. Dropping the special case simplifies the code and fixes this over-rejection.

♻️ Proposed simplification
-type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = [Keys] extends [never]- ? never- : string extends Keys- ? unknown- : {- [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;- };+type _NoExtraObject<T extends object, Shape, Keys extends PropertyKey = _ObjectKeys<Shape>> = string extends Keys+ ? unknown+ : {+ [K in keyof T]: K extends Keys ? NoExtraProperties<T[K], _ObjectValue<Shape, K>> : never;+ };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/orm/src/utils/type-utils.ts` around lines 99 - 105, Update the
_NoExtraObject type to remove the special-case branch that returns never when
Keys extends never. Always use the mapped-object branch so an empty T remains
valid while any keys in T are still rejected by the K extends Keys condition;
preserve the existing string-key handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/options.ts`:
- Around line 142-152: Update the JSDoc for typing.exactQueryArgs in the options
definition to describe its current limited coverage: it applies to create,
update, and upsert argument shapes, rather than recursively rejecting unknown
properties across all query arguments. Do not imply enforcement for read,
delete, or aggregate operations until those overloads propagate Options.
---
Nitpick comments:
In `@packages/orm/src/utils/type-utils.ts`:
- Around line 99-105: Update the _NoExtraObject type to remove the special-case
branch that returns never when Keys extends never. Always use the mapped-object
branch so an empty T remains valid while any keys in T are still rejected by the
K extends Keys condition; preserve the existing string-key handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e08d5665-02ff-4bcf-8e4b-d1bdb366b47f

📥 Commits

Reviewing files that changed from the base of the PR and between 371687e and add2903.

📒 Files selected for processing (5)
  • packages/orm/src/client/contract.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/options.ts
  • packages/orm/src/utils/type-utils.ts
  • tests/e2e/orm/schemas/typing/typecheck.ts

Comment threadpackages/orm/src/client/options.ts
@ymc9

ymc9 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Hi @olup , thanks for the follow up. I agree that opt-in is a good idea. Could you check the comment from @coderabbitai? Currently the option is only wired to the write APIs but not read. After including read path for recursive epc check, my observation is about 30% increase in type instantiation and memory usage (TS 6). Not too bad but for sure justifies the default-false opt-in.

@coderabbitai

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@ymc9 Agreed. The current diff propagates Options only through the write-method constraints, so typing.exactQueryArgs does not yet consistently cover read calls. The read API signatures should thread the same option into their Subset/SelectSubset constraints.

Your TS 6 measurement—roughly +30% instantiations and memory once read-path recursive EPC is included—makes the default-false opt-in especially important. That preserves existing performance for most users while allowing projects that prioritize schema-refactor safety to explicitly choose stricter validation.

🐇✅


✏️ Learnings added
Learnt from: ymc9
URL: https://github.com/zenstackhq/zenstack/pull/2720
Timestamp: 2026-07-19T03:18:43.560Z
Learning: In ZenStack ORM client typing, the `typing.exactQueryArgs` strict recursive excess-property-checking option must be propagated to both read and write CRUD API argument constraints; wiring it only to write methods leaves direct read queries unprotected. Applying recursive EPC to read paths can increase TypeScript 6 type instantiations and memory by about 30%, so this feature should remain opt-in and default to false.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Of course - I wanted your opinion first before taking more attention on this. Will work a proper pr in the following days

@olup

olup commented Jul 19, 2026

Copy link
Copy Markdown
ContributorAuthor

The read path and the remaining delete and analytical operations are now wired to the recursive guard. The empty expected-shape edge case reported by CodeRabbit is fixed as well.

A fresh three-run A/B on the identical final corpus gives:

CompilerMemoryInstantiationsCheck time
TypeScript 6.0.3+17,047K (+1.80%)+31,921 (+1.34%)+0.53s (+10.19%)
TS7 native preview+7,604K (+1.18%)+25,445 (+0.81%)+0.215s (+27.42%)

The current input-key-driven implementation therefore does not reproduce the approximate 30% TS6 memory/instantiation increase. TS7 timing rises more in relative terms, but by 0.215 seconds on this corpus. The option remains default-false because workload shape can change the cost.

@ymc9ymc9 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I'm merging it and will make it part of v3.9. Will also publish a canary build first. Thanks @olup !

@ymc9
ymc9 merged commit b83efad into zenstackhq:devJul 22, 2026
9 checks passed
evgenovalov pushed a commit to evgenovalov/zenstack that referenced this pull request Jul 24, 2026
…ds-read-contexts
Brings in upstream's latest dev, including its own parameterized computed
fields implementation (zenstackhq#2744, orderBy-only) and the opt-in deep exact query
argument checking (zenstackhq#2720, exactQueryArgs), plus other fixes.
Conflict resolution: this branch's parameterized-computed-field support is a
strict superset of upstream's (it works in where/having, select/include, the
aggregate inputs, and groupBy `by` via query-time `args`, not just orderBy),
so all conflicts were resolved in favor of this branch's design while keeping
all of upstream's unrelated features.
Resolved conflicts:
- packages/schema/src/schema.ts — kept the broader `params` doc comment
- packages/orm/src/client/crud-types.ts — kept args-based support in
WhereInput, CountAggregateInput, MinMaxInput, and GroupByArgs `by`
- packages/orm/src/client/zod/factory.ts — kept the runtime `args` schemas
and addComputedArgsToFilter helper
- tests/e2e/orm/client-api/computed-fields.test.ts — kept the expanded tests;
dropped upstream assertions that now-supported contexts are rejected
Fixed silent (marker-free) semantic merge issues where upstream's exclusion
guards were auto-combined with this branch's args-based support:
- crud-types NumericFields no longer excludes parameterized computed fields
(so SumAvgInput's args branch is reachable)
- GroupByArgs `by` retains the single plain-field-name option
- factory.makeWhereSchema no longer early-`continue`s on parameterized
computed fields (so they reach addComputedArgsToFilter)
Verified: orm + e2e typecheck clean; computed-fields suite 19/19; full
client-api suite 625 passed (only pre-existing MySQL-only timezone tests fail
for lack of a local MySQL server).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tsgo accepts invalid select keys on ZenStack client calls (with repro repo + fixing PR)

2 participants

@olup@ymc9