-
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.
- related to
-
SERVER-36681 Dotted path {$ne: null} against array of scalars returns incorrect results
-
- Closed
-
-
SERVER-131544 SBE ExpressionCompare doesn't correctly handle MinKey semantics
-
- Closed
-
-
SERVER-133164 trySbeRestricted results do not match classic internalQueryLegacyDottedPathNullSemantics
-
- Closed
-