ExportXMLWordPrintableJSON

    • Query Execution
    • Fully Compatible
    • ALL
    • v9.0
    • 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

      Inside an object $elemMatch, SBE checks path arrayness for the child's element-relative path against the root document. If the collection has a non-multikey index on a root field with the same name, SBE skips array traversal for that field inside the array elements. Elements whose field is an array then never match. The query returns no error, only fewer rows. This happens with default settings whenever the $match runs in SBE.

      Repro

      Fails at master 2e6f9b7f8e7 (2026-10-05). Self-contained jstests/noPassthrough-style test:

      /**
       * F002: inside an object $elemMatch, SBE checks path arrayness for the child's
       * element-relative path ("b") against the root document. A non-multikey root
       * index on "b" marks "b" non-array, so SBE skips traversal of arr.$.b and
       * misses array elements whose "b" is an array.
       *
       * @tags: [
       *   requires_fcv_90,
       *   requires_sbe
       * ]
       */
      
      // Default settings except the plan cache: toggled knobs do not clear cached plans.
      const conn = MongoRunner.runMongod({setParameter: {internalQueryDisablePlanCache: true}});
      const db = conn.getDB(jsTestName());
      const coll = db.c;
      
      assert.commandWorked(coll.insert({_id: 0, b: 1, arr: [{b: [1, 2]}]}));
      // The root index on "b" is what marks "b" as non-array; without it the bug is gone.
      assert.commandWorked(coll.createIndex({b: 1}));
      
      // $group lets the $match run in SBE under the default trySbeRestricted.
      const pipeline = [{$match: {arr: {$elemMatch: {b: 2}}}}, {$group: {_id: "$_id"}}];
      const expected = [{_id: 0}];
      
      function run(label) {
          const results = coll.aggregate(pipeline).toArray();
          const usedSbe = tojson(coll.explain().aggregate(pipeline)).includes("slotBasedPlan");
          const findResults = coll.find({arr: {$elemMatch: {b: 2}}}).toArray();
          jsTest.log.info("F002 run", {label, usedSbe, results, findResults});
          return {results, usedSbe};
      }
      
      const withIndex = run("default, root b index");
      assert.commandWorked(db.adminCommand({setParameter: 1, internalEnablePathArrayness: false}));
      const arraynessOff = run("internalEnablePathArrayness=false");
      assert.commandWorked(db.adminCommand({setParameter: 1, internalEnablePathArrayness: true}));
      assert.commandWorked(coll.dropIndex({b: 1}));
      const noIndex = run("default, no root b index");
      
      // Every run must take the SBE path, or the comparison proves nothing.
      assert(withIndex.usedSbe && arraynessOff.usedSbe && noIndex.usedSbe, "SBE was not used");
      
      assert.sameMembers(arraynessOff.results, expected, "path arrayness off");
      assert.sameMembers(noIndex.results, expected, "no root b index");
      // Fails at 2e6f9b7f8e7: returns [] because traverseF is elided for arr.$.b.
      assert.sameMembers(withIndex.results, expected, "default, root b index");
      
      MongoRunner.stopMongod(conn);
      

      Results of the repro:

      Run Engine Result (expected 1 row)
      default, root index on b SBE 0 rows
      internalEnablePathArrayness=false SBE 1 row
      default, no root index on b SBE 1 row

      A plain find with the same filter returns the correct row because it runs in the classic engine.

      Cause (from code reading)

      • generatePredicate() in src/mongo/db/query/stage_builder/sbe/gen_filter.cpp computes isFieldPathOnRootDoc = context->framesCount() == 1 but uses it only for slot lookup.
      • The arrayness check asks expCtx->canPathBeArrayForNss(path, expCtx->getNamespaceString()) for every frame.
      • The ElemMatchObjectMatchExpression visitor pushes a frame whose input is the array element, so path is relative to the element (b), and the root b metadata is applied to arr.$.b. traverseF is elided and getField(elem, "b") == 2 is evaluated on [1, 2].

      Impact

      • Silent wrong results under default settings (featureFlagPathArrayness and internalEnablePathArrayness default true) when the $match runs in SBE, for example ahead of $group or $lookup, or with trySbeEngine.
      • The v9.0 branch has the same code with the flag on by default.

      Why existing tests miss it

      elemmatch_object_index_correctness_pbt.js compares index plans with a $natural scan. Path arrayness is on for both sides and the pipelines are $match-only, which run in the classic engine.

      Related

      • SERVER-125100, SERVER-121667: traverseF elision using path arrayness in collection scans and fetch residual predicates.
      • SERVER-136259: another case where path arrayness is applied to a path it does not describe (join optimizer, foreign collection).

            Assignee:
            Evan Bergeron
            Reporter:
            Zixuan Zhuang
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated:
              Resolved: