summaryrefslogtreecommitdiff
path: root/jstests/core/query/expr/expr_index_use.js
diff options
context:
space:
mode:
Diffstat (limited to 'jstests/core/query/expr/expr_index_use.js')
-rw-r--r--jstests/core/query/expr/expr_index_use.js340
1 files changed, 340 insertions, 0 deletions
diff --git a/jstests/core/query/expr/expr_index_use.js b/jstests/core/query/expr/expr_index_use.js
new file mode 100644
index 00000000000..a0f85bc69c9
--- /dev/null
+++ b/jstests/core/query/expr/expr_index_use.js
@@ -0,0 +1,340 @@
+// Confirms expected index use when performing a match with a $expr statement.
+// @tags: [
+// assumes_read_concern_local,
+// ]
+
+(function() {
+"use strict";
+
+load("jstests/libs/analyze_plan.js");
+load("jstests/libs/sbe_util.js"); // For checkSBEEnabled.
+
+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}));
+
+const isSBEEnabled = checkSBEEnabled(db);
+
+/**
+ * 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", isSBEEnabled /* useQueryPlannerSection */) ||
+ checkBothEnginesAreRunOnCluster(db) &&
+ (getAggPlanStage(explain, "COLLSCAN", false /* useQueryPlannerSection */) ||
+ getAggPlanStage(explain, "COLLSCAN", true /* useQueryPlannerSection */)),
+ 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(metricsToCheck.expectedIndex, stage.keyPattern, 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}});
+confirmExpectedExprExecution({$lt: ["$x", 1]}, {nReturned: 20, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$lt: [1, "$x"]}, {nReturned: 2, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$lte: ["$x", 1]}, {nReturned: 21, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$lte: [1, "$x"]}, {nReturned: 3, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$gt: ["$x", 1]}, {nReturned: 2, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$gt: [1, "$x"]}, {nReturned: 20, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$gte: ["$x", 1]}, {nReturned: 3, expectedIndex: {x: 1, y: 1}});
+confirmExpectedExprExecution({$gte: [1, "$x"]}, {nReturned: 21, 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}});
+confirmExpectedExprExecution({$and: [{$gt: ["$x", 1]}, {$lt: ["$y", 5]}]},
+ {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}});
+confirmExpectedExprExecution({$and: [{$gt: ["$x", 1]}, {$lte: ["$x", "$y"]}]},
+ {nReturned: 2, 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}});
+confirmExpectedExprExecution(
+ {$and: [{$gt: ["$x", 1]}, {$or: [{$gt: ["$y", "$x"]}, {$lt: ["$y", 5]}]}]},
+ {nReturned: 2, expectedIndex: {x: 1, y: 1}});
+
+// Comparison against non-multikey dotted path field is expected to use an index.
+confirmExpectedExprExecution({$eq: ["$c.d", 1]}, {nReturned: 1, expectedIndex: {"c.d": 1}});
+confirmExpectedExprExecution({$gt: ["$c.d", 0]}, {nReturned: 1, expectedIndex: {"c.d": 1}});
+confirmExpectedExprExecution({$lt: ["$c.d", 0]}, {nReturned: 22, expectedIndex: {"c.d": 1}});
+
+// $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({$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});
+confirmExpectedExprExecution({$gt: ["$a.b", [1]]}, {nReturned: 1});
+confirmExpectedExprExecution({$gte: ["$a.b", [1]]}, {nReturned: 3});
+confirmExpectedExprExecution({$lt: ["$w", [1]]}, {nReturned: 23});
+confirmExpectedExprExecution({$lte: ["$w", [1]]}, {nReturned: 23});
+
+// 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});
+confirmExpectedExprExecution({$gt: [0, 1]}, {nReturned: 0});
+confirmExpectedExprExecution({$gte: [1, 0]}, {nReturned: 23});
+confirmExpectedExprExecution({$lt: [0, 1]}, {nReturned: 23});
+confirmExpectedExprExecution({$lte: [1, 0]}, {nReturned: 0});
+
+// Comparison of 2 fields is not expected to use an index.
+confirmExpectedExprExecution({$eq: ["$x", "$y"]}, {nReturned: 20});
+confirmExpectedExprExecution({$gt: ["$x", "$y"]}, {nReturned: 1});
+confirmExpectedExprExecution({$gte: ["$x", "$y"]}, {nReturned: 21});
+confirmExpectedExprExecution({$lt: ["$x", "$y"]}, {nReturned: 2});
+confirmExpectedExprExecution({$lte: ["$x", "$y"]}, {nReturned: 22});
+
+// 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});
+confirmExpectedExprExecution({$gt: ["$a.b", 1]}, {nReturned: 3});
+confirmExpectedExprExecution({$gte: ["$a.b", 1]}, {nReturned: 4});
+confirmExpectedExprExecution({$lt: ["$g.h", 1]}, {nReturned: 22});
+confirmExpectedExprExecution({$lte: ["$g.h", 1]}, {nReturned: 22});
+
+// 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);
+confirmExpectedExprExecution({$gt: ["$i", 0]}, metricsToCheck);
+confirmExpectedExprExecution({$gte: ["$i", 0]}, metricsToCheck);
+metricsToCheck.nReturned = 0;
+confirmExpectedExprExecution({$eq: ["$i", 2]}, metricsToCheck);
+metricsToCheck.nReturned = 22;
+confirmExpectedExprExecution({$lt: ["$i", 1]}, metricsToCheck);
+confirmExpectedExprExecution({$lte: ["$i", 0]}, metricsToCheck);
+
+// Comparison to NaN can use an index.
+confirmExpectedExprExecution({$eq: ["$w", NaN]}, {nReturned: 1, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$gt: ["$w", NaN]}, {nReturned: 5, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$gte: ["$w", NaN]}, {nReturned: 6, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$lt: ["$w", NaN]}, {nReturned: 17, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$lte: ["$w", NaN]}, {nReturned: 18, expectedIndex: {w: 1}});
+
+// Comparison to undefined and comparison to missing cannot use an index.
+confirmExpectedExprExecution({$eq: ["$w", undefined]}, {nReturned: 16});
+confirmExpectedExprExecution({$gt: ["$w", undefined]}, {nReturned: 7});
+confirmExpectedExprExecution({$gte: ["$w", undefined]}, {nReturned: 23});
+confirmExpectedExprExecution({$lt: ["$w", undefined]}, {nReturned: 0});
+confirmExpectedExprExecution({$lte: ["$w", undefined]}, {nReturned: 16});
+confirmExpectedExprExecution({$eq: ["$w", "$$REMOVE"]}, {nReturned: 16});
+confirmExpectedExprExecution({$gt: ["$w", "$$REMOVE"]}, {nReturned: 7});
+confirmExpectedExprExecution({$gte: ["$w", "$$REMOVE"]}, {nReturned: 23});
+confirmExpectedExprExecution({$lt: ["$w", "$$REMOVE"]}, {nReturned: 0});
+confirmExpectedExprExecution({$lte: ["$w", "$$REMOVE"]}, {nReturned: 16});
+
+// Comparison to null can use an index.
+confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$gt: ["$w", null]}, {nReturned: 6, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$gte: ["$w", null]}, {nReturned: 7, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$lt: ["$w", null]}, {nReturned: 16, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$lte: ["$w", null]}, {nReturned: 17, expectedIndex: {w: 1}});
+
+// Comparison inside a nested object can use a non-multikey index.
+confirmExpectedExprExecution({$eq: ["$w.z", 2]}, {nReturned: 1, expectedIndex: {"w.z": 1}});
+confirmExpectedExprExecution({$gt: ["$w.z", 1]}, {nReturned: 1, expectedIndex: {"w.z": 1}});
+confirmExpectedExprExecution({$gte: ["$w.z", 1]}, {nReturned: 2, expectedIndex: {"w.z": 1}});
+confirmExpectedExprExecution({$lt: ["$w.z", 2]}, {nReturned: 22, expectedIndex: {"w.z": 1}});
+confirmExpectedExprExecution({$lte: ["$w.z", 2]}, {nReturned: 23, 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
+};
+confirmExpectedExprExecution({$eq: ["$w", "FoO"]}, {nReturned: 2}, caseInsensitiveCollation);
+confirmExpectedExprExecution({$gt: ["$w", "FoO"]}, {nReturned: 2}, caseInsensitiveCollation);
+confirmExpectedExprExecution({$gte: ["$w", "FoO"]}, {nReturned: 4}, caseInsensitiveCollation);
+confirmExpectedExprExecution({$lt: ["$w", "FoO"]}, {nReturned: 19}, caseInsensitiveCollation);
+confirmExpectedExprExecution({$lte: ["$w", "FoO"]}, {nReturned: 21}, 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});
+
+// Test that equality to null queries can use a sparse index.
+assert.commandWorked(coll.dropIndex({w: "hashed"}));
+assert.commandWorked(coll.createIndex({w: 1}, {sparse: true}));
+confirmExpectedExprExecution({$eq: ["$w", null]}, {nReturned: 1, expectedIndex: {w: 1}});
+
+// 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 comparison 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({$gt: ["$a", "foo"]}, {nReturned: 0, expectedIndex: {a: 1}});
+confirmExpectedExprExecution({$gte: ["$a", "foo"]}, {nReturned: 2, expectedIndex: {a: 1}});
+confirmExpectedExprExecution({$lt: ["$a", "foo"]}, {nReturned: 0, expectedIndex: {a: 1}});
+confirmExpectedExprExecution({$lte: ["$a", "foo"]}, {nReturned: 2, expectedIndex: {a: 1}});
+confirmExpectedExprExecution({$eq: ["$b", "bar"]}, {nReturned: 2});
+confirmExpectedExprExecution({$gt: ["$b", "bar"]}, {nReturned: 0});
+confirmExpectedExprExecution({$gte: ["$b", "bar"]}, {nReturned: 2});
+confirmExpectedExprExecution({$lt: ["$b", "bar"]}, {nReturned: 0});
+confirmExpectedExprExecution({$lte: ["$b", "bar"]}, {nReturned: 2});
+
+// Test that comparisons to subobjects containing undefined and array types succeed.
+assert.commandWorked(db.runCommand({drop: coll.getName()}));
+assert.commandWorked(db.createCollection(coll.getName()));
+
+const docs = [
+ {},
+ {w: undefined},
+ {w: null},
+ {w: NaN},
+ {w: 123},
+ {w: "foo"},
+ {w: {z: 1}},
+ {w: {z: undefined, u: ["array"]}}
+];
+assert.commandWorked(coll.insert(docs));
+assert.commandWorked(coll.createIndex({w: 1}));
+
+confirmExpectedExprExecution({$eq: ["$w", {z: undefined, u: ["array"]}]},
+ {nReturned: 1, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$gt: ["$w", {z: undefined, u: ["array"]}]},
+ {nReturned: 1, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$gte: ["$w", {z: undefined, u: ["array"]}]},
+ {nReturned: 2, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$lt: ["$w", {z: undefined, u: ["array"]}]},
+ {nReturned: 6, expectedIndex: {w: 1}});
+confirmExpectedExprExecution({$lte: ["$w", {z: undefined, u: ["array"]}]},
+ {nReturned: 7, expectedIndex: {w: 1}});
+})();