mirror of https://github.com/mongodb/mongo
238 lines
11 KiB
JavaScript
238 lines
11 KiB
JavaScript
// Confirms expected index use when performing a match with a $expr statement.
|
|
|
|
(function() {
|
|
"use strict";
|
|
|
|
load("jstests/libs/analyze_plan.js");
|
|
|
|
const coll = db.expr_index_use;
|
|
coll.drop();
|
|
|
|
assert.commandWorked(coll.insert({a: {b: 1}}));
|
|
assert.commandWorked(coll.insert({a: {b: [1]}}));
|
|
assert.commandWorked(coll.insert({a: [{b: 1}]}));
|
|
assert.commandWorked(coll.insert({a: [{b: [1]}]}));
|
|
assert.commandWorked(coll.createIndex({"a.b": 1}));
|
|
|
|
assert.commandWorked(coll.insert({c: {d: 1}}));
|
|
assert.commandWorked(coll.createIndex({"c.d": 1}));
|
|
|
|
assert.commandWorked(coll.insert({e: [{f: 1}]}));
|
|
assert.commandWorked(coll.createIndex({"e.f": 1}));
|
|
|
|
assert.commandWorked(coll.insert({g: {h: [1]}}));
|
|
assert.commandWorked(coll.createIndex({"g.h": 1}));
|
|
|
|
assert.commandWorked(coll.insert({i: 1, j: [1]}));
|
|
assert.commandWorked(coll.createIndex({i: 1, j: 1}));
|
|
|
|
assert.commandWorked(coll.insert({k: 1, l: "abc"}));
|
|
assert.commandWorked(coll.createIndex({k: 1, l: "text"}));
|
|
|
|
assert.commandWorked(coll.insert({x: 0}));
|
|
assert.commandWorked(coll.insert({x: 1, y: 1}));
|
|
assert.commandWorked(coll.insert({x: 2, y: 2}));
|
|
assert.commandWorked(coll.insert({x: 3, y: 10}));
|
|
assert.commandWorked(coll.insert({y: 20}));
|
|
assert.commandWorked(coll.createIndex({x: 1, y: 1}));
|
|
|
|
assert.commandWorked(coll.insert({w: 123}));
|
|
assert.commandWorked(coll.insert({}));
|
|
assert.commandWorked(coll.insert({w: null}));
|
|
assert.commandWorked(coll.insert({w: undefined}));
|
|
assert.commandWorked(coll.insert({w: NaN}));
|
|
assert.commandWorked(coll.insert({w: "foo"}));
|
|
assert.commandWorked(coll.insert({w: "FOO"}));
|
|
assert.commandWorked(coll.insert({w: {z: 1}}));
|
|
assert.commandWorked(coll.insert({w: {z: 2}}));
|
|
assert.commandWorked(coll.createIndex({w: 1}));
|
|
assert.commandWorked(coll.createIndex({"w.z": 1}));
|
|
|
|
/**
|
|
* Executes the expression 'expr' as both a find and an aggregate. Then confirms
|
|
* 'metricsToCheck', which is an object containing:
|
|
* - nReturned: The number of documents the pipeline is expected to return.
|
|
* - expectedIndex: Either an index specification object when index use is expected or
|
|
* 'null' if a collection scan is expected.
|
|
*/
|
|
function confirmExpectedExprExecution(expr, metricsToCheck, collation) {
|
|
assert(metricsToCheck.hasOwnProperty("nReturned"),
|
|
"metricsToCheck must contain an nReturned field");
|
|
|
|
let aggOptions = {};
|
|
if (collation) {
|
|
aggOptions.collation = collation;
|
|
}
|
|
|
|
const pipeline = [{$match: {$expr: expr}}];
|
|
|
|
// Verify that $expr returns the correct number of results when run inside the $match stage
|
|
// of an aggregate.
|
|
assert.eq(metricsToCheck.nReturned, coll.aggregate(pipeline, aggOptions).itcount());
|
|
|
|
// Verify that $expr returns the correct number of results when run in a find command.
|
|
let cursor = coll.find({$expr: expr});
|
|
if (collation) {
|
|
cursor = cursor.collation(collation);
|
|
}
|
|
assert.eq(metricsToCheck.nReturned, cursor.itcount());
|
|
|
|
// Verify that $expr returns the correct number of results when evaluated inside a $project,
|
|
// with optimizations inhibited. We expect the plan to be COLLSCAN.
|
|
const pipelineWithProject = [
|
|
{$_internalInhibitOptimization: {}},
|
|
{$project: {result: {$cond: [expr, true, false]}}},
|
|
{$match: {result: true}}
|
|
];
|
|
assert.eq(metricsToCheck.nReturned, coll.aggregate(pipelineWithProject, aggOptions).itcount());
|
|
let explain = coll.explain("executionStats").aggregate(pipelineWithProject, aggOptions);
|
|
assert(getAggPlanStage(explain, "COLLSCAN"), tojson(explain));
|
|
|
|
// Verifies that there are no rejected plans, and that the winning plan uses the expected
|
|
// index.
|
|
//
|
|
// 'getPlanStageFunc' is a function which can be called to obtain stage-specific information
|
|
// from the explain output. There are different versions of this function for find and
|
|
// aggregate explain output.
|
|
function verifyExplainOutput(explain, getPlanStageFunc) {
|
|
assert(!hasRejectedPlans(explain), tojson(explain));
|
|
|
|
if (metricsToCheck.hasOwnProperty("expectedIndex")) {
|
|
const stage = getPlanStageFunc(explain, "IXSCAN");
|
|
assert.neq(null, stage, tojson(explain));
|
|
assert(stage.hasOwnProperty("keyPattern"), tojson(explain));
|
|
assert.docEq(stage.keyPattern, metricsToCheck.expectedIndex, tojson(explain));
|
|
} else {
|
|
assert(getPlanStageFunc(explain, "COLLSCAN"), tojson(explain));
|
|
}
|
|
}
|
|
|
|
explain = assert.commandWorked(coll.explain("executionStats").aggregate(pipeline, aggOptions));
|
|
verifyExplainOutput(explain, getPlanStage);
|
|
|
|
cursor = coll.explain("executionStats").find({$expr: expr});
|
|
if (collation) {
|
|
cursor = cursor.collation(collation);
|
|
}
|
|
explain = assert.commandWorked(cursor.finish());
|
|
verifyExplainOutput(explain, getPlanStage);
|
|
}
|
|
|
|
// Comparison of field and constant.
|
|
confirmExpectedExprExecution({$eq: ["$x", 1]}, {nReturned: 1, expectedIndex: {x: 1, y: 1}});
|
|
confirmExpectedExprExecution({$eq: [1, "$x"]}, {nReturned: 1, expectedIndex: {x: 1, y: 1}});
|
|
|
|
// $and with both children eligible for index use.
|
|
confirmExpectedExprExecution({$and: [{$eq: ["$x", 2]}, {$eq: ["$y", 2]}]},
|
|
{nReturned: 1, expectedIndex: {x: 1, y: 1}});
|
|
|
|
// $and with one child eligible for index use and one that is not.
|
|
confirmExpectedExprExecution({$and: [{$eq: ["$x", 1]}, {$eq: ["$x", "$y"]}]},
|
|
{nReturned: 1, expectedIndex: {x: 1, y: 1}});
|
|
|
|
// $and with one child eligible for index use and a second child containing a $or where one of
|
|
// the two children are eligible.
|
|
confirmExpectedExprExecution(
|
|
{$and: [{$eq: ["$x", 1]}, {$or: [{$eq: ["$x", "$y"]}, {$eq: ["$y", 1]}]}]},
|
|
{nReturned: 1, expectedIndex: {x: 1, y: 1}});
|
|
|
|
// Equality comparison against non-multikey dotted path field is expected to use an index.
|
|
confirmExpectedExprExecution({$eq: ["$c.d", 1]}, {nReturned: 1, expectedIndex: {"c.d": 1}});
|
|
|
|
// $lt, $lte, $gt, $gte, $in, $ne, and $cmp are not expected to use an index. This is because we
|
|
// have not yet implemented a rewrite of these operators to indexable MatchExpression.
|
|
confirmExpectedExprExecution({$lt: ["$x", 1]}, {nReturned: 20});
|
|
confirmExpectedExprExecution({$lt: [1, "$x"]}, {nReturned: 2});
|
|
confirmExpectedExprExecution({$lte: ["$x", 1]}, {nReturned: 21});
|
|
confirmExpectedExprExecution({$lte: [1, "$x"]}, {nReturned: 3});
|
|
confirmExpectedExprExecution({$gt: ["$x", 1]}, {nReturned: 2});
|
|
confirmExpectedExprExecution({$gt: [1, "$x"]}, {nReturned: 20});
|
|
confirmExpectedExprExecution({$gte: ["$x", 1]}, {nReturned: 3});
|
|
confirmExpectedExprExecution({$gte: [1, "$x"]}, {nReturned: 21});
|
|
confirmExpectedExprExecution({$in: ["$x", [1, 3]]}, {nReturned: 2});
|
|
confirmExpectedExprExecution({$cmp: ["$x", 1]}, {nReturned: 22});
|
|
confirmExpectedExprExecution({$ne: ["$x", 1]}, {nReturned: 22});
|
|
|
|
// Comparison with an array value is not expected to use an index.
|
|
confirmExpectedExprExecution({$eq: ["$a.b", [1]]}, {nReturned: 2});
|
|
confirmExpectedExprExecution({$eq: ["$w", [1]]}, {nReturned: 0});
|
|
|
|
// A constant expression is not expected to use an index.
|
|
confirmExpectedExprExecution(1, {nReturned: 23});
|
|
confirmExpectedExprExecution(false, {nReturned: 0});
|
|
confirmExpectedExprExecution({$eq: [1, 1]}, {nReturned: 23});
|
|
confirmExpectedExprExecution({$eq: [0, 1]}, {nReturned: 0});
|
|
|
|
// Comparison of 2 fields is not expected to use an index.
|
|
confirmExpectedExprExecution({$eq: ["$x", "$y"]}, {nReturned: 20});
|
|
|
|
// Comparison against multikey field not expected to use an index.
|
|
confirmExpectedExprExecution({$eq: ["$a.b", 1]}, {nReturned: 1});
|
|
confirmExpectedExprExecution({$eq: ["$e.f", [1]]}, {nReturned: 1});
|
|
confirmExpectedExprExecution({$eq: ["$e.f", 1]}, {nReturned: 0});
|
|
confirmExpectedExprExecution({$eq: ["$g.h", [1]]}, {nReturned: 1});
|
|
confirmExpectedExprExecution({$eq: ["$g.h", 1]}, {nReturned: 0});
|
|
|
|
// Comparison against a non-multikey field of a multikey index can use an index
|
|
const metricsToCheck = {
|
|
nReturned: 1
|
|
};
|
|
metricsToCheck.expectedIndex = {
|
|
i: 1,
|
|
j: 1
|
|
};
|
|
confirmExpectedExprExecution({$eq: ["$i", 1]}, metricsToCheck);
|
|
metricsToCheck.nReturned = 0;
|
|
confirmExpectedExprExecution({$eq: ["$i", 2]}, metricsToCheck);
|
|
|
|
// Equality to NaN can use an index.
|
|
confirmExpectedExprExecution({$eq: ["$w", NaN]}, {nReturned: 1, expectedIndex: {w: 1}});
|
|
|
|
// Equality to undefined and equality to missing cannot use an index.
|
|
confirmExpectedExprExecution({$eq: ["$w", undefined]}, {nReturned: 16});
|
|
confirmExpectedExprExecution({$eq: ["$w", "$$REMOVE"]}, {nReturned: 16});
|
|
|
|
// Equality to null can use an index.
|
|
confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: 1}});
|
|
|
|
// Equality inside a nested object can use a non-multikey index.
|
|
confirmExpectedExprExecution({$eq: ["$w.z", 2]}, {nReturned: 1, expectedIndex: {"w.z": 1}});
|
|
|
|
// Test that the collation is respected. Since the collations do not match, we should not use
|
|
// the index.
|
|
const caseInsensitiveCollation = {
|
|
locale: "en_US",
|
|
strength: 2
|
|
};
|
|
if (db.getMongo().useReadCommands()) {
|
|
confirmExpectedExprExecution({$eq: ["$w", "FoO"]}, {nReturned: 2}, caseInsensitiveCollation);
|
|
}
|
|
|
|
// Test equality queries against a hashed index.
|
|
assert.commandWorked(coll.dropIndex({w: 1}));
|
|
assert.commandWorked(coll.createIndex({w: "hashed"}));
|
|
confirmExpectedExprExecution({$eq: ["$w", 123]}, {nReturned: 1, expectedIndex: {w: "hashed"}});
|
|
confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: "hashed"}});
|
|
confirmExpectedExprExecution({$eq: ["$w", NaN]}, {nReturned: 1, expectedIndex: {w: "hashed"}});
|
|
confirmExpectedExprExecution({$eq: ["$w", undefined]}, {nReturned: 16});
|
|
confirmExpectedExprExecution({$eq: ["$w", "$$REMOVE"]}, {nReturned: 16});
|
|
|
|
// Equality match against text index prefix is expected to fail. Equality predicates are
|
|
// required against the prefix fields of a text index, but currently $eq inside $expr does not
|
|
// qualify.
|
|
assert.throws(
|
|
() => coll.aggregate([{$match: {$expr: {$eq: ["$k", 1]}, $text: {$search: "abc"}}}]).itcount());
|
|
|
|
// Test that equality match in $expr respects the collection's default collation, both when
|
|
// there is an index with a matching collation and when there isn't.
|
|
assert.commandWorked(db.runCommand({drop: coll.getName()}));
|
|
assert.commandWorked(db.createCollection(coll.getName(), {collation: caseInsensitiveCollation}));
|
|
assert.commandWorked(coll.insert({a: "foo", b: "bar"}));
|
|
assert.commandWorked(coll.insert({a: "FOO", b: "BAR"}));
|
|
assert.commandWorked(coll.createIndex({a: 1}));
|
|
assert.commandWorked(coll.createIndex({b: 1}, {collation: {locale: "simple"}}));
|
|
|
|
confirmExpectedExprExecution({$eq: ["$a", "foo"]}, {nReturned: 2, expectedIndex: {a: 1}});
|
|
confirmExpectedExprExecution({$eq: ["$b", "bar"]}, {nReturned: 2});
|
|
})();
|