feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@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

feat: parameterized computed fields in where / select / include / aggregate / groupBy - #2762

Merged
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Jul 28, 2026
Merged

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
ymc9 merged 14 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Follow-up to #2744 (per your review) — exposes parameterized @computed fields in the remaining read contexts, not just orderBy.

Stacked on #2744. This branch is based on feat/parameterized-computed-fields, so until that PR merges the diff here also includes its commit (ad7b6c32). The net-new work is the four commits listed below; I'll rebase to a clean diff against dev once #2744 lands.

Everything flows through the single channel #2744 added — fieldRef(..., computedArgs)computer(eb, ctx, args). Each context is the same recipe: input type + zod (reusing makeFieldArgsSchema) + a runtime seam that extracts args and forwards it.

Contexts added

  • where (and having, which reuses WhereInput) — args alongside the filter operators:
    db.productSite.findMany({where: {tagNameInCategory: {args: {categoryId: 5},contains: 'shoe'}},});
  • select / includeinclude: { field: { args } }, and select: { field: { args } } for free (because SelectInput = { …boolean } & IncludeInput):
    db.user.findMany({include: {recentPostCount: {args: { since }}}});
  • _count / _sum / _avg / _min / _max (in aggregate and groupBy) and count's select{ field: { args } } instead of the bare true.
  • groupByby — a keyed { field, args } entry:
    db.product.groupBy({by: [{field: 'priceTier',args: {threshold: 30}}],_count: {_all: true}});
  • nested include — a parameterized computed field on a related model, inlined with its args in both dialect JSON assemblers (SQLite + lateral-join).

Field selection (your note)

You suggested driving selection via include: { computedField: { args } } since these fields can't be auto-selected. Done — and because SelectInput intersects IncludeInput, the same entry also works under select. They stay excluded from default / auto-selection at every layer.

Commits

  1. where (+ having) + select / include
  2. _count / _sum / _avg / _min / _max (aggregate + groupBy) + count select
  3. groupByby (keyed shape)
  4. nested include + a Postgres groupBy fix

Limitation

Grouping by a computed field backed by a correlated subquery is subject to the database's own rules for correlated GROUP BY (Postgres rejects it; SQLite allows it) — the same constraint as any correlated GROUP BY expression. Row-local computed fields group fine on all dialects. A general fix would need a materialize-then-group subquery in group-by.ts — happy to add it if you'd like.

Testing

Extended tests/e2e/orm/client-api/computed-fields.test.ts with focused cases per context, each proving different args → different results. Green on SQLite and Postgres:

  • computed-fields.test.ts: 19 tests
  • full orm/client-api suite: 625 passed (SQLite) / 551 passed (Postgres)
  • the only failures anywhere are the pre-existing mysql-timezone tests, which need a live MySQL server (unrelated to this change)

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields across filtering, sorting, selection, inclusion, aggregation, and grouping.
    • Query-time arguments are now applied consistently to computed values, including nested relations.
    • Added validation requiring explicit arguments where computed fields need them.
    • Added support for defining parameter metadata for computed fields.
  • Bug Fixes

    • Prevented parameterized computed fields from appearing automatically when arguments are unavailable.
    • Improved cursor, aggregation, and group-by handling for computed fields.

evgenovalovand others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.
model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int): String? @computed
}
// implementation receives the args as a 3rd parameter
computedFields: {
ProductSite: {
tagNameInCategory: (eb, ctx, args) =>
eb.selectFrom('tag')
.innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
.whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
.where('tag.category_id', '=', args.categoryId)
.select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
},
},
// `args` is plain data, so this whole object can come from a client
db.productSite.findMany({
orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
});
This wires the feature end-to-end for `orderBy`:
- ZModel grammar: a field may declare a `(params): Type` signature; a validator
rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
stub signature, so the implementation type (`ComputedFieldsOptions`) and the
query input types derive the args type from a single source and can't drift.
The params are also emitted as `FieldDef.params` metadata for the runtime and
the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
computed field. Such fields require args, so they are excluded from default
selection, explicit `select`, and `where` (usable via `orderBy` for now);
`where`/`select` support are natural follow-ups using the same mechanism.
Refs zenstackhq#2743
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
`field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
`orderBy` (its sort key is not a real column), matching the existing
relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
(those names aren't in scope in the generated schema) — same convention as
computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
`_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
`computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
`@computed`) + e2e regression asserting the excluded contexts reject it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lude
Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.
- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
— args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
the operator components from the field's existing filter schema and requires
`args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
surface the field as its scalar return type (new additive term in ModelResult's
include branch; ModelSelectResult already mapped it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oupBy on Postgres
- nested include/select: a parameterized computed field on a related model is now inlined
with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
on both SQLite and Postgres (19 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Jul 20, 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

Parameterized computed fields now accept typed query-time arguments across filtering, ordering, selection, inclusion, aggregation, and grouping. Dialects and CRUD operations propagate those arguments, while implicit selection and unsupported query forms are restricted. End-to-end tests cover the updated behavior.

Changes

Parameterized computed fields

Layer / File(s)Summary
Field and query contracts
packages/schema/src/schema.ts, packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Field parameters are declared and query schemas/types require { args } for parameterized computed fields across CRUD contexts.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/base-dialect.ts, packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filtering, ordering, scalar selection, relation materialization, and computed-field evaluation; parameterized fields are excluded from implicit selection.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts
Aggregations and grouped queries use argument-bearing computed expressions and reject conflicting arguments for the same materialized field.
End-to-end query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
Tests cover ordering, filtering, selection, inclusion, aggregation, grouping, nested queries, and invalid unsupported forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2744 — Changes the same CRUD type definitions and runtime computed-field argument forwarding surfaces.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title accurately summarizes the main change: adding support for parameterized computed fields across query and aggregation paths.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ 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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts (1)

215-216: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Pass query-time args to fieldRef for parameterized computed fields during relation sort.

When sorting scalar fields natively within an array aggregation (e.g., PostgreSQL lateral join json_agg), the extraction of the sorting expression must forward computedArgs if the field is a parameterized computed field.
Currently, this.fieldRef is invoked without computedArgs. This will evaluate the field's expression without its query-time parameters, leading to functionally incorrect queries or runtime crashes during array aggregations.

Extract args from value to ensure parity with the logic in applyScalarOrderBy (from base-dialect.ts).

🐛 Proposed fix
- const expr = this.fieldRef(model, field, modelAlias);+ const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;+ const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
let sort = typeof value === 'string' ? value : value.sort;
🤖 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/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 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/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.
---
Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 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: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

Reviewing files that changed from the base of the PR and between 7767140 and fffbe77.

⛔ Files ignored due to path filters (2)
  • packages/language/src/generated/ast.ts is excluded by !**/generated/**
  • packages/language/src/generated/grammar.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/language/test/parameterized-computed-field.test.ts
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.ts
  • packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts
  • packages/orm/src/client/crud/dialects/sqlite.ts
  • packages/orm/src/client/crud/operations/aggregate.ts
  • packages/orm/src/client/crud/operations/base.ts
  • packages/orm/src/client/crud/operations/count.ts
  • packages/orm/src/client/crud/operations/group-by.ts
  • packages/orm/src/client/zod/factory.ts
  • packages/schema/src/schema.ts
  • packages/sdk/src/ts-schema-generator.ts
  • tests/e2e/orm/client-api/computed-fields.test.ts

Comment threadpackages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment threadpackages/schema/src/schema.ts
evgenovalovand others added 5 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
same field with different `args` in one query would silently use whichever `args`
was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).
Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline "exclude parameterized computed field" mapped-type
exclusions with the named NonParamComputedNonRelationFields type in
SelectInput and FlatModelResult, per PR review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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>
@evgenovalov

evgenovalov commented Jul 24, 2026

Copy link
Copy Markdown
ContributorAuthor

@ymc9 I fixed the conflicts and now it can be merged complementary to the merged #2744

Also docs: zenstackhq/zenstack-docs#630

Thank you!

@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.

Hey @evgenovalov ,

Thanks for getting this done. Quite a big change and it looks awesome! Left a minor comment there.

Comment threadpackages/orm/src/client/crud/operations/group-by.ts Outdated
Only parameterized computed fields carry `args` in `by` entries, and those
always take the `sql.ref` branch, so passing `e.args` to `fieldRef` in the
non-computed branch was dead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Thanks for the quick follow up @evgenovalov . I'm merging it and v3.9 will be published soon.

@ymc9
ymc9 merged commit 78e741e into zenstackhq:devJul 28, 2026
11 checks passed
ymc9 pushed a commit to zenstackhq/zenstack-docs that referenced this pull request Aug 3, 2026
* docs: document parameterized computed fields
Add a "Parameterized Computed Fields" section to the ORM computed-fields page:
the ZModel parameter syntax, the 3-argument implementation signature, how to
supply `args` across orderBy / where / select / include / aggregate / groupBy,
and a note on grouping by a correlated-subquery computed field.
Documents zenstackhq/zenstack#2744 and zenstackhq/zenstack#2762.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add AvailableSince v3.9.0 marker to parameterized computed fields
New-feature docs must carry an <AvailableSince> release marker (review feedback).
Imported from `../_components/AvailableSince` to match this file's other component
imports (the suggested `./_components` path is incorrect — the component lives at
docs/_components/).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: evgenovalov <evgenii@flowlity.com>
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.

2 participants

@evgenovalov@ymc9