feat: support parameterized computed fields - #2744

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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

Merged
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields
Jul 23, 2026
Merged

feat: support parameterized computed fields#2744
ymc9 merged 7 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields

Conversation

@evgenovalov

@evgenovalovevgenovalov commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Update (review addressed): dropped the : before the return type so a parameterized field reads like a regular one (name(params) Type). Also closed a consistency gap the feature introduced — parameterized computed fields are now excluded at the zod and aggregate-input layers too (not just the TS types), so _sum/_count/by/distinct no longer typecheck-then-crash. Follow-up PR extending this capability to where/select/include/aggregate/groupBy: #2762.

Feature request: #2743

Lets a @computed field declare typed parameters, with the arguments supplied at query time
wherever the field is used. This first cut wires it end-to-end for orderBy.

model User {
id Int @id
posts Post[]
recentPostCount(since: DateTime) Int @computed
}
// the implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,computedFields: {User: {recentPostCount: (eb,ctx,args)=>eb.selectFrom('Post').whereRef('Post.authorId','=',sql.ref(`${ctx.modelAlias}.id`)).where('Post.createdAt','>=',args.since).select(({ fn })=>fn.countAll().as('cnt')),},},});// `args` is plain data, so the whole orderBy can come from a clientawaitdb.user.findMany({orderBy: {recentPostCount: {args: { since },sort: 'desc'}},});

The motivating case from #2743 — sort products by their tag name in a chosen category:

model ProductSite {
id Int @id
tags ProductTag[]
tagNameInCategory(categoryId: Int) String? @computed
}
// implementation receives the args as a 3rd parameter (after eb + context)constdb=newZenStackClient(schema,{
dialect,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 the whole orderBy can come from a clientawaitdb.productSite.findMany({orderBy: {tagNameInCategory: {args: {categoryId: 5},sort: 'asc',nulls: 'last'}},});

Why

Closes the gap raised in #2743. A @computed field is evaluated in SQL and is usable in
orderBy/where/select, but it takes no arguments — so you can't express a DB-side sort
that depends on a runtime value (e.g. "sort products by their tag name in a chosen category"). The
only workarounds today ($qb/raw SQL, or an onKyselyQuery plugin) give up access policies,
select-narrowed result types, and/or single-query execution.

Because the arguments are plain data (not a function like where.$expr), they serialize over
the wire, so a frontend can drive the sort through the auto-CRUD API while the query stays one
policy-checked, typed statement.

How

  • ZModel grammar (zmodel.langium): a DataField may declare a (params): Type signature
    (reusing the existing FunctionParam shape). A validator rejects parameters on non-@computed
    fields. Langium AST/grammar regenerated.
  • Schema codegen (ts-schema-generator.ts): the declared params flow into the generated
    computed-field stub signature, so the implementation type (ComputedFieldsOptions) and the query
    input types both derive the args type from a single source and can't drift. Params are also
    emitted as FieldDef.params metadata (shape mirrors ProcedureParam) for the runtime + zod.
  • Runtime (base-dialect.ts): query-time args are forwarded to the implementation as a third
    argument through the single fieldRef chokepoint; extracted from the orderBy value in
    applyScalarOrderBy.
  • Types & zod (crud-types.ts, zod/factory.ts): orderBy accepts
    { args, sort, nulls? } for a parameterized computed field. Since these fields require args,
    they're excluded from default selection (also at runtime, so a plain findMany() is safe),
    explicit select, and where.

Scope / follow-ups

Intentionally scoped to orderBy (the motivating use case). where and select with args are
natural extensions on the same mechanism (the fieldRef chokepoint already forwards args; the
input/result types would lift the same exclusions) and can follow in a separate PR.

Testing

  • Two new e2e tests in tests/e2e/orm/client-api/computed-fields.test.ts — one with an Int
    param, one with a DateTime param (recentPostCount) — each verifying that different args
    produce different orderings
    (proving the arg reaches the SQL), that ascending/descending
    behave, and that the field is not auto-returned.
  • Full suites green locally: @zenstackhq/language (84), orm client-api e2e (618 passed; the only
    failures are the mysql-timezone tests, which need a live MySQL server unavailable in my sandbox
    and are unrelated to this change). No type errors reported by the type-tests.
  • Tested in a real app on a large database (not just the SQLite test fixtures). I built this branch as local tarballs, linked it in with pnpm overrides, and added the [Feature request]: parameterized computed fields — accept arguments in orderBy/where/select #2743 field for real — tagNameInCategory(categoryId: Int): String?, which combines a row's tag names in the given category. Then I sorted a list by it, with the orderBy sent from the frontend, over a table of ~9.5k rows where the chosen category only tags ~150 of them:
    • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
    • the list still showed all ~9.5k rows — it sorts, it doesn't filter (the count with the same where didn't change);
    • I double-checked the order against a separate SQL query;
    • it ran as one query, with access policies and select narrowing still applied (no raw SQL).

Checklist

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added support for parameterized computed fields, including schema parsing and generation of query-time argument metadata.
    • Enabled parameterized computed fields in orderBy using { args, sort, nulls? }, with validated args.
  • Bug Fixes

    • Enforced that only @computed fields can declare parameters.
    • Prevented parameterized computed fields from being used in contexts that don’t support args (e.g., filtering, distinct/omit, and aggregation/groupBy).
  • Tests

    • Added e2e coverage verifying orderBy behavior with varying args and ensuring cursor pagination can’t combine with these sorts.

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

coderabbitaiBot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds support for parameterized @computed fields across parsing, validation, schema and TypeScript generation, CRUD typing, Zod query validation, runtime ordering, and end-to-end tests.

Changes

Parameterized Computed Fields

Layer / File(s)Summary
Grammar and validator changes
packages/language/src/zmodel.langium, packages/language/src/validators/datamodel-validator.ts, packages/language/test/parameterized-computed-field.test.ts
DataField accepts parameter lists, parameters reuse function parameter types, and non-@computed fields with parameters are rejected and tested.
Schema type and TS codegen for params
packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
FieldDef and generated schema metadata include computed-field parameters, while generated computed-field stubs emit typed args.
CRUD type-level exclusions and OrderBy shape
packages/orm/src/client/crud-types.ts
Parameterized computed fields receive an args-bearing OrderBy shape and are excluded from select, where, distinct, aggregation, and groupBy inputs that cannot provide args.
Zod query-shape validation
packages/orm/src/client/zod/factory.ts
OrderBy args are validated against declared parameter metadata, while unsupported query contexts omit parameterized computed fields.
Runtime dialect: args forwarding and auto-select skip
packages/orm/src/client/crud/dialects/base-dialect.ts
OrderBy extracts and forwards computed args, cursor pagination rejects these order keys, and automatic field selection skips parameterized computed fields.
Parameterized computed field query coverage
tests/e2e/orm/client-api/computed-fields.test.ts
End-to-end tests cover numeric and date-based ordering, sort direction, default selection, cursor rejection, and unsupported query contexts.

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

Possibly related issues

Possibly related PRs

  • zenstackhq/zenstack#2762: Directly overlaps with the grammar validation and ORM/Zod/type plumbing for parameterized computed-field arguments.
🚥 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 clearly matches the main change: adding support for parameterized computed fields.
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.

…unt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🧹 Nitpick comments (1)
packages/schema/src/schema.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this JSDoc with the current API surface.

The new CRUD types intentionally exclude parameterized computed fields from where and select, so this comment is advertising entry points that the type system now rejects. Tightening it to orderBy only would keep the exported contract accurate.

🤖 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/schema/src/schema.ts` around lines 85 - 90, Update the JSDoc on the
computed field params property in schema.ts so it matches the current API
surface: the comment should no longer mention where or select as supported
query-time entry points. Keep the documentation aligned with the exported types
by describing parameterized computed fields as usable in orderBy only, and
ensure the wording around ProcedureParam and params reflects that restriction.
🤖 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/language/src/zmodel.langium`:
- Around line 189-194: The optional parameter group in the DataField grammar
currently allows empty parentheses, so field(): String parses even when it
should not. Update the zmodel.langium rule around
DataField/RegularIDWithTypeNames to require at least one DataFieldParam when
parentheses are present, and keep empty () invalid for non-@computed fields.
Make sure the validator and grammar stay aligned by using the existing
DataFieldParam and DataFieldAttribute symbols to locate the affected rule.
In `@packages/orm/src/client/crud/dialects/base-dialect.ts`:
- Around line 1234-1238: The cursor path in buildCursorFilter is not handling
args-bearing computed orderBy entries correctly, so a cursor against
parameterized computed fields can compare the wrong sort direction and reference
a non-column field. Update buildCursorFilter to recognize the new { args, sort }
shape used by base-dialect.ts, extract the actual sort value, and block or
special-case cursor filtering for computed fields that require args so the
cursor subquery uses a valid field reference.
In `@packages/sdk/src/ts-schema-generator.ts`:
- Around line 647-656: The computed-field parameter type mapping in
mapFunctionParamTypeToTSType should not emit bare referenced names that may be
out of scope in schema.ts. Update the generator logic so referenced
FunctionParamType values are resolved to in-scope TypeScript types by importing
or qualifying the referenced symbol before returning it, and ensure
model/enum/type-def refs used by mapFunctionParamTypeToTSType are declared in
the generated file’s context rather than returning type.reference?.ref?.name
directly.
---
Nitpick comments:
In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the JSDoc on the computed field params property in
schema.ts so it matches the current API surface: the comment should no longer
mention where or select as supported query-time entry points. Keep the
documentation aligned with the exported types by describing parameterized
computed fields as usable in orderBy only, and ensure the wording around
ProcedureParam and params reflects that restriction.
🪄 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: 2c24331c-f8d9-4bcd-8c5d-0b84e466a410

📥 Commits

Reviewing files that changed from the base of the PR and between 53e9165 and 4f5a860.

⛔ 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 (8)
  • packages/language/src/validators/datamodel-validator.ts
  • packages/language/src/zmodel.langium
  • packages/orm/src/client/crud-types.ts
  • packages/orm/src/client/crud/dialects/base-dialect.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/language/src/zmodel.langium Outdated
Comment threadpackages/orm/src/client/crud/dialects/base-dialect.ts
Comment threadpackages/sdk/src/ts-schema-generator.ts Outdated
- 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>
@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Thanks for the review — all four addressed in ef09757:

  • Grammar (empty ()):DataField now requires at least one DataFieldParam when parentheses are present, so field(): String no longer parses. Grammar + AST regenerated.
  • Cursor + args-bearing computed orderBy (major): good catch. Extended the existing offendingKey guard (which already blocks _fuzzyRelevance/_ftsRelevance) to detect the { args, sort } shape and throw cursor pagination cannot be combined with "<field>" ordering. Added a test asserting this.
  • Referenced param types (major):mapFunctionParamTypeToTSType now falls back to unknown for model/enum/type-def references instead of emitting a bare, out-of-scope name — same convention mapFieldTypeToTSType uses for computed-field return types. Runtime zod still validates these values precisely (via makeScalarSchema, incl. enums).
  • FieldDef.params JSDoc (nitpick): tightened to mention orderBy only, matching the type-level exclusions from where/select.

@zenstackhq/language (84) and the computed-fields e2e (now 13, incl. the cursor-block assertion) pass with no type errors.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

Besides the tests, I tried this in a real app on a large database, to be sure the parameterized orderBy works outside the test fixtures.

I built this branch as local tarballs, linked it in with pnpm overrides, and added the exact #2743 field:

tagNameInCategory(categoryId: Int): String? @computed// combines a row's tag names in the chosen category

then sorted a list by it, with the orderBy sent straight from the frontend:

orderBy: {tagNameInCategory: {args: { categoryId },sort: 'asc'|'desc',nulls: 'last'}}

The table has ~9.5k rows, and the chosen category only tags ~150 of them — so it's easy to tell sorting from filtering. What I saw:

  • asc/desc ordered by the tag name, and nulls: 'last' put the untagged rows at the end;
  • the list still showed all ~9.5k rows — it sorts, it does not filter (the count with the same where didn't change);
  • the order matched a separate SQL query on the same data;
  • it ran as one query, with access policies and select narrowing still applied — no raw SQL.

So sending the whole orderBy from the client as plain data works on real data and real volume, not just the test fixtures.

@evgenovalov

Copy link
Copy Markdown
ContributorAuthor

@olup@ymc9 hi guys, would be nice to get your feedback on this PR/feature request

I see many use-cases already and would be happy to see the feature in the next version. Feature is not usage-specific but very wide in terms of flexibility.

Let me know if you see some remarks.

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.

Hi @evgenovalov ,

Thanks for working on this PR and my apologies for the delayed review.

I think it's a very powerful feature, and the PR looks good go me. To release it as a feature, it probably makes more sense to expose the capability in other common contexts like where, select, _count, etc., and other read operations like aggregate. Would you like to make a follow up PR? If so, this can be a candidate for the v3.9 release.

Field selection is a bit tricky because the old convention is scalar fields are selected by default, but this can't happen automatically for parameterized fields as args need to be provided. Maybe let it be controlled by include is an acceptable solution?

awaitdb.user.findMany({include: {computedField: {args: { ... }}}});

Comment threadpackages/language/src/zmodel.langium Outdated
DataField:
(comments+=TRIPLE_SLASH_COMMENT)*
name=RegularIDWithTypeNames type=DataFieldType (attributes+=DataFieldAttribute)*;
name=RegularIDWithTypeNames ('(' params+=DataFieldParam (',' params+=DataFieldParam)* ')' ':')? type=DataFieldType (attributes+=DataFieldAttribute)*;

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.

Do we need the ":" token here? I think dropping it will make parameterized fields more consistent with regular ones.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thank you for the feedback @ymc9 !

I updated the syntax you mentioned. Also there is a new PR for where, select, groupBy etc:
#2762

And docs:
zenstackhq/zenstack-docs#630

Glad to help and would be nice to see the feature in the next release 🤞

- 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>
Comment threadpackages/orm/src/client/crud-types.ts Outdated
evgenovalovand others added 2 commits July 22, 2026 14:47
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>
Previous run failed on a flaky, unrelated CLI codegen test
(import-extension.test.ts); the change in this PR is type-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ymc9

ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

FYI on the CI failures here: they are not related to this PR. The Build-and-Test failures (import-extension, migrate reset, generate — a different test each run) were root-caused to a latent CLI bug that reproduces on main as well: the preAction version-check fetch can strand its promise on a stalled connection, draining the event loop so zen silently exits 0 without running the command — hence the "output file missing after a successful CLI run" assertions. Diagnosed on #2766 and fixed in #2767; once that merges into dev, a rerun here should come back green. Sorry for the noise, and thanks for the patience with the re-triggers. 🤖

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