Skip to content

feat(mongo-query-ast): add aggregation expression AST#303

Merged
wmadden merged 12 commits intomainfrom
tml-2209-aggregation-expression-ast
Apr 7, 2026
Merged

feat(mongo-query-ast): add aggregation expression AST#303
wmadden merged 12 commits intomainfrom
tml-2209-aggregation-expression-ast

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented Apr 6, 2026

closes TML-2209

Key snippet (new capability)

// Typed aggregation expression: { $gt: ["$qty", "$minQty"] }
const expr = MongoAggOperator.of('$gt', [
  MongoAggFieldRef.of('qty'),
  MongoAggFieldRef.of('minQty'),
]);

// Bridge into filter context via $expr
const filter = MongoExprFilter.of(expr);

// Use in a $match stage — lowers to { $match: { $expr: { $gt: ["$qty", "$minQty"] } } }
const stage = new MongoMatchStage(filter);

Intent

Add a typed representation of MongoDB aggregation expressions — the expression language used inside pipeline stages like $group, $project, and $addFields — so that the pipeline builder (Milestone 4) has a foundation to construct computed values, and so that cross-field comparisons become possible via the $expr bridge into filter context.

Change map

The story

  1. Introduce the aggregation expression node classes. MongoDB's aggregation pipeline needs a way to represent computed values — field references ("$name"), arithmetic ({ $add: [...] }), conditionals ({ $cond: {...} }), array transforms ({ $map: {...} }), and more. Eleven concrete classes model the distinct structural shapes of MongoDB aggregation expressions, following the same patterns as the existing filter expression AST: hidden abstract base, kind discriminant, immutable frozen instances, static factories.

  2. Add visitor and rewriter interfaces. The exhaustive MongoAggExprVisitor<R> ensures every expression kind is handled (compile-time enforcement). The partial MongoAggExprRewriter enables selective bottom-up transformations — rewrite children first, then apply the hook for the current node. Both interfaces are wired into accept() and rewrite() on every node class.

  3. Implement lowering to MongoDB driver documents. lowerAggExpr() translates the typed AST into plain objects the MongoDB driver expects. Key behaviors: field refs get $-prefixed, ambiguous literals are wrapped in { $literal: ... }, operators dispatch between single-arg and array-arg forms, and the $count accumulator emits {}.

  4. Bridge aggregation expressions into filter context via MongoExprFilter. The $expr operator in MongoDB allows aggregation expressions inside $match — this is the only way to do cross-field comparisons in a filter. MongoExprFilter wraps a MongoAggExpr and joins the MongoFilterExpr union. The filter rewriter treats the inner aggregation expression as opaque (intentional: the two rewriter systems are independent).

  5. Validate end-to-end with integration tests. Three integration tests execute $expr-based filters through the full runtime pipeline against mongodb-memory-server: a simple cross-field comparison (qty > minQty), nested arithmetic (price > discount * 2), and a combined $and with both regular and $expr filters.

Behavior changes & evidence

Compatibility / migration / risk

  • MongoFilterVisitor<R> gains a required expr() method. Any existing implementations of MongoFilterVisitor (in-repo or external) must add this method. This is a compile-time breaking change for visitor consumers. Within this repo, all consumers are updated in this branch.

  • MongoFilterExpr union is extended. Exhaustive switches on MongoFilterExpr.kind must add a case 'expr' arm. This is also a compile-time breaking change for exhaustive consumers.

  • lowerFilter() is extended. The new case 'expr' arm calls lowerAggExpr(), introducing a runtime dependency between filter lowering and aggregation expression lowering. This is a forward-only change with no backward compatibility concern.

  • No regressions in existing filter, stage, or command behavior. The changes are additive.

Follow-ups / open questions

  • Accumulator integration testing: MongoAggAccumulator nodes are unit-tested and lowering-tested, but cannot be integration-tested until MongoGroupStage exists (Milestone 3).
  • Plan doc naming: The plan uses MongoAggFilter but the implementation uses MongoAggArrayFilter. The plan should be updated.
  • MongoAggLiteral.value type: Design doc says MongoValue, implementation uses unknown. A decision should be recorded.

Non-goals / intentionally out of scope

  • New pipeline stages (MongoGroupStage, MongoAddFieldsStage, etc.) — Milestone 3
  • Pipeline builder with shape tracking — Milestone 4
  • Pipeline-style updates — Milestone 5

Summary by CodeRabbit

  • New Features
    • Added a complete aggregation-expression system with visitor/rewriter support and $expr-based filters enabling cross-field comparisons, arithmetic, conditionals, array transforms, let-bindings and object merges.
  • Public API
    • Made aggregation-expression types and a lowering entrypoint available so expressions can be translated to MongoDB syntax.
  • Tests
    • Added unit, type-level, and integration tests for construction, immutability, rewriting, lowering, and $expr behavior; enabled typechecking for tests.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 6, 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
📝 Walkthrough

Walkthrough

Adds an immutable MongoDB aggregation-expression AST (11 node kinds) with visitor/rewriter support, extends filter AST with an expr node, implements lowering of agg AST to MongoDB expression documents (lowerAggExpr) and integrates it into lowerFilter, and adds comprehensive type, unit, and integration tests for $expr behavior.

Changes

Cohort / File(s) Summary
Aggregation Expression AST
packages/2-mongo-family/4-query/query-ast/src/aggregation-expressions.ts
Adds 11 immutable aggregation-expression node classes (fieldRef, literal, operator, accumulator, cond, switch, array filter, map, reduce, let, mergeObjects) with accept and rewrite protocols, factory helpers, validation, and a MongoAggExpr union.
Visitor / Rewriter Types
packages/2-mongo-family/4-query/query-ast/src/visitors.ts
Adds MongoAggExprVisitor (required handlers) and MongoAggExprRewriter (optional hooks) for all agg node kinds; extends MongoFilterVisitor/MongoFilterRewriter with expr handler/hook.
Filter Expression Extension
packages/2-mongo-family/4-query/query-ast/src/filter-expressions.ts
Adds MongoExprFilter (kind: 'expr') wrapping a MongoAggExpr, with visitor/rewriter support and inclusion in MongoFilterExpr union.
Public Exports
packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
Re-exports new agg AST classes/types, MongoExprFilter, and agg visitor/rewriter types.
AST Type Tests
packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test-d.ts
Adds compile-time type assertions for MongoAggExpr union, visitor/rewriter typings, discriminant exhaustiveness, and structural checks.
AST Runtime Tests
packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test.ts
Adds runtime tests for construction, validation (e.g., non-empty field paths), immutability, visitor dispatch, and rewrite traversal/order across agg nodes.
Filter Type & Runtime Tests
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
Adds type-checks and runtime tests for MongoExprFilter, visitor dispatch, and rewriter behavior.
Vitest Typecheck Config
packages/2-mongo-family/4-query/query-ast/vitest.config.ts
Enables vitest typechecking for test/**/*.test-d.ts.
Lowering Implementation
packages/3-mongo-target/2-mongo-adapter/src/lowering.ts, packages/3-mongo-target/2-mongo-adapter/src/exports/index.ts
Adds lowerAggExpr(expr) to convert agg AST to MongoDB expression documents (handles field refs, literal-wrapping heuristics, operators/accumulators, $cond/$switch, $filter/$map/$reduce, $let, $mergeObjects) and updates lowerFilter to emit { $expr: ... }; re-exports lowerAggExpr.
Lowering Tests
packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
Adds tests validating lowering of field refs, literal-wrapping rules, operators/accumulators (including nullary count), control/array/let/merge ops, and MongoExprFilter lowering.
Integration Tests
test/integration/test/mongo/expr-filter.test.ts
Adds end-to-end Mongo integration tests for $expr filters: cross-field comparisons, arithmetic expressions, and combining $expr with standard predicates.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant AggAST as Aggregation\nExpression AST
    participant Visitor as Visitor/\nRewriter
    participant Lowering as lowerAggExpr
    participant Mongo as MongoDB

    Client->>AggAST: construct immutable expression nodes
    Client->>Visitor: expr.accept(visitor)
    Visitor->>Visitor: dispatch node-specific handler
    Visitor-->>Client: handler result

    Client->>Lowering: lowerAggExpr(expr)
    Lowering->>Visitor: traverse via visitor
    Visitor-->>Lowering: lowered subexpressions
    Lowering->>Lowering: assemble Mongo expression doc
    Lowering-->>Mongo: return expression document
Loading
sequenceDiagram
    participant QueryLayer as Query Layer
    participant ExprFilter as MongoExprFilter
    participant Lowering as lowerFilter
    participant LowerAgg as lowerAggExpr
    participant Mongo as MongoDB

    QueryLayer->>ExprFilter: create filter with aggExpr
    QueryLayer->>Lowering: lowerFilter(filter)
    Lowering->>ExprFilter: inspect kind === 'expr'
    Lowering->>LowerAgg: lowerAggExpr(aggExpr)
    LowerAgg-->>Lowering: expression document
    Lowering-->>Mongo: return { $match: { $expr: ... } } or match doc
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble nodes and hop through trees,

I bind my vars and chase the breeze,
Visitors call and rewrites play,
I lower docs to light the way,
$expr glows bright in query-days.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and concisely summarizes the main change: adding aggregation expression AST support to mongo-query-ast package.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2209-aggregation-expression-ast

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 6, 2026

Open in StackBlitz

@prisma-next/mongo-orm

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

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

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

@prisma-next/runtime-executor

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/adapter-postgres

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

@prisma-next/driver-postgres

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

commit: d1c8289

@wmadden wmadden force-pushed the tml-2209-aggregation-expression-ast branch from 5ed0d2b to 1459b2f Compare April 6, 2026 16:06
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: 1

🧹 Nitpick comments (2)
packages/2-mongo-family/4-query/query-ast/src/aggregation-expressions.ts (1)

116-123: Rewrite always creates new nodes even when unchanged.

The rewrite() method creates a new MongoAggOperator instance even when no rewriter hooks modify anything. This is consistent with the other node types and keeps the implementation simple, but for performance-sensitive scenarios with large expression trees, you could consider returning this when children are unchanged (referential equality check).

This is fine as-is for correctness; flagging as an optional consideration if performance becomes a concern.

🤖 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/aggregation-expressions.ts`
around lines 116 - 123, The rewrite method currently always constructs a new
MongoAggOperator even when children are unchanged; change rewrite(rewriter:
MongoAggExprRewriter) to compute rewrittenArgs (as you already do) then check
referential equality: if args is an array ensure every rewrittenArgs[i] ===
args[i], or if args is not an array check rewrittenArgs === args; in that
unchanged case return rewriter.operator ? rewriter.operator(this) : this;
otherwise proceed to create const rebuilt = new MongoAggOperator(this.op,
rewrittenArgs) and return rewriter.operator ? rewriter.operator(rebuilt) :
rebuilt. This uses the existing symbols rewrite, this.args, MongoAggOperator and
rewriter.operator.
packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test.ts (1)

1-621: Comprehensive runtime test coverage.

The test suite thoroughly validates:

  • Node construction and immutability (Object.isFrozen checks)
  • Input validation (empty path rejection)
  • Visitor dispatch for all 11 node types
  • Rewriter behavior including deep nesting and null-arg preservation

The file exceeds the 500-line guideline at 621 lines. Consider splitting into aggregation-expressions.nodes.test.ts, aggregation-expressions.visitor.test.ts, and aggregation-expressions.rewriter.test.ts if this grows further, though the current structure is logically cohesive.

🤖 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/aggregation-expressions.test.ts`
around lines 1 - 621, The test file is 621 lines and exceeds the 500-line
guideline; split it into three focused test files: move all
construction/immutability and validation tests (describes referencing
MongoAggFieldRef, MongoAggLiteral, MongoAggOperator, MongoAggAccumulator,
MongoAggCond, MongoAggSwitch, MongoAggArrayFilter, MongoAggMap, MongoAggReduce,
MongoAggLet, MongoAggMergeObjects) into aggregation-expressions.nodes.test.ts;
move the visitor tests that use the kindVisitor and assertions invoking accept
on MongoAggFieldRef, MongoAggLiteral, MongoAggOperator, MongoAggAccumulator,
MongoAggCond, MongoAggSwitch, MongoAggArrayFilter, MongoAggMap, MongoAggReduce,
MongoAggLet, MongoAggMergeObjects into aggregation-expressions.visitor.test.ts;
and move all rewriter tests that reference MongoAggExprRewriter, rewrite calls
on MongoAggFieldRef.rewrite, MongoAggOperator.rewrite,
MongoAggAccumulator.rewrite, MongoAggCond.rewrite, MongoAggSwitch.rewrite,
MongoAggArrayFilter.rewrite, MongoAggMap.rewrite, MongoAggReduce.rewrite,
MongoAggLet.rewrite, MongoAggMergeObjects.rewrite into
aggregation-expressions.rewriter.test.ts, ensuring each new file includes the
same imports (MongoAgg* types and visitors/rewriter types) and that describe/it
blocks are preserved unchanged.
🤖 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 104-111: The needsLiteralWrap function currently only checks
top-level strings and object keys, missing '$' occurrences inside nested
objects/arrays; update needsLiteralWrap to recursively traverse values (handle
arrays, plain objects, and primitive strings) and return true if any encountered
string startsWith('$'), using the existing function name needsLiteralWrap to
locate and replace the logic so that MongoAggLiteral.of([...]) and
MongoAggLiteral.of({label: '$qty'}) are correctly detected and wrapped.

---

Nitpick comments:
In `@packages/2-mongo-family/4-query/query-ast/src/aggregation-expressions.ts`:
- Around line 116-123: The rewrite method currently always constructs a new
MongoAggOperator even when children are unchanged; change rewrite(rewriter:
MongoAggExprRewriter) to compute rewrittenArgs (as you already do) then check
referential equality: if args is an array ensure every rewrittenArgs[i] ===
args[i], or if args is not an array check rewrittenArgs === args; in that
unchanged case return rewriter.operator ? rewriter.operator(this) : this;
otherwise proceed to create const rebuilt = new MongoAggOperator(this.op,
rewrittenArgs) and return rewriter.operator ? rewriter.operator(rebuilt) :
rebuilt. This uses the existing symbols rewrite, this.args, MongoAggOperator and
rewriter.operator.

In
`@packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test.ts`:
- Around line 1-621: The test file is 621 lines and exceeds the 500-line
guideline; split it into three focused test files: move all
construction/immutability and validation tests (describes referencing
MongoAggFieldRef, MongoAggLiteral, MongoAggOperator, MongoAggAccumulator,
MongoAggCond, MongoAggSwitch, MongoAggArrayFilter, MongoAggMap, MongoAggReduce,
MongoAggLet, MongoAggMergeObjects) into aggregation-expressions.nodes.test.ts;
move the visitor tests that use the kindVisitor and assertions invoking accept
on MongoAggFieldRef, MongoAggLiteral, MongoAggOperator, MongoAggAccumulator,
MongoAggCond, MongoAggSwitch, MongoAggArrayFilter, MongoAggMap, MongoAggReduce,
MongoAggLet, MongoAggMergeObjects into aggregation-expressions.visitor.test.ts;
and move all rewriter tests that reference MongoAggExprRewriter, rewrite calls
on MongoAggFieldRef.rewrite, MongoAggOperator.rewrite,
MongoAggAccumulator.rewrite, MongoAggCond.rewrite, MongoAggSwitch.rewrite,
MongoAggArrayFilter.rewrite, MongoAggMap.rewrite, MongoAggReduce.rewrite,
MongoAggLet.rewrite, MongoAggMergeObjects.rewrite into
aggregation-expressions.rewriter.test.ts, ensuring each new file includes the
same imports (MongoAgg* types and visitors/rewriter types) and that describe/it
blocks are preserved unchanged.
🪄 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: 1ed1c555-8183-4f84-8a25-e6616d9ea702

📥 Commits

Reviewing files that changed from the base of the PR and between 8541d7e and 1459b2f.

⛔ 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 (13)
  • 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/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/vitest.config.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-2209-aggregation-expression-ast branch from ec57723 to 2cc70dd Compare April 6, 2026 16:22
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/3-mongo-target/2-mongo-adapter/src/lowering.ts (2)

124-138: Consider adding exhaustiveness guard to lowerFilter switch.

The switch lacks a default case or assertNever guard. If a new filter kind is added to MongoFilterExpr, TypeScript won't error here—the function will silently return undefined. An exhaustiveness check ensures compile-time safety.

♻️ Proposed fix
     case 'expr':
       return { $expr: lowerAggExpr(filter.aggExpr) };
+    default: {
+      const _exhaustive: never = filter;
+      throw new Error(`Unhandled filter kind: ${(_exhaustive as MongoFilterExpr).kind}`);
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-mongo-target/2-mongo-adapter/src/lowering.ts` around lines 124 -
138, The switch in lowerFilter (function lowerFilter, type MongoFilterExpr) is
missing an exhaustiveness guard; add a default path that calls an
assertNever-like helper (or throws) to ensure the compiler errors if a new
filter kind is added. Implement or reuse a helper such as assertNever(x: never)
and invoke it in a final default block (e.g., default: return
assertNever(filter)), or explicitly throw an Error including the unknown kind,
so lowerFilter never silently returns undefined.

11-15: Minor duplication of isExprArray type guard.

This function is duplicated from aggregation-expressions.ts. While the duplication is minor and acceptable for package isolation (query-ast vs adapter), consider extracting to a shared utility if the pattern expands.

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

In `@packages/3-mongo-target/2-mongo-adapter/src/lowering.ts` around lines 11 -
15, The isExprArray type guard is duplicated; to fix, remove the duplicate
definition in lowering.ts and instead import the single exported helper from the
shared location (e.g., aggregation-expressions.ts or a new utilities module) so
the adapter reuses that implementation; update lowering.ts to import isExprArray
and delete the local function definition, or create a new shared util (e.g.,
src/utils/isExprArray) and import it from both aggregation-expressions.ts and
lowering.ts to avoid future divergence.
🤖 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/3-mongo-target/2-mongo-adapter/src/lowering.ts`:
- Around line 124-138: The switch in lowerFilter (function lowerFilter, type
MongoFilterExpr) is missing an exhaustiveness guard; add a default path that
calls an assertNever-like helper (or throws) to ensure the compiler errors if a
new filter kind is added. Implement or reuse a helper such as assertNever(x:
never) and invoke it in a final default block (e.g., default: return
assertNever(filter)), or explicitly throw an Error including the unknown kind,
so lowerFilter never silently returns undefined.
- Around line 11-15: The isExprArray type guard is duplicated; to fix, remove
the duplicate definition in lowering.ts and instead import the single exported
helper from the shared location (e.g., aggregation-expressions.ts or a new
utilities module) so the adapter reuses that implementation; update lowering.ts
to import isExprArray and delete the local function definition, or create a new
shared util (e.g., src/utils/isExprArray) and import it from both
aggregation-expressions.ts and lowering.ts to avoid future divergence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: cf115c1b-280d-4e60-a8d9-cf6eb6ffd2a1

📥 Commits

Reviewing files that changed from the base of the PR and between ec57723 and 2cc70dd.

⛔ 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 (13)
  • 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/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/vitest.config.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
✅ Files skipped from review due to trivial changes (5)
  • packages/2-mongo-family/4-query/query-ast/vitest.config.ts
  • packages/3-mongo-target/2-mongo-adapter/src/exports/index.ts
  • test/integration/test/mongo/expr-filter.test.ts
  • packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/src/exports/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/2-mongo-family/4-query/query-ast/test/filter-expressions.test-d.ts
  • packages/2-mongo-family/4-query/query-ast/src/filter-expressions.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.

Actionable comments posted: 1

🤖 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/test/lowering.test.ts`:
- Around line 335-377: The tests assert single-$ field refs for scoped variables
but Mongo requires $$ for vars created by $filter/$map/$reduce/$let; update the
lowering so lowerAggExpr becomes context-aware: when descending into
MongoAggArrayFilter (MongoAggArrayFilter.of), MongoAggMap.of, MongoAggReduce.of
and any MongoAggLet node, pass the scoped-variable name (the "as" param or
accumulator name) and when lowering MongoAggFieldRef resolve to a scoped var
reference by prefixing its root with '$$' (e.g., 'score' -> '$$score',
'item.price' -> '$$item.price', 'value'/'this' -> '$$value'/'$$this') instead of
single '$'; adjust test expectations accordingly.
🪄 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: 0d6004d0-abe7-40bd-a3f3-7f5f5c787d27

📥 Commits

Reviewing files that changed from the base of the PR and between 2cc70dd and 2dc966e.

📒 Files selected for processing (2)
  • 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 (1)
  • packages/3-mongo-target/2-mongo-adapter/src/lowering.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 (1)
packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts (1)

292-333: Simplify $cond and $switch test assertions.

The Object.fromEntries with a thenKey variable adds unnecessary indirection. The keyword then is valid in object literals and doesn't conflict with Promise semantics in this context.

♻️ Proposed simplification
   it('lowers $cond', () => {
     const expr = MongoAggCond.of(
       MongoAggOperator.of('$gte', [MongoAggFieldRef.of('age'), MongoAggLiteral.of(18)]),
       MongoAggLiteral.of('adult'),
       MongoAggLiteral.of('minor'),
     );
-    const thenKey = 'then';
     expect(lowerAggExpr(expr)).toEqual({
-      $cond: Object.fromEntries([
-        ['if', { $gte: ['$age', 18] }],
-        [thenKey, 'adult'],
-        ['else', 'minor'],
-      ]),
+      $cond: {
+        if: { $gte: ['$age', 18] },
+        then: 'adult',
+        else: 'minor',
+      },
     });
   });

   it('lowers $switch', () => {
     const expr = MongoAggSwitch.of(
       [
         {
           case_: MongoAggOperator.of('$eq', [
             MongoAggFieldRef.of('status'),
             MongoAggLiteral.of('active'),
           ]),
           then_: MongoAggLiteral.of('Active'),
         },
       ],
       MongoAggLiteral.of('Unknown'),
     );
-    const thenKey = 'then';
     expect(lowerAggExpr(expr)).toEqual({
       $switch: {
-        branches: [
-          Object.fromEntries([
-            ['case', { $eq: ['$status', 'active'] }],
-            [thenKey, 'Active'],
-          ]),
-        ],
+        branches: [{ case: { $eq: ['$status', 'active'] }, then: 'Active' }],
         default: 'Unknown',
       },
     });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts` around lines
292 - 333, The tests for lowering $cond and $switch use an unnecessary
indirection with Object.fromEntries and a thenKey variable; update the
assertions in lowering.test.ts to use direct object literals with the explicit
"then" property (remove thenKey and Object.fromEntries) so the expected outputs
for lowerAggExpr(MongoAggCond.of(...)) and lowerAggExpr(MongoAggSwitch.of(...))
assert { $cond: { if: ..., then: 'adult', else: 'minor' } } and { $switch: {
branches: [{ case: ..., then: 'Active' }], default: 'Unknown' } } respectively,
leaving the MongoAggCond, MongoAggSwitch, and lowerAggExpr identifiers
unchanged.
🤖 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/3-mongo-target/2-mongo-adapter/test/lowering.test.ts`:
- Around line 292-333: The tests for lowering $cond and $switch use an
unnecessary indirection with Object.fromEntries and a thenKey variable; update
the assertions in lowering.test.ts to use direct object literals with the
explicit "then" property (remove thenKey and Object.fromEntries) so the expected
outputs for lowerAggExpr(MongoAggCond.of(...)) and
lowerAggExpr(MongoAggSwitch.of(...)) assert { $cond: { if: ..., then: 'adult',
else: 'minor' } } and { $switch: { branches: [{ case: ..., then: 'Active' }],
default: 'Unknown' } } respectively, leaving the MongoAggCond, MongoAggSwitch,
and lowerAggExpr identifiers unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 4c0c03d5-925d-4474-ae42-be9558ef41e3

📥 Commits

Reviewing files that changed from the base of the PR and between 2dc966e and 2774ff1.

📒 Files selected for processing (2)
  • packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test.ts
  • packages/3-mongo-target/2-mongo-adapter/test/lowering.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/2-mongo-family/4-query/query-ast/test/aggregation-expressions.test.ts

wmadden added 12 commits April 6, 2026 22:23
Defines the implementation approach for the MongoAggExpr class hierarchy
in @prisma-next/mongo-query-ast: 11 concrete expression node classes,
visitor/rewriter interfaces, lowerAggExpr() lowering, and the
MongoExprFilter bridge for $expr in $match.
Add dedicated type test section verifying union exhaustiveness, visitor
exhaustiveness, rewriter optionality, and structural type constraints.
Add integration test section for $expr cross-field comparisons against
mongodb-memory-server. Include test file inventory and note that $group
integration tests follow in Milestone 3.
Introduce 11 aggregation expression AST node classes in
@prisma-next/mongo-query-ast: MongoAggFieldRef, MongoAggLiteral,
MongoAggOperator, MongoAggAccumulator, MongoAggCond, MongoAggSwitch,
MongoAggArrayFilter, MongoAggMap, MongoAggReduce, MongoAggLet, and
MongoAggMergeObjects.

Add MongoAggExprVisitor<R> (exhaustive) and MongoAggExprRewriter
(optional hooks) interfaces to visitors.ts. All node classes implement
accept() and rewrite() following the established filter expression
pattern (bottom-up rewriting for container nodes).

72 unit tests cover construction, freezing, kind discriminants,
static helpers, visitor dispatch, and rewriter transforms including
deep nesting.
Verify compile-time guarantees: MongoAggExpr union exhaustiveness
(all 11 kinds), MongoAggExprVisitor requires all methods, rewriter
accepts empty object, accept() returns R, rewrite() returns the
union type, and structural constraints on args/branches/vars.

Enable vitest typecheck in query-ast vitest config. Fix
MongoAggOperator.rewrite() narrowing with an explicit type guard
for ReadonlyArray vs single-expression args.
Implement lowerAggExpr() as a MongoAggExprVisitor<unknown> in the
adapter lowering module. Handles all 11 node types: field refs get
$-prefix, literals pass through (with $literal wrapping for
ambiguous values), operators preserve single-arg vs array-arg form,
accumulators emit {} for $count, and structural nodes produce their
specific MongoDB shapes with recursively lowered children.

Export all MongoAggExpr types and visitor/rewriter interfaces from
@prisma-next/mongo-query-ast. Export lowerAggExpr from
@prisma-next/adapter-mongo.
Introduce MongoExprFilter, a filter expression node that wraps a
MongoAggExpr to enable aggregation expressions inside $match stages
via the $expr operator. This bridges the two expression systems,
enabling cross-field comparisons and computed predicates in filters.

Add expr() to MongoFilterVisitor and MongoFilterRewriter interfaces.
Update lowerFilter() to handle the new expr kind. Export
MongoExprFilter from @prisma-next/mongo-query-ast. Add unit tests,
type tests, and lowering tests.
Verify end-to-end execution of MongoExprFilter against a live
MongoDB instance: cross-field comparison ($gt with two field refs),
computed expression with nested arithmetic ($multiply inside $gt),
and $expr combined with a regular MongoFieldFilter via $and.
Reconcile MongoAggFilter→MongoAggArrayFilter naming across plan, design,
and parent plan docs (NB-F02). Add Biome noThenProperty rationale comment
on THEN_KEY/condBranch (NB-F03). Update design doc to reflect the
intentional unknown (not MongoValue) type for MongoAggLiteral.value
(NB-F04). Add empty-path guard to MongoAggFieldRef constructor with
matching test (NB-F05).
describeWithMongoDB drops the database in beforeEach, so tests sharing a
collection name are isolated. Make this visible at the test site.
Array.isArray does not narrow ReadonlyArray out of a union with
non-array types. Use the same isExprArray type guard pattern as in
aggregation-expressions.ts.
…ss guard

needsLiteralWrap now recursively checks arrays and object values for
$-prefixed strings, not just top-level keys. Previously, literals like
["$qty"] or {label: "$qty"} would be emitted without $literal wrapping,
causing MongoDB to misinterpret them as field references.

Also adds a never-typed default case to lowerFilter switch for
compile-time exhaustiveness.
MongoDB scoped variables ($filter as, $map as, $reduce $$value/$$this,
$let vars) require $$ prefix. MongoAggFieldRef.of("$score") lowers to
"$$score" since lowering prepends $. Updated tests to use this convention
so lowered output matches correct MongoDB semantics.
@wmadden wmadden force-pushed the tml-2209-aggregation-expression-ast branch from 2774ff1 to d1c8289 Compare April 6, 2026 20:23
@wmadden wmadden merged commit d345bd8 into main Apr 7, 2026
16 checks passed
@wmadden wmadden deleted the tml-2209-aggregation-expression-ast branch April 7, 2026 05:47
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