Skip to content

feat(mongo-query-ast): add core pipeline stage extensions#306

Merged
wmadden merged 8 commits intomainfrom
tml-2210-core-pipeline-stage-extensions-group-end-to-end
Apr 7, 2026
Merged

feat(mongo-query-ast): add core pipeline stage extensions#306
wmadden merged 8 commits intomainfrom
tml-2210-core-pipeline-stage-extensions-group-end-to-end

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented Apr 7, 2026

closes TML-2210

Key snippet (new capability)

// NEW — MongoStageRewriterContext composes filter and agg-expr rewriters
export interface MongoStageRewriterContext {
  filter?: MongoFilterRewriter;
  aggExpr?: MongoAggExprRewriter;
}

// NEW — MongoGroupStage: typed $group with accumulator enforcement
export class MongoGroupStage extends MongoStageNode {
  readonly kind = 'group' as const;
  readonly groupId: MongoGroupId;
  readonly accumulators: Readonly<Record<string, MongoAggAccumulator>>;
  // ...
}

Intent

Extend the MongoDB pipeline stage AST with the core data-transformation stages ($group, $addFields, $replaceRoot, $count, $sortByCount, $sample, $redact) and widen three existing stages ($project with computed expressions, $lookup with correlated pipelines, $unwind with array index), so that typed AST pipelines can express grouping, computed projections, and document reshaping without falling back to raw Record<string, unknown>. This is Milestone 3 of the Mongo Pipeline Builder project and unblocks the typed pipeline builder (TML-2211).

Change map

The story

  1. Introduce a composite rewriter context: The existing rewrite(MongoFilterRewriter) signature cannot recurse into MongoAggExpr nodes. A new MongoStageRewriterContext interface bundles both filter? and aggExpr? rewriters, and all existing stages are migrated to accept it. This resolves Open Question 1 from the spec with Option (a).

  2. Add 7 new pipeline stage classes: MongoGroupStage, MongoAddFieldsStage, MongoReplaceRootStage, MongoCountStage, MongoSortByCountStage, MongoSampleStage, MongoRedactStage — all following the established MongoStageNode pattern (kind discriminant, frozen, accept/rewrite). Each stage's rewrite() recurses into its embedded MongoAggExpr fields using the context's aggExpr rewriter.

  3. Widen 3 existing stages for richer usage: MongoProjectStage now accepts MongoAggExpr values alongside 0 | 1 (computed projections). MongoLookupStage supports the correlated pipeline form (let_/pipeline without localField/foreignField). MongoUnwindStage gains optional includeArrayIndex.

  4. Wire adapter lowering: lowerStage() in adapter-mongo gets cases for all 7 new stage kinds plus updated lowering for the 3 widened stages. Expression-bearing stages call through lowerAggExpr(), lowerGroupId(), lowerExprRecord(), and lowerProjectionValue() — no raw pass-through.

  5. Prove the full stack end-to-end: The integration test replaces the raw-object $group pipeline with typed MongoMatchStageMongoGroupStageMongoSortStage and verifies correct aggregation results against mongodb-memory-server.

  6. Harden with type-level tests and invariant checks: Adds stages.test-d.ts verifying compile-time contracts (visitor requires all 14 methods, kind union exhaustiveness, accumulator type constraint). Adds constructor validation to MongoLookupStage (at least one of equality or pipeline form required). Adds exhaustiveness guard to lowerStage().

Behavior changes & evidence

  • Adds 7 typed pipeline stage classes that can represent grouping, computed fields, document replacement, counting, sampling, and redaction without the raw escape hatch

  • Stage rewriting now composes filter and agg-expr rewriters via MongoStageRewriterContext: rewrite() changed from accepting MongoFilterRewriter to MongoStageRewriterContext { filter?, aggExpr? }, recursing into both expression types.

  • MongoProjectStage accepts computed projection values (0 | 1 | MongoAggExpr): enables expressions like { fullName: { $concat: ["$first", " ", "$last"] } }.

  • MongoLookupStage supports correlated pipeline form: localField/foreignField now optional; let_ supported. Constructor validates at least one form is present.

  • Adapter lowerStage() handles all new and widened stage kinds with exhaustiveness guard.

Compatibility / migration / risk

  • All type widenings are supertype-compatible. Existing ORM compile.ts compiles unchanged.
  • rewrite() signature change is internal — all call sites updated, no external consumers.
  • MongoReadStage union is widened. Existing switch statements will get compile errors for unhandled members — intended forcing function.

Follow-ups / open questions

  • Accumulator rewrite type safety: as MongoAggAccumulator cast in MongoGroupStage.rewrite() should be addressed by narrowing MongoAggAccumulator.rewrite() return type — tracked as a follow-up for the aggregation expression AST.

Non-goals / intentionally out of scope

  • Long-tail stages ($facet, $graphLookup, $unionWith, $bucket, etc.) — deferred to Milestone 5
  • Renaming MongoReadStageMongoPipelineStage — deferred to Milestone 5
  • Removing AggregatePipelineEntry escape hatch — deferred to Milestone 5
  • Pipeline builder with shape tracking — TML-2211 (Milestone 4)

Summary by CodeRabbit

  • New Features

    • Added MongoDB aggregation stages: group, addFields, replaceRoot, count, sortByCount, sample, redact.
    • Projections can include computed aggregation expressions.
  • Improvements

    • More flexible $lookup (optional fields, pipeline + let) and $unwind (includeArrayIndex).
    • Richer rewrite/context support and stricter validation when lowering pipelines.
  • Tests

    • Expanded compile-time and runtime tests covering new stages, rewrite recursion, constructor validation, and lowering.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c39b6b36-9171-4fad-8cfd-2befb41d6065

📥 Commits

Reviewing files that changed from the base of the PR and between 44fe826 and bf0154d.

📒 Files selected for processing (9)
  • packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
  • packages/2-mongo-family/4-query/query-ast/src/stages.ts
  • packages/2-mongo-family/4-query/query-ast/src/visitors.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test.ts
  • packages/2-mongo-family/7-runtime/test/aggregate.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/lowering.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/2-mongo-family/7-runtime/test/aggregate.test.ts
  • packages/2-mongo-family/4-query/query-ast/src/visitors.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts

📝 Walkthrough

Walkthrough

Adds seven aggregation stages, unifies stage rewrite APIs under a MongoStageRewriterContext (filter + aggExpr), extends visitors and public exports, updates lowering to handle new stage shapes and expressions, and expands type and runtime tests.

Changes

Cohort / File(s) Summary
Core Stage Definitions
packages/2-mongo-family/4-query/query-ast/src/stages.ts
Added stages: MongoGroupStage, MongoAddFieldsStage, MongoReplaceRootStage, MongoCountStage, MongoSortByCountStage, MongoSampleStage, MongoRedactStage. Changed MongoStageNode.rewrite(...) to accept MongoStageRewriterContext. Added MongoGroupId, MongoProjectionValue. Updated MongoProjectStage, MongoLookupStage, MongoUnwindStage signatures, validation, and rewrite behavior.
Visitor Interface
packages/2-mongo-family/4-query/query-ast/src/visitors.ts
Exported MongoStageRewriterContext and extended MongoStageVisitor<R> with visit methods for the new aggregation stages.
Public Exports
packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
Re-exported new stage classes and types (MongoGroupId, MongoProjectionValue) and exported MongoStageRewriterContext alongside existing stage exports.
Type-level Tests
packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
Added declaration tests validating stage union exhaustiveness, visitor completeness, projection value constraints, and assignability of each concrete stage to MongoReadStage.
AST Unit Tests
packages/2-mongo-family/4-query/query-ast/test/stages.test.ts, packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts
Updated rewrite tests to use context objects; added constructor validation for Lookup; expanded Project/Unwind tests; added exhaustive tests for new stages, immutability, visitor dispatch, and rewrite recursion.
Runtime Integration Test
packages/2-mongo-family/7-runtime/test/aggregate.test.ts
Rewrote pipeline test to use typed AST nodes and typed agg expressions (MongoMatchStage, MongoGroupStage, MongoSortStage) instead of raw BSON objects.
Lowering Implementation
packages/3-mongo-target/2-mongo-adapter/src/lowering.ts
Added helpers lowerGroupId, lowerExprRecord, lowerProjectionValue. Refactored $project, $lookup, $unwind lowering to handle optional fields and computed values. Extended lowerStage to emit new operators and added an exhaustive default error.
Lowering Tests
packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
Added tests covering lowering for new stages and variants: $group id shapes, $addFields, $replaceRoot, $count, $sortByCount, $sample, $redact, $lookup with let+pipeline, computed $project, and unwind.includeArrayIndex.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Stage as MongoStageNode
  participant Context as MongoStageRewriterContext
  participant AggRewriter as MongoAggExprRewriter
  participant Lowerer as LoweringAdapter
  participant Runtime as MongoDB Driver

  Stage->>Context: rewrite(context)
  Context->>AggRewriter: (optional) rewrite agg expressions
  AggRewriter-->>Stage: return rewritten agg-expr nodes
  Stage-->>Lowerer: provide lowered stage shape
  Lowerer->>Runtime: emit BSON stage for execution
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibble through nodes, adding seven new hops,
Rewriter in paw, I tidy filters and ops,
I lower to BSON, neat as a treat,
Pipelines march on with rhythm and beat—
Hooray for AST carrots! 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mongo-query-ast): add core pipeline stage extensions' clearly and concisely describes the main change: adding pipeline stage extensions to the mongo-query-ast module.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-2210-core-pipeline-stage-extensions-group-end-to-end

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts (1)

154-154: Use an explicit mock-boundary double cast here.

Direct cast on a synthetic mock object is discouraged in test files; use unknown as the unsafe boundary.

♻️ Suggested change
-  const stage = {} as MongoMatchStage;
+  const stage = {} as unknown as MongoMatchStage;

As per coding guidelines: **/*.{test.ts,test-d.ts}: “Use double casts (as unknown as X) for mocks and dynamic proxies in tests instead of direct casts”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts` at line 154,
Replace the direct cast of the synthetic mock object to MongoMatchStage by
introducing the mock boundary using unknown; change the construction of the
stage mock (symbol: stage) so the empty object is first cast to unknown and then
to MongoMatchStage (i.e., use a double cast) to follow test mock guidelines.
packages/2-mongo-family/4-query/query-ast/src/stages.ts (1)

180-184: Constructor validation allows let_ without pipeline, which may be invalid MongoDB syntax.

The validation permits constructing a MongoLookupStage with only let_ (no pipeline, localField, or foreignField), which would be invalid when executed against MongoDB. Consider tightening the validation to require pipeline when let_ is provided.

Suggested stricter validation
-    if (!options.localField && !options.foreignField && !options.pipeline && !options.let_) {
+    const hasEqualityFields = options.localField && options.foreignField;
+    const hasPipeline = !!options.pipeline;
+    if (!hasEqualityFields && !hasPipeline) {
       throw new Error(
         'MongoLookupStage requires either equality fields (localField/foreignField) or a pipeline',
       );
     }
+    if (options.let_ && !hasPipeline) {
+      throw new Error('MongoLookupStage let_ requires a pipeline');
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/src/stages.ts` around lines 180 -
184, The constructor validation for MongoLookupStage currently allows
options.let_ without options.pipeline; update the check in the MongoLookupStage
constructor so that if options.let_ is present it also requires options.pipeline
(i.e., reject cases where options.let_ is set but options.pipeline is falsy),
while keeping the existing requirement that at least one of
(localField/foreignField) or pipeline is present; adjust the thrown Error
message accordingly to mention that let_ requires a pipeline when provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/2-mongo-family/4-query/query-ast/src/stages.ts`:
- Around line 180-184: The constructor validation for MongoLookupStage currently
allows options.let_ without options.pipeline; update the check in the
MongoLookupStage constructor so that if options.let_ is present it also requires
options.pipeline (i.e., reject cases where options.let_ is set but
options.pipeline is falsy), while keeping the existing requirement that at least
one of (localField/foreignField) or pipeline is present; adjust the thrown Error
message accordingly to mention that let_ requires a pipeline when provided.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts`:
- Line 154: Replace the direct cast of the synthetic mock object to
MongoMatchStage by introducing the mock boundary using unknown; change the
construction of the stage mock (symbol: stage) so the empty object is first cast
to unknown and then to MongoMatchStage (i.e., use a double cast) to follow test
mock guidelines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 0ad0429f-0123-4cff-9e8f-ea5a03005865

📥 Commits

Reviewing files that changed from the base of the PR and between d345bd8 and 6aab003.

⛔ Files ignored due to path filters (3)
  • projects/mongo-pipeline-builder/plan.md is excluded by !projects/**
  • projects/mongo-pipeline-builder/plans/aggregation-expression-ast-plan.md is excluded by !projects/**
  • projects/orm-consolidation/plans/aggregation-expression-ast-design.md is excluded by !projects/**
📒 Files selected for processing (17)
  • packages/2-mongo-family/4-query/query-ast/src/aggregation-expressions.ts
  • packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
  • packages/2-mongo-family/4-query/query-ast/src/filter-expressions.ts
  • packages/2-mongo-family/4-query/query-ast/src/stages.ts
  • packages/2-mongo-family/4-query/query-ast/src/visitors.ts
  • packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test.ts
  • packages/2-mongo-family/4-query/query-ast/test/filter-expressions.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/filter-expressions.test.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test.ts
  • packages/2-mongo-family/4-query/query-ast/vitest.config.ts
  • packages/2-mongo-family/7-runtime/test/aggregate.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/exports/index.ts
  • packages/3-mongo-target/2-mongo-adapter/src/lowering.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
  • test/integration/test/mongo/expr-filter.test.ts

@wmadden wmadden force-pushed the tml-2210-core-pipeline-stage-extensions-group-end-to-end branch from 6aab003 to a47a548 Compare April 7, 2026 06:50
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 7, 2026

Open in StackBlitz

@prisma-next/mongo-orm

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

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

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

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@306

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/adapter-postgres

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

@prisma-next/driver-postgres

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

commit: bf0154d

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/2-mongo-family/4-query/query-ast/test/stages.test.ts (2)

295-611: Split this test file to satisfy the 500-line limit.

Line [611] shows this file now exceeds the repository cap for test size. The new stage-specific blocks provide natural split points (e.g., stage construction/rewrite vs visitor dispatch), so this should be split into focused test files for maintainability.

As per coding guidelines, "**/*.test.ts: Keep test files under 500 lines ... Split test files when they exceed 500 lines ...`"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test.ts` around lines
295 - 611, This file exceeds the 500-line test limit; split it into smaller,
focused test files by grouping related describe blocks (e.g., one file per stage
or a file for stage behavior tests and another for visitor dispatch tests). Move
the describe blocks for MongoGroupStage, MongoAddFieldsStage,
MongoReplaceRootStage, MongoCountStage, MongoSortByCountStage, MongoSampleStage,
MongoRedactStage (and their rewrite/is-frozen tests) into one or more
stage-specific test files, and move the MongoStageVisitor dispatch tests (the
kindVisitor and its accept assertions) into a separate visitor test file; update
test suite imports/exports if any references exist so symbols like
MongoGroupStage, MongoAddFieldsStage, MongoReplaceRootStage, MongoCountStage,
MongoSortByCountStage, MongoSampleStage, MongoRedactStage, and MongoStageVisitor
remain resolvable. Ensure each new file stays under 500 lines and retains the
same test bodies and descriptions.

329-523: Factor repeated aggExpr rewriter setup into a small helper.

The same MongoAggExprRewriter object is repeated across multiple tests (Line [330], Line [342], Line [382], Line [408], Line [451], Line [511]). A local helper would reduce duplication.

♻️ Suggested refactor
+const prefixFieldRefRewriter = (prefix: string): MongoAggExprRewriter => ({
+  fieldRef: (expr) => MongoAggFieldRef.of(`${prefix}${expr.path}`),
+});
...
-const aggExpr: MongoAggExprRewriter = {
-  fieldRef: (expr) => MongoAggFieldRef.of(`r.${expr.path}`),
-};
+const aggExpr = prefixFieldRefRewriter('r.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test.ts` around lines
329 - 523, Multiple tests repeat the same MongoAggExprRewriter setup; create a
small helper function (e.g., makeAggExprRewriter or aggExprRewriter) that
returns a MongoAggExprRewriter with fieldRef: (expr) =>
MongoAggFieldRef.of(`r.${expr.path}`) and use it in tests that call
stage.rewrite({ aggExpr }) (references: MongoAggExprRewriter, fieldRef,
MongoAggFieldRef.of, and rewrite methods on MongoGroupStage,
MongoAddFieldsStage, MongoReplaceRootStage, MongoSortByCountStage,
MongoRedactStage); add the helper near the top of the describe block (or test
file) and replace inline aggExpr objects with a call to the helper to remove
duplication while preserving types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/3-mongo-target/2-mongo-adapter/src/lowering.ts`:
- Around line 152-159: The current discriminator in lowerGroupId uses the
presence of the string key 'kind' to decide if a MongoGroupId is a MongoAggExpr,
which misclassifies document-shaped _id values like {_id: { kind: expr }} and
causes lowerAggExpr to be called on plain objects (leading to missing accept()).
Replace the `'kind' in groupId` check with a proper AST node guard that detects
an actual MongoAggExpr node (e.g., check for the node shape/property that real
AST nodes expose such as a type tag or an isNode/isAggExpr predicate used
elsewhere), update the corresponding helper in stages.ts to use the same guard,
and add a regression test that lowers a composite group _id containing a key
named "kind" to ensure it stays a document and does not call lowerAggExpr.
Ensure references to lowerAggExpr and accept() are preserved so the guard
prevents passing plain objects into lowerAggExpr.
- Around line 192-204: MongoLookupStage currently allows half-specified
equality-style lookups; update MongoLookupStage.constructor() validation so it
rejects instances where only one of localField or foreignField is provided.
Require either both localField AND foreignField (equality syntax) or a pipeline
(pipeline present) — let_ may be present only when pipeline is used. Modify the
constructor to throw/raise on the invalid case (localField xor foreignField) and
keep existing acceptance for pipeline (+ optional let_).

---

Nitpick comments:
In `@packages/2-mongo-family/4-query/query-ast/test/stages.test.ts`:
- Around line 295-611: This file exceeds the 500-line test limit; split it into
smaller, focused test files by grouping related describe blocks (e.g., one file
per stage or a file for stage behavior tests and another for visitor dispatch
tests). Move the describe blocks for MongoGroupStage, MongoAddFieldsStage,
MongoReplaceRootStage, MongoCountStage, MongoSortByCountStage, MongoSampleStage,
MongoRedactStage (and their rewrite/is-frozen tests) into one or more
stage-specific test files, and move the MongoStageVisitor dispatch tests (the
kindVisitor and its accept assertions) into a separate visitor test file; update
test suite imports/exports if any references exist so symbols like
MongoGroupStage, MongoAddFieldsStage, MongoReplaceRootStage, MongoCountStage,
MongoSortByCountStage, MongoSampleStage, MongoRedactStage, and MongoStageVisitor
remain resolvable. Ensure each new file stays under 500 lines and retains the
same test bodies and descriptions.
- Around line 329-523: Multiple tests repeat the same MongoAggExprRewriter
setup; create a small helper function (e.g., makeAggExprRewriter or
aggExprRewriter) that returns a MongoAggExprRewriter with fieldRef: (expr) =>
MongoAggFieldRef.of(`r.${expr.path}`) and use it in tests that call
stage.rewrite({ aggExpr }) (references: MongoAggExprRewriter, fieldRef,
MongoAggFieldRef.of, and rewrite methods on MongoGroupStage,
MongoAddFieldsStage, MongoReplaceRootStage, MongoSortByCountStage,
MongoRedactStage); add the helper near the top of the describe block (or test
file) and replace inline aggExpr objects with a call to the helper to remove
duplication while preserving types.
🪄 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: aa6148c8-4f46-4054-a061-8cf4ba53bdb0

📥 Commits

Reviewing files that changed from the base of the PR and between 6aab003 and a47a548.

📒 Files selected for processing (8)
  • packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
  • packages/2-mongo-family/4-query/query-ast/src/stages.ts
  • packages/2-mongo-family/4-query/query-ast/src/visitors.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test.ts
  • packages/2-mongo-family/7-runtime/test/aggregate.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/lowering.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/2-mongo-family/7-runtime/test/aggregate.test.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/src/stages.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
  • packages/2-mongo-family/4-query/query-ast/src/visitors.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts (1)

50-57: Strengthen compound groupId assertions to validate values, not just presence.

toBeDefined() here can pass even if the wrong expressions are assigned. Assert the resolved field paths to validate wiring.

Proposed test tightening
   it('accepts compound groupId', () => {
     const stage = new MongoGroupStage(
       { dept: MongoAggFieldRef.of('department'), year: MongoAggFieldRef.of('year') },
       { total: MongoAggAccumulator.sum(MongoAggFieldRef.of('amount')) },
     );
-    expect((stage.groupId as Record<string, MongoAggExpr>)['dept']).toBeDefined();
-    expect((stage.groupId as Record<string, MongoAggExpr>)['year']).toBeDefined();
+    const groupId = stage.groupId as Record<string, MongoAggExpr>;
+    expect({
+      dept: (groupId['dept'] as MongoAggFieldRef).path,
+      year: (groupId['year'] as MongoAggFieldRef).path,
+    }).toMatchObject({
+      dept: 'department',
+      year: 'year',
+    });
   });

As per coding guidelines "**/*.test.ts: Prefer object matchers (toMatchObject) over multiple individual expect().toBe() calls when checking 2 or more related values in tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts`
around lines 50 - 57, Replace the loose presence checks on stage.groupId with a
stronger object equality assertion: instead of two expect(...).toBeDefined()
calls, assert that stage.groupId matches the expected mapping (use
expect(stage.groupId).toMatchObject(...)) and compare the values to the exact
MongoAggFieldRef/MongoAggExpr instances you expect (e.g., dept should equal
MongoAggFieldRef.of('department') and year should equal
MongoAggFieldRef.of('year')), so the test verifies the resolved field
paths/expressions rather than just their existence; refer to MongoGroupStage,
groupId, MongoAggFieldRef, and MongoAggExpr to locate the code to change.
packages/2-mongo-family/4-query/query-ast/test/stages.test.ts (2)

66-82: Add the no-op branch for expression-valued projections.

The new MongoAggExpr path is only exercised with aggExpr present. A regression that assumes context.aggExpr exists for expression-bearing projections would still pass because the only identity check here is the scalar-only case.

Suggested test addition
   it('rewrite() returns this for scalar-only projections', () => {
     const stage = new MongoProjectStage({ name: 1 });
     expect(stage.rewrite({})).toBe(stage);
   });
 
+  it('rewrite() returns this when no aggExpr rewriter is provided', () => {
+    const stage = new MongoProjectStage({
+      fullName: MongoAggFieldRef.of('name'),
+      _id: 0,
+    });
+    expect(stage.rewrite({})).toBe(stage);
+  });
+
   it('rewrite() recurses into expression projection values', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test.ts` around lines
66 - 82, Add a test that exercises the no-op branch when projections contain
expression values but no aggExpr is provided: create a MongoProjectStage whose
projection includes a MongoAggFieldRef (e.g., fullName:
MongoAggFieldRef.of('name')), call stage.rewrite({}) and assert it returns the
same instance (toBe(stage)). This ensures MongoProjectStage.rewrite does not
assume context.aggExpr exists for expression-valued projections and covers the
MongoAggExpr/MongoAggFieldRef path without providing aggExpr.

211-225: Cover { aggExpr } propagation through $lookup.pipeline.

This proves nested filter rewrites, and the later let_ case proves top-level let_ rewrites, but the suite still never places an expression-bearing stage inside pipeline. A bug that skips rewrite(context) for sub-pipeline stages when only aggExpr is supplied would slip through.

Suggested test addition
   it('rewrite() rewrites nested pipeline stages', () => {
     const filter: MongoFilterRewriter = {
       field: (expr) => MongoFieldFilter.of(expr.field, '$ne', expr.value),
     };
@@
     const match = rewritten.pipeline![0] as MongoMatchStage;
     expect((match.filter as MongoFieldFilter).op).toBe('$ne');
   });
+
+  it('rewrite() propagates aggExpr into nested pipeline stages', () => {
+    const aggExpr: MongoAggExprRewriter = {
+      fieldRef: (expr) => MongoAggFieldRef.of(`r.${expr.path}`),
+    };
+    const stage = new MongoLookupStage({
+      from: 'orders',
+      as: 'matchingOrders',
+      pipeline: [new MongoProjectStage({ userId: MongoAggFieldRef.of('_id') })],
+    });
+    const rewritten = stage.rewrite({ aggExpr }) as MongoLookupStage;
+    expect(
+      ((rewritten.pipeline![0] as MongoProjectStage).projection['userId'] as MongoAggFieldRef).path,
+    ).toBe('r._id');
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test.ts` around lines
211 - 225, The test case for MongoLookupStage rewrite misses verifying that
aggExpr-only rewrite contexts propagate into nested lookup.pipeline; update the
'rewrite() rewrites nested pipeline stages' test (referencing MongoLookupStage,
MongoMatchStage, and MongoFieldFilter) to include a pipeline stage that carries
an expression (e.g., a MongoProjectStage or a stage with an aggExpr-bearing
field) and assert that its rewrite(context with only aggExpr) is applied (for
example, transform an agg expression or resulting operator and assert the
rewritten pipeline stage reflects the change), ensuring rewrite(context) is
invoked for sub-pipeline stages when only aggExpr is provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts`:
- Around line 50-57: Replace the loose presence checks on stage.groupId with a
stronger object equality assertion: instead of two expect(...).toBeDefined()
calls, assert that stage.groupId matches the expected mapping (use
expect(stage.groupId).toMatchObject(...)) and compare the values to the exact
MongoAggFieldRef/MongoAggExpr instances you expect (e.g., dept should equal
MongoAggFieldRef.of('department') and year should equal
MongoAggFieldRef.of('year')), so the test verifies the resolved field
paths/expressions rather than just their existence; refer to MongoGroupStage,
groupId, MongoAggFieldRef, and MongoAggExpr to locate the code to change.

In `@packages/2-mongo-family/4-query/query-ast/test/stages.test.ts`:
- Around line 66-82: Add a test that exercises the no-op branch when projections
contain expression values but no aggExpr is provided: create a MongoProjectStage
whose projection includes a MongoAggFieldRef (e.g., fullName:
MongoAggFieldRef.of('name')), call stage.rewrite({}) and assert it returns the
same instance (toBe(stage)). This ensures MongoProjectStage.rewrite does not
assume context.aggExpr exists for expression-valued projections and covers the
MongoAggExpr/MongoAggFieldRef path without providing aggExpr.
- Around line 211-225: The test case for MongoLookupStage rewrite misses
verifying that aggExpr-only rewrite contexts propagate into nested
lookup.pipeline; update the 'rewrite() rewrites nested pipeline stages' test
(referencing MongoLookupStage, MongoMatchStage, and MongoFieldFilter) to include
a pipeline stage that carries an expression (e.g., a MongoProjectStage or a
stage with an aggExpr-bearing field) and assert that its rewrite(context with
only aggExpr) is applied (for example, transform an agg expression or resulting
operator and assert the rewritten pipeline stage reflects the change), ensuring
rewrite(context) is invoked for sub-pipeline stages when only aggExpr is
provided.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9b49cf55-3a3e-48d8-9198-5cfb20036f20

📥 Commits

Reviewing files that changed from the base of the PR and between a47a548 and 90ff16c.

📒 Files selected for processing (6)
  • packages/2-mongo-family/4-query/query-ast/src/stages.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages-extended.test.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/test/stages.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/lowering.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/2-mongo-family/4-query/query-ast/test/stages.test-d.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
  • packages/2-mongo-family/4-query/query-ast/src/stages.ts

@wmadden wmadden force-pushed the tml-2210-core-pipeline-stage-extensions-group-end-to-end branch from 90ff16c to 44fe826 Compare April 7, 2026 10:05
wmadden added 8 commits April 7, 2026 12:25
Stage rewrite() now accepts a context with optional filter and aggExpr
rewriters, enabling new stages that contain MongoAggExpr to recurse
into both filter and aggregation expression trees.
MongoGroupStage, MongoAddFieldsStage, MongoReplaceRootStage,
MongoCountStage, MongoSortByCountStage, MongoSampleStage, and
MongoRedactStage — each with kind discriminant, immutability,
accept/rewrite, and unit tests. MongoStageVisitor and MongoReadStage
union expanded to cover all new kinds.
MongoProjectStage accepts MongoAggExpr for computed projections.
MongoLookupStage supports correlated pipeline form with let_ and
optional localField/foreignField. MongoUnwindStage gains optional
includeArrayIndex. All extensions are backward compatible.
All 7 new stage classes and 2 new type aliases now exported from
@prisma-next/mongo-query-ast barrel.
lowerStage() now handles group, addFields, replaceRoot, count,
sortByCount, sample, and redact stages. Project stage lowering calls
lowerAggExpr for computed projection values. Lookup lowering emits let
for correlated pipelines. Unwind lowering includes includeArrayIndex.
Replace raw $group/$sort objects with MongoGroupStage and
MongoSortStage AST nodes, proving typed stages execute correctly
through the full runtime stack against mongodb-memory-server.
- F01: Add exhaustiveness guard default case to lowerStage()
- F02: Document MongoAggAccumulator.rewrite() cast limitation
- F03: Add constructor validation to MongoLookupStage (equality or pipeline required)
- F04: Add stages.test-d.ts with compile-time type tests for visitor/union contracts
- F05: Document structural discrimination convention for isAggExpr
- AC10: Add MongoMatchStage to integration test pipeline to match spec
- Use accept-based guard to discriminate MongoGroupId instead of kind check,
  preventing misclassification of compound _id records with a "kind" key
- Tighten MongoLookupStage validation: require localField/foreignField as a
  pair, require pipeline when let_ is provided
- Use double cast (as unknown as) for mock objects in type tests
- Split stages.test.ts into stages.test.ts (308 lines) and
  stages-extended.test.ts (349 lines) to stay under 500-line limit
- Extract prefixFieldRefRewriter helper to reduce test duplication
- Add regression tests for compound groupId with "kind" key in both
  query-ast and adapter-mongo lowering
@wmadden wmadden force-pushed the tml-2210-core-pipeline-stage-extensions-group-end-to-end branch from 44fe826 to bf0154d Compare April 7, 2026 10:27
@wmadden wmadden merged commit a61f079 into main Apr 7, 2026
16 checks passed
@wmadden wmadden deleted the tml-2210-core-pipeline-stage-extensions-group-end-to-end branch April 7, 2026 10:36
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