• 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.

            Assignee:
            Unassigned
            Reporter:
            Zixuan Zhuang
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: