summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/core/explain_count.js60
-rw-r--r--jstests/core/idhack.js170
-rw-r--r--jstests/core/index_bounds_code.js55
-rw-r--r--jstests/core/index_bounds_maxkey.js39
-rw-r--r--jstests/core/index_bounds_minkey.js39
-rw-r--r--jstests/core/index_bounds_object.js61
-rw-r--r--jstests/core/index_bounds_timestamp.js30
-rw-r--r--jstests/libs/analyze_plan.js39
-rw-r--r--src/mongo/db/query/index_bounds_builder_test.cpp323
-rw-r--r--src/mongo/db/query/indexability.h5
10 files changed, 669 insertions, 152 deletions
diff --git a/jstests/core/explain_count.js b/jstests/core/explain_count.js
index ee7cb31de94..6374c1952f2 100644
--- a/jstests/core/explain_count.js
+++ b/jstests/core/explain_count.js
@@ -1,38 +1,12 @@
// Test running explains on count commands.
-load("jstests/libs/analyze_plan.js");
+load("jstests/libs/analyze_plan.js"); // For assertExplainCount and checkCountScanIndexExplain.
var collName = "jstests_explain_count";
var t = db[collName];
t.drop();
/**
- * Given explain output 'explain' at executionStats level verbosity,
- * confirms that the root stage is COUNT and that the result of the
- * count is equal to 'nCounted'.
- */
-function checkCountExplain(explain, nCounted) {
- printjson(explain);
- var execStages = explain.executionStats.executionStages;
-
- // If passed through mongos, then the root stage should be the mongos SINGLE_SHARD stage or
- // SHARD_MERGE stages, with COUNT as the root stage on each shard. If explaining directly on the
- // shard, then COUNT is the root stage.
- if ("SINGLE_SHARD" == execStages.stage || "SHARD_MERGE" == execStages.stage) {
- let totalCounted = 0;
- for (let shardExplain of execStages.shards) {
- const countStage = shardExplain.executionStages;
- assert.eq(countStage.stage, "COUNT", "root stage on shard is not COUNT");
- totalCounted += countStage.nCounted;
- }
- assert.eq(totalCounted, nCounted, "wrong count result");
- } else {
- assert.eq(execStages.stage, "COUNT", "root stage is not COUNT");
- assert.eq(execStages.nCounted, nCounted, "wrong count result");
- }
-}
-
-/**
* Given an explain output from a COUNT_SCAN stage, check that a indexBounds field is present.
*/
function checkCountScanIndexExplain(explain, startKey, endKey, startInclusive, endInclusive) {
@@ -49,30 +23,30 @@ function checkCountScanIndexExplain(explain, startKey, endKey, startInclusive, e
// Collection does not exist.
assert.eq(0, t.count());
var explain = db.runCommand({explain: {count: collName}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
// Collection does not exist with skip, limit, and/or query.
assert.eq(0, db.runCommand({count: collName, skip: 3}).n);
explain = db.runCommand({explain: {count: collName, skip: 3}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
assert.eq(0, db.runCommand({count: collName, limit: 3}).n);
explain = db.runCommand({explain: {count: collName, limit: 3}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
assert.eq(0, db.runCommand({count: collName, limit: -3}).n);
explain = db.runCommand({explain: {count: collName, limit: -3}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
assert.eq(0, db.runCommand({count: collName, limit: -3, skip: 4}).n);
explain =
db.runCommand({explain: {count: collName, limit: -3, skip: 4}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
assert.eq(0, db.runCommand({count: collName, query: {a: 1}, limit: -3, skip: 4}).n);
explain = db.runCommand(
{explain: {count: collName, query: {a: 1}, limit: -3, skip: 4}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
// Now add a bit of data to the collection.
t.ensureIndex({a: 1});
@@ -83,47 +57,47 @@ for (var i = 0; i < 10; i++) {
// Trivial count with no skip, limit, or query.
assert.eq(10, t.count());
explain = db.runCommand({explain: {count: collName}, verbosity: "executionStats"});
-checkCountExplain(explain, 10);
+assertExplainCount({explainResults: explain, expectedCount: 10});
// Trivial count with skip.
assert.eq(7, db.runCommand({count: collName, skip: 3}).n);
explain = db.runCommand({explain: {count: collName, skip: 3}, verbosity: "executionStats"});
-checkCountExplain(explain, 7);
+assertExplainCount({explainResults: explain, expectedCount: 7});
// Trivial count with limit.
assert.eq(3, db.runCommand({count: collName, limit: 3}).n);
explain = db.runCommand({explain: {count: collName, limit: 3}, verbosity: "executionStats"});
-checkCountExplain(explain, 3);
+assertExplainCount({explainResults: explain, expectedCount: 3});
// Trivial count with negative limit.
assert.eq(3, db.runCommand({count: collName, limit: -3}).n);
explain = db.runCommand({explain: {count: collName, limit: -3}, verbosity: "executionStats"});
-checkCountExplain(explain, 3);
+assertExplainCount({explainResults: explain, expectedCount: 3});
// Trivial count with both limit and skip.
assert.eq(3, db.runCommand({count: collName, limit: -3, skip: 4}).n);
explain =
db.runCommand({explain: {count: collName, limit: -3, skip: 4}, verbosity: "executionStats"});
-checkCountExplain(explain, 3);
+assertExplainCount({explainResults: explain, expectedCount: 3});
// With a query.
assert.eq(10, db.runCommand({count: collName, query: {a: 1}}).n);
explain = db.runCommand({explain: {count: collName, query: {a: 1}}, verbosity: "executionStats"});
-checkCountExplain(explain, 10);
+assertExplainCount({explainResults: explain, expectedCount: 10});
checkCountScanIndexExplain(explain, {a: 1}, {a: 1}, true, true);
// With a query and skip.
assert.eq(7, db.runCommand({count: collName, query: {a: 1}, skip: 3}).n);
explain = db.runCommand(
{explain: {count: collName, query: {a: 1}, skip: 3}, verbosity: "executionStats"});
-checkCountExplain(explain, 7);
+assertExplainCount({explainResults: explain, expectedCount: 7});
checkCountScanIndexExplain(explain, {a: 1}, {a: 1}, true, true);
// With a query and limit.
assert.eq(3, db.runCommand({count: collName, query: {a: 1}, limit: 3}).n);
explain = db.runCommand(
{explain: {count: collName, query: {a: 1}, limit: 3}, verbosity: "executionStats"});
-checkCountExplain(explain, 3);
+assertExplainCount({explainResults: explain, expectedCount: 3});
checkCountScanIndexExplain(explain, {a: 1}, {a: 1}, true, true);
// Insert one more doc for the last few tests.
@@ -133,12 +107,12 @@ t.insert({a: 2});
assert.eq(0, db.runCommand({count: collName, query: {a: 2}, skip: 2}).n);
explain = db.runCommand(
{explain: {count: collName, query: {a: 2}, skip: 2}, verbosity: "executionStats"});
-checkCountExplain(explain, 0);
+assertExplainCount({explainResults: explain, expectedCount: 0});
checkCountScanIndexExplain(explain, {a: 2}, {a: 2}, true, true);
// Case where we have a limit, but we don't hit it.
assert.eq(1, db.runCommand({count: collName, query: {a: 2}, limit: 2}).n);
explain = db.runCommand(
{explain: {count: collName, query: {a: 2}, limit: 2}, verbosity: "executionStats"});
-checkCountExplain(explain, 1);
+assertExplainCount({explainResults: explain, expectedCount: 1});
checkCountScanIndexExplain(explain, {a: 2}, {a: 2}, true, true);
diff --git a/jstests/core/idhack.js b/jstests/core/idhack.js
index 2ef3041c759..422222de951 100644
--- a/jstests/core/idhack.js
+++ b/jstests/core/idhack.js
@@ -1,85 +1,87 @@
// @tags: [requires_non_retryable_writes]
-
-t = db.idhack;
-t.drop();
-
-// Include helpers for analyzing explain output.
-load("jstests/libs/analyze_plan.js");
-
-t.insert({_id: {x: 1}, z: 1});
-t.insert({_id: {x: 2}, z: 2});
-t.insert({_id: {x: 3}, z: 3});
-t.insert({_id: 1, z: 4});
-t.insert({_id: 2, z: 5});
-t.insert({_id: 3, z: 6});
-
-assert.eq(2, t.findOne({_id: {x: 2}}).z, "A1");
-assert.eq(2, t.find({_id: {$gte: 2}}).count(), "A2");
-assert.eq(2, t.find({_id: {$gte: 2}}).itcount(), "A3");
-
-t.update({_id: {x: 2}}, {$set: {z: 7}});
-assert.eq(7, t.findOne({_id: {x: 2}}).z, "B1");
-
-t.update({_id: {$gte: 2}}, {$set: {z: 8}}, false, true);
-assert.eq(4, t.findOne({_id: 1}).z, "C1");
-assert.eq(8, t.findOne({_id: 2}).z, "C2");
-assert.eq(8, t.findOne({_id: 3}).z, "C3");
-
-// explain output should show that the ID hack was applied.
-var query = {_id: {x: 2}};
-var explain = t.find(query).explain(true);
-print("explain for " + tojson(query, "", true) + " = " + tojson(explain));
-assert.eq(1, explain.executionStats.nReturned, "D1");
-assert.eq(1, explain.executionStats.totalKeysExamined, "D2");
-assert(isIdhack(db, explain.queryPlanner.winningPlan), "D3");
-
-// ID hack cannot be used with hint().
-t.ensureIndex({_id: 1, a: 1});
-var hintExplain = t.find(query).hint({_id: 1, a: 1}).explain();
-print("explain for hinted query = " + tojson(hintExplain));
-assert(!isIdhack(db, hintExplain.queryPlanner.winningPlan), "E1");
-
-// ID hack cannot be used with skip().
-var skipExplain = t.find(query).skip(1).explain();
-print("explain for skip query = " + tojson(skipExplain));
-assert(!isIdhack(db, skipExplain.queryPlanner.winningPlan), "F1");
-
-// Covered query returning _id field only can be handled by ID hack.
-var coveredExplain = t.find(query, {_id: 1}).explain();
-print("explain for covered query = " + tojson(coveredExplain));
-assert(isIdhack(db, coveredExplain.queryPlanner.winningPlan), "G1");
-// Check doc from covered ID hack query.
-assert.eq({_id: {x: 2}}, t.findOne(query, {_id: 1}), "G2");
-
-//
-// Non-covered projection for idhack.
-//
-
-t.drop();
-t.insert({_id: 0, a: 0, b: [{c: 1}, {c: 2}]});
-t.insert({_id: 1, a: 1, b: [{c: 3}, {c: 4}]});
-
-// Simple inclusion.
-assert.eq({_id: 1, a: 1}, t.find({_id: 1}, {a: 1}).next());
-assert.eq({a: 1}, t.find({_id: 1}, {_id: 0, a: 1}).next());
-assert.eq({_id: 0, a: 0}, t.find({_id: 0}, {_id: 1, a: 1}).next());
-
-// Non-simple: exclusion.
-assert.eq({_id: 1, a: 1}, t.find({_id: 1}, {b: 0}).next());
-assert.eq({
- _id: 0,
-},
- t.find({_id: 0}, {a: 0, b: 0}).next());
-
-// Non-simple: dotted fields.
-assert.eq({b: [{c: 1}, {c: 2}]}, t.find({_id: 0}, {_id: 0, "b.c": 1}).next());
-assert.eq({_id: 1}, t.find({_id: 1}, {"foo.bar": 1}).next());
-
-// Non-simple: elemMatch projection.
-assert.eq({_id: 1, b: [{c: 4}]}, t.find({_id: 1}, {b: {$elemMatch: {c: 4}}}).next());
-
-// Non-simple: .returnKey().
-assert.eq({_id: 1}, t.find({_id: 1}).returnKey().next());
-
-// Non-simple: .returnKey() overrides other projections.
-assert.eq({_id: 1}, t.find({_id: 1}, {a: 1}).returnKey().next());
+(function() {
+ "use strict";
+
+ const t = db.idhack;
+ t.drop();
+
+ // Include helpers for analyzing explain output.
+ load("jstests/libs/analyze_plan.js");
+
+ assert.writeOK(t.insert({_id: {x: 1}, z: 1}));
+ assert.writeOK(t.insert({_id: {x: 2}, z: 2}));
+ assert.writeOK(t.insert({_id: {x: 3}, z: 3}));
+ assert.writeOK(t.insert({_id: 1, z: 4}));
+ assert.writeOK(t.insert({_id: 2, z: 5}));
+ assert.writeOK(t.insert({_id: 3, z: 6}));
+
+ assert.eq(2, t.findOne({_id: {x: 2}}).z);
+ assert.eq(2, t.find({_id: {$gte: 2}}).count());
+ assert.eq(2, t.find({_id: {$gte: 2}}).itcount());
+
+ t.update({_id: {x: 2}}, {$set: {z: 7}});
+ assert.eq(7, t.findOne({_id: {x: 2}}).z);
+
+ t.update({_id: {$gte: 2}}, {$set: {z: 8}}, false, true);
+ assert.eq(4, t.findOne({_id: 1}).z);
+ assert.eq(8, t.findOne({_id: 2}).z);
+ assert.eq(8, t.findOne({_id: 3}).z);
+
+ // explain output should show that the ID hack was applied.
+ const query = {_id: {x: 2}};
+ let explain = t.find(query).explain(true);
+ assert.eq(1, explain.executionStats.nReturned);
+ assert.eq(1, explain.executionStats.totalKeysExamined);
+ assert(isIdhack(db, explain.queryPlanner.winningPlan));
+
+ // ID hack cannot be used with hint().
+ t.ensureIndex({_id: 1, a: 1});
+ explain = t.find(query).hint({_id: 1, a: 1}).explain();
+ assert(!isIdhack(db, explain.queryPlanner.winningPlan));
+
+ // ID hack cannot be used with skip().
+ explain = t.find(query).skip(1).explain();
+ assert(!isIdhack(db, explain.queryPlanner.winningPlan));
+
+ // ID hack cannot be used with a regex predicate.
+ assert.writeOK(t.insert({_id: "abc"}));
+ explain = t.find({_id: /abc/}).explain();
+ assert.eq({_id: "abc"}, t.findOne({_id: /abc/}));
+ assert(!isIdhack(db, explain.queryPlanner.winningPlan));
+
+ // Covered query returning _id field only can be handled by ID hack.
+ explain = t.find(query, {_id: 1}).explain();
+ assert(isIdhack(db, explain.queryPlanner.winningPlan));
+ // Check doc from covered ID hack query.
+ assert.eq({_id: {x: 2}}, t.findOne(query, {_id: 1}));
+
+ //
+ // Non-covered projection for idhack.
+ //
+
+ t.drop();
+ assert.writeOK(t.insert({_id: 0, a: 0, b: [{c: 1}, {c: 2}]}));
+ assert.writeOK(t.insert({_id: 1, a: 1, b: [{c: 3}, {c: 4}]}));
+
+ // Simple inclusion.
+ assert.eq({_id: 1, a: 1}, t.find({_id: 1}, {a: 1}).next());
+ assert.eq({a: 1}, t.find({_id: 1}, {_id: 0, a: 1}).next());
+ assert.eq({_id: 0, a: 0}, t.find({_id: 0}, {_id: 1, a: 1}).next());
+
+ // Non-simple: exclusion.
+ assert.eq({_id: 1, a: 1}, t.find({_id: 1}, {b: 0}).next());
+ assert.eq({_id: 0}, t.find({_id: 0}, {a: 0, b: 0}).next());
+
+ // Non-simple: dotted fields.
+ assert.eq({b: [{c: 1}, {c: 2}]}, t.find({_id: 0}, {_id: 0, "b.c": 1}).next());
+ assert.eq({_id: 1}, t.find({_id: 1}, {"foo.bar": 1}).next());
+
+ // Non-simple: elemMatch projection.
+ assert.eq({_id: 1, b: [{c: 4}]}, t.find({_id: 1}, {b: {$elemMatch: {c: 4}}}).next());
+
+ // Non-simple: .returnKey().
+ assert.eq({_id: 1}, t.find({_id: 1}).returnKey().next());
+
+ // Non-simple: .returnKey() overrides other projections.
+ assert.eq({_id: 1}, t.find({_id: 1}, {a: 1}).returnKey().next());
+})();
diff --git a/jstests/core/index_bounds_code.js b/jstests/core/index_bounds_code.js
new file mode 100644
index 00000000000..5070c3fe0d0
--- /dev/null
+++ b/jstests/core/index_bounds_code.js
@@ -0,0 +1,55 @@
+// Index bounds generation tests for Code/CodeWSCope values.
+// @tags: [requires_non_retryable_writes, assumes_unsharded_collection]
+(function() {
+ "use strict";
+
+ load("jstests/libs/analyze_plan.js"); // For assertCoveredQueryAndCount.
+
+ const coll = db.index_bounds_code;
+ coll.drop();
+
+ assert.commandWorked(coll.createIndex({a: 1}));
+ const insertedFunc = function() {
+ return 1;
+ };
+ assert.writeOK(coll.insert({a: insertedFunc}));
+
+ // Test that queries involving comparison operators with values of type Code are covered.
+ const proj = {a: 1, _id: 0};
+ const func = function() {
+ return 2;
+ };
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: func}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: func}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: func}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: func}}, project: proj, count: 1});
+
+ // Test for equality against the original inserted function.
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: insertedFunc}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: insertedFunc}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: insertedFunc}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: insertedFunc}}, project: proj, count: 1});
+
+ // Test that documents that lie outside of the generated index bounds are not returned.
+ coll.remove({});
+ assert.writeOK(coll.insert({a: "string"}));
+ assert.writeOK(coll.insert({a: {b: 1}}));
+ assert.writeOK(coll.insert({a: MaxKey}));
+
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: func}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: func}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: func}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: func}}, project: proj, count: 0});
+})();
diff --git a/jstests/core/index_bounds_maxkey.js b/jstests/core/index_bounds_maxkey.js
new file mode 100644
index 00000000000..b22af082b13
--- /dev/null
+++ b/jstests/core/index_bounds_maxkey.js
@@ -0,0 +1,39 @@
+// Index bounds generation tests for MaxKey values.
+// @tags: [requires_non_retryable_writes, assumes_unsharded_collection]
+(function() {
+ "use strict";
+
+ load("jstests/libs/analyze_plan.js"); // For assertCoveredQueryAndCount.
+
+ const coll = db.index_bounds_maxkey;
+ coll.drop();
+
+ assert.commandWorked(coll.createIndex({a: 1}));
+ assert.writeOK(coll.insert({a: MaxKey}));
+
+ // Test that queries involving comparison operators with MaxKey are covered.
+ const proj = {a: 1, _id: 0};
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: MaxKey}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: MaxKey}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: MaxKey}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: MaxKey}}, project: proj, count: 1});
+
+ // Test that all documents are considered less than MaxKey, regardless of the presence of
+ // the queried field 'a'.
+ coll.remove({});
+ assert.writeOK(coll.insert({a: "string"}));
+ assert.writeOK(coll.insert({a: {b: 1}}));
+ assert.writeOK(coll.insert({}));
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: MaxKey}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: MaxKey}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: MaxKey}}, project: proj, count: 3});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: MaxKey}}, project: proj, count: 3});
+})();
diff --git a/jstests/core/index_bounds_minkey.js b/jstests/core/index_bounds_minkey.js
new file mode 100644
index 00000000000..6fa9d4f0d1e
--- /dev/null
+++ b/jstests/core/index_bounds_minkey.js
@@ -0,0 +1,39 @@
+// Index bounds generation tests for MinKey values.
+// @tags: [requires_non_retryable_writes, assumes_unsharded_collection]
+(function() {
+ "use strict";
+
+ load("jstests/libs/analyze_plan.js"); // For assertCoveredQueryAndCount.
+
+ const coll = db.index_bounds_minkey;
+ coll.drop();
+
+ assert.commandWorked(coll.createIndex({a: 1}));
+ assert.writeOK(coll.insert({a: MinKey}));
+
+ // Test that queries involving comparison operators with MinKey are covered.
+ const proj = {a: 1, _id: 0};
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: MinKey}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: MinKey}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: MinKey}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: MinKey}}, project: proj, count: 1});
+
+ // Test that all documents are considered greater than MinKey, regardless of the presence of
+ // the queried field 'a'.
+ coll.remove({});
+ assert.writeOK(coll.insert({a: "string"}));
+ assert.writeOK(coll.insert({a: {b: 1}}));
+ assert.writeOK(coll.insert({}));
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: MinKey}}, project: proj, count: 3});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: MinKey}}, project: proj, count: 3});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: MinKey}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: MinKey}}, project: proj, count: 0});
+})();
diff --git a/jstests/core/index_bounds_object.js b/jstests/core/index_bounds_object.js
new file mode 100644
index 00000000000..22a7f433efd
--- /dev/null
+++ b/jstests/core/index_bounds_object.js
@@ -0,0 +1,61 @@
+// Index bounds generation tests for Object values.
+// @tags: [requires_non_retryable_writes, assumes_unsharded_collection]
+(function() {
+ "use strict";
+
+ load("jstests/libs/analyze_plan.js"); // For assertCoveredQueryAndCount.
+
+ const coll = db.index_bounds_object;
+ coll.drop();
+
+ assert.commandWorked(coll.createIndex({a: 1}));
+ assert.writeOK(coll.insert({a: {b: 1}}));
+
+ // Test that queries involving comparison operators with objects are covered.
+ const proj = {a: 1, _id: 0};
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: {b: 0}}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: {b: 2}}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: {b: 1}}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: {b: 1, c: 2}}}, project: proj, count: 0});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: {b: 2}}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lte: {b: 1}}}, project: proj, count: 1});
+
+ // Test that queries involving comparisons with an empty object are covered.
+ assert.writeOK(coll.insert({a: {}}));
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$gt: {}}}, project: proj, count: 1});
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$gte: {}}}, project: proj, count: 2});
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$lt: {}}}, project: proj, count: 0});
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$lte: {}}}, project: proj, count: 1});
+
+ // Test that queries involving comparisons with a range of objects are covered.
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gt: {}, $lt: {b: 2}}}, project: proj, count: 1});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$gte: {}, $lt: {b: 2}}}, project: proj, count: 2});
+ assertCoveredQueryAndCount(
+ {collection: coll, query: {a: {$lt: {}, $gte: {}}}, project: proj, count: 0});
+
+ // Test that documents that lie outside of the generated index bounds are not returned. Cannot
+ // test empty array upper bounds since that would force the index to be multi-key.
+ coll.remove({});
+ assert.writeOK(coll.insert({a: "string"}));
+ assert.writeOK(coll.insert({a: true}));
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$gt: {}}}, project: proj, count: 0});
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$gte: {}}}, project: proj, count: 0});
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$lt: {}}}, project: proj, count: 0});
+ assertCoveredQueryAndCount({collection: coll, query: {a: {$lte: {}}}, project: proj, count: 0});
+
+ // Adding a document containing an array makes the index multi-key which can never be used for a
+ // covered query.
+ assert.writeOK(coll.insert({a: []}));
+ assert(!isIndexOnly(db, coll.find({a: {$gt: {}}}, proj).explain().queryPlanner.winningPlan));
+ assert(!isIndexOnly(db, coll.find({a: {$gte: {}}}, proj).explain().queryPlanner.winningPlan));
+ assert(!isIndexOnly(db, coll.find({a: {$lt: {}}}, proj).explain().queryPlanner.winningPlan));
+ assert(!isIndexOnly(db, coll.find({a: {$lte: {}}}, proj).explain().queryPlanner.winningPlan));
+})();
diff --git a/jstests/core/index_bounds_timestamp.js b/jstests/core/index_bounds_timestamp.js
index a0114f2a4e3..1f7cc261c30 100644
--- a/jstests/core/index_bounds_timestamp.js
+++ b/jstests/core/index_bounds_timestamp.js
@@ -7,19 +7,6 @@
load("jstests/libs/analyze_plan.js");
- // Helper function to get the nCounted from the COUNT stage in an explain plan's executionStats.
- function getCountFromExecutionStats(executionStats) {
- let stages = getPlanStages(executionStats.executionStages, "COUNT");
- // In a sharded cluster, there may be multiple COUNT stages. The sum of the 'nCounted'
- // fields is what we're interested in here.
- assert.gte(stages.length, 1, "executionStats should have at least 1 COUNT stage");
- let totalCounted = 0;
- stages.forEach(function(countStage) {
- totalCounted += countStage.nCounted;
- });
- return totalCounted;
- }
-
// Setup the test collection.
let coll = db.index_bounds_timestamp;
coll.drop();
@@ -46,7 +33,7 @@
plan = coll.explain("executionStats").find({ts: {$gt: Timestamp(0, 0)}}).count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $gt count should be a covered query");
- assert.eq(5, getCountFromExecutionStats(plan.executionStats), "ts $gt count should be 5");
+ assertExplainCount({explainResults: plan, expectedCount: 5});
// Check that find over (Timestamp(0, 0), Timestamp(2^32 - 1, 2^32 - 1)] does not require a
// FETCH stage when the query is covered by an index.
@@ -59,7 +46,7 @@
plan = coll.explain("executionStats").find({ts: {$gte: Timestamp(0, 0)}}).count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $gte count should be a covered query");
- assert.eq(5, getCountFromExecutionStats(plan.executionStats), "ts $gte count should be 5");
+ assertExplainCount({explainResults: plan, expectedCount: 5});
// Check that find over [Timestamp(0, 0), Timestamp(2^32 - 1, 2^32 - 1)] does not require a
// FETCH stage when the query is covered by an index.
@@ -73,7 +60,7 @@
plan = coll.explain("executionStats").find({ts: {$lt: Timestamp(1, 0)}}).count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $lt count should be a covered query");
- assert.eq(3, getCountFromExecutionStats(plan.executionStats), "ts $lt count should be 3");
+ assertExplainCount({explainResults: plan, expectedCount: 3});
// Check that find over [Timestamp(0, 0), Timestamp(1, 0)) does not require a FETCH stage when
// the query is covered by an index.
@@ -86,7 +73,7 @@
plan = coll.explain("executionStats").find({ts: {$lte: Timestamp(1, 0)}}).count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $lte count should be a covered query");
- assert.eq(4, getCountFromExecutionStats(plan.executionStats), "ts $lte count should be 4");
+ assertExplainCount({explainResults: plan, expectedCount: 4});
// Check that find over [Timestamp(0, 0), Timestamp(1, 0)] does not require a FETCH stage when
// the query is covered by an index.
@@ -102,7 +89,7 @@
.count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $gt, $lt count should be a covered query");
- assert.eq(2, getCountFromExecutionStats(plan.executionStats), "ts $gt, $lt count should be 2");
+ assertExplainCount({explainResults: plan, expectedCount: 2});
// Check that find over (Timestamp(0, 1), Timestamp(1, 0)) does not require a FETCH stage when
// the query is covered by an index.
@@ -118,7 +105,7 @@
.count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $gt, $lte count should be a covered query");
- assert.eq(3, getCountFromExecutionStats(plan.executionStats), "ts $gt, $lte count should be 3");
+ assertExplainCount({explainResults: plan, expectedCount: 3});
// Check that find over (Timestamp(0, 1), Timestamp(1, 0)] does not require a FETCH stage when
// the query is covered by an index.
@@ -134,7 +121,7 @@
.count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $gte, $lt count should be a covered query");
- assert.eq(3, getCountFromExecutionStats(plan.executionStats), "ts $gte, $lt count should be 3");
+ assertExplainCount({explainResults: plan, expectedCount: 3});
// Check that find over [Timestamp(0, 1), Timestamp(1, 0)) does not require a FETCH stage when
// the query is covered by an index.
@@ -150,8 +137,7 @@
.count();
assert(isIndexOnly(db, plan.queryPlanner.winningPlan),
"ts $gte, $lte count should be a covered query");
- assert.eq(
- 4, getCountFromExecutionStats(plan.executionStats), "ts $gte, $lte count should be 4");
+ assertExplainCount({explainResults: plan, expectedCount: 4});
// Check that find over [Timestamp(0, 1), Timestamp(1, 0)] does not require a FETCH stage when
// the query is covered by an index.
diff --git a/jstests/libs/analyze_plan.js b/jstests/libs/analyze_plan.js
index aed394f6b83..6a719d611f6 100644
--- a/jstests/libs/analyze_plan.js
+++ b/jstests/libs/analyze_plan.js
@@ -248,3 +248,42 @@ function getChunkSkips(root) {
return 0;
}
+
+/**
+ * Given explain output at executionStats level verbosity, confirms that the root stage is COUNT and
+ * that the result of the count is equal to 'expectedCount'.
+ */
+function assertExplainCount({explainResults, expectedCount}) {
+ const execStages = explainResults.executionStats.executionStages;
+
+ // If passed through mongos, then the root stage should be the mongos SINGLE_SHARD stage or
+ // SHARD_MERGE stages, with COUNT as the root stage on each shard. If explaining directly on the
+ // shard, then COUNT is the root stage.
+ if ("SINGLE_SHARD" == execStages.stage || "SHARD_MERGE" == execStages.stage) {
+ let totalCounted = 0;
+ for (let shardExplain of execStages.shards) {
+ const countStage = shardExplain.executionStages;
+ assert.eq(countStage.stage, "COUNT", "root stage on shard is not COUNT");
+ totalCounted += countStage.nCounted;
+ }
+ assert.eq(totalCounted, expectedCount, "wrong count result");
+ } else {
+ assert.eq(execStages.stage, "COUNT", "root stage is not COUNT");
+ assert.eq(execStages.nCounted, expectedCount, "wrong count result");
+ }
+}
+
+/**
+ * Verifies that a given query uses an index and is covered when used in a count command.
+ */
+function assertCoveredQueryAndCount({collection, query, project, count}) {
+ let explain = collection.find(query, project).explain();
+ assert(isIndexOnly(db, explain.queryPlanner.winningPlan),
+ "Winning plan was not covered: " + tojson(explain.queryPlanner.winningPlan));
+
+ // Same query as a count command should also be covered.
+ explain = collection.explain("executionStats").find(query).count();
+ assert(isIndexOnly(db, explain.queryPlanner.winningPlan),
+ "Winning plan for count was not covered: " + tojson(explain.queryPlanner.winningPlan));
+ assertExplainCount({explainResults: explain, expectedCount: count});
+}
diff --git a/src/mongo/db/query/index_bounds_builder_test.cpp b/src/mongo/db/query/index_bounds_builder_test.cpp
index f865979f3eb..e030c12afa3 100644
--- a/src/mongo/db/query/index_bounds_builder_test.cpp
+++ b/src/mongo/db/query/index_bounds_builder_test.cpp
@@ -223,6 +223,86 @@ TEST(IndexBoundsBuilderTest, TranslateLteNegativeInfinity) {
ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
}
+TEST(IndexBoundsBuilderTest, TranslateLteObject) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = fromjson("{a: {$lte: {b: 1}}}");
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': {}, '': {b: 1}}"), true, true)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLteCode) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lte" << BSONCode("function(){ return 0; }")));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[, function(){ return 0; }]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLteCodeWScope) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lte" << BSONCodeWScope("this.b == c", BSON("c" << 1))));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(),
+ "[CodeWScope( , {}), CodeWScope( this.b == c, { c: 1 })]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLteMinKey) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lte" << MINKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[MinKey, MinKey]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLteMaxKey) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lte" << MAXKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[MinKey, MaxKey]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
TEST(IndexBoundsBuilderTest, TranslateLtNumber) {
IndexEntry testIndex = IndexEntry(BSONObj());
BSONObj obj = fromjson("{a: {$lt: 1}}");
@@ -284,6 +364,85 @@ TEST(IndexBoundsBuilderTest, TranslateLtDate) {
ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
}
+TEST(IndexBoundsBuilderTest, TranslateLtObject) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = fromjson("{a: {$lt: {b: 1}}}");
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(
+ Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': {}, '': {b: 1}}"), true, false)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLtCode) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lt" << BSONCode("function(){ return 0; }")));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[, function(){ return 0; })");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_FALSE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLtCodeWScope) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lt" << BSONCodeWScope("this.b == c", BSON("c" << 1))));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(),
+ "[CodeWScope( , {}), CodeWScope( this.b == c, { c: 1 }))");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_FALSE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+// Nothing can be less than MinKey so the resulting index bounds would be a useless empty range.
+TEST(IndexBoundsBuilderTest, TranslateLtMinKeyDoesNotGenerateBounds) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lt" << MINKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 0U);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateLtMaxKey) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$lt" << MAXKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[MinKey, MaxKey]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
TEST(IndexBoundsBuilderTest, TranslateGtTimestamp) {
IndexEntry testIndex = IndexEntry(BSONObj());
BSONObj obj = BSON("a" << GT << Timestamp(2, 3));
@@ -348,6 +507,100 @@ TEST(IndexBoundsBuilderTest, TranslateGtPositiveInfinity) {
ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
}
+TEST(IndexBoundsBuilderTest, TranslateGtString) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = fromjson("{a: {$gt: 'abc'}}");
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(
+ Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': 'abc', '': {}}"), false, false)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGtObject) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = fromjson("{a: {$gt: {b: 1}}}");
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(
+ Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': {b: 1}, '': []}"), false, false)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGtCode) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gt" << BSONCode("function(){ return 0; }")));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "(function(){ return 0; }, CodeWScope( , {}))");
+ ASSERT_FALSE(oil.intervals[0].startInclusive);
+ ASSERT_FALSE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGtCodeWScope) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gt" << BSONCodeWScope("this.b == c", BSON("c" << 1))));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "(CodeWScope( this.b == c, { c: 1 }), MaxKey)");
+ ASSERT_FALSE(oil.intervals[0].startInclusive);
+ ASSERT_FALSE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGtMinKey) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gt" << MINKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[MinKey, MaxKey]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+// Nothing can be greater than MaxKey so the resulting index bounds would be a useless empty range.
+TEST(IndexBoundsBuilderTest, TranslateGtMaxKeyDoesNotGenerateBounds) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gt" << MAXKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 0U);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
TEST(IndexBoundsBuilderTest, TranslateGteNumber) {
IndexEntry testIndex = IndexEntry(BSONObj());
BSONObj obj = fromjson("{a: {$gte: 1}}");
@@ -396,9 +649,9 @@ TEST(IndexBoundsBuilderTest, TranslateGtePositiveInfinity) {
ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
}
-TEST(IndexBoundsBuilderTest, TranslateGtString) {
+TEST(IndexBoundsBuilderTest, TranslateGteObject) {
IndexEntry testIndex = IndexEntry(BSONObj());
- BSONObj obj = fromjson("{a: {$gt: 'abc'}}");
+ BSONObj obj = fromjson("{a: {$gte: {b: 1}}}");
unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
BSONElement elt = obj.firstElement();
OrderedIntervalList oil;
@@ -408,7 +661,71 @@ TEST(IndexBoundsBuilderTest, TranslateGtString) {
ASSERT_EQUALS(oil.intervals.size(), 1U);
ASSERT_EQUALS(
Interval::INTERVAL_EQUALS,
- oil.intervals[0].compare(Interval(fromjson("{'': 'abc', '': {}}"), false, false)));
+ oil.intervals[0].compare(Interval(fromjson("{'': {b: 1}, '': []}"), true, false)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGteCode) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gte" << BSONCode("function(){ return 0; }")));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[function(){ return 0; }, CodeWScope( , {}))");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_FALSE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGteCodeWScope) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gte" << BSONCodeWScope("this.b == c", BSON("c" << 1))));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[CodeWScope( this.b == c, { c: 1 }), MaxKey)");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_FALSE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGteMinKey) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gte" << MINKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[MinKey, MaxKey]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateGteMaxKey) {
+ IndexEntry testIndex = IndexEntry(BSONObj());
+ BSONObj obj = BSON("a" << BSON("$gte" << MAXKEY));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ BSONElement elt = obj.firstElement();
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(oil.intervals[0].toString(), "[MaxKey, MaxKey]");
+ ASSERT_TRUE(oil.intervals[0].startInclusive);
+ ASSERT_TRUE(oil.intervals[0].endInclusive);
ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
}
diff --git a/src/mongo/db/query/indexability.h b/src/mongo/db/query/indexability.h
index f892d91b37c..824bca47696 100644
--- a/src/mongo/db/query/indexability.h
+++ b/src/mongo/db/query/indexability.h
@@ -146,6 +146,11 @@ public:
case BSONType::bsonTimestamp:
case BSONType::jstOID:
case BSONType::BinData:
+ case BSONType::Object:
+ case BSONType::Code:
+ case BSONType::CodeWScope:
+ case BSONType::MinKey:
+ case BSONType::MaxKey:
return true;
default:
return false;