diff options
-rw-r--r-- | jstests/core/explain_count.js | 60 | ||||
-rw-r--r-- | jstests/core/idhack.js | 170 | ||||
-rw-r--r-- | jstests/core/index_bounds_code.js | 55 | ||||
-rw-r--r-- | jstests/core/index_bounds_maxkey.js | 39 | ||||
-rw-r--r-- | jstests/core/index_bounds_minkey.js | 39 | ||||
-rw-r--r-- | jstests/core/index_bounds_object.js | 61 | ||||
-rw-r--r-- | jstests/core/index_bounds_timestamp.js | 30 | ||||
-rw-r--r-- | jstests/libs/analyze_plan.js | 39 | ||||
-rw-r--r-- | src/mongo/db/query/index_bounds_builder_test.cpp | 323 | ||||
-rw-r--r-- | src/mongo/db/query/indexability.h | 5 |
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; |