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,
* ]
*/
// adding temporary comment
import {getOptimizer, getWinningPlan, isIxscan} from "jstests/libs/analyze_plan.js";
const coll = db.elemMatch_index;

View File

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

View File

@ -11,7 +11,7 @@
// 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.
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");
assert.commandWorked(testDB.dropDatabase());
@ -22,11 +22,27 @@ const view = testDB.getCollection(viewName);
function confirmWinningPlanUsesExpectedIndex(
explainResult, expectedKeyPattern, stageName, pipelineOptimizedAway) {
const planStage = pipelineOptimizedAway ? getPlanStage(explainResult, stageName)
: getAggPlanStage(explainResult, stageName);
assert.neq(null, planStage);
const optimizer = getOptimizer(explainResult);
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));
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.
@ -37,7 +53,10 @@ function confirmCommandUsesIndex({
command = null,
hintKeyPattern = null,
expectedKeyPattern = null,
stageName = "IXSCAN",
stageName = {
"classic": "IXSCAN",
"CQF": "IndexScan"
},
pipelineOptimizedAway = false
} = {}) {
if (hintKeyPattern) {
@ -60,7 +79,10 @@ function confirmAggUsesIndex({
aggPipeline = [],
hintKeyPattern = null,
expectedKeyPattern = null,
stageName = "IXSCAN",
stageName = {
"classic": "IXSCAN",
"CQF": "IndexScan"
},
pipelineOptimizedAway = false
} = {}) {
let options = {};
@ -248,16 +270,18 @@ for (let i = 0; i < 5; ++i) {
}
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({
command: {count: view.getName(), query: {x: 3}},
expectedKeyPattern: {x: 1},
stageName: "COUNT_SCAN"
stageName: {"classic": "COUNT_SCAN", "CQF": ""}
});
confirmCommandUsesIndex({
command: {count: view.getName(), query: {x: 3}},
hintKeyPattern: {x: 1},
expectedKeyPattern: {x: 1},
stageName: "COUNT_SCAN"
stageName: {"classic": "COUNT_SCAN", "CQF": ""}
});
confirmCommandUsesIndex({
command: {count: view.getName(), query: {x: 3}},

View File

@ -9,7 +9,15 @@
* Test covering behavior for queries over a multikey index.
*/
// 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;
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}));
let explainRes = coll.explain("queryPlanner").find({a: 1, b: 2}, {_id: 0, a: 1}).finish();
let winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(isIxscan(db, winningPlan));
switch (getOptimizer(explainRes)) {
case "classic": {
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"));
coll.drop();
@ -47,10 +66,20 @@ assert.commandWorked(coll.createIndex({a: 1}));
assert.eq({a: []}, coll.findOne({a: []}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: []}, {_id: 0, a: 1}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(planHasStage(db, winningPlan, "IXSCAN"));
assert(planHasStage(db, winningPlan, "FETCH"));
let ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.eq(true, ixscanStage.isMultiKey);
switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan));
assert(planHasStage(db, winningPlan, "FETCH"));
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.
assert(isCollscan(db, winningPlan));
break
}
}
// Verify that a query cannot be covered over a path which is multikey due to a single-element
// array.
@ -60,10 +89,20 @@ assert.commandWorked(coll.createIndex({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();
winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(planHasStage(db, winningPlan, "IXSCAN"));
assert(planHasStage(db, winningPlan, "FETCH"));
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.eq(true, ixscanStage.isMultiKey);
switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan));
assert(planHasStage(db, winningPlan, "FETCH"));
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.
assert(isCollscan(db, winningPlan));
break;
}
}
// 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.
@ -74,10 +113,20 @@ assert.commandWorked(coll.update({}, {$set: {a: [2]}}));
assert.eq({a: [2]}, coll.findOne({a: 2}, {_id: 0, a: 1}));
explainRes = coll.explain("queryPlanner").find({a: 2}, {_id: 0, a: 1}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner);
assert(planHasStage(db, winningPlan, "IXSCAN"));
assert(planHasStage(db, winningPlan, "FETCH"));
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.eq(true, ixscanStage.isMultiKey);
switch (getOptimizer(explainRes)) {
case "classic": {
assert(isIxscan(db, winningPlan));
assert(planHasStage(db, winningPlan, "FETCH"));
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.
assert(isCollscan(db, winningPlan));
break;
}
}
// Verify that a trailing empty array makes a 2dsphere index multikey.
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]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
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.eq(false, ixscanStage.isMultiKey);
assert.commandWorked(coll.insert({a: {b: []}, c: {type: "Point", coordinates: [0, 0]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(true, ixscanStage.isMultiKey);
switch (getOptimizer(explainRes)) {
case "classic": {
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.
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]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(true, ixscanStage.isMultiKey);
switch (getOptimizer(explainRes)) {
case "classic": {
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.
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]}}));
explainRes = coll.explain().find().hint({"a.b": 1, c: "2dsphere"}).finish();
winningPlan = getWinningPlan(explainRes.queryPlanner);
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
assert.neq(null, ixscanStage);
assert.eq(true, ixscanStage.isMultiKey);
switch (getOptimizer(explainRes)) {
case "classic": {
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,
// ]
import {hasRejectedPlans} from "jstests/libs/analyze_plan.js";
/**
* 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.
@ -55,25 +57,10 @@ assert.doesNotThrow(function() {
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();
assert.commandWorked(res);
assertHasRejectedPlans(res);
assert(hasRejectedPlans(res));
res = coll.explain("executionStats").find({a: {$gte: 1}}).finish();
assert.commandWorked(res);
assertHasRejectedPlans(res);
assert(hasRejectedPlans(res));

View File

@ -8,6 +8,8 @@
// assumes_against_mongod_not_mongos,
// ]
import {getOptimizer} from "jstests/libs/analyze_plan.js";
const coll = db.explain_winning_plan;
coll.drop();
@ -43,7 +45,16 @@ assert.eq(explain.executionStats.nReturned, numDocs);
// representing two candidate plans evaluated by the multi-planner.
assert(explain.executionStats.hasOwnProperty("allPlansExecution"), explain);
assert(Array.isArray(explain.executionStats.allPlansExecution), explain);
assert.eq(explain.executionStats.allPlansExecution.length, 2, explain);
switch (getOptimizer(explain)) {
case "classic":
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
// trial period.

View File

@ -4,7 +4,7 @@
// requires_non_retryable_writes,
// ]
// 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";
const t = db.idhack;
@ -35,14 +35,25 @@ const query = {
};
let explain = t.find(query).explain("allPlansExecution");
assert.eq(1, explain.executionStats.nReturned, explain);
assert.eq(1, explain.executionStats.totalKeysExamined, explain);
let winningPlan = getWinningPlan(explain.queryPlanner);
assert(isIdhack(db, winningPlan), winningPlan);
switch (getOptimizer(explain)) {
case "classic": {
assert.eq(1, explain.executionStats.totalKeysExamined, explain);
let winningPlan = getWinningPlan(explain.queryPlanner);
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().
t.createIndex({_id: 1, a: 1});
explain = t.find(query).hint({_id: 1, a: 1}).explain();
winningPlan = getWinningPlan(explain.queryPlanner);
let winningPlan = getWinningPlan(explain.queryPlanner);
assert(!isIdhack(db, winningPlan), winningPlan);
// ID hack cannot be used with skip().
@ -61,7 +72,18 @@ assert(!isIdhack(db, winningPlan), winningPlan);
const parentStage = checkSBEEnabled(db) ? "PROJECTION_COVERED" : "FETCH";
explain = t.find(query, {_id: 1}).explain();
winningPlan = getWinningPlan(explain.queryPlanner);
assert(isIdhack(db, winningPlan), winningPlan);
switch (getOptimizer(explain)) {
case "classic": {
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.
assert.eq({_id: {x: 2}}, t.findOne(query, {_id: 1}), explain);

View File

@ -8,7 +8,13 @@
* 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 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.
*/
function assertExplainIxscan(explainPlan, expectedIndexSpec, expectedKeysExamined = 1) {
switch (getOptimizer(explainPlan)) {
case "classic": {
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);
execStages.forEach(execStage => {
if (execStage.stage == "SHARDING_FILTER" && execStage.nReturned == 0) {

View File

@ -7,7 +7,7 @@
// # TODO SERVER-77719: Enable this check for Bonsai.
// 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";
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
// an ixscan, because the chosen plan may not be the same.
const explainRes = assert.commandWorked(coll.find({$or: [{a: 1}, {b: 1}]}).explain());
const orStages = getPlanStages(getWinningPlan(explainRes.queryPlanner), "OR");
assert(orStages.length > 0, "Expected to find OR stage in explain: " + tojson(explainRes));
assert(orStages.every(orStage => (getPlanStages(orStage, "IXSCAN").length > 0)),
switch (getOptimizer(explainRes)) {
case "classic": {
const orStages = getPlanStages(getWinningPlan(explainRes.queryPlanner), "OR");
assert(orStages.length > 0, "Expected to find OR stage in explain: " + tojson(explainRes));
assert(
orStages.every(orStage => (getPlanStages(orStage, "IXSCAN").length > 0)),
"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.
const cursor = coll.find({$or: [{a: 1}, {b: 1}]}).batchSize(100);

View File

@ -9,7 +9,14 @@
* when appropriate.
*/
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"];
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});
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");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)), explain);
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)), explain);
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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
// that the plan is covered.
resultDoc = coll.findOne({a: 1}, {_id: 0, "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");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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
// 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);
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");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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.
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);
explain = coll.find({a: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}).explain("queryPlanner");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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
// covered.
resultDoc = coll.findOne({a: 1}, {_id: 0, "b.c": 1, "b.z": 1, c: 1});
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");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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.
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);
explain = coll.find({_id: 1}, {_id: 0, "b.c": 1, "b.e": 1, c: 1}).explain("queryPlanner");
assert(isIdhack(db, getWinningPlan(explain.queryPlanner)), explain);
switch (getOptimizer(explain)) {
case "classic": {
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
// 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});
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");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(!isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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});
assert.eq(resultDoc, {b: {c: 1}});
explain = coll.find({a: 2}, {_id: 0, "b.c": 1}).explain("queryPlanner");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// Path-level multikey info allows for generating a covered plan.
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
// Path-level multikey info allows for generating a covered plan.
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.
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});
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");
assert(isIxscan(db, getWinningPlan(explain.queryPlanner)));
assert(isIndexOnly(db, getWinningPlan(explain.queryPlanner)));
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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
// 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.insert({a: {b: {c: 1, d: 1}}}));
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(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}}});
}
@ -112,8 +226,19 @@ assert.eq(resultDoc, {x: {y: {y: null, z: null}, z: null}});
const filter = {"a.b": {c: 1, d: 1}};
explain = coll.find(filter, {_id: 0, "a.b": 1}).explain();
switch (getOptimizer(explain)) {
case "classic": {
assert(isIxscan(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}}});
}

View File

@ -17,6 +17,7 @@
* assumes_no_implicit_index_creation,
* ]
*/
import {getOptimizer} from "jstests/libs/analyze_plan.js";
import {FixtureHelpers} from "jstests/libs/fixture_helpers.js";
import {checkSBEEnabled} from "jstests/libs/sbe_util.js";
@ -41,6 +42,12 @@ let assertPlanCacheField = function(
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'
// 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

View File

@ -7,6 +7,7 @@
*/
import {
getExecutionStages,
getOptimizer,
getPlanStages,
getRejectedPlan,
getRejectedPlans,
@ -38,18 +39,27 @@ const a1IndexName = "a_1";
const b1IndexName = "b_1";
const explain = coll.find({a: 7, b: 9}).explain("executionStats");
// Verify that the winner plan has index scan stage on 'a_1_b_1'.
let ixscans = getPlanStages(getWinningPlan(explain.queryPlanner), "IXSCAN");
assert.neq(ixscans.length, 0, explain);
for (let ixscan of ixscans) {
switch (getOptimizer(explain)) {
case "classic": {
// Verify that the winner plan has index scan stage on 'a_1_b_1'.
let ixscans = getPlanStages(getWinningPlan(explain.queryPlanner), "IXSCAN");
assert.neq(ixscans.length, 0, explain);
for (let ixscan of ixscans) {
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'.
const executionStages = getExecutionStages(explain);
assert.neq(executionStages.length, 0, explain);
for (let executionStage of executionStages) {
ixscans = getPlanStages(executionStage, "ixseek");
let ixscans = getPlanStages(executionStage, "ixseek");
assert.neq(ixscans.length, 0);
for (let ixscan of ixscans) {
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'.
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);
for (let ixscan of ixscans) {
assert.contains(ixscan.indexName, [a1IndexName, b1IndexName], ixscan);

View File

@ -228,6 +228,8 @@ export function getRejectedPlans(root) {
export function hasRejectedPlans(root) {
function sectionHasRejectedPlans(explainSection, optimizer = "classic") {
if (optimizer == "CQF") {
// TODO SERVER-77719: The existence of alternative/rejected plans will be re-evaluated
// in the future.
return true;
}
@ -255,6 +257,8 @@ export function hasRejectedPlans(root) {
return cursorStages.find((cursorStage) => cursorStageHasRejectedPlans(cursorStage)) !==
undefined;
} else {
let optimizer = getOptimizer(root);
// This is some sort of query explain.
assert(root.hasOwnProperty("queryPlanner"), 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
// CQF explains are empty, when the optimizer returns alternative plans, we should
// address this.
let optimizer = getOptimizer(root);
// This is an unsharded explain.
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
// 'rejectedPlans'.
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]);
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;
}