-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Minor - P4
-
None
-
Affects Version/s: None
-
Component/s: None
-
Query Integration
-
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
When a view is the from of a $lookup, the execution-time view binder binds the view onto every stage of the resolved pipeline, including the stages of the view's own definition. Reading the same view directly or through $unionWith binds an empty namespace to the definition stages. A definition stage that reacts to its bound view therefore behaves differently through $lookup. SERVER-133321 fixed this only for views whose first stage is a source stage.
Repro
Fails at master ba06f31d06c (2026-10-05). It uses the in-tree test extension $disallowViews, a pass-through stage that fails with 11507700 when it is bound to a view:
/** * $lookup from a view whose definition contains a bind-sensitive extension stage binds the view * onto the view's own definition stage at execution time, so the stage rejects the view. Reading * the view directly or through $unionWith binds an empty namespace to the definition stages and * succeeds. * * @tags: [featureFlagExtensionsAPI] */ import { checkPlatformCompatibleWithExtensions, deleteExtensionConfigs, generateExtensionConfigs, getExtensionConfDir, } from "jstests/noPassthrough/libs/extension_helpers.js"; checkPlatformCompatibleWithExtensions(); const extNames = generateExtensionConfigs(["libdisallow_view_mongo_extension.so"]); const conn = MongoRunner.runMongod({ loadExtensions: extNames, extensionsConfigPath: getExtensionConfDir(), extensionsSignaturePublicKeyPath: "src/mongo/db/extension/test_examples/test_extensions_signing_keys/test_extensions_signing_public_key.asc", }); const db = conn.getDB(jsTestName()); assert.commandWorked(db.c.insertMany([{_id: 1, a: 1}, {_id: 2, a: 2}])); assert.commandWorked(db.l.insert({_id: 1, a: 1})); // $disallowViews is a pass-through stage that uasserts 11507700 when bound to a view. assert.commandWorked(db.createView("v", "c", [{$disallowViews: {}}])); assert.commandWorked(db.createView("vMatch", "c", [{$match: {a: {$gte: 0}}}])); function run(label, fn) { try { const res = fn(); jsTest.log.info("B run", {label, res}); return {res}; } catch (e) { jsTest.log.info("B run", {label, code: e.code, err: String(e.message).slice(0, 200)}); return {code: e.code}; } } const ids = (docs) => docs.map((d) => d._id).sort(); const direct = run("aggregate on v", () => ids(db.v.aggregate([]).toArray())); const union = run("$unionWith v", () => ids(db.l.aggregate([{$match: {_id: -1}}, {$unionWith: {coll: "v", pipeline: []}}]).toArray())); const lookupMatchView = run("$lookup lf/ff from vMatch", () => ids(db.l.aggregate([{$lookup: {from: "vMatch", localField: "a", foreignField: "a", as: "j"}}]).toArray()[0].j)); const onView = run("$disallowViews in a user pipeline on vMatch", () => db.vMatch.aggregate([{$disallowViews: {}}]).toArray()); run("$lookup pipeline from v", () => ids(db.l.aggregate([{$lookup: {from: "v", pipeline: [], as: "j"}}]).toArray()[0].j)); const lookupV = run("$lookup lf/ff from v", () => ids(db.l.aggregate([{$lookup: {from: "v", localField: "a", foreignField: "a", as: "j"}}]).toArray()[0].j)); // Controls: the view definition stage runs fine on the backing collection. assert.eq(direct.res, [1, 2], "aggregate on v"); assert.eq(union.res, [1, 2], "$unionWith v"); assert.eq(lookupMatchView.res, [1], "$lookup from a plain view"); // Control: the stage does reject a view when it is a user stage running on one. assert.eq(onView.code, 11507700, "$disallowViews on a view"); // Fails at ba06f31d06c: error 11507700 "Attempted to use in view: v". assert.eq(lookupV.res, [1], "$lookup lf/ff from v"); MongoRunner.stopMongod(conn); deleteExtensionConfigs(extNames);
Results of the repro (view v is collection c followed by one $disallowViews stage):
| Access | Result |
|---|---|
| aggregate on v | ok, 2 documents |
| $unionWith v | ok, 2 documents |
| $lookup from a plain view | ok |
| $lookup from v (pipeline or localField/foreignField) | error 11507700 "Attempted to use in view: v" |
Cause (from code reading)
- LookUpStage installs makeLookupViewBinder(fromExpCtx, viewBindingStart) (src/mongo/db/exec/agg/lookup_stage.cpp).
- viewBindingStart comes from foreignViewSourceGeneratorPrefixLength() (src/mongo/db/pipeline/document_source_lookup.cpp), which returns 0 unless the view's first stage is a source stage.
- With a start of 0, resolveInvolvedNamespacesImpl (src/mongo/db/views/pipeline_resolver.cpp) binds the view onto every stage from stage 0, and the resolved pipeline starts with the view's own stages.
- The top-level and $unionWith paths bind definition stages with an empty ResolvedNamespace (src/mongo/db/pipeline/lite_parsed_pipeline.cpp).
Impact
In-tree, only test extensions react to the bound view, so no shipping stage is known to hit this. An extension whose bindResolvedNamespace rejects or records a non-empty view, placed as a non-first stage of a view definition, would fail or behave differently through $lookup.
Related
SERVER-133321: the same self-binding, fixed for views that begin with a source stage.
- is related to
-
SERVER-136361 $lookup into a view whose definition $unionWith's another view does not apply the inner view
-
- Needs Scheduling
-
- related to
-
SERVER-133321 $lookup on a view definition with extension $search stage binds the view into its own $_internalSearchIdLookup w/ different behavior on topology
-
- Closed
-