SERVER-83098: Make existing jstest work with the new CQF explain (core/query and core/sbe) (2)

This commit is contained in:
Matt Olma 2023-11-22 20:21:37 +00:00 committed by Evergreen Agent
parent 40c8c6eb88
commit 1aea6febd3
13 changed files with 414 additions and 103 deletions

View File

@ -6,8 +6,6 @@
* assumes_read_concern_local, * assumes_read_concern_local,
* ] * ]
*/ */
// adding temporary comment
import {getOptimizer, getWinningPlan, isIxscan} from "jstests/libs/analyze_plan.js"; import {getOptimizer, getWinningPlan, isIxscan} from "jstests/libs/analyze_plan.js";
const coll = db.elemMatch_index; const coll = db.elemMatch_index;

View File

@ -44,5 +44,3 @@ switch (getOptimizer(explainResult)) {
default: default:
break break
} }
t.drop();

View File

@ -11,7 +11,7 @@
// where agg execution differs from query. It also includes confirmation that hint works for find // where agg execution differs from query. It also includes confirmation that hint works for find
// command against views, which is converted to a hinted aggregation on execution. // command against views, which is converted to a hinted aggregation on execution.
import {getAggPlanStage, getPlanStage} from "jstests/libs/analyze_plan.js"; import {getAggPlanStage, getOptimizer, getPlanStage} from "jstests/libs/analyze_plan.js";
const testDB = db.getSiblingDB("agg_hint"); const testDB = db.getSiblingDB("agg_hint");
assert.commandWorked(testDB.dropDatabase()); assert.commandWorked(testDB.dropDatabase());
@ -22,11 +22,27 @@ const view = testDB.getCollection(viewName);
function confirmWinningPlanUsesExpectedIndex( function confirmWinningPlanUsesExpectedIndex(
explainResult, expectedKeyPattern, stageName, pipelineOptimizedAway) { explainResult, expectedKeyPattern, stageName, pipelineOptimizedAway) {
const planStage = pipelineOptimizedAway ? getPlanStage(explainResult, stageName) const optimizer = getOptimizer(explainResult);
: getAggPlanStage(explainResult, stageName);
assert.neq(null, planStage);
if (!(optimizer in stageName) || stageName[optimizer] === "") {
// TODO SERVER-77719: Ensure that the expected operator is defined for all optimizers. There
// should be an exception here.
return;
}
const planStage = pipelineOptimizedAway ? getPlanStage(explainResult, stageName[optimizer])
: getAggPlanStage(explainResult, stageName[optimizer]);
switch (optimizer) {
case "classic":
assert.neq(null, planStage);
assert.eq(planStage.keyPattern, expectedKeyPattern, tojson(planStage)); assert.eq(planStage.keyPattern, expectedKeyPattern, tojson(planStage));
break;
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
} }
// Runs explain on 'command', with the hint specified by 'hintKeyPattern' when not null. // Runs explain on 'command', with the hint specified by 'hintKeyPattern' when not null.
@ -37,7 +53,10 @@ function confirmCommandUsesIndex({
command = null, command = null,
hintKeyPattern = null, hintKeyPattern = null,
expectedKeyPattern = null, expectedKeyPattern = null,
stageName = "IXSCAN", stageName = {
"classic": "IXSCAN",
"CQF": "IndexScan"
},
pipelineOptimizedAway = false pipelineOptimizedAway = false
} = {}) { } = {}) {
if (hintKeyPattern) { if (hintKeyPattern) {
@ -60,7 +79,10 @@ function confirmAggUsesIndex({
aggPipeline = [], aggPipeline = [],
hintKeyPattern = null, hintKeyPattern = null,
expectedKeyPattern = null, expectedKeyPattern = null,
stageName = "IXSCAN", stageName = {
"classic": "IXSCAN",
"CQF": "IndexScan"
},
pipelineOptimizedAway = false pipelineOptimizedAway = false
} = {}) { } = {}) {
let options = {}; let options = {};
@ -248,16 +270,18 @@ for (let i = 0; i < 5; ++i) {
} }
assert.commandWorked(testDB.createView(viewName, collName, [])); assert.commandWorked(testDB.createView(viewName, collName, []));
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
confirmCommandUsesIndex({ confirmCommandUsesIndex({
command: {count: view.getName(), query: {x: 3}}, command: {count: view.getName(), query: {x: 3}},
expectedKeyPattern: {x: 1}, expectedKeyPattern: {x: 1},
stageName: "COUNT_SCAN" stageName: {"classic": "COUNT_SCAN", "CQF": ""}
}); });
confirmCommandUsesIndex({ confirmCommandUsesIndex({
command: {count: view.getName(), query: {x: 3}}, command: {count: view.getName(), query: {x: 3}},
hintKeyPattern: {x: 1}, hintKeyPattern: {x: 1},
expectedKeyPattern: {x: 1}, expectedKeyPattern: {x: 1},
stageName: "COUNT_SCAN" stageName: {"classic": "COUNT_SCAN", "CQF": ""}
}); });
confirmCommandUsesIndex({ confirmCommandUsesIndex({
command: {count: view.getName(), query: {x: 3}}, command: {count: view.getName(), query: {x: 3}},

View File

@ -9,7 +9,15 @@
* Test covering behavior for queries over a multikey index. * Test covering behavior for queries over a multikey index.
*/ */
// For making assertions about explain output. // For making assertions about explain output.
import {getPlanStage, getWinningPlan, isIxscan, planHasStage} from "jstests/libs/analyze_plan.js"; import {
getOptimizer,
getPlanStage,
getWinningPlan,
isCollscan,
isIxscan,
isIxscanMultikey,
planHasStage
} from "jstests/libs/analyze_plan.js";
let coll = db.covered_multikey; let coll = db.covered_multikey;
coll.drop(); coll.drop();
@ -21,7 +29,18 @@ assert.eq(1, coll.find({a: 1, b: 2}, {_id: 0, a: 1}).itcount());
assert.eq({a: 1}, coll.findOne({a: 1, b: 2}, {_id: 0, a: 1})); assert.eq({a: 1}, coll.findOne({a: 1, b: 2}, {_id: 0, a: 1}));
let explainRes = coll.explain("queryPlanner").find({a: 1, b: 2}, {_id: 0, a: 1}).finish(); let explainRes = coll.explain("queryPlanner").find({a: 1, b: 2}, {_id: 0, a: 1}).finish();
let winningPlan = getWinningPlan(explainRes.queryPlanner); let winningPlan = getWinningPlan(explainRes.queryPlanner);
switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan)); assert(isIxscan(db, winningPlan));
break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, winningPlan));
break
}
}
assert(!planHasStage(db, winningPlan, "FETCH")); assert(!planHasStage(db, winningPlan, "FETCH"));
coll.drop(); coll.drop();
@ -47,10 +66,20 @@ assert.commandWorked(coll.createIndex({a: 1}));
assert.eq({a: []}, coll.findOne({a: []}, {_id: 0, a: 1})); assert.eq({a: []}, coll.findOne({a: []}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: []}, {_id: 0, a: 1}).finish(); explainRes = coll.explain("queryPlanner").find({a: []}, {_id: 0, a: 1}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(planHasStage(db, winningPlan, "IXSCAN")); switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan));
assert(planHasStage(db, winningPlan, "FETCH")); assert(planHasStage(db, winningPlan, "FETCH"));
let ixscanStage = getPlanStage(winningPlan, "IXSCAN"); assert(isIxscanMultikey(winningPlan));
assert.eq(true, ixscanStage.isMultiKey); break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, winningPlan));
break
}
}
// Verify that a query cannot be covered over a path which is multikey due to a single-element // Verify that a query cannot be covered over a path which is multikey due to a single-element
// array. // array.
@ -60,10 +89,20 @@ assert.commandWorked(coll.createIndex({a: 1}));
assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1})); assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish(); explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(planHasStage(db, winningPlan, "IXSCAN")); switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan));
assert(planHasStage(db, winningPlan, "FETCH")); assert(planHasStage(db, winningPlan, "FETCH"));
ixscanStage = getPlanStage(winningPlan, "IXSCAN"); assert(isIxscanMultikey(winningPlan));
assert.eq(true, ixscanStage.isMultiKey); break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, winningPlan));
break;
}
}
// Verify that a query cannot be covered over a path which is multikey due to a single-element // Verify that a query cannot be covered over a path which is multikey due to a single-element
// array, where the path is made multikey by an update rather than an insert. // array, where the path is made multikey by an update rather than an insert.
@ -74,10 +113,20 @@ assert.commandWorked(coll.update({}, {$set: {a: [2]}}));
assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1})); assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish(); explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(planHasStage(db, winningPlan, "IXSCAN")); switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan));
assert(planHasStage(db, winningPlan, "FETCH")); assert(planHasStage(db, winningPlan, "FETCH"));
ixscanStage = getPlanStage(winningPlan, "IXSCAN"); assert(isIxscanMultikey(winningPlan));
assert.eq(true, ixscanStage.isMultiKey); break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, winningPlan));
break;
}
}
// Verify that a trailing empty array makes a 2dsphere index multikey. // Verify that a trailing empty array makes a 2dsphere index multikey.
assert(coll.drop()); assert(coll.drop());
@ -85,15 +134,26 @@ assert.commandWorked(coll.createIndex({"a.b": 1, c: "2dsphere"}));
assert.commandWorked(coll.insert({a: {b: 1}, c: {type: "Point", coordinates: [0, 0]}})); assert.commandWorked(coll.insert({a: {b: 1}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish(); explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN"); let optimizer = getOptimizer(explainRes);
let stages = {"classic": "IXSCAN", "CQF": "IndexScan"};
let ixscanStage = getPlanStage(winningPlan, stages[optimizer]);
assert.neq(null, ixscanStage); assert.neq(null, ixscanStage);
assert.eq(false, ixscanStage.isMultiKey); assert.eq(false, ixscanStage.isMultiKey);
assert.commandWorked(coll.insert({a: {b: []}, c: {type: "Point", coordinates: [0, 0]}})); assert.commandWorked(coll.insert({a: {b: []}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish(); explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN"); switch (getOptimizer(explainRes)) {
assert.neq(null, ixscanStage); case "classic": {
assert.eq(true, ixscanStage.isMultiKey); assert(isIxscan(db, winningPlan));
assert(isIxscanMultikey(winningPlan));
break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
}
// Verify that a mid-path empty array makes a 2dsphere index multikey. // Verify that a mid-path empty array makes a 2dsphere index multikey.
assert(coll.drop()); assert(coll.drop());
@ -101,9 +161,18 @@ assert.commandWorked(coll.createIndex({"a.b": 1, c: "2dsphere"}));
assert.commandWorked(coll.insert({a: [], c: {type: "Point", coordinates: [0, 0]}})); assert.commandWorked(coll.insert({a: [], c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish(); explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN"); switch (getOptimizer(explainRes)) {
assert.neq(null, ixscanStage); case "classic": {
assert.eq(true, ixscanStage.isMultiKey); assert(isIxscan(db, winningPlan));
assert(isIxscanMultikey(winningPlan));
break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
}
// Verify that a single-element array makes a 2dsphere index multikey. // Verify that a single-element array makes a 2dsphere index multikey.
assert(coll.drop()); assert(coll.drop());
@ -111,6 +180,15 @@ assert.commandWorked(coll.createIndex({"a.b": 1, c: "2dsphere"}));
assert.commandWorked(coll.insert({a: {b: [3]}, c: {type: "Point", coordinates: [0, 0]}})); assert.commandWorked(coll.insert({a: {b: [3]}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish(); explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner); winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN"); switch (getOptimizer(explainRes)) {
assert.neq(null, ixscanStage); case "classic": {
assert.eq(true, ixscanStage.isMultiKey); assert(isIxscan(db, winningPlan));
assert(isIxscanMultikey(winningPlan));
break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
}

View File

@ -7,6 +7,8 @@
// does_not_support_causal_consistency, // does_not_support_causal_consistency,
// ] // ]
import {hasRejectedPlans} from "jstests/libs/analyze_plan.js";
/** /**
* Tests running explain on a variety of explainable commands (find, update, remove, etc.) when * Tests running explain on a variety of explainable commands (find, update, remove, etc.) when
* there are multiple plans available. This is a regression test for SERVER-20849 and SERVER-21376. * there are multiple plans available. This is a regression test for SERVER-20849 and SERVER-21376.
@ -55,25 +57,10 @@ assert.doesNotThrow(function() {
coll.explain("allPlansExecution").distinct("a", {a: {$gte: 1}}); coll.explain("allPlansExecution").distinct("a", {a: {$gte: 1}});
}); });
// SERVER-21376: Make sure the 'rejectedPlans' field is filled in appropriately.
function assertHasRejectedPlans(explainOutput) {
var queryPlannerOutput = explainOutput.queryPlanner;
// The 'rejectedPlans' section will be in a different place if passed through a mongos.
if ("SINGLE_SHARD" == queryPlannerOutput.winningPlan.stage) {
var shards = queryPlannerOutput.winningPlan.shards;
shards.forEach(function assertShardHasRejectedPlans(shard) {
assert.gt(shard.rejectedPlans.length, 0);
});
} else {
assert.gt(queryPlannerOutput.rejectedPlans.length, 0);
}
}
var res = coll.explain("queryPlanner").find({a: {$gte: 1}}).finish(); var res = coll.explain("queryPlanner").find({a: {$gte: 1}}).finish();
assert.commandWorked(res); assert.commandWorked(res);
assertHasRejectedPlans(res); assert(hasRejectedPlans(res));
res = coll.explain("executionStats").find({a: {$gte: 1}}).finish(); res = coll.explain("executionStats").find({a: {$gte: 1}}).finish();
assert.commandWorked(res); assert.commandWorked(res);
assertHasRejectedPlans(res); assert(hasRejectedPlans(res));

View File

@ -8,6 +8,8 @@
// assumes_against_mongod_not_mongos, // assumes_against_mongod_not_mongos,
// ] // ]
import {getOptimizer} from "jstests/libs/analyze_plan.js";
const coll = db.explain_winning_plan; const coll = db.explain_winning_plan;
coll.drop(); coll.drop();
@ -43,7 +45,16 @@ assert.eq(explain.executionStats.nReturned, numDocs);
// representing two candidate plans evaluated by the multi-planner. // representing two candidate plans evaluated by the multi-planner.
assert(explain.executionStats.hasOwnProperty("allPlansExecution"), explain); assert(explain.executionStats.hasOwnProperty("allPlansExecution"), explain);
assert(Array.isArray(explain.executionStats.allPlansExecution), explain); assert(Array.isArray(explain.executionStats.allPlansExecution), explain);
switch (getOptimizer(explain)) {
case "classic":
assert.eq(explain.executionStats.allPlansExecution.length, 2, explain); assert.eq(explain.executionStats.allPlansExecution.length, 2, explain);
break;
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// Each candidate plan should have returned exactly 'maxResults' number of documents during the // Each candidate plan should have returned exactly 'maxResults' number of documents during the
// trial period. // trial period.

View File

@ -4,7 +4,7 @@
// requires_non_retryable_writes, // requires_non_retryable_writes,
// ] // ]
// Include helpers for analyzing explain output. // Include helpers for analyzing explain output.
import {getWinningPlan, isIdhack} from "jstests/libs/analyze_plan.js"; import {getOptimizer, getWinningPlan, isIdhack} from "jstests/libs/analyze_plan.js";
import {checkSBEEnabled} from "jstests/libs/sbe_util.js"; import {checkSBEEnabled} from "jstests/libs/sbe_util.js";
const t = db.idhack; const t = db.idhack;
@ -35,14 +35,25 @@ const query = {
}; };
let explain = t.find(query).explain("allPlansExecution"); let explain = t.find(query).explain("allPlansExecution");
assert.eq(1, explain.executionStats.nReturned, explain); assert.eq(1, explain.executionStats.nReturned, explain);
switch (getOptimizer(explain)) {
case "classic": {
assert.eq(1, explain.executionStats.totalKeysExamined, explain); assert.eq(1, explain.executionStats.totalKeysExamined, explain);
let winningPlan = getWinningPlan(explain.queryPlanner); let winningPlan = getWinningPlan(explain.queryPlanner);
assert(isIdhack(db, winningPlan), winningPlan); assert(isIdhack(db, winningPlan), winningPlan);
break;
}
case "CQF":
// TODO SERVER-70847, how to recognize the case of an IDHACK for Bonsai?
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// ID hack cannot be used with hint(). // ID hack cannot be used with hint().
t.createIndex({_id: 1, a: 1}); t.createIndex({_id: 1, a: 1});
explain = t.find(query).hint({_id: 1, a: 1}).explain(); explain = t.find(query).hint({_id: 1, a: 1}).explain();
winningPlan = getWinningPlan(explain.queryPlanner); let winningPlan = getWinningPlan(explain.queryPlanner);
assert(!isIdhack(db, winningPlan), winningPlan); assert(!isIdhack(db, winningPlan), winningPlan);
// ID hack cannot be used with skip(). // ID hack cannot be used with skip().
@ -61,7 +72,18 @@ assert(!isIdhack(db, winningPlan), winningPlan);
const parentStage = checkSBEEnabled(db) ? "PROJECTION_COVERED" : "FETCH"; const parentStage = checkSBEEnabled(db) ? "PROJECTION_COVERED" : "FETCH";
explain = t.find(query, {_id: 1}).explain(); explain = t.find(query, {_id: 1}).explain();
winningPlan = getWinningPlan(explain.queryPlanner); winningPlan = getWinningPlan(explain.queryPlanner);
switch (getOptimizer(explain)) {
case "classic": {
assert(isIdhack(db, winningPlan), winningPlan); assert(isIdhack(db, winningPlan), winningPlan);
break;
}
case "CQF":
// TODO SERVER-70847, how to recognize the case of an IDHACK for Bonsai?
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// Check doc from covered ID hack query. // Check doc from covered ID hack query.
assert.eq({_id: {x: 2}}, t.findOne(query, {_id: 1}), explain); assert.eq({_id: {x: 2}}, t.findOne(query, {_id: 1}), explain);

View File

@ -8,7 +8,13 @@
* requires_fcv_70, * requires_fcv_70,
* ] * ]
*/ */
import {getExecutionStages, getPlanStages, isIxscan} from "jstests/libs/analyze_plan.js"; import {
getExecutionStages,
getOptimizer,
getPlanStages,
isCollscan,
isIxscan
} from "jstests/libs/analyze_plan.js";
const collName = jsTestName(); const collName = jsTestName();
const coll = db.getCollection(collName); const coll = db.getCollection(collName);
@ -40,7 +46,18 @@ function getHash(coll, filterSpec, field, indexSpec) {
* @param {int} expectedKeysExamined - The expected number of keys in the index that were examined. * @param {int} expectedKeysExamined - The expected number of keys in the index that were examined.
*/ */
function assertExplainIxscan(explainPlan, expectedIndexSpec, expectedKeysExamined = 1) { function assertExplainIxscan(explainPlan, expectedIndexSpec, expectedKeysExamined = 1) {
switch (getOptimizer(explainPlan)) {
case "classic": {
assert(isIxscan(db, explainPlan), explainPlan); assert(isIxscan(db, explainPlan), explainPlan);
break;
}
case "CQF": {
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explainPlan));
break;
}
}
let execStages = getExecutionStages(explainPlan); let execStages = getExecutionStages(explainPlan);
execStages.forEach(execStage => { execStages.forEach(execStage => {
if (execStage.stage == "SHARDING_FILTER" && execStage.nReturned == 0) { if (execStage.stage == "SHARDING_FILTER" && execStage.nReturned == 0) {

View File

@ -7,7 +7,7 @@
// # TODO SERVER-77719: Enable this check for Bonsai. // # TODO SERVER-77719: Enable this check for Bonsai.
// cqf_experimental_incompatible // cqf_experimental_incompatible
// ] // ]
import {getPlanStages, getWinningPlan} from "jstests/libs/analyze_plan.js"; import {getOptimizer, getPlanStages, getWinningPlan} from "jstests/libs/analyze_plan.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js"; import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
const coll = db.jstests_ord; const coll = db.jstests_ord;
@ -32,10 +32,20 @@ for (let i = 0; i < 1000; ++i) {
// preliminary check here, using explain. However, this does not guarantee the query below will use // preliminary check here, using explain. However, this does not guarantee the query below will use
// an ixscan, because the chosen plan may not be the same. // an ixscan, because the chosen plan may not be the same.
const explainRes = assert.commandWorked(coll.find({$or: [{a: 1}, {b: 1}]}).explain()); const explainRes = assert.commandWorked(coll.find({$or: [{a: 1}, {b: 1}]}).explain());
switch (getOptimizer(explainRes)) {
case "classic": {
const orStages = getPlanStages(getWinningPlan(explainRes.queryPlanner), "OR"); const orStages = getPlanStages(getWinningPlan(explainRes.queryPlanner), "OR");
assert(orStages.length > 0, "Expected to find OR stage in explain: " + tojson(explainRes)); assert(orStages.length > 0, "Expected to find OR stage in explain: " + tojson(explainRes));
assert(orStages.every(orStage => (getPlanStages(orStage, "IXSCAN").length > 0)), assert(
orStages.every(orStage => (getPlanStages(orStage, "IXSCAN").length > 0)),
"Expected the plan to be an OR which has (at least) one IXSCAN: " + tojson(explainRes)); "Expected the plan to be an OR which has (at least) one IXSCAN: " + tojson(explainRes));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// This query should match 180 out of 1180 total documents. // This query should match 180 out of 1180 total documents.
const cursor = coll.find({$or: [{a: 1}, {b: 1}]}).batchSize(100); const cursor = coll.find({$or: [{a: 1}, {b: 1}]}).batchSize(100);

View File

@ -9,7 +9,14 @@
* when appropriate. * when appropriate.
*/ */
import {arrayEq} from "jstests/aggregation/extras/utils.js"; import {arrayEq} from "jstests/aggregation/extras/utils.js";
import {getWinningPlan, isIdhack, isIndexOnly, isIxscan} from "jstests/libs/analyze_plan.js"; import {
getOptimizer,
getWinningPlan,
isCollscan,
isIdhack,
isIndexOnly,
isIxscan
} from "jstests/libs/analyze_plan.js";
let coll = db["projection_dotted_paths"]; let coll = db["projection_dotted_paths"];
coll.drop(); coll.drop();
@ -21,16 +28,38 @@ assert.commandWorked(coll.insert({_id: 1, a: 1, b: {c: 1, d: 1, e: 1}, c: 1, e:
let resultDoc = coll.findOne({a: 1}, {_id: 0, a: 1, "b.c": 1, "b.d": 1, c: 1}); let resultDoc = coll.findOne({a: 1}, {_id: 0, a: 1, "b.c": 1, "b.d": 1, c: 1});
assert.eq(resultDoc, {a: 1, b: {c: 1, d: 1}, c: 1}); assert.eq(resultDoc, {a: 1, b: {c: 1, d: 1}, c: 1});
let explain = coll.find({a: 1}, {_id: 0, a: 1, "b.c": 1, "b.d": 1, c: 1}).explain("queryPlanner"); let explain = coll.find({a: 1}, {_id: 0, a: 1, "b.c": 1, "b.d": 1, c: 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)), explain); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)), explain);
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)), explain); assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)), explain);
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
// Project a subset of the indexed fields. Verify that the projection is computed correctly and // Project a subset of the indexed fields. Verify that the projection is computed correctly and
// that the plan is covered. // that the plan is covered.
resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, c: 1}); resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, c: 1});
assert.eq(resultDoc, {b: {c: 1}, c: 1}); assert.eq(resultDoc, {b: {c: 1}, c: 1});
explain = coll.find({a: 1}, {_id: 0, "b.c": 1, c: 1}).explain("queryPlanner"); explain = coll.find({a: 1}, {_id: 0, "b.c": 1, c: 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
// Project exactly the set of fields in the index but also include _id. Verify that the // Project exactly the set of fields in the index but also include _id. Verify that the
// projection is computed correctly and that the plan cannot be covered. // projection is computed correctly and that the plan cannot be covered.
@ -38,30 +67,72 @@ resultDoc = coll.findOne({a: 1}, {_id: 1, a: 1, "b.c": 1, "b.d": 1, c: 1});
assert.docEq({_id: 1, a: 1, b: {c: 1, d: 1}, c: 1}, resultDoc); assert.docEq({_id: 1, a: 1, b: {c: 1, d: 1}, c: 1}, resultDoc);
explain = coll.find({a: 1}, {_id: 0, "b.c": 1, c: 1}).explain("queryPlanner"); explain = coll.find({a: 1}, {_id: 0, "b.c": 1, c: 1}).explain("queryPlanner");
explain = coll.find({a: 1}, {_id: 1, a: 1, "b.c": 1, "b.d": 1, c: 1}).explain("queryPlanner"); explain = coll.find({a: 1}, {_id: 1, a: 1, "b.c": 1, "b.d": 1, c: 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
// Project a not-indexed field that exists in the collection. The plan should not be covered. // Project a not-indexed field that exists in the collection. The plan should not be covered.
resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}); resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1});
assert.docEq({b: {c: 1, e: 1}, c: 1}, resultDoc); assert.docEq({b: {c: 1, e: 1}, c: 1}, resultDoc);
explain = coll.find({a: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}).explain("queryPlanner"); explain = coll.find({a: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
// Project a not-indexed field that does not exist in the collection. The plan should not be // Project a not-indexed field that does not exist in the collection. The plan should not be
// covered. // covered.
resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, "b.z": 1, c: 1}); resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, "b.z": 1, c: 1});
assert.docEq({b: {c: 1}, c: 1}, resultDoc); assert.docEq({b: {c: 1}, c: 1}, resultDoc);
explain = coll.find({a: 1}, {_id: 0, "b.c": 1, "b.z": 1, c: 1}).explain("queryPlanner"); explain = coll.find({a: 1}, {_id: 0, "b.c": 1, "b.z": 1, c: 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// Verify that the correct projection is computed with an idhack query. // Verify that the correct projection is computed with an idhack query.
resultDoc = coll.findOne({_id: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}); resultDoc = coll.findOne({_id: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1});
assert.docEq({b: {c: 1, e: 1}, c: 1}, resultDoc); assert.docEq({b: {c: 1, e: 1}, c: 1}, resultDoc);
explain = coll.find({_id: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}).explain("queryPlanner"); explain = coll.find({_id: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIdhack(db, getWinningPlan(explain.queryPlanner)), explain); assert(isIdhack(db, getWinningPlan(explain.queryPlanner)), explain);
break;
}
case "CQF":
// TODO SERVER-70847, how to recognize the case of an IDHACK for Bonsai?
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// If we make a dotted path multikey, projections using that path cannot be covered. But // If we make a dotted path multikey, projections using that path cannot be covered. But
// projections which do not include the multikey path can still be covered. // projections which do not include the multikey path can still be covered.
@ -70,15 +141,37 @@ assert.commandWorked(coll.insert({a: 2, b: {c: 1, d: [1, 2, 3]}}));
resultDoc = coll.findOne({a: 2}, {_id: 0, "b.c": 1, "b.d": 1}); resultDoc = coll.findOne({a: 2}, {_id: 0, "b.c": 1, "b.d": 1});
assert.eq(resultDoc, {b: {c: 1, d: [1, 2, 3]}}); assert.eq(resultDoc, {b: {c: 1, d: [1, 2, 3]}});
explain = coll.find({a: 2}, {_id: 0, "b.c": 1, "b.d": 1}).explain("queryPlanner"); explain = coll.find({a: 2}, {_id: 0, "b.c": 1, "b.d": 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
resultDoc = coll.findOne({a: 2}, {_id: 0, "b.c": 1}); resultDoc = coll.findOne({a: 2}, {_id: 0, "b.c": 1});
assert.eq(resultDoc, {b: {c: 1}}); assert.eq(resultDoc, {b: {c: 1}});
explain = coll.find({a: 2}, {_id: 0, "b.c": 1}).explain("queryPlanner"); explain = coll.find({a: 2}, {_id: 0, "b.c": 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// Path-level multikey info allows for generating a covered plan. // Path-level multikey info allows for generating a covered plan.
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
// Verify that dotted projections work for multiple levels of nesting. // Verify that dotted projections work for multiple levels of nesting.
assert.commandWorked(coll.createIndex({a: 1, "x.y.y": 1, "x.y.z": 1, "x.z": 1})); assert.commandWorked(coll.createIndex({a: 1, "x.y.y": 1, "x.y.z": 1, "x.z": 1}));
@ -86,8 +179,18 @@ assert.commandWorked(coll.insert({a: 3, x: {y: {y: 1, f: 1, z: 1}, f: 1, z: 1}})
resultDoc = coll.findOne({a: 3}, {_id: 0, "x.y.y": 1, "x.y.z": 1, "x.z": 1}); resultDoc = coll.findOne({a: 3}, {_id: 0, "x.y.y": 1, "x.y.z": 1, "x.z": 1});
assert.eq(resultDoc, {x: {y: {y: 1, z: 1}, z: 1}}); assert.eq(resultDoc, {x: {y: {y: 1, z: 1}, z: 1}});
explain = coll.find({a: 3}, {_id: 0, "x.y.y": 1, "x.y.z": 1, "x.z": 1}).explain("queryPlanner"); explain = coll.find({a: 3}, {_id: 0, "x.y.y": 1, "x.y.z": 1, "x.z": 1}).explain("queryPlanner");
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// If projected nested paths do not exist in the indexed document, then they will get filled in // If projected nested paths do not exist in the indexed document, then they will get filled in
// with nulls. This is a bug tracked by SERVER-23229. // with nulls. This is a bug tracked by SERVER-23229.
@ -99,8 +202,19 @@ assert.eq(resultDoc, {x: {y: {y: null, z: null}, z: null}});
assert.commandWorked(coll.createIndex({"a.b.c": 1, "a.b": 1})); assert.commandWorked(coll.createIndex({"a.b.c": 1, "a.b": 1}));
assert.commandWorked(coll.insert({a: {b: {c: 1, d: 1}}})); assert.commandWorked(coll.insert({a: {b: {c: 1, d: 1}}}));
explain = coll.find({"a.b.c": 1}, {_id: 0, "a.b": 1}).explain(); explain = coll.find({"a.b.c": 1}, {_id: 0, "a.b": 1}).explain();
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
assert.eq(coll.findOne({"a.b.c": 1}, {_id: 0, "a.b": 1}), {a: {b: {c: 1, d: 1}}}); assert.eq(coll.findOne({"a.b.c": 1}, {_id: 0, "a.b": 1}), {a: {b: {c: 1, d: 1}}});
} }
@ -112,8 +226,19 @@ assert.eq(resultDoc, {x: {y: {y: null, z: null}, z: null}});
const filter = {"a.b": {c: 1, d: 1}}; const filter = {"a.b": {c: 1, d: 1}};
explain = coll.find(filter, {_id: 0, "a.b": 1}).explain(); explain = coll.find(filter, {_id: 0, "a.b": 1}).explain();
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner))); assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner))); assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
assert(isCollscan(db, explain));
break;
}
assert.eq(coll.findOne(filter, {_id: 0, "a.b": 1}), {a: {b: {c: 1, d: 1}}}); assert.eq(coll.findOne(filter, {_id: 0, "a.b": 1}), {a: {b: {c: 1, d: 1}}});
} }

View File

@ -17,6 +17,7 @@
* assumes_no_implicit_index_creation, * assumes_no_implicit_index_creation,
* ] * ]
*/ */
import {getOptimizer} from "jstests/libs/analyze_plan.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js"; import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
import {checkSBEEnabled} from "jstests/libs/sbe_util.js"; import {checkSBEEnabled} from "jstests/libs/sbe_util.js";
@ -41,6 +42,12 @@ let assertPlanCacheField = function(
tojson(firstExplain) + " with " + tojson(secondExplain)); tojson(firstExplain) + " with " + tojson(secondExplain));
}; };
// TODO SERVER-77719: Ensure that the test is valid for different combinations of optimizer used
// for with/without index cases.
if (!(getOptimizer(firstExplain) == getOptimizer(secondExplain))) {
return;
}
// SERVER-56980: When running in a sharded environment, we group the values for 'planCacheField' // SERVER-56980: When running in a sharded environment, we group the values for 'planCacheField'
// by shard. This is because in a multi-version environment, we want to ensure that we are // by shard. This is because in a multi-version environment, we want to ensure that we are
// comparing the results produced by the same shard in the event that the planCacheKey format // comparing the results produced by the same shard in the event that the planCacheKey format

View File

@ -7,6 +7,7 @@
*/ */
import { import {
getExecutionStages, getExecutionStages,
getOptimizer,
getPlanStages, getPlanStages,
getRejectedPlan, getRejectedPlan,
getRejectedPlans, getRejectedPlans,
@ -38,18 +39,27 @@ const a1IndexName = "a_1";
const b1IndexName = "b_1"; const b1IndexName = "b_1";
const explain = coll.find({a: 7, b: 9}).explain("executionStats"); const explain = coll.find({a: 7, b: 9}).explain("executionStats");
switch (getOptimizer(explain)) {
case "classic": {
// Verify that the winner plan has index scan stage on 'a_1_b_1'. // Verify that the winner plan has index scan stage on 'a_1_b_1'.
let ixscans = getPlanStages(getWinningPlan(explain.queryPlanner), "IXSCAN"); let ixscans = getPlanStages(getWinningPlan(explain.queryPlanner), "IXSCAN");
assert.neq(ixscans.length, 0, explain); assert.neq(ixscans.length, 0, explain);
for (let ixscan of ixscans) { for (let ixscan of ixscans) {
assert.eq(ixscan.indexName, a1b1IndexName, explain); assert.eq(ixscan.indexName, a1b1IndexName, explain);
} }
break;
}
case "CQF":
// TODO SERVER-77719: Ensure that the decision for using the scan lines up with CQF
// optimizer. M2: allow only collscans, M4: check bonsai behavior for index scan.
break;
}
// Verify that the winning SBE plan has index scan stage on 'a_1_b_1'. // Verify that the winning SBE plan has index scan stage on 'a_1_b_1'.
const executionStages = getExecutionStages(explain); const executionStages = getExecutionStages(explain);
assert.neq(executionStages.length, 0, explain); assert.neq(executionStages.length, 0, explain);
for (let executionStage of executionStages) { for (let executionStage of executionStages) {
ixscans = getPlanStages(executionStage, "ixseek"); let ixscans = getPlanStages(executionStage, "ixseek");
assert.neq(ixscans.length, 0); assert.neq(ixscans.length, 0);
for (let ixscan of ixscans) { for (let ixscan of ixscans) {
assert.eq(ixscan.indexName, a1b1IndexName, ixscan); assert.eq(ixscan.indexName, a1b1IndexName, ixscan);
@ -58,7 +68,9 @@ for (let executionStage of executionStages) {
// Verify that rejected plans should have index scan on 'a_1' or 'b_1'. // Verify that rejected plans should have index scan on 'a_1' or 'b_1'.
for (let rejectedPlan of getRejectedPlans(explain)) { for (let rejectedPlan of getRejectedPlans(explain)) {
ixscans = getPlanStages(getRejectedPlan(rejectedPlan), "IXSCAN"); let stages = {"classic": "IXSCAN", "CQF": "IndexScan"};
let optimizer = getOptimizer(explain);
let ixscans = getPlanStages(getRejectedPlan(rejectedPlan), stages[optimizer]);
assert.neq(ixscans.length, 0, explain); assert.neq(ixscans.length, 0, explain);
for (let ixscan of ixscans) { for (let ixscan of ixscans) {
assert.contains(ixscan.indexName, [a1IndexName, b1IndexName], ixscan); assert.contains(ixscan.indexName, [a1IndexName, b1IndexName], ixscan);

View File

@ -228,6 +228,8 @@ export function getRejectedPlans(root) {
export function hasRejectedPlans(root) { export function hasRejectedPlans(root) {
function sectionHasRejectedPlans(explainSection, optimizer = "classic") { function sectionHasRejectedPlans(explainSection, optimizer = "classic") {
if (optimizer == "CQF") { if (optimizer == "CQF") {
// TODO SERVER-77719: The existence of alternative/rejected plans will be re-evaluated
// in the future.
return true; return true;
} }
@ -255,6 +257,8 @@ export function hasRejectedPlans(root) {
return cursorStages.find((cursorStage) => cursorStageHasRejectedPlans(cursorStage)) !== return cursorStages.find((cursorStage) => cursorStageHasRejectedPlans(cursorStage)) !==
undefined; undefined;
} else { } else {
let optimizer = getOptimizer(root);
// This is some sort of query explain. // This is some sort of query explain.
assert(root.hasOwnProperty("queryPlanner"), tojson(root)); assert(root.hasOwnProperty("queryPlanner"), tojson(root));
assert(root.queryPlanner.hasOwnProperty("winningPlan"), tojson(root)); assert(root.queryPlanner.hasOwnProperty("winningPlan"), tojson(root));
@ -262,15 +266,22 @@ export function hasRejectedPlans(root) {
// SERVER-77719: Update regarding the expected behavior of the CQF optimizer. Currently // SERVER-77719: Update regarding the expected behavior of the CQF optimizer. Currently
// CQF explains are empty, when the optimizer returns alternative plans, we should // CQF explains are empty, when the optimizer returns alternative plans, we should
// address this. // address this.
let optimizer = getOptimizer(root);
// This is an unsharded explain. // This is an unsharded explain.
return sectionHasRejectedPlans(root.queryPlanner, optimizer); return sectionHasRejectedPlans(root.queryPlanner, optimizer);
} }
if ("SINGLE_SHARD" == root.queryPlanner.winningPlan.stage) {
var shards = root.queryPlanner.winningPlan.shards;
shards.forEach(function assertShardHasRejectedPlans(shard) {
sectionHasRejectedPlans(shard, optimizer);
});
}
// This is a sharded explain. Each entry in the shards array contains a 'winningPlan' and // This is a sharded explain. Each entry in the shards array contains a 'winningPlan' and
// 'rejectedPlans'. // 'rejectedPlans'.
return root.queryPlanner.winningPlan.shards.find( return root.queryPlanner.winningPlan.shards.find(
(shard) => sectionHasRejectedPlans(shard)) !== undefined; (shard) => sectionHasRejectedPlans(shard, optimizer)) !== undefined;
} }
} }
@ -895,3 +906,14 @@ export function getNumberOfColumnScans(explain) {
const columnIndexScans = getPlanStages(getWinningPlan(explain.queryPlanner), stages[optimizer]); const columnIndexScans = getPlanStages(getWinningPlan(explain.queryPlanner), stages[optimizer]);
return columnIndexScans.length; return columnIndexScans.length;
} }
/*
* Returns whether a query is using a multikey index.
*
* This helper function can be used only for "classic" optimizer.
*/
export function isIxscanMultikey(winningPlan) {
// SERVER-77719: Update to expected this method to allow also use with CQF optimizer.
let ixscanStage = getPlanStage(winningPlan, "IXSCAN");
return ixscanStage.isMultiKey;
}