Skip to content

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

Open
evgenovalov wants to merge 11 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts
Open

feat: parameterized computed fields in where / select / include / aggregate / groupBy#2762
evgenovalov wants to merge 11 commits into
zenstackhq:devfrom
evgenovalov:feat/parameterized-computed-fields-read-contexts

Conversation

@evgenovalov

@evgenovalov evgenovalov commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

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

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

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

Contexts added

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

Field selection (your note)

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

Commits

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

Limitation

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

Testing

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

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

Checklist

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for parameterized computed fields with query-time arguments across where, orderBy, select/include, aggregates, and groupBy.
    • Extended the schema/grammar to declare parameterized computed field signatures and updated generated TypeScript metadata accordingly.
  • Bug Fixes
    • Strengthened validation to require { args: ... } in supported contexts and reject parameter declarations on non-computed fields.
    • Prevented parameterized computed fields from auto-appearing in unsupported features and improved cursor pagination handling.
  • Tests
    • Added end-to-end coverage for ordering, filtering, selection/inclusion, aggregation, grouping, and nested results with args.

evgenovalov and others added 8 commits June 30, 2026 11:01
A `@computed` field can now declare typed parameters, with the arguments
supplied at query time wherever the field is used. Because the arguments are
plain data, they serialize over the wire, so a client can drive a DB-side
computed sort through the auto-generated CRUD API — no custom endpoint, no raw
SQL, one query, with access policies and result types intact.

  model ProductSite {
    id   Int @id
    tags ProductTag[]
    tagNameInCategory(categoryId: Int): String? @computed
  }

  // implementation receives the args as a 3rd parameter
  computedFields: {
    ProductSite: {
      tagNameInCategory: (eb, ctx, args) =>
        eb.selectFrom('tag')
          .innerJoin('product_tag', 'product_tag.tag_id', 'tag.id')
          .whereRef('product_tag.product_site_id', '=', sql.ref(`${ctx.modelAlias}.id`))
          .where('tag.category_id', '=', args.categoryId)
          .select(sql<string>`string_agg(tag.name, ', ' order by tag.name)`.as('v')),
    },
  },

  // `args` is plain data, so this whole object can come from a client
  db.productSite.findMany({
    orderBy: { tagNameInCategory: { args: { categoryId: 5 }, sort: 'asc', nulls: 'last' } },
  });

This wires the feature end-to-end for `orderBy`:

- ZModel grammar: a field may declare a `(params): Type` signature; a validator
  rejects parameters on non-`@computed` fields.
- Schema codegen: the declared params flow into the generated computed-field
  stub signature, so the implementation type (`ComputedFieldsOptions`) and the
  query input types derive the args type from a single source and can't drift.
  The params are also emitted as `FieldDef.params` metadata for the runtime and
  the zod input-validation factory.
- Runtime: query-time args are forwarded to the implementation as a third
  argument through the single `fieldRef` chokepoint.
- Types & zod: `orderBy` accepts `{ args, sort, nulls? }` for a parameterized
  computed field. Such fields require args, so they are excluded from default
  selection, explicit `select`, and `where` (usable via `orderBy` for now);
  `where`/`select` support are natural follow-ups using the same mechanism.

Refs zenstackhq#2743

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: require at least one parameter when a field declares `(...)`, so
  `field(): T` no longer parses (empty param lists are meaningless)
- runtime: cursor pagination now rejects a parameterized computed field in
  `orderBy` (its sort key is not a real column), matching the existing
  relevance-ordering guard
- codegen: a param typed with a model/enum/type-def reference maps to `unknown`
  (those names aren't in scope in the generated schema) — same convention as
  computed-field return types; zod still validates the value precisely
- schema: tighten the FieldDef.params doc to mention `orderBy` only
- test: assert cursor + parameterized computed sort is rejected

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- grammar: a parameterized field is now `name(params) Type` (no `:` before the
  return type), consistent with regular fields; Langium grammar regenerated
- types + zod: a parameterized computed field is now excluded from every read
  context that can't supply `args` (where/select/omit, `_count`/`_sum`/`_avg`/
  `_min`/`_max`, groupBy `by`, `distinct`) — it previously typechecked but hit
  `computer(eb, ctx, undefined)` at runtime; it remains usable via `orderBy`
- test: language grammar test (no-colon parses, colon rejected, params require
  `@computed`) + e2e regression asserting the excluded contexts reject it

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

Extends parameterized `@computed` fields (previously orderBy-only) to more read
contexts, threading query-time `args` through the single `fieldRef` channel.

- where (+ having, which reuses WhereInput): `where: { field: { args, ...ops } }`
  — args supplied alongside the filter operators. `addComputedArgsToFilter` lifts
  the operator components from the field's existing filter schema and requires
  `args` (dropping the bare-value shorthand); `buildFilter` strips `args` and
  forwards it to the implementation.
- select / include: `include: { field: { args } }`, and `select: { field: { args } }`
  via the `SelectInput = {…boolean} & IncludeInput` intersection. Result types
  surface the field as its scalar return type (new additive term in ModelResult's
  include branch; ModelSelectResult already mapped it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_count`, `_sum`, `_avg`, `_min`, `_max` (in aggregate and groupBy) and count's
`select` now take `{ field: { args } }` for a parameterized computed field instead
of the bare `true`. count/aggregate materialize the field into the `$sub` subquery
with its args; groupBy re-inlines it via `fieldRef` (passing the grouped-table alias
so a computed field referencing `ctx.modelAlias` resolves). Result types map the keys
to numbers as before. Reverses the 1b exclusions on CountAggregateInput / SumAvgInput /
MinMaxInput and their zod builders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`by` now accepts a keyed `{ field, args }` entry for a parameterized computed field
(alongside plain field names), so groups can be formed on the query-time-parameterized
value. The having/orderBy membership refinements normalize `by` entries to field names;
the runtime groups and selects via `fieldRef(field, args)`; `GroupByResult` projects the
field name out of the keyed entry.

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

- nested include/select: a parameterized computed field on a related model is now inlined
  with its args in both dialect JSON assemblers (sqlite + lateral-join), and excluded from
  the relation select-all (never auto-returned). Types/zod already recursed via 2b.
- groupBy `by` a computed field: group by the SELECT output alias instead of re-inlining, so
  GROUP BY and the projected expression stay identical (Postgres treats a re-inlined
  parameterized computer as a distinct expression). Grouping by a correlated-subquery computed
  field remains subject to the DB's own correlated-GROUP-BY rules.
- tests: nested include/select cases; groupBy uses a row-local computed field; computed-field
  count expressions cast to integer so results are numbers on Postgres (bigint) too. Verified
  on both SQLite and Postgres (19 tests).

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

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Parameterized computed fields now support typed declarations, generated metadata, argument-aware query validation, ORM execution, selection, ordering, filtering, aggregation, grouping, and nested relation results.

Changes

Parameterized computed fields

Layer / File(s) Summary
Field syntax and generated metadata
packages/language/src/*, packages/language/test/*, packages/schema/src/schema.ts, packages/sdk/src/ts-schema-generator.ts
Parameterized computed-field declarations, validation, generated parameter metadata, and typed implementation arguments are added.
Typed and runtime query contracts
packages/orm/src/client/crud-types.ts, packages/orm/src/client/zod/factory.ts
Query types and runtime schemas require { args } for parameterized computed fields across filtering, selection, ordering, aggregation, distinct, and grouping.
Argument propagation and field materialization
packages/orm/src/client/crud/dialects/*, packages/orm/src/client/crud/operations/base.ts, packages/orm/src/client/crud/operations/count.ts
Computed arguments flow through filters, ordering, selections, relation JSON construction, count subqueries, and computed-field evaluation; implicit scalar selection excludes parameterized fields.
Aggregation and grouping execution
packages/orm/src/client/crud/operations/aggregate.ts, packages/orm/src/client/crud/operations/group-by.ts, tests/e2e/orm/client-api/computed-fields.test.ts
Aggregations and groupings use supplied computed-field arguments, with end-to-end coverage for supported and rejected query forms.

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

Possibly related PRs

  • zenstackhq/zenstack#2744 — Implements related parameterized @computed field parsing, validation, query argument typing, and ORM argument propagation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding parameterized computed fields to query contexts like where, select, include, aggregate, and groupBy.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

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

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

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

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

🐛 Proposed fix
-                const expr = this.fieldRef(model, field, modelAlias);
+                const computedArgs = value && typeof value === 'object' && 'args' in value ? (value as any).args : undefined;
+                const expr = this.fieldRef(model, field, modelAlias, true, computedArgs);
                 let sort = typeof value === 'string' ? value : value.sort;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts` around
lines 215 - 216, Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/orm/src/client/crud/operations/aggregate.ts`:
- Around line 36-37: Update the aggregation field-processing logic around
selectedFields and computedArgsByField to validate repeated fields before
assigning args: when an existing field has args, require the new args to be
deeply identical; otherwise throw an error. Preserve the existing
single-projection behavior and assignment for first-time fields or matching
args.

In `@packages/schema/src/schema.ts`:
- Around line 85-90: Update the documentation for the params property in the
parameterized computed-field schema to remove the orderBy-only restriction and
describe that its arguments are supplied at query time wherever the field is
used, including filtering, selection, aggregation, and grouping contexts.

---

Outside diff comments:
In `@packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts`:
- Around line 215-216: Update the relation-sort logic around fieldRef in the
lateral-join dialect to extract query-time args from value and pass them as
computedArgs to this.fieldRef(model, field, modelAlias). Match the argument
handling used by applyScalarOrderBy, while preserving the existing sort
selection behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a86edbaf-c798-481d-b4dd-4bb098d06740

📥 Commits

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

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

Comment thread packages/orm/src/client/crud/operations/aggregate.ts Outdated
Comment thread packages/schema/src/schema.ts
evgenovalov and others added 3 commits July 20, 2026 12:48
The parameterized-computed-field entry in `IncludeInput` was added as a separate
intersection member, which disabled excess-property checking on `include`/`select`
literals — invalid keys (a sliced-out relation, or `_count` on a model with no
to-many relations) stopped producing type errors. Fold it into the single relations
mapped type so excess-property checking is preserved. Fixes the tests/e2e `tsc`
build (TS2578 unused '@ts-expect-error' in find.test.ts / slicing.test.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review feedback on zenstackhq#2762:
- aggregate: a computed field is materialized once into `$sub`, so aggregating the
  same field with different `args` in one query would silently use whichever `args`
  was seen last. Throw an input-validation error instead, with a regression test.
- schema: `FieldDef.params` doc no longer says the args are `orderBy`-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cast(count(*) as integer)` is invalid MySQL syntax (MySQL uses SIGNED, and no CAST
target works across sqlite/pg/mysql), which broke the count-based computed-field
tests on the MySQL CI job. Revert to plain `count(*)` for the tests that don't assert
the raw count (they sort/filter/aggregate, and aggregate post-processes to a number).
The select/include test — the only one asserting a returned computed value — now uses
a row-local arithmetic field (`price * factor`), which returns a plain integer on
every dialect (no bigint string, no cast).

Verified: computed-fields 19/19 and the full orm/client-api suite green on SQLite,
PostgreSQL, and MySQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to 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.

1 participant