mirror of https://github.com/mongodb/mongo
SERVER-83098: Make existing jstest work with the new CQF explain (core/query and core/sbe) (2)
This commit is contained in:
parent
40c8c6eb88
commit
1aea6febd3
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -44,5 +44,3 @@ switch (getOptimizer(explainResult)) {
|
|||
default:
|
||||
break
|
||||
}
|
||||
|
||||
t.drop();
|
||||
|
|
|
|||
|
|
@ -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}},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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"));
|
||||
switch (getOptimizer(explainRes)) {
|
||||
case "classic": {
|
||||
assert(isIxscan(db, winningPlan));
|
||||
assert(planHasStage(db, winningPlan, "FETCH"));
|
||||
let ixscanStage = getPlanStage(winningPlan, "IXSCAN");
|
||||
assert.eq(true, ixscanStage.isMultiKey);
|
||||
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"));
|
||||
switch (getOptimizer(explainRes)) {
|
||||
case "classic": {
|
||||
assert(isIxscan(db, winningPlan));
|
||||
assert(planHasStage(db, winningPlan, "FETCH"));
|
||||
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
|
||||
assert.eq(true, ixscanStage.isMultiKey);
|
||||
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"));
|
||||
switch (getOptimizer(explainRes)) {
|
||||
case "classic": {
|
||||
assert(isIxscan(db, winningPlan));
|
||||
assert(planHasStage(db, winningPlan, "FETCH"));
|
||||
ixscanStage = getPlanStage(winningPlan, "IXSCAN");
|
||||
assert.eq(true, ixscanStage.isMultiKey);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
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)),
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
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}}});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue