import type { Expr, Pipeline, UpdateFilter } from "./ast.ts"; import { type GenerateCtx } from "./codegen.ts"; import { type MatchTranslation } from "./match-translation.ts"; type Lambda = Extract; /** * Caller-supplied sub-pipeline lowerer. lookup-translation lives "below" * pipeline.ts in the dependency graph (pipeline.ts imports from here), * so a direct import back into pipeline.ts would be circular. The * top-level orchestrator in pipeline.ts wires this through when it * invokes `lowerLookup` / `extractLookupCalls`. */ export type SubPipelineLowerer = (block: Pipeline, ctx: GenerateCtx) => object[]; /** * State carried into a lookup translation when it's *nested* inside another * lookup's predicate. Empty at the top level; populated as we recurse. * * - `foreignParams` — every enclosing lookup's lambda param name, outermost * first. References to these (e.g. `o.x`, `o.user._id`) need to lower as * paths on the local doc of the enclosing pipeline, so we pre-rewrite them * to `FieldRef()` before the inner's let-extractor runs. The * extractor then auto-lets them into the inner's `$lookup.let` clause * exactly like ordinary `$.x` refs. * - `inScopeLetNames` — let-var names already visible via the enclosing * `$lookup.let` clauses. MQL `$$` is lexically scoped through nested * `$lookup.pipeline` boundaries, so the inner shouldn't re-let them. We * thread the names into the inner's `lambdaParams` so codegen emits * `$$` instead of throwing `UnknownIdentifier`. */ export type EnclosingLookupContext = { foreignParams: ReadonlyArray; inScopeLetNames: ReadonlySet; }; export declare const EMPTY_ENCLOSING: EnclosingLookupContext; export type LookupCall = { /** Position of the context-ref prefix `$$$` / `$$$$` (for errors). */ pos: number; /** Position of the method call (for errors specific to the call shape). */ callPos: number; /** * Database name extracted from `$$$$..` (any bracket combination). * Undefined for the same-database form `$$$.` — in which case * `$lookup.from` is emitted as a bare collection-name string. When set, * `$lookup.from` is emitted as `{ db, coll }` — the Atlas Data Federation * shape for cross-database joins. See docs/specs/lookup-stage.md. */ db?: string; /** Collection name extracted from `.` or `[""]`. */ collection: string; /** `.find` returns scalar-or-null; `.filter` returns an array. */ method: "find" | "filter"; /** The predicate lambda. May be expression-body OR block-body. */ lambda: Lambda; }; /** Extracted target of a lookup-shaped receiver: `$$$.` or `$$$$..`. */ export type LookupTarget = { pos: number; db?: string; collection: string; }; /** * Walk back through one or two levels of static (dot or string-bracket) access * to recognise the four lookup-receiver shapes: * * | Source | AST shape (outermost first) | * | ----------------------- | ----------------------------------------------------------- | * | `$$$.` | one-level access onto `DatabaseRef` | * | `$$$[""]` | one-level bracket-access onto `DatabaseRef` | * | `$$$$..` | two-level access; inner onto `ClusterRef` | * | `$$$$["db"]["coll"]` | two-level bracket access; inner onto `ClusterRef` | * | `$$$$.db["coll"]` | two-level mixed (bracket outer, dot inner); onto `ClusterRef` | * | `$$$$["db"].coll` | two-level mixed (dot outer, bracket inner); onto `ClusterRef` | * * Non-static indices (`$$$$[someVar].coll`) break the classification — we * can't materialise a runtime-computed db/coll name into `$lookup.from` * (the field is a compile-time string in MQL). Returns null for those * shapes; the bare-reference codegen path then surfaces the standard * actionable error. */ export declare function extractLookupTarget(receiver: Expr, ctx: GenerateCtx): LookupTarget | null; /** * Recognise `$$$..find(pred)` / `$$$..filter(pred)` / * `$$$$...find(pred)` / `$$$$...filter(pred)` and all * their bracket variants — including `jsmql.compile`-parameter-bound * bracket indices (resolved through `ctx.bindings`). Returns `null` if * `expr` is not the shape, or if the shape is malformed (which surfaces * an actionable error elsewhere — see `validateLookupShape`). * * Callers in mode-gate / position-locator code paths that don't have a * meaningful `ctx` pass `EMPTY_CTX` — bound-bracket lookups won't be * detected from those paths, but those paths only run in expression * contexts (Filter / `jsmql.expr` / `jsmql.update`) where lookups would * be rejected wholesale anyway, so the trade-off is benign. */ export declare function detectLookupCall(expr: Expr, ctx: GenerateCtx): LookupCall | null; /** * Cheap recursive walk: does `node` (or any sub-tree thereof) contain a * lookup call? Used by mode-gates in `index.ts` to pre-reject lookup * syntax in Filter / expression / update modes with an actionable error, * and by `lowerWithCtx` to detect lookup-bearing `UpdateFilter` inputs so * they can be rerouted through the lookup-aware pipeline lowerer. * * `ctx` defaults to `EMPTY_CTX` for the mode-gate call sites that don't * have a meaningful context. When the caller has the real ctx * (`lowerWithCtx`, `rejectNestedLookup`), pass it so bound bracket-index * lookups (`$$$[boundParam].find(...)`) detect correctly — without the * ctx, the binding can't resolve and the detection silently fails. */ export declare function containsLookupCall(node: Expr | Pipeline | UpdateFilter, ctx?: GenerateCtx): boolean; /** * If `expr` looks like a lookup but is malformed (wrong method, wrong arity, * non-lambda arg), throw the targeted error. Used by the prologue extractor * before falling through to a generic walk. */ export declare function validateLookupShape(expr: Expr): void; /** * Per-pipeline counter shared across `extractLookupCalls` invocations so * `__jsmql.__lookup1` / `__lookup2` / … stay distinct within one pipeline. * The caller owns the counter. */ export type SlotAllocator = () => string; export declare function createSlotAllocator(): SlotAllocator; export type BasicFormPredicate = { kind: "basic"; localField: string; foreignField: string; }; export type PipelineFormPredicate = { kind: "pipeline"; /** let-vars to expose at the $lookup level. Key = letVarName; value = MQL field path string (e.g. "$_id"). */ letVars: Record; /** Stages making up the $lookup.pipeline body. */ pipeline: object[]; }; /** * Translate a lookup-call's lambda into either the basic-form fields * (when the body is a single `===` between a foreign-path and a `$.` * local-path) or the correlated-pipeline form (everything else). * * `outerCtx` is the GenerateCtx the consuming pipeline is running under * — passed through so sub-pipeline codegen sees the same function-form * `bindings` (compile-time constants cross sub-pipeline boundaries; lets * do not, per `freshSubPipelineCtx`). */ export declare function translatePredicate(call: LookupCall, outerCtx: GenerateCtx, lowerBlock: SubPipelineLowerer, enclosing?: EnclosingLookupContext): BasicFormPredicate | PipelineFormPredicate; /** * Translate a `.filter()` predicate into the pipeline-form * components — `{ letVars, pipelineBody }` — usable as a `$lookup.let` + * `$lookup.pipeline` payload OR as the seed of a longer sub-pipeline that * chain methods will extend. Exported so callers outside this module * (`pipeline.ts`'s lookup-pivot and chain-extension paths) can build * pipeline-form lookups without re-implementing the predicate translation. * * Same algorithm as `translatePredicate`'s pipeline branch — expression * bodies route through `extractLetsFromExpr` (auto-`let` extraction + * foreign-path rewriting) and emit a `$match: { $expr: }`; * block bodies route through `extractLetsFromPipeline` + the caller- * supplied `lowerBlock` for the full sub-pipeline shape. */ export declare function buildPipelineFormPredicate(lambda: Lambda, outerCtx: GenerateCtx, lowerBlock: SubPipelineLowerer, enclosing?: EnclosingLookupContext): { letVars: Record; pipelineBody: object[]; }; /** * Does the predicate reference any outer-doc context — either a `$.` * path on the current document, or an in-scope `let` binding (a name bound * via `let foo = …` in the surrounding pipeline)? Used by `pipeline.ts` to * decide whether a `$$ = $$$..filter()` source-switch needs the * `$lookup`-pivot lowering (when the predicate correlates per-outer-doc) vs * the `$limit:0 + $unionWith` lowering (when it's a flat source-collection * scan). * * Detection mirrors what `extractLetsFromExpr` would produce — if there are * any `$.` paths OR outer-let references in the body that would be * hoisted into `$lookup.let` vars, this returns true. */ export declare function predicateReferencesOuterDoc(lambda: Lambda, outerCtx: GenerateCtx): boolean; export declare function extractLetsFromExpr(body: Expr, foreignParam: string, outerLets?: ReadonlyMap): { rewritten: Expr; letVars: Record; }; export declare function extractLetsFromPipeline(block: Pipeline, foreignParam: string, outerLets?: ReadonlyMap): { rewritten: Pipeline; letVars: Record; }; /** * Emit the `$match` stages for a translated expression-body predicate. Lifted * verbatim from the union/facet/out translators, which all needed the same * four-way split: vacuous predicate → no stage; pure query → `{ $match: query }`; * pure residual → `{ $match: { $expr } }`; both → merged. Keeping it in one * place means the index-friendly/`$expr`-residual emission can't drift between * the sub-pipeline translators. */ export declare function matchStagesFromTranslation(t: MatchTranslation, subCtx: GenerateCtx): object[]; /** * Lower a single-parameter predicate lambda — the foreign/current document is * the param — into sub-pipeline stages. Shared by the `$unionWith`, `$facet`, * and `$out` translators, which differ only in (a) the message thrown when the * predicate references the *local* doc (`$.`, which would need a `let` * slot the target stage lacks) and (b) which fresh sub-pipeline ctx they build. * Both are injected; the expr-body / block-body / missing-body skeleton and the * `$match` emission are identical and live here. * * The caller validates the lambda's parameter count first (with its own * stage-specific message) — this helper assumes `lambda.params[0]` is the * document parameter. */ export declare function lowerLambdaPredicate(lambda: Lambda, outerCtx: GenerateCtx, lowerBlock: SubPipelineLowerer, opts: { freshCtx: (outer: GenerateCtx) => GenerateCtx; onLocalRef: (letVars: Record, param: string, pos: number) => never; missingBody: () => never; }): object[]; /** * Build the `$lookup` (+ optional `$set { $first }`) stage list for a single * lookup call, writing its result into the `as` slot. The `as` slot may be a * user-named field path (when the lookup is the whole RHS of an assignment / * `let`) or an internal `__jsmql.__lookupN` slot (when chained). */ export declare function lowerLookup(call: LookupCall, as: string, outerCtx: GenerateCtx, lowerBlock: SubPipelineLowerer, enclosing?: EnclosingLookupContext): object[]; /** * Top-down walk of `expr`. At each sub-tree, recognise either a * directly-consumed lookup or a "chained terminal" (`.length`, * `.reduce(fn, init)`). For each recognised lookup, allocate a * fresh slot, emit the prologue stages (the lookup itself, plus any * chained transform), and substitute a `FieldRef(slot)` into the * returned expression so the surrounding stage's codegen runs over the * materialised result. * * Anything not recognised as a lookup pattern is left alone but * recursed into so a lookup buried in (say) an arithmetic operand still * materialises correctly. * * Nested lookups inside an expression-body predicate of an outer lookup * materialise through the same recursive descent (with * `EnclosingLookupContext` threading), landing as prologue `$lookup` * stages inside the outer's `$lookup.pipeline`. Block-body nested * lookups are still rejected — see [DEF-023] in `translatePredicate`. */ export declare function extractLookupCalls(expr: Expr, outerCtx: GenerateCtx, allocSlot: SlotAllocator, lowerBlock: SubPipelineLowerer, enclosing?: EnclosingLookupContext): { stages: object[]; rewritten: Expr; }; export {}; //# sourceMappingURL=lookup-translation.d.ts.map