Skip to content

TML-2964: Mongo aggregation helpers source their codecs from the adapter (context-bound fn)#909

Open
wmadden-electric wants to merge 3 commits into
mainfrom
tml-2964-agg-codec-source
Open

TML-2964: Mongo aggregation helpers source their codecs from the adapter (context-bound fn)#909
wmadden-electric wants to merge 3 commits into
mainfrom
tml-2964-agg-codec-source

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

At a glance

// BEFORE — fn was a free-floating import, and it hardcoded codec ids:
import { fn } from '@prisma-next/mongo-query-builder'
//     fn.concat(...) stamped { codecId: 'mongo/string@1' } — a literal in the family layer
db.query.from('products')
  .addFields((f) => ({ label: fn.concat(f.status, f.region) }))

// AFTER — fn arrives in the stage callback, bound to the adapter's codec table:
db.query.from('products')
  .addFields((f, fn) => ({ label: fn.concat(f.status, f.region) }))

// Contract fields now pass into typed helper params with no cast:
  .addFields((f, fn) => ({ day: fn.dateToString({ date: f.createdAt, format: fn.literal('%Y-%m-%d') }) }))
//                                                      ^^^^^^^^^^^ previously needed `as Parameters<…>` — deleted

// And computed scalars decode: fn.toDate(...) comes back a real Date at runtime, not raw BSON.

The decision

The family layer must not know specific codec ids — that knowledge belongs to the adapter. So the Mongo adapter now declares an operation→output-codec table (mongoOperationOutputCodecs, 55 operators — the Mongo analog of SQL's queryOperationTypes), and the query builder consumes it instead of hardcoding mongo/double@1/mongo/string@1/… literals. The table is exported as a value and a type, so one declaration drives runtime codec stamping, compile-time output types, and result-shape decoding.

This is the first PR of TML-2964. acc (accumulators) and structural array/document outputs follow in the next two PRs; acc still carries its literals for now (tracked, unchanged behavior).

How it works

  • Threading: adapter descriptor → MongoExecutionContextmongoQuery({ contractJson, operationCodecs }) (both required, no default — a builder without codec knowledge cannot mint the helpers) → PipelineChain. The query-builder package defines the minimal table type it consumes and imports nothing from the runtime or target layers (lint:deps green).
  • Context-bound fn: minted by createFn(table), exposed on the query root (for standalone construction — the control adapter builds fn.setUnion(...) outside any stage) and passed as the second parameter to project/addFields/replaceRoot/sortByCount/redact/match callbacks. Existing one-param callbacks keep compiling. The free-floating fn export is deleted — a detached helper has no codec source, so no context-free form exists.
  • Type level: computed outputs are typed from the table's literal types (fn.concat → the table's $concat codec → string), so the family's emitted .d.ts carries no codec-id literal either. Helper input params (dateToString's date, trim's input, …) are constrained by decoded output type — a type-level filter over the contract's codec-type map — so a contract date field and a computed fn.toDate(...) both satisfy a date param, uncast.
  • Runtime decode: the result-shape reifier resolves any in-table operator to a leaf with the table's codec, so computed scalars decode through the same walk as every other field. Ops outside the table (array/document results, $map bodies, …) stay unknown — pass-through, as before.
  • Decode safety: the accepted imprecision ($toDecimal → double codec; $add over a date) is pass-through, not corruption — the numeric codec's decode is identity, and the decode walk returns null/undefined before invoking any codec (onNull paths safe).

Breaking change

Consumer-facing: the free fn import is gone and mongoQuery requires operationCodecs. Migration paths (stage-callback param / query-root fn / adapter table for direct mongoQuery calls) are documented in the 0.14→0.15 upgrade instructions for both app authors and extension authors (extensions declaring a MongoRuntimeAdapterDescriptor must add the required operationOutputCodecs field).

Verification

  • Query-builder: 418 tests + typecheck green; table-typed output assertions run against two different tables (an alt/*@9 table proves types come from the table, not a residual hardcode); grep-guard: no mongo/*@1 literal in the package source or emitted .d.ts (only accumulator-helpers.ts, the tracked acc remainder).
  • New integration tests (real MongoDB): a computed fn.toDate(...) returns instanceof Date with no _bsontype leakage, plus row-type assertions; fn.dateToString({ date: f.createdAt }) uncast returns the formatted string.
  • Full gate green: build (68/68), typecheck (143/143), lint + lint:deps + lint:casts (delta −15), fixtures:check, test:packages, test:integration (195 files), test:e2e (109 tests), retail-store next build.
  • Independent review pass on the full diff: no correctness defects; migrations strengthened assertions (casts deleted, expectTypeOf checks added).

Alternatives considered

  • A scalar-"kind" vocabulary (string/number/… beside codecs): rejected — codecs are the single type vocabulary; a parallel classification also split computed and contract fields into non-interoperable representations.
  • An adapter role→codec table (numeric/textual/… → codec): lighter than per-operation declarations, but reintroduces scalar-category labels into the family and can't express per-operation facts (which numeric codec $count returns; date-arithmetic signatures).
  • Trait-based resolution: traits are a compile-time input-typing device; no runtime trait query exists, date/objectId carry no trait, and numeric is ambiguous (double and int32 both carry it).
  • Defaulting operationCodecs: a default requires a family-level fallback codec table — the thing this PR removes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Query building now supports context-aware expression helpers during pipeline stages, improving typed computed fields and stage callbacks.
    • Mongo query setup now accepts runtime codec information, enabling more accurate result typing and decoding.
  • Bug Fixes

    • Improved handling of computed fields so projections and additions resolve more reliably instead of falling back to unknown shapes.
    • Updated runtime integration to carry codec metadata consistently across query execution and tests.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… literals

The adapter declares an operation-to-output-codec table (value + type, the
Mongo analog of SQL queryOperationTypes), threaded through the execution
context into mongoQuery({ contractJson, operationCodecs }) and the pipeline
chain. fn is minted from that table and delivered context-bound: exposed on
the query root and passed as the second parameter to the stage callbacks
(project/addFields/replaceRoot/sortByCount/redact/match). The free-floating
fn export is deleted.

Computed outputs are table-sourced at the value and type level; input params
are constrained by decoded output type, so a contract date field passes to
fn.dateToString/dateDiff uncast. The result-shape reifier resolves in-table
operators to leaf codecs, so computed scalars decode at runtime. Array and
document helpers stamp a structural unresolved marker (resolves to unknown,
as before); acc is unchanged for now.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Examples and integration tests move off the deleted free fn import to the
stage-callback parameter or the query root; direct mongoQuery calls supply
operationCodecs. New integration tests prove computed scalars decode
(fn.toDate returns a real Date) and that a contract date field passes to
fn.dateToString uncast (the old Parameters<> cast is deleted). Upgrade
instructions document the breaking change for app and extension authors.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric requested a review from a team as a code owner July 3, 2026 05:41
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces the Mongo query builder's fixed, imported fn expression-helper object with a context-bound createFn factory driven by an adapter-provided operation-to-codec table (TOps). PipelineChain, QueryRoot, CollectionHandle, FilteredCollection, and mongoQuery() are all parameterized by this table, which also drives pipeline result-shape inference for computed fields. The Mongo runtime adapter now exposes operationOutputCodecs, and tests/examples/docs are updated accordingly.

Changes

Operation-codec typing for Mongo query builder

Layer / File(s) Summary
Core type contracts
packages/2-mongo-family/5-query-builders/query-builder/src/types.ts, .../accumulator-helpers.ts, .../resolve-path.ts
Adds MongoOperationCodecTable, CodecTypesBase, CodecIdsWithOutput, ComputedField, and UnresolvedField types, replacing hardcoded field aliases like NumericField/ArrayField/LiteralValue.
Expression helpers rewrite
.../expression-helpers.ts, .../exports/index.ts
Replaces exported fn object with MongoFn interface and createFn(table) factory; updates public re-exports to include StageFn, MongoFn, createFn, and revised codec/shape types.
Pipeline result-shape computation
.../pipeline-result-shape.ts
computePipelineResultShape now requires operationCodecs and infers computed field shapes from MongoAggOperator expressions using the table.
Builder TOps threading
.../builder.ts
PipelineChain is parameterized by TOps, adds a memoized fn getter and StageFn type, threads operationCodecs through all stage methods and terminals.
Query root and state-class wiring
.../query.ts, .../state-classes.ts
mongoQuery() now requires operationCodecs and exposes fn; CollectionHandle/FilteredCollection/BindingContext thread TOps and pass it to PipelineChain.
Runtime adapter wiring
packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts, packages/3-mongo-target/2-mongo-adapter/src/core/operation-output-codecs.ts, .../exports/runtime.ts, .../mongo-control-adapter.ts, packages/3-extensions/mongo/src/static/mongo-static.ts
Adds mongoOperationOutputCodecs mapping and MongoOperationOutputCodecs type, exposes operationOutputCodecs on the adapter descriptor and execution context, and wires it into mongoQuery/createFn calls.
Query-builder tests and fixtures
.../query-builder/test/*
Updates unit and type-level tests/fixtures to build mongoQuery with contractJson/operationCodecs and createFn, adding coverage for table-sourced codec typing and unresolved fields.
Runtime/integration tests, examples, and docs
packages/2-mongo-family/7-runtime/test/*, test/integration/test/*, examples/mongo-demo/*, skills/*/0.14-to-0.15/instructions.md
Updates runtime/integration tests and the mongo-demo example to pass operationCodecs, and adds upgrade-guide documentation describing the breaking fn/mongoQuery and adapter descriptor changes.

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

Possibly related PRs

  • prisma/prisma-next#408: Both PRs affect how PipelineChain/query plans compute and populate MongoQueryPlan.resultShape.
  • prisma/prisma-next#888: Updates mongoStatic to construct/type query using adapter-provided operationOutputCodecs, aligning with the new codec-driven mongoQuery API.
  • prisma/prisma-next#897: Both PRs change the Mongo pipeline result-shape reification logic for $project/$addFields.

Suggested reviewers: wmadden

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Mongo aggregation helpers now source codecs from the adapter with context-bound fn.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2964-agg-codec-source

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.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 160.74 KB (0%)
postgres / emit 147.87 KB (0%)
mongo / no-emit 99.92 KB (+1.75% 🔺)
mongo / emit 91.12 KB (+1.94% 🔺)
cf-worker / no-emit 188.88 KB (0%)
cf-worker / emit 174.17 KB (0%)

@pkg-pr-new

pkg-pr-new Bot commented Jul 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@909

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@909

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@909

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@909

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@909

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@909

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@909

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@909

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@909

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@909

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@909

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@909

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@909

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@909

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@909

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@909

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@909

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@909

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@909

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@909

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@909

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@909

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@909

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@909

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@909

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@909

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@909

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@909

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@909

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@909

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@909

@prisma-next/config-loader

npm i https://pkg.pr.new/@prisma-next/config-loader@909

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@909

@prisma-next/language-server

npm i https://pkg.pr.new/@prisma-next/language-server@909

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@909

prisma-next

npm i https://pkg.pr.new/prisma-next@909

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@909

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@909

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@909

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@909

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@909

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@909

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@909

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@909

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@909

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@909

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@909

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@909

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@909

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@909

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@909

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@909

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@909

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@909

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@909

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@909

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@909

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@909

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@909

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@909

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@909

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@909

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@909

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@909

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@909

commit: 625e3c3

@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/2-mongo-family/5-query-builders/query-builder/src/builder.ts (1)

368-413: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Bare as cast introduced in project()'s callback branch.

Line 395 casts args[0] with a bare as, which is disallowed by the coding guideline. castAs is already imported and used elsewhere in this file (see the build() method), so it can be reused here.

🛠️ Proposed fix using `castAs`
     if (args.length === 1 && typeof args[0] === 'function') {
-      const fn = args[0] as (
-        fields: FieldAccessor<Shape, N>,
-        helpers: StageFn<TContract, TOps>,
-      ) => Record<string, 1 | TypedAggExpr<DocField>>;
+      const fn = castAs<
+        (
+          fields: FieldAccessor<Shape, N>,
+          helpers: StageFn<TContract, TOps>,
+        ) => Record<string, 1 | TypedAggExpr<DocField>>
+      >(args[0]);
       const accessor = createFieldAccessor<Shape, N>();
       const spec = fn(accessor, this.fn);

As per coding guidelines: "No bare as in production code. Use blindCast<T, "Reason"> or castAs<T> from @prisma-next/utils/casts... as const and test files are exempt."

🤖 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/2-mongo-family/5-query-builders/query-builder/src/builder.ts` around
lines 368 - 413, The callback branch in `project()` uses a bare type cast on
`args[0]`, which violates the no-`as` coding guideline. Update the `project`
method in `PipelineChain` to use the existing `castAs` helper instead of the
inline cast when treating `args[0]` as the callback, matching the pattern
already used in `build()`.

Source: Coding guidelines

🧹 Nitpick comments (1)
packages/2-mongo-family/5-query-builders/query-builder/src/builder.ts (1)

137-187: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

fn is rebuilt on every chain instance instead of being reused.

Each #withStage call produces a fresh PipelineChain with #fn uninitialized. Since TOps/TContract never change across a chain, the same createFn(operationCodecs) object (≈80 closures) gets rebuilt every time a different chain instance's .fn is accessed (e.g. .match(fn1).project(fn2) triggers two separate createFn calls for logically identical helpers). The same duplication also occurs whenever CollectionHandle/FilteredCollection are re-instantiated in state-classes.ts match().

Since the helper object doesn't depend on Shape/markers, it's safe to thread the already-built instance forward through #withStage (and analogously through state-classes.ts constructors) instead of rebuilding.

⚡ Proposed fix to reuse the minted `fn` across chain transitions
   readonly `#operationCodecs`: TOps;
   `#fn`: StageFn<TContract, TOps> | undefined;
 
-  constructor(contract: TContract, state: PipelineChainState, operationCodecs: TOps) {
+  constructor(
+    contract: TContract,
+    state: PipelineChainState,
+    operationCodecs: TOps,
+    fn?: StageFn<TContract, TOps>,
+  ) {
     this.#contract = contract;
     this.#state = state;
     this.#operationCodecs = operationCodecs;
+    this.#fn = fn;
   }
   >(stage: MongoPipelineStage): PipelineChain<TContract, NewShape, NewU, NewF, NewL, NewN, TOps> {
     return new PipelineChain<TContract, NewShape, NewU, NewF, NewL, NewN, TOps>(
       this.#contract,
       {
         ...this.#state,
         stages: [...this.#state.stages, stage],
       },
       this.#operationCodecs,
+      this.#fn,
     );
   }
🤖 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/2-mongo-family/5-query-builders/query-builder/src/builder.ts` around
lines 137 - 187, The StageFn helper in PipelineChain is being recreated for
every new chain instance, even though it only depends on the shared operation
codecs and contract. Thread the already-minted `fn` through
`PipelineChain.#withStage` so new chains reuse the existing instance instead of
leaving `#fn` undefined, and apply the same reuse pattern in the
`state-classes.ts` constructors used by `CollectionHandle` and
`FilteredCollection` so `createFn` is not called again for identical helpers.
🤖 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/2-mongo-family/5-query-builders/query-builder/src/expression-helpers.ts`:
- Around line 349-358: The computed() helper currently falls back to an empty
codecId when lookup[op] is missing, which hides missing adapter mappings; update
computed() in expression-helpers.ts to fail fast instead of minting a
ComputedField with an empty codec id. Use the lookup[op] result inside
computed<Op> and throw or otherwise surface an explicit error when no codec
mapping exists before constructing the _field and MongoAggOperator.of(op, args)
result.

In `@packages/2-mongo-family/5-query-builders/query-builder/src/types.ts`:
- Around line 31-51: The computed-expression typing is too narrow because
ComputedField currently hardcodes nullable: false for every table-backed
operator. Update the operator metadata/type flow in types.ts and the shape
builder used by computePipelineResultShape() so each op can carry its own
nullability, or route null-producing ops through a separate nullable path. Make
sure the resulting row shape matches decodeMongoRow() behavior by preserving
null where the operator can legitimately return it.

---

Outside diff comments:
In `@packages/2-mongo-family/5-query-builders/query-builder/src/builder.ts`:
- Around line 368-413: The callback branch in `project()` uses a bare type cast
on `args[0]`, which violates the no-`as` coding guideline. Update the `project`
method in `PipelineChain` to use the existing `castAs` helper instead of the
inline cast when treating `args[0]` as the callback, matching the pattern
already used in `build()`.

---

Nitpick comments:
In `@packages/2-mongo-family/5-query-builders/query-builder/src/builder.ts`:
- Around line 137-187: The StageFn helper in PipelineChain is being recreated
for every new chain instance, even though it only depends on the shared
operation codecs and contract. Thread the already-minted `fn` through
`PipelineChain.#withStage` so new chains reuse the existing instance instead of
leaving `#fn` undefined, and apply the same reuse pattern in the
`state-classes.ts` constructors used by `CollectionHandle` and
`FilteredCollection` so `createFn` is not called again for identical helpers.
🪄 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.yml

Review profile: CHILL

Plan: Pro

Run ID: 3aabb192-36b7-417e-832b-9305cc9c970c

📥 Commits

Reviewing files that changed from the base of the PR and between 284a838 and 625e3c3.

⛔ Files ignored due to path filters (2)
  • projects/mongo-agg-codec-source/plan.md is excluded by !projects/**
  • projects/mongo-agg-codec-source/spec.md is excluded by !projects/**
📒 Files selected for processing (46)
  • examples/mongo-demo/src/server.ts
  • examples/mongo-demo/test/cache-middleware.test.ts
  • examples/mongo-demo/test/query-builder-writes.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/accumulator-helpers.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/builder.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/exports/index.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/expression-helpers.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/pipeline-result-shape.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/query.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/resolve-path.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/state-classes.ts
  • packages/2-mongo-family/5-query-builders/query-builder/src/types.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/accumulator-helpers.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/accumulator-helpers.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/builder-new-stages.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/builder.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/builder.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/contract-free-collection.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/expression-helpers.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/expression-helpers.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/field-accessor.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/find-and-modify.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/fixtures/test-contract.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-result-shape.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-updates.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/raw-command.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/state-machine-surface.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/state-machine.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/types.test-d.ts
  • packages/2-mongo-family/5-query-builders/query-builder/test/writes.test.ts
  • packages/2-mongo-family/7-runtime/src/mongo-execution-stack.ts
  • packages/2-mongo-family/7-runtime/test/decode-via-query-builder.test.ts
  • packages/2-mongo-family/7-runtime/test/decode.integration.test.ts
  • packages/2-mongo-family/7-runtime/test/mongo-middleware.test.ts
  • packages/2-mongo-family/7-runtime/test/mongo-runtime-abort.test.ts
  • packages/2-mongo-family/7-runtime/test/runtime-types.test-d.ts
  • packages/3-extensions/mongo/src/static/mongo-static.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/mongo-control-adapter.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/operation-output-codecs.ts
  • packages/3-mongo-target/2-mongo-adapter/src/exports/runtime.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.14-to-0.15/instructions.md
  • test/integration/test/cross-package/cross-family-middleware.test.ts
  • test/integration/test/mongo-runtime/query-builder.test.ts
  • test/integration/test/mongo/query-builder.test.ts

Comment on lines +349 to +358
function computed<Op extends string>(
op: Op,
args: MongoAggExpr | ReadonlyArray<MongoAggExpr> | Readonly<Record<string, MongoAggExpr>>,
): Computed<TOps, Op> {
return {
_field: { codecId: DOC.codecId, nullable: true },
node: MongoAggOperator.of('$first', a.node),
_field: blindCast<
ComputedField<TOps, Op>,
'codecId is read from the adapter table entry for this operator'
>({ codecId: lookup[op] ?? '', nullable: false }),
node: MongoAggOperator.of(op, args),

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail fast when an operator has no codec mapping.

lookup[op] ?? '' silently mints a computed field with an empty codec id when the adapter table omits an operator. That hides adapter/config mistakes and can produce plans whose type-level field says “computed” without a real runtime codec source.

Proposed fix
   function computed<Op extends string>(
     op: Op,
     args: MongoAggExpr | ReadonlyArray<MongoAggExpr> | Readonly<Record<string, MongoAggExpr>>,
   ): Computed<TOps, Op> {
+    const codecId = lookup[op];
+    if (codecId === undefined) {
+      throw new Error(`Missing Mongo output codec for aggregation operator "${op}".`);
+    }
     return {
       _field: blindCast<
         ComputedField<TOps, Op>,
         'codecId is read from the adapter table entry for this operator'
-      >({ codecId: lookup[op] ?? '', nullable: false }),
+      >({ codecId, nullable: false }),
       node: MongoAggOperator.of(op, args),
     };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function computed<Op extends string>(
op: Op,
args: MongoAggExpr | ReadonlyArray<MongoAggExpr> | Readonly<Record<string, MongoAggExpr>>,
): Computed<TOps, Op> {
return {
_field: { codecId: DOC.codecId, nullable: true },
node: MongoAggOperator.of('$first', a.node),
_field: blindCast<
ComputedField<TOps, Op>,
'codecId is read from the adapter table entry for this operator'
>({ codecId: lookup[op] ?? '', nullable: false }),
node: MongoAggOperator.of(op, args),
function computed<Op extends string>(
op: Op,
args: MongoAggExpr | ReadonlyArray<MongoAggExpr> | Readonly<Record<string, MongoAggExpr>>,
): Computed<TOps, Op> {
const codecId = lookup[op];
if (codecId === undefined) {
throw new Error(`Missing Mongo output codec for aggregation operator "${op}".`);
}
return {
_field: blindCast<
ComputedField<TOps, Op>,
'codecId is read from the adapter table entry for this operator'
>({ codecId, nullable: false }),
node: MongoAggOperator.of(op, args),
🤖 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/2-mongo-family/5-query-builders/query-builder/src/expression-helpers.ts`
around lines 349 - 358, The computed() helper currently falls back to an empty
codecId when lookup[op] is missing, which hides missing adapter mappings; update
computed() in expression-helpers.ts to fail fast instead of minting a
ComputedField with an empty codec id. Use the lookup[op] result inside
computed<Op> and throw or otherwise surface an explicit error when no codec
mapping exists before constructing the _field and MongoAggOperator.of(op, args)
result.

Comment on lines +31 to +51
export type MongoOperationCodecTable = Readonly<Record<string, string>>;

export type CodecTypesBase = Record<string, { readonly output: unknown }>;

/**
* Codec ids from the contract's codec-type map whose decoded output extends
* `TOutput` — the Mongo analog of SQL's `CodecIdsWithTrait`, keyed on
* decoded output type instead of traits.
*/
export type CodecIdsWithOutput<CT extends CodecTypesBase, TOutput> = {
[K in keyof CT & string]: CT[K]['output'] extends TOutput ? K : never;
}[keyof CT & string];

/**
* Field stamped on a computed expression whose output codec is declared by
* the adapter table for `Op`.
*/
export type ComputedField<TOps extends MongoOperationCodecTable, Op extends string> = {
readonly codecId: TOps[Op & keyof TOps];
readonly nullable: false;
};

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## types.ts\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/query-builder/src/types.ts

printf '\n## pipeline-result-shape.ts\n'
sed -n '1,260p' packages/2-mongo-family/5-query-builders/query-builder/src/pipeline-result-shape.ts

printf '\n## search for ComputedField and nullable usage\n'
rg -n "ComputedField|nullable: false|nullable: true|MongoOperationCodecTable|CodecIdsWithOutput|pipeline-result-shape" packages/2-mongo-family/5-query-builders/query-builder/src

Repository: prisma/prisma-next

Length of output: 19973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## expression-helpers.ts (computed-related sections)\n'
sed -n '1,260p' packages/2-mongo-family/5-query-builders/query-builder/src/expression-helpers.ts
printf '\n---\n'
sed -n '260,420p' packages/2-mongo-family/5-query-builders/query-builder/src/expression-helpers.ts

printf '\n## result-shape.ts\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/query-builder/src/result-shape.ts

printf '\n## accumulator-helpers.ts\n'
sed -n '1,140p' packages/2-mongo-family/5-query-builders/query-builder/src/accumulator-helpers.ts

Repository: prisma/prisma-next

Length of output: 22339


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "nullable" packages/2-mongo-family -g '*.ts' -g '*.tsx' | sed -n '1,240p'

Repository: prisma/prisma-next

Length of output: 40444


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## references to MongoFieldShape.nullable and shape consumption\n'
rg -n "MongoFieldShape|freezeMongoFieldShape|freezeMongoResultShape|nullable" packages -g '*.ts' -g '*.tsx' | sed -n '1,260p'

printf '\n## search for result-shape decoders / readers\n'
rg -n "kind: 'leaf'|kind: 'array'|kind: 'document'|kind: 'unknown'" packages/2-mongo-family -g '*.ts' -g '*.tsx' | sed -n '1,260p'

Repository: prisma/prisma-next

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## decoder implementation references\n'
rg -n "decodeMongoRow|nullable|kind: 'leaf'|kind: 'array'|kind: 'document'" packages/2-mongo-family/7-runtime/src packages/2-mongo-family/7-runtime/test -g '*.ts' -g '*.tsx' | sed -n '1,240p'

printf '\n## result-shape tests\n'
sed -n '1,240p' packages/2-mongo-family/5-query-builders/query-builder/test/pipeline-result-shape.test.ts
printf '\n---\n'
sed -n '1,220p' packages/2-mongo-family/5-query-builders/query-builder/test/result-shape.test.ts

Repository: prisma/prisma-next

Length of output: 26870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' packages/2-mongo-family/7-runtime/src/codecs/decoding.ts

Repository: prisma/prisma-next

Length of output: 5380


Thread nullability through computed-expression shapes

ComputedField and computePipelineResultShape() still stamp every table-backed operator as nullable: false, but several of these operators can return null when their inputs/options allow it. That makes the projected row type too narrow even though decodeMongoRow() will pass null through unchanged. Carry nullable in the op metadata, or split null-producing ops into a nullable path.

🤖 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/2-mongo-family/5-query-builders/query-builder/src/types.ts` around
lines 31 - 51, The computed-expression typing is too narrow because
ComputedField currently hardcodes nullable: false for every table-backed
operator. Update the operator metadata/type flow in types.ts and the shape
builder used by computePipelineResultShape() so each op can carry its own
nullability, or route null-producing ops through a separate nullable path. Make
sure the resulting row shape matches decodeMongoRow() behavior by preserving
null where the operator can legitimately return it.

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