mirror of https://github.com/mongodb/mongo
425 lines
13 KiB
JavaScript
425 lines
13 KiB
JavaScript
import {getAggPlanStage, isAggregationPlan} from "jstests/libs/analyze_plan.js";
|
|
|
|
/**
|
|
* Utility for checking if the Cascades optimizer code path is enabled (checks framework control).
|
|
*/
|
|
export function checkCascadesOptimizerEnabled(theDB) {
|
|
const val = theDB.adminCommand({getParameter: 1, internalQueryFrameworkControl: 1})
|
|
.internalQueryFrameworkControl;
|
|
return val == "tryBonsai" || val == "tryBonsaiExperimental" || val == "forceBonsai";
|
|
}
|
|
|
|
// TODO SERVER-82185: Remove this once M2-eligibility checker + E2E parameterization implemented
|
|
export function checkPlanCacheParameterization(theDB) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Utility for checking if the experimental Cascades optimizer code path is enabled (checks
|
|
* framework control for M4+).
|
|
*/
|
|
export function checkExperimentalCascadesOptimizerEnabled(theDB) {
|
|
const val = theDB.adminCommand({getParameter: 1, internalQueryFrameworkControl: 1})
|
|
.internalQueryFrameworkControl;
|
|
return val == "tryBonsaiExperimental" || val == "forceBonsai";
|
|
}
|
|
|
|
/**
|
|
* Utility for checking if the Cascades optimizer feature flag is on.
|
|
*/
|
|
export function checkCascadesFeatureFlagEnabled(theDB) {
|
|
const featureFlag = theDB.adminCommand({getParameter: 1, featureFlagCommonQueryFramework: 1});
|
|
return featureFlag.hasOwnProperty("featureFlagCommonQueryFramework") &&
|
|
featureFlag.featureFlagCommonQueryFramework.value;
|
|
}
|
|
|
|
/**
|
|
* Given the result of an explain command, returns whether the bonsai optimizer was used.
|
|
*/
|
|
export function usedBonsaiOptimizer(explain) {
|
|
if (explain.hasOwnProperty("shards")) {
|
|
// This section handles the explain output for aggregations against sharded colls.
|
|
for (let shardName of Object.keys(explain.shards)) {
|
|
if (explain.shards[shardName].queryPlanner.queryFramework !== "cqf") {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} else if (explain.hasOwnProperty("queryPlanner") &&
|
|
explain.queryPlanner.winningPlan.hasOwnProperty("shards")) {
|
|
// This section handles the explain output for find queries against sharded colls.
|
|
for (let shardExplain of explain.queryPlanner.winningPlan.shards) {
|
|
if (shardExplain.queryFramework !== "cqf") {
|
|
return false;
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// This section handles the explain output for unsharded queries.
|
|
return explain.hasOwnProperty("queryPlanner") && explain.queryPlanner.queryFramework === "cqf";
|
|
}
|
|
|
|
/**
|
|
* Given a query plan or explain output, follow the leftmost child until
|
|
* we reach a leaf stage, and return it.
|
|
*
|
|
* This is useful for finding the access path part of a plan, typically a PhysicalScan or IndexScan.
|
|
*/
|
|
export function leftmostLeafStage(node) {
|
|
for (;;) {
|
|
if (node.queryPlanner) {
|
|
node = node.queryPlanner;
|
|
} else if (node.winningPlan) {
|
|
node = node.winningPlan;
|
|
} else if (node.queryPlan) {
|
|
node = node.queryPlan;
|
|
} else if (node.child) {
|
|
node = node.child;
|
|
} else if (node.inputStage) {
|
|
node = node.inputStage;
|
|
} else if (node.leftChild) {
|
|
node = node.leftChild;
|
|
} else if (node.children) {
|
|
node = node.children[0];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the cardinality estimate from a node in explain.
|
|
*/
|
|
export function extractLogicalCEFromNode(node) {
|
|
const ce = node.properties.logicalProperties.cardinalityEstimate[0].ce;
|
|
assert.neq(ce, null, tojson(node));
|
|
return ce;
|
|
}
|
|
|
|
/**
|
|
* Get a very simplified version of a plan, which only includes nodeType and nesting structure.
|
|
*/
|
|
export function getPlanSkeleton(node, options = {}) {
|
|
const {extraKeepKeys = [], keepKeysDeep = [], printFilter = false, printLogicalCE = false} =
|
|
options;
|
|
|
|
const keepKeys = [
|
|
'nodeType',
|
|
|
|
'queryPlanner',
|
|
'winningPlan',
|
|
'queryPlan',
|
|
'child',
|
|
'children',
|
|
'leftChild',
|
|
'rightChild',
|
|
].concat(extraKeepKeys);
|
|
|
|
if (Array.isArray(node)) {
|
|
return node.map(n => getPlanSkeleton(n, options));
|
|
} else if (node === null || typeof node !== 'object') {
|
|
return node;
|
|
} else {
|
|
return Object.fromEntries(
|
|
Object.keys(node)
|
|
.filter(key => (keepKeys.includes(key) || keepKeysDeep.includes(key)))
|
|
.map(key => {
|
|
if (key === 'interval') {
|
|
return [key, prettyInterval(node[key])];
|
|
} else if (key === 'filter' && printFilter) {
|
|
return [key, prettyExpression(node[key])];
|
|
} else if (key === "properties" && printLogicalCE) {
|
|
return ["logicalCE", extractLogicalCEFromNode(node)];
|
|
} else if (keepKeysDeep.includes(key)) {
|
|
return [key, node[key]];
|
|
} else {
|
|
return [key, getPlanSkeleton(node[key], options)];
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recur into every object and array; return any subtree that matches 'predicate'.
|
|
* Only calls 'predicate' on objects: not arrays or scalars.
|
|
*
|
|
* This is completely ignorant of the structure of a query: for example if there
|
|
* are literals match the predicate, it will also match those.
|
|
*/
|
|
export function findSubtrees(tree, predicate) {
|
|
let result = [];
|
|
const visit = subtree => {
|
|
if (typeof subtree === 'object' && subtree != null) {
|
|
if (Array.isArray(subtree)) {
|
|
for (const child of subtree) {
|
|
visit(child);
|
|
}
|
|
} else {
|
|
if (predicate(subtree)) {
|
|
result.push(subtree);
|
|
}
|
|
for (const key of Object.keys(subtree)) {
|
|
visit(subtree[key]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
visit(tree);
|
|
return result;
|
|
}
|
|
|
|
export function printBound(bound) {
|
|
if (!Array.isArray(bound.bound)) {
|
|
return [false, ""];
|
|
}
|
|
|
|
let result = "";
|
|
let first = true;
|
|
for (const element of bound.bound) {
|
|
if (element.nodeType !== "Const") {
|
|
return [false, ""];
|
|
}
|
|
|
|
result += tojson(element.value);
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
result += " | ";
|
|
}
|
|
}
|
|
|
|
return [true, result];
|
|
}
|
|
|
|
export function prettyInterval(compoundInterval) {
|
|
// Takes an array of intervals, each one applying to one component of a compound index key.
|
|
// Try to format it as a string.
|
|
// If either bound is not Constant, return the original JSON unchanged.
|
|
|
|
const lowBound = compoundInterval.lowBound;
|
|
const highBound = compoundInterval.highBound;
|
|
const lowInclusive = lowBound.inclusive;
|
|
const highInclusive = highBound.inclusive;
|
|
assert.eq(typeof lowInclusive, 'boolean');
|
|
assert.eq(typeof highInclusive, 'boolean');
|
|
|
|
let result = '';
|
|
{
|
|
const res = printBound(lowBound);
|
|
if (!res[0]) {
|
|
return compoundInterval;
|
|
}
|
|
result += lowInclusive ? '[ ' : '( ';
|
|
result += res[1];
|
|
}
|
|
result += ", ";
|
|
{
|
|
const res = printBound(highBound);
|
|
if (!res[0]) {
|
|
return compoundInterval;
|
|
}
|
|
result += res[1];
|
|
result += highInclusive ? ' ]' : ' )';
|
|
}
|
|
return result.trim();
|
|
}
|
|
|
|
export function prettyExpression(expr) {
|
|
switch (expr.nodeType) {
|
|
case 'Variable':
|
|
return expr.name;
|
|
case 'Const':
|
|
return tojson(expr.value);
|
|
case 'FunctionCall':
|
|
return `${expr.name}(${expr.arguments.map(a => prettyExpression(a)).join(', ')})`;
|
|
case 'If': {
|
|
const if_ = prettyExpression(expr.condition);
|
|
const then_ = prettyExpression(expr.then);
|
|
const else_ = prettyExpression(expr.else);
|
|
return `if ${if_} then ${then_} else ${else_}`;
|
|
}
|
|
case 'Let': {
|
|
const x = expr.variable;
|
|
const b = prettyExpression(expr.bind);
|
|
const e = prettyExpression(expr.expression);
|
|
return `let ${x} = ${b} in ${e}`;
|
|
}
|
|
case 'LambdaAbstraction': {
|
|
return `(${expr.variable} -> ${prettyExpression(expr.input)})`;
|
|
}
|
|
case 'BinaryOp': {
|
|
const left = prettyExpression(expr.left);
|
|
const right = prettyExpression(expr.right);
|
|
const op = prettyOp(expr.op);
|
|
return `(${left} ${op} ${right})`;
|
|
}
|
|
case 'UnaryOp': {
|
|
const op = prettyOp(expr.op);
|
|
const input = prettyExpression(expr.input);
|
|
return `(${op} ${input})`;
|
|
}
|
|
default:
|
|
return tojson(expr);
|
|
}
|
|
}
|
|
|
|
export function prettyOp(op) {
|
|
// See src/mongo/db/query/optimizer/syntax/syntax.h, PATHSYNTAX_OPNAMES.
|
|
switch (op) {
|
|
/* comparison operations */
|
|
case 'Eq':
|
|
return '==';
|
|
case 'EqMember':
|
|
return 'in';
|
|
case 'Neq':
|
|
return '!=';
|
|
case 'Gt':
|
|
return '>';
|
|
case 'Gte':
|
|
return '>=';
|
|
case 'Lt':
|
|
return '<';
|
|
case 'Lte':
|
|
return '<=';
|
|
case 'Cmp3w':
|
|
return '<=>';
|
|
|
|
/* binary operations */
|
|
case 'Add':
|
|
return '+';
|
|
case 'Sub':
|
|
return '-';
|
|
case 'Mult':
|
|
return '*';
|
|
case 'Div':
|
|
return '/';
|
|
|
|
/* unary operations */
|
|
case 'Neg':
|
|
return '-';
|
|
|
|
/* logical operations */
|
|
case 'And':
|
|
return 'and';
|
|
case 'Or':
|
|
return 'or';
|
|
case 'Not':
|
|
return 'not';
|
|
|
|
default:
|
|
return op;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to remove UUIDs of collections in the supplied database from a V1 or V2 optimizer
|
|
* explain.
|
|
*/
|
|
export function removeUUIDsFromExplain(db, explain) {
|
|
const listCollsRes = db.runCommand({listCollections: 1}).cursor.firstBatch;
|
|
let plan = explain.queryPlanner.winningPlan.queryPlan.plan.toString();
|
|
|
|
for (let entry of listCollsRes) {
|
|
const uuidStr = entry.info.uuid.toString().slice(6).slice(0, -2);
|
|
plan = plan.replaceAll(uuidStr, "");
|
|
}
|
|
return plan;
|
|
}
|
|
|
|
export function navigateToPath(doc, path) {
|
|
let result;
|
|
let field;
|
|
|
|
try {
|
|
result = doc;
|
|
for (field of path.split(".")) {
|
|
assert(result.hasOwnProperty(field));
|
|
result = result[field];
|
|
}
|
|
return result;
|
|
} catch (e) {
|
|
jsTestLog("Error navigating to path '" + path + "'");
|
|
jsTestLog("Missing field: " + field);
|
|
printjson(result);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function navigateToPlanPath(doc, path) {
|
|
return navigateToPath(doc, "queryPlanner.winningPlan.queryPlan." + path);
|
|
}
|
|
|
|
export function navigateToRootNode(doc) {
|
|
return navigateToPath(doc, "queryPlanner.winningPlan.queryPlan");
|
|
}
|
|
|
|
export function assertValueOnPathFn(value, doc, path, fn) {
|
|
try {
|
|
assert.eq(value, fn(doc, path));
|
|
} catch (e) {
|
|
jsTestLog("Assertion error.");
|
|
printjson(doc);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function assertValueOnPath(value, doc, path) {
|
|
assertValueOnPathFn(value, doc, path, navigateToPath);
|
|
}
|
|
|
|
export function assertValueOnPlanPath(value, doc, path) {
|
|
assertValueOnPathFn(value, doc, path, navigateToPlanPath);
|
|
}
|
|
|
|
export function runWithParams(keyValPairs, fn) {
|
|
let prevVals = [];
|
|
|
|
try {
|
|
for (let i = 0; i < keyValPairs.length; i++) {
|
|
const flag = keyValPairs[i].key;
|
|
const valIn = keyValPairs[i].value;
|
|
const val = (typeof valIn === 'object') ? JSON.stringify(valIn) : valIn;
|
|
|
|
let getParamObj = {};
|
|
getParamObj["getParameter"] = 1;
|
|
getParamObj[flag] = 1;
|
|
const prevVal = db.adminCommand(getParamObj);
|
|
prevVals.push(prevVal[flag]);
|
|
|
|
let setParamObj = {};
|
|
setParamObj["setParameter"] = 1;
|
|
setParamObj[flag] = val;
|
|
assert.commandWorked(db.adminCommand(setParamObj));
|
|
}
|
|
|
|
return fn();
|
|
} finally {
|
|
for (let i = 0; i < keyValPairs.length; i++) {
|
|
const flag = keyValPairs[i].key;
|
|
|
|
let setParamObj = {};
|
|
setParamObj["setParameter"] = 1;
|
|
setParamObj[flag] = prevVals[i];
|
|
|
|
assert.commandWorked(db.adminCommand(setParamObj));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function round2(n) {
|
|
return (Math.round(n * 100) / 100);
|
|
}
|
|
|
|
/**
|
|
* Force cardinality estimation mode: "histogram", "heuristic", or "sampling". We need to force the
|
|
* use of the new optimizer.
|
|
*/
|
|
export function forceCE(mode) {
|
|
assert.commandWorked(
|
|
db.adminCommand({setParameter: 1, internalQueryFrameworkControl: "forceBonsai"}));
|
|
assert.commandWorked(
|
|
db.adminCommand({setParameter: 1, internalQueryCardinalityEstimatorMode: mode}));
|
|
}
|