-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Critical - P2
-
None
-
Affects Version/s: None
-
Component/s: None
-
DB Integration & Observability
-
ALL
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Filed by the AI query bug hunter (qhunt), an agent that reviews query code for corner-case bugs. A differential oracle and the repro below confirmed the bug.
Problem
A $lookup with localField/foreignField and a pipeline containing a $rankFusion with two or more input pipelines, whose from is a view, returns documents that the view filters out. Input pipelines after the first read the view's backing collection without the view's stages. The query returns no error, only extra documents. Default settings; no extensions; the inputs are ordinary $sort pipelines (no $search).
Repro
Fails at master ba06f31d06c (2026-10-05). Passes at 6135c13099c (2026-06-02). Self-contained jstests/noPassthrough-style test:
/** * $lookup with localField/foreignField and a pipeline holding a multi-input $rankFusion, from a * view: input pipelines after the first read the backing collection without the view's stages. * * @tags: [requires_fcv_90] */ const conn = MongoRunner.runMongod({setParameter: {internalQueryDisablePlanCache: true}}); const db = conn.getDB(jsTestName()); assert.commandWorked(db.c.insertMany([{_id: 1, a: 1, k: 1}, {_id: 2, a: 2, k: 1}])); assert.commandWorked(db.l.insert({_id: 0, k: 1})); // v keeps only a: 1, so _id 2 must never appear through it. assert.commandWorked(db.createView("v", "c", [{$match: {a: 1}}])); const rf2 = {$rankFusion: {input: {pipelines: {p1: [{$sort: {a: 1}}], p2: [{$sort: {a: -1}}]}}}}; const rf1 = {$rankFusion: {input: {pipelines: {p1: [{$sort: {a: 1}}]}}}}; const ids = (docs) => docs.map((d) => d._id).sort(); function run(label, fn) { const got = fn(); jsTest.log.info("C run", {label, ids: got}); return got; } const lookup = (spec) => ids(db.l.aggregate([{$lookup: Object.assign({from: "v", as: "r"}, spec)}]).toArray()[0].r); const direct = run("aggregate on v with 2-input $rankFusion", () => ids(db.v.aggregate([rf2]).toArray())); const union = run("$unionWith v with 2-input $rankFusion", () => ids(db.l.aggregate([{$match: {_id: -1}}, {$unionWith: {coll: "v", pipeline: [rf2]}}]).toArray())); const pipeOnly = run("$lookup pipeline-only from v with 2-input $rankFusion", () => lookup({pipeline: [rf2]})); const lf1 = run("$lookup lf/ff from v with 1-input $rankFusion", () => lookup({localField: "k", foreignField: "k", pipeline: [rf1]})); const lf2 = run("$lookup lf/ff from v with 2-input $rankFusion", () => lookup({localField: "k", foreignField: "k", pipeline: [rf2]})); // Controls: every other access path applies v's $match to both inputs. assert.eq(direct, [1], "aggregate on v"); assert.eq(union, [1], "$unionWith v"); assert.eq(pipeOnly, [1], "$lookup pipeline-only"); assert.eq(lf1, [1], "$lookup lf/ff, 1 input"); // Fails at ba06f31d06c: returns [1, 2]; the second input reads c without v's $match. assert.eq(lf2, [1], "$lookup lf/ff, 2 inputs"); MongoRunner.stopMongod(conn);
Results of the repro (view v keeps only a: 1, so _id: 2 must never appear):
| Access to v with $rankFusion | _id values returned |
|---|---|
| aggregate on v, 2 inputs | 1 |
| $unionWith v, 2 inputs | 1 |
| $lookup pipeline only, 2 inputs | 1 |
| $lookup localField/foreignField, 1 input | 1 |
| $lookup localField/foreignField, 2 inputs | 1, 2 |
Cause (from code reading)
- For localField/foreignField joins, DocumentSourceLookUp skips re-serializing the view-bound introspection pipeline into the resolved pipeline (src/mongo/db/pipeline/document_source_lookup.cpp, the hybrid-search block marked "Skipped for localField/foreignField joins").
- At execution the raw $rankFusion is lite-parsed again under the backing collection's namespace, and the desugarer uses getOriginalParseNss() as the $unionWith collection for inputs 2..N (src/mongo/db/pipeline/lite_parsed_hybrid_search_desugarer.cpp), so those inputs read the backing collection.
- The
SERVER-129249test for this shape (rank_fusion_lookup_local_foreign_field_view.js) uses $search inputs and only runs with mongot, where the search stages apply the view themselves.
Impact
Silent wrong results for any $lookup using localField/foreignField with a multi-input $rankFusion pipeline on a view. SERVER-129826 banned this shape only when featureFlagExtensionsInsideHybridSearch is off; with the flag on (the default) it is allowed and returns wrong results.
Related
SERVER-129826: banned this shape with the flag off.SERVER-129249: fixed the pipeline-only form and the $search localField/foreignField form.
- is related to
-
SERVER-129249 Nested $unionWith inside of a $lookup drops view pipeline
-
- Closed
-
- related to
-
SERVER-129826 Ban local/foreign field joins with hybrid search
-
- Closed
-