SERVER-106643 Expose cursorType in explain for $cursor (#40592)

GitOrigin-RevId: 2da874fc9ce72c97227e40088bc10bd160ec9b65
This commit is contained in:
Max Verbinnen 2025-08-27 11:56:45 +01:00 committed by MongoDB Bot
parent f91be9182d
commit ac174efbaf
4 changed files with 82 additions and 2 deletions

View File

@ -0,0 +1,64 @@
// Tests the behavior of explain() when used with aggregation pipeline to
// verify cursorType field. This test verifies that the cursorType field
// is correctly exposed in the $cursor.queryPlanner stage explain output.
//
// @tags: [
// assumes_unsharded_collection,
// do_not_wrap_aggregations_in_facets,
// ]
import {checkSbeRestrictedOrFullyEnabled} from "jstests/libs/query/sbe_util.js";
import {getAggPlanStage} from "jstests/libs/query/analyze_plan.js";
if (checkSbeRestrictedOrFullyEnabled(db)) {
jsTest.log.info("Skipping test because $count queries don't use the emptyDocuments cursor in SBE");
quit();
}
const coll = db.explain_cursor;
coll.drop();
const kNumDocs = 10;
for (let i = 0; i < kNumDocs; i++) {
assert.commandWorked(coll.insert({_id: i, a: i, b: i % 2}));
}
function getCursorType(explainOutput) {
const cursorStage = getAggPlanStage(explainOutput, "$cursor");
assert.neq(null, cursorStage, "No $cursor stage present");
assert(cursorStage.$cursor.hasOwnProperty("queryPlanner"), "No $cursor.queryPlanner present");
assert(cursorStage.$cursor.queryPlanner.hasOwnProperty("cursorType"), "No $cursor.queryPlanner.cursorType present");
return cursorStage.$cursor.queryPlanner.cursorType;
}
// Normal aggregation queries should use the regular cursor.
const groupExplain = coll.explain().aggregate([{$match: {b: 1}}, {$group: {_id: "$a", count: {$sum: 1}}}]);
assert.eq("regular", getCursorType(groupExplain));
const multiStageExplain = coll
.explain()
.aggregate([
{$match: {a: {$lt: 8}}},
{$project: {a: 1, category: {$cond: [{$gt: ["$a", 5]}, "high", "low"]}}},
{$sort: {category: 1, a: -1}},
]);
assert.eq("regular", getCursorType(multiStageExplain));
// Count-like queries should use the emptyDocuments cursor.
const countExplain = coll.explain().aggregate([{$match: {a: {$gte: 5}}}, {$count: "filtered"}]);
assert.eq("emptyDocuments", getCursorType(countExplain));
const multiStageCountExplain = coll.explain().aggregate([
{
"$addFields": {
"c": NumberInt(0),
},
},
{
"$project": {
"_id": 0,
"c": 1,
},
},
]);
assert.eq("emptyDocuments", getCursorType(multiStageCountExplain));

View File

@ -43,7 +43,11 @@ const mongosIgnoredFields = ["works", "needTime", "queryHash", "planCacheShapeHa
stagesIgnoredFields,
);
const queryPlannerIgnoredFields = ["optimizedPipeline", "optimizationTimeMillis"].concat(stagesIgnoredFields);
// We ignore `cursorType` because it's only set when there's a $cursor stage, which could be
// the case for the union but not for the regular query or vice versa.
const queryPlannerIgnoredFields = ["optimizedPipeline", "optimizationTimeMillis", "cursorType"].concat(
stagesIgnoredFields,
);
function buildErrorString(unionExplain, realExplain, field) {
return (

View File

@ -79,13 +79,15 @@ Value DocumentSourceCursor::serialize(const SerializationOptions& opts) const {
BSONObjBuilder explainStatsBuilder;
tassert(
10769400, "Expected the plannerContext to be set for explain", _plannerContext.has_value());
BSONObj extraInfo = BSON("cursorType" << toString(_cursorType));
Explain::explainStages(
_sharedState->exec.get(),
*_plannerContext,
opts.verbosity.value(),
_sharedState->execStatus,
_winningPlanTrialStats,
BSONObj(),
extraInfo,
SerializationContext::stateCommandReply(getExpCtx()->getSerializationContext()),
BSONObj(),
&explainStatsBuilder);

View File

@ -203,6 +203,16 @@ private:
friend boost::intrusive_ptr<exec::agg::Stage> documentSourceGeoNearCursorToStageFn(
const boost::intrusive_ptr<DocumentSource>&);
static constexpr StringData toString(CursorType type) {
switch (type) {
case CursorType::kRegular:
return "regular"_sd;
case CursorType::kEmptyDocuments:
return "emptyDocuments"_sd;
}
MONGO_UNREACHABLE;
}
// Handle to catalog state.
boost::intrusive_ptr<CatalogResourceHandle> _catalogResourceHandle;