feat($lookup): sub-pipeline + let (correlated lateral)#5
Merged
Conversation
LookupStage gains `let?: Record<string, Expr>` and uses the existing `pipeline?: Stage[]` slot. `localField` is now optional; at least one of localField/pipeline is required (enforced at compile). CTE-chaining made reentrant (`chainCtes(stages, opts, prefix, outerVars)` in src/stages.ts). The sub-pipeline compiles to its own `WITH lk0..lkN` chain inside the LATERAL subquery, threading `let` bindings through StageInput.outerVars -> prevCtx so `$$var` resolves to the correlated outer-row reference. `localField`/`foreignField` and `pipeline` compose: the equality predicate is ANDed onto the sub-pipeline's final select. Typed builder gets a second overload accepting a builder callback `pipeline: (q: Pipeline<TFrom>) => Pipeline<TOut>` with result element type `TOut`. The non-generic implementation signature converts the callback into Stage[] by running it against a fresh sub-builder. Tests: - 2 new unit tests in test/pipeline.test.ts assert the nested WITH shape, the lk0..lkN naming, the `__sub` wrapper, and the `"s0".doc #>` correlation back to the outer CTE. - New test/integration/lookup-pipeline.test.ts proves Postgres accepts the correlated nested WITH inside the LATERAL (3 cases: let+$expr, localField+pipeline combo, classic equality unchanged). README documents the new form, the compiled SQL shape, and the all-three-type-params caveat. CLAUDE.md notes updated. 84 pass / 0 fail. tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
$lookupwith a sub-pipeline +let-bindings, the one remaining "intentionally not implemented" item that was philosophically aligned with the library (compiles to CTEs + a lateral aggregate, no hidden round-trips, stages stay serializable).Design
LookupStage.localFieldis now optional; newlet?: Record<string, Expr>; existingpipeline?: Stage[]is now actually wired up. At least one oflocalField/pipelineis required (enforced incompileLookup).chainCtes(stages, opts, prefix, outerVars)insrc/stages.ts. The sub-pipeline gets its ownlk0..lkNchain inside theLATERALsubquery. Same one-stage-one-CTE invariant the outer pipeline has, stillEXPLAIN-inspectable.letthreading:StageInput.outerVarsis merged into every sub-stage'sprevCtx, so$$pidresolves through the existing$$-var machinery incompileExpr. Theletexpressions are compiled against the outer row, producing correlated references the LATERAL carries into the nestedWITH.localField/foreignFieldandpipelineare present, the equality predicate is ANDed onto the sub-pipeline's finalselect(Mongo 4.4+ semantics)..lookup()now has two overloads — the existing<TFrom, TAs>equality form (unchanged) and a new<TFrom, TOut, TAs>callback formpipeline: (q: Pipeline<TFrom>) => Pipeline<TOut>. Result key type isTOut[]. The non-generic implementation signature converts the callback toStage[]by running it against a fresh sub-builder rooted atfrom.Compiled SQL
Tests
test/pipeline.test.ts— tightened the existingemits lateral jointo assert the parenthesized lateral subquery (left join lateral (…) __lk on true); two new unit tests assert the nested-WITHshape,lk0..lk3naming, the__subwrapper, the"s0".doc #>correlation reference, and thelocalField-ANDed-with-pipeline form.test/integration/lookup-pipeline.test.ts— new file, three end-to-end cases against real Postgres: correlatedlet+$expr(latest comment per post),localField/foreignFieldAND-ed with a sub-pipeline (sorted comments per post), and classic equality$lookupunchanged. The correlated case is the load-bearing one — it proves Postgres accepts the nestedWITHinside theLATERALreferencing outer-row columns, which was the one design question I'd flagged for real-DB verification.bun test→ 84 pass / 0 fail.bunx tsc --noEmitclean.Docs
$lookupwith apipelinesub-stage" from What's not (yet) supported; new### $lookup with a sub-pipelinesubsection with the callback example, the compiled-SQL sketch, theletcorrelation note, and the all-three-type-params caveat.CLAUDE.md— updated What's intentionally not implemented and Type-inference quirks.Notes
The "supply all type params" requirement on the typed builder is unavoidable TS partial-inference behavior — same trap the existing equality
.lookup<TFrom, TAs>already has, just with one extra slot. Documented in README and CLAUDE.md.🤖 Generated with Claude Code