• Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • Query Execution
    • ALL
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Found by an AI bug hunt and confirmed by a differential oracle.

      Problem

      For a comparison against MinKey or MaxKey on a dotted path that crosses arrays, the SBE engine returns the same documents whatever internalQueryLegacyDottedPathNullSemantics is set to, and differs from the classic engine under both settings. Comparisons against null agree between the engines under both settings.

      Repro

      Collection c holds these documents; the query is run for each predicate below:

      db.c.insertMany([{_id: 1, a: [1]}, {_id: 2, a: []}, {_id: 3, a: {b: 1}}, {_id: 4, a: [{b: 2}]},
                       {_id: 5, a: 5}, {_id: 6, a: [{c: 1}]}, {_id: 7, a: ["x", 2]}]);
      db.c.aggregate([{$match: {"a.b": PREDICATE}}, {$project: {_id: 1}}]);
      

      Returned _id values:

      Predicate SBE, legacy (default) classic, legacy SBE, non-legacy classic, non-legacy
      $eq: null 5, 6 5, 6 1, 2, 5, 6, 7 1, 2, 5, 6, 7
      $gte: null 5, 6 5, 6 1, 2, 5, 6, 7 1, 2, 5, 6, 7
      $gte: MinKey 1, 3, 4, 5, 6, 7 3, 4, 5, 6 1, 3, 4, 5, 6, 7 1, 2, 3, 4, 5, 6, 7
      $lte: MaxKey 1, 3, 4, 5, 6, 7 3, 4, 5, 6 1, 3, 4, 5, 6, 7 1, 2, 3, 4, 5, 6, 7

      Self-contained jstest (noPassthrough style); fails at its first assertion after its controls pass:

      /**
       * SBE ignores internalQueryLegacyDottedPathNullSemantics for MinKey/MaxKey comparisons on a
       * dotted path that crosses arrays: SBE returns the same documents under both settings, and
       * differs from the classic engine under each. Comparisons against null agree in both engines.
       */
      const conn = MongoRunner.runMongod({setParameter: {internalQueryDisablePlanCache: true}});
      const db = conn.getDB(jsTestName());
      assert.commandWorked(db.c.insertMany([
          {_id: 1, a: [1]},
          {_id: 2, a: []},
          {_id: 3, a: {b: 1}},
          {_id: 4, a: [{b: 2}]},
          {_id: 5, a: 5},
          {_id: 6, a: [{c: 1}]},
          {_id: 7, a: ["x", 2]},
      ]));
      const ids = (docs) => docs.map((d) => d._id).sort();
      
      function run(pred, engine, legacy) {
          assert.commandWorked(db.adminCommand({
              setParameter: 1,
              internalQueryFrameworkControl: engine,
              internalQueryLegacyDottedPathNullSemantics: legacy,
          }));
          const pipeline = [{$match: {"a.b": pred}}, {$project: {_id: 1}}];
          const usedSbe = tojson(db.c.explain().aggregate(pipeline)).includes("slotBasedPlan");
          assert.eq(usedSbe, engine === "trySbeEngine", "engine setting did not take effect");
          const got = ids(db.c.aggregate(pipeline).toArray());
          jsTest.log.info("run", {pred, engine, legacy, got});
          return got;
      }
      
      // Controls: null comparisons agree between the engines under both settings.
      for (const legacy of [true, false]) {
          assert.eq(run({$gte: null}, "trySbeEngine", legacy), run({$gte: null}, "forceClassicEngine", legacy),
                    "$gte null, legacy=" + legacy);
      }
      // Per SERVER-131544, {field: {$gte: MinKey}} matches every document, no exceptions.
      assert.eq(run({$gte: MinKey}, "forceClassicEngine", false), [1, 2, 3, 4, 5, 6, 7], "classic, non-legacy");
      
      // Bug 1, default (legacy) setting. Fails at the local build (2026-09-17): SBE also returns 1 and 7.
      assert.eq(run({$gte: MinKey}, "trySbeEngine", true), run({$gte: MinKey}, "forceClassicEngine", true),
                "$gte MinKey, legacy");
      // Bug 2, non-legacy setting: SBE drops _id 2 (a: []).
      assert.eq(run({$gte: MinKey}, "trySbeEngine", false), run({$gte: MinKey}, "forceClassicEngine", false),
                "$gte MinKey, non-legacy");
      MongoRunner.stopMongod(conn);
      

      Cause (from code reading, not verified)

      In src/mongo/db/query/stage_builder/sbe/gen_filter.cpp the object guard for non-leaf array elements is generated only when needsNothingCheck is set, and matchesNothing is set only for $eq, $lte and $gte against null (around lines 522-527). $gte MinKey and $lte MaxKey compile to constant true (around lines 1362-1393) but also match a missing field, so scalar array elements are not skipped (legacy mode) and an empty intermediate array is not handled (non-legacy mode).

      Impact

      Silent wrong results for MinKey/MaxKey comparisons on dotted paths through arrays, in either null-semantics mode. SERVER-131544 states that a $gte: MinKey comparison on a field should match every document, which the classic engine does under the non-legacy setting.

      Related

      • SERVER-133164: engine mismatch for $eq null with the same knob; fixed by changing classic to match SBE.
      • SERVER-131544: SBE $gte MinKey on time-series collections.
      • SERVER-36681: introduced the dotted-path null semantics and the knob.

      Which engine is the reference for the legacy setting is for the owners to decide; the classic engine skips scalar array elements there.

            Assignee:
            Logan Karbiner
            Reporter:
            Zixuan Zhuang
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: