summaryrefslogtreecommitdiff
path: root/src/mongo/db/query/query_planner_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/query/query_planner_test.cpp')
-rw-r--r--src/mongo/db/query/query_planner_test.cpp7109
1 files changed, 3722 insertions, 3387 deletions
diff --git a/src/mongo/db/query/query_planner_test.cpp b/src/mongo/db/query/query_planner_test.cpp
index e1abb290262..2c0ca9167a5 100644
--- a/src/mongo/db/query/query_planner_test.cpp
+++ b/src/mongo/db/query/query_planner_test.cpp
@@ -39,3443 +39,3778 @@
namespace {
- using namespace mongo;
+using namespace mongo;
- //
- // Equality
- //
+//
+// Equality
+//
- TEST_F(QueryPlannerTest, EqualityIndexScan) {
- addIndex(BSON("x" << 1));
+TEST_F(QueryPlannerTest, EqualityIndexScan) {
+ addIndex(BSON("x" << 1));
- runQuery(BSON("x" << 5));
+ runQuery(BSON("x" << 5));
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, EqualityIndexScanWithTrailingFields) {
- addIndex(BSON("x" << 1 << "y" << 1));
-
- runQuery(BSON("x" << 5));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1, y: 1}}}}}");
- }
-
- //
- // indexFilterApplied
- // Check that index filter flag is passed from planner params
- // to generated query solution.
- //
-
- TEST_F(QueryPlannerTest, IndexFilterAppliedDefault) {
- addIndex(BSON("x" << 1));
-
- runQuery(BSON("x" << 5));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
-
- // Check indexFilterApplied in query solutions;
- for (std::vector<QuerySolution*>::const_iterator it = solns.begin();
- it != solns.end();
- ++it) {
- QuerySolution* soln = *it;
- ASSERT_FALSE(soln->indexFilterApplied);
- }
- }
-
- TEST_F(QueryPlannerTest, IndexFilterAppliedTrue) {
- params.indexFiltersApplied = true;
-
- addIndex(BSON("x" << 1));
-
- runQuery(BSON("x" << 5));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
-
- // Check indexFilterApplied in query solutions;
- for (std::vector<QuerySolution*>::const_iterator it = solns.begin();
- it != solns.end();
- ++it) {
- QuerySolution* soln = *it;
- ASSERT_EQUALS(params.indexFiltersApplied, soln->indexFilterApplied);
- }
- }
-
- //
- // <
- //
-
- TEST_F(QueryPlannerTest, LessThan) {
- addIndex(BSON("x" << 1));
-
- runQuery(BSON("x" << BSON("$lt" << 5)));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: {$lt: 5}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- //
- // <=
- //
-
- TEST_F(QueryPlannerTest, LessThanEqual) {
- addIndex(BSON("x" << 1));
-
- runQuery(BSON("x" << BSON("$lte" << 5)));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: {$lte: 5}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- //
- // >
- //
-
- TEST_F(QueryPlannerTest, GreaterThan) {
- addIndex(BSON("x" << 1));
-
- runQuery(BSON("x" << BSON("$gt" << 5)));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: {$gt: 5}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- //
- // >=
- //
-
- TEST_F(QueryPlannerTest, GreaterThanEqual) {
- addIndex(BSON("x" << 1));
-
- runQuery(BSON("x" << BSON("$gte" << 5)));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: {$gte: 5}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- //
- // Mod
- //
-
- TEST_F(QueryPlannerTest, Mod) {
- addIndex(BSON("a" << 1));
-
- runQuery(fromjson("{a: {$mod: [2, 0]}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: {$mod: [2, 0]}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: {a: {$mod: [2, 0]}}, pattern: {a: 1}}}}}");
- }
-
- //
- // Exists
- //
-
- TEST_F(QueryPlannerTest, ExistsTrue) {
- addIndex(BSON("x" << 1));
-
- runQuery(fromjson("{x: {$exists: true}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsFalse) {
- addIndex(BSON("x" << 1));
-
- runQuery(fromjson("{x: {$exists: false}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsTrueSparseIndex) {
- addIndex(BSON("x" << 1), false, true);
-
- runQuery(fromjson("{x: {$exists: true}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsFalseSparseIndex) {
- addIndex(BSON("x" << 1), false, true);
-
- runQuery(fromjson("{x: {$exists: false}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsTrueOnUnindexedField) {
- addIndex(BSON("x" << 1));
-
- runQuery(fromjson("{x: 1, y: {$exists: true}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsFalseOnUnindexedField) {
- addIndex(BSON("x" << 1));
-
- runQuery(fromjson("{x: 1, y: {$exists: false}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsTrueSparseIndexOnOtherField) {
- addIndex(BSON("x" << 1), false, true);
-
- runQuery(fromjson("{x: 1, y: {$exists: true}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsFalseSparseIndexOnOtherField) {
- addIndex(BSON("x" << 1), false, true);
-
- runQuery(fromjson("{x: 1, y: {$exists: false}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsBounds) {
- addIndex(BSON("b" << 1));
-
- runQuery(fromjson("{b: {$exists: true}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b: {$exists: true}}, node: "
- "{ixscan: {pattern: {b: 1}, bounds: "
- "{b: [['MinKey', 'MaxKey', true, true]]}}}}}");
-
- // This ends up being a double negation, which we currently don't index.
- runQuery(fromjson("{b: {$not: {$exists: false}}}"));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- runQuery(fromjson("{b: {$exists: false}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b: {$exists: false}}, node: "
- "{ixscan: {pattern: {b: 1}, bounds: "
- "{b: [[null, null, true, true]]}}}}}");
-
- runQuery(fromjson("{b: {$not: {$exists: true}}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b: {$exists: false}}, node: "
- "{ixscan: {pattern: {b: 1}, bounds: "
- "{b: [[null, null, true, true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ExistsBoundsCompound) {
- addIndex(BSON("a" << 1 << "b" << 1));
-
- runQuery(fromjson("{a: 1, b: {$exists: true}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b: {$exists: true}}, node: "
- "{ixscan: {pattern: {a: 1, b: 1}, bounds: "
- "{a: [[1,1,true,true]], b: [['MinKey','MaxKey',true,true]]}}}}}");
-
- // This ends up being a double negation, which we currently don't index.
- runQuery(fromjson("{a: 1, b: {$not: {$exists: false}}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, bounds: "
- "{a: [[1,1,true,true]], b: [['MinKey','MaxKey',true,true]]}}}}}");
-
- runQuery(fromjson("{a: 1, b: {$exists: false}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b: {$exists: false}}, node: "
- "{ixscan: {pattern: {a: 1, b: 1}, bounds: "
- "{a: [[1,1,true,true]], b: [[null,null,true,true]]}}}}}");
-
- runQuery(fromjson("{a: 1, b: {$not: {$exists: true}}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b: {$exists: false}}, node: "
- "{ixscan: {pattern: {a: 1, b: 1}, bounds: "
- "{a: [[1,1,true,true]], b: [[null,null,true,true]]}}}}}");
- }
-
- //
- // skip and limit
- //
-
- TEST_F(QueryPlannerTest, BasicSkipNoIndex) {
- addIndex(BSON("a" << 1));
-
- runQuerySkipLimit(BSON("x" << 5), 3, 0);
-
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{skip: {n: 3, node: {cscan: {dir: 1, filter: {x: 5}}}}}");
- }
-
- TEST_F(QueryPlannerTest, BasicSkipWithIndex) {
- addIndex(BSON("a" << 1 << "b" << 1));
-
- runQuerySkipLimit(BSON("a" << 5), 8, 0);
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{skip: {n: 8, node: {cscan: {dir: 1, filter: {a: 5}}}}}");
- assertSolutionExists("{skip: {n: 8, node: {fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, BasicLimitNoIndex) {
- addIndex(BSON("a" << 1));
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
+ assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
+}
- runQuerySkipLimit(BSON("x" << 5), 0, -3);
+TEST_F(QueryPlannerTest, EqualityIndexScanWithTrailingFields) {
+ addIndex(BSON("x" << 1 << "y" << 1));
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{limit: {n: 3, node: {cscan: {dir: 1, filter: {x: 5}}}}}");
- }
+ runQuery(BSON("x" << 5));
- TEST_F(QueryPlannerTest, BasicSoftLimitNoIndex) {
- addIndex(BSON("a" << 1));
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
+ assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1, y: 1}}}}}");
+}
- runQuerySkipLimit(BSON("x" << 5), 0, 3);
+//
+// indexFilterApplied
+// Check that index filter flag is passed from planner params
+// to generated query solution.
+//
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
- }
+TEST_F(QueryPlannerTest, IndexFilterAppliedDefault) {
+ addIndex(BSON("x" << 1));
- TEST_F(QueryPlannerTest, BasicLimitWithIndex) {
- addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(BSON("x" << 5));
- runQuerySkipLimit(BSON("a" << 5), 0, -5);
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
+ assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{limit: {n: 5, node: {cscan: {dir: 1, filter: {a: 5}}}}}");
- assertSolutionExists("{limit: {n: 5, node: {fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}}}");
+ // Check indexFilterApplied in query solutions;
+ for (std::vector<QuerySolution*>::const_iterator it = solns.begin(); it != solns.end(); ++it) {
+ QuerySolution* soln = *it;
+ ASSERT_FALSE(soln->indexFilterApplied);
}
+}
- TEST_F(QueryPlannerTest, BasicSoftLimitWithIndex) {
- addIndex(BSON("a" << 1 << "b" << 1));
-
- runQuerySkipLimit(BSON("a" << 5), 0, 5);
+TEST_F(QueryPlannerTest, IndexFilterAppliedTrue) {
+ params.indexFiltersApplied = true;
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: 5}}}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}");
- }
+ addIndex(BSON("x" << 1));
- TEST_F(QueryPlannerTest, SkipAndLimit) {
- addIndex(BSON("x" << 1));
+ runQuery(BSON("x" << 5));
- runQuerySkipLimit(BSON("x" << BSON("$lte" << 4)), 7, -2);
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
+ assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{limit: {n: 2, node: {skip: {n: 7, node: "
- "{cscan: {dir: 1, filter: {x: {$lte: 4}}}}}}}}");
- assertSolutionExists("{limit: {n: 2, node: {skip: {n: 7, node: {fetch: "
- "{filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}}}}}");
+ // Check indexFilterApplied in query solutions;
+ for (std::vector<QuerySolution*>::const_iterator it = solns.begin(); it != solns.end(); ++it) {
+ QuerySolution* soln = *it;
+ ASSERT_EQUALS(params.indexFiltersApplied, soln->indexFilterApplied);
}
+}
- TEST_F(QueryPlannerTest, SkipAndSoftLimit) {
- addIndex(BSON("x" << 1));
+//
+// <
+//
- runQuerySkipLimit(BSON("x" << BSON("$lte" << 4)), 7, 2);
+TEST_F(QueryPlannerTest, LessThan) {
+ addIndex(BSON("x" << 1));
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{skip: {n: 7, node: "
- "{cscan: {dir: 1, filter: {x: {$lte: 4}}}}}}");
- assertSolutionExists("{skip: {n: 7, node: {fetch: "
- "{filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}}}");
- }
+ runQuery(BSON("x" << BSON("$lt" << 5)));
- //
- // tree operations
- //
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: {$lt: 5}}}}");
+ assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1}}}}}");
+}
- TEST_F(QueryPlannerTest, TwoPredicatesAnding) {
- addIndex(BSON("x" << 1));
+//
+// <=
+//
- runQuery(fromjson("{$and: [ {x: {$gt: 1}}, {x: {$lt: 3}} ] }"));
+TEST_F(QueryPlannerTest, LessThanEqual) {
+ addIndex(BSON("x" << 1));
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
+ runQuery(BSON("x" << BSON("$lte" << 5)));
- TEST_F(QueryPlannerTest, SimpleOr) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a: 20}, {a: 21}]}"));
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: {$lte: 5}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a: 20}, {a: 21}]}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a:1}}}}}");
- }
+//
+// >
+//
- TEST_F(QueryPlannerTest, OrWithoutEnoughIndices) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a: 20}, {b: 21}]}"));
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a: 20}, {b: 21}]}}}");
- }
+TEST_F(QueryPlannerTest, GreaterThan) {
+ addIndex(BSON("x" << 1));
- TEST_F(QueryPlannerTest, OrWithAndChild) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a: 20}, {$and: [{a:1}, {b:7}]}]}"));
+ runQuery(BSON("x" << BSON("$gt" << 5)));
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {filter: null, pattern: {a: 1}}}, "
- "{fetch: {filter: {b: 7}, node: {ixscan: "
- "{filter: null, pattern: {a: 1}}}}}]}}}}");
- }
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: {$gt: 5}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
- TEST_F(QueryPlannerTest, AndWithUnindexedOrChild) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{a:20, $or: [{b:1}, {c:7}]}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- // Logical rewrite means we could get one of these two outcomes:
- size_t matches = 0;
- matches += numSolutionMatches("{fetch: {filter: {$or: [{b: 1}, {c: 7}]}, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}");
- matches += numSolutionMatches("{or: {filter: null, nodes: ["
- "{fetch: {filter: {b:1}, node: {"
- "ixscan: {filter: null, pattern: {a:1}}}}},"
- "{fetch: {filter: {c:7}, node: {"
- "ixscan: {filter: null, pattern: {a:1}}}}}]}}");
- ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
- }
+//
+// >=
+//
+TEST_F(QueryPlannerTest, GreaterThanEqual) {
+ addIndex(BSON("x" << 1));
- TEST_F(QueryPlannerTest, AndWithOrWithOneIndex) {
- addIndex(BSON("b" << 1));
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{b:1}, {c:7}], a:20}"));
-
- // Logical rewrite gives us at least one of these:
- assertSolutionExists("{cscan: {dir: 1}}");
- size_t matches = 0;
- matches += numSolutionMatches("{fetch: {filter: {$or: [{b: 1}, {c: 7}]}, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- matches += numSolutionMatches("{or: {filter: null, nodes: ["
- "{fetch: {filter: {b:1}, node: {"
- "ixscan: {filter: null, pattern: {a:1}}}}},"
- "{fetch: {filter: {c:7}, node: {"
- "ixscan: {filter: null, pattern: {a:1}}}}}]}}");
- ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
- }
+ runQuery(BSON("x" << BSON("$gte" << 5)));
- //
- // Additional $or tests
- //
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: {$gte: 5}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
- TEST_F(QueryPlannerTest, OrCollapsesToSingleScan) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a:{$gt:2}}, {a:{$gt:0}}]}"));
+//
+// Mod
+//
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [[0,Infinity,false,true]]}}}}}");
- }
+TEST_F(QueryPlannerTest, Mod) {
+ addIndex(BSON("a" << 1));
- TEST_F(QueryPlannerTest, OrCollapsesToSingleScan2) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a:{$lt:2}}, {a:{$lt:4}}]}"));
+ runQuery(fromjson("{a: {$mod: [2, 0]}}"));
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [[-Infinity,4,true,false]]}}}}}");
- }
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: {$mod: [2, 0]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: {a: {$mod: [2, 0]}}, pattern: {a: 1}}}}}");
+}
- TEST_F(QueryPlannerTest, OrCollapsesToSingleScan3) {
- addIndex(BSON("a" << 1));
- runQueryHint(fromjson("{$or: [{a:1},{a:3}]}"), fromjson("{a:1}"));
+//
+// Exists
+//
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [[1,1,true,true], [3,3,true,true]]}}}}}");
- }
+TEST_F(QueryPlannerTest, ExistsTrue) {
+ addIndex(BSON("x" << 1));
- TEST_F(QueryPlannerTest, OrOnlyOneBranchCanUseIndex) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a:1}, {b:2}]}"));
+ runQuery(fromjson("{x: {$exists: true}}"));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
- TEST_F(QueryPlannerTest, OrOnlyOneBranchCanUseIndexHinted) {
- addIndex(BSON("a" << 1));
- runQueryHint(fromjson("{$or: [{a:1}, {b:2}]}"), fromjson("{a:1}"));
+TEST_F(QueryPlannerTest, ExistsFalse) {
+ addIndex(BSON("x" << 1));
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: {$or:[{a:1},{b:2}]}, node: {ixscan: "
- "{pattern: {a:1}, bounds: "
- "{a: [['MinKey','MaxKey',true,true]]}}}}}");
- }
+ runQuery(fromjson("{x: {$exists: false}}"));
- TEST_F(QueryPlannerTest, OrNaturalHint) {
- addIndex(BSON("a" << 1));
- runQueryHint(fromjson("{$or: [{a:1}, {a:3}]}"), fromjson("{$natural:1}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
+TEST_F(QueryPlannerTest, ExistsTrueSparseIndex) {
+ addIndex(BSON("x" << 1), false, true);
- // SERVER-13714. A non-top-level indexable negation exposed a bug in plan enumeration.
- TEST_F(QueryPlannerTest, NonTopLevelIndexedNegation) {
- addIndex(BSON("state" << 1));
- addIndex(BSON("is_draft" << 1));
- addIndex(BSON("published_date" << 1));
- addIndex(BSON("newsroom_id" << 1));
-
- BSONObj queryObj = fromjson("{$and:[{$or:[{is_draft:false},{creator_id:1}]},"
- "{$or:[{state:3,is_draft:false},"
- "{published_date:{$ne:null}}]},"
- "{newsroom_id:{$in:[1]}}]}");
- runQuery(queryObj);
- }
-
- TEST_F(QueryPlannerTest, NonTopLevelIndexedNegationMinQuery) {
- addIndex(BSON("state" << 1));
- addIndex(BSON("is_draft" << 1));
- addIndex(BSON("published_date" << 1));
-
- // This is the min query to reproduce SERVER-13714
- BSONObj queryObj = fromjson("{$or:[{state:1, is_draft:1}, {published_date:{$ne: 1}}]}");
- runQuery(queryObj);
- }
-
- // SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
- TEST_F(QueryPlannerTest, OrOfAnd) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a:{$gt:2,$lt:10}}, {a:{$gt:0,$lt:5}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {pattern: {a:1}, bounds: {a: [[2,10,false,false]]}}}, "
- "{ixscan: {pattern: {a:1}, bounds: "
- "{a: [[0,5,false,false]]}}}]}}}}");
- }
-
- // SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
- TEST_F(QueryPlannerTest, OrOfAnd2) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a:{$gt:2,$lt:10}}, {a:{$gt:0,$lt:15}}, {a:{$gt:20}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {pattern: {a:1}, bounds: {a: [[2,10,false,false]]}}}, "
- "{ixscan: {pattern: {a:1}, bounds: {a: [[0,15,false,false]]}}}, "
- "{ixscan: {pattern: {a:1}, bounds: "
- "{a: [[20,Infinity,false,true]]}}}]}}}}");
- }
-
- // SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
- TEST_F(QueryPlannerTest, OrOfAnd3) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a:{$gt:1,$lt:5},b:6}, {a:3,b:{$gt:0,$lt:10}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {b:6}, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [[1,5,false,false]]}}}}}, "
- "{fetch: {filter: {$and:[{b:{$lt:10}},{b:{$gt:0}}]}, node: "
- "{ixscan: {pattern: {a:1}, bounds: {a:[[3,3,true,true]]}}}}}]}}");
- }
-
- // SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
- TEST_F(QueryPlannerTest, OrOfAnd4) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{$or: [{a:{$gt:1,$lt:5}, b:{$gt:0,$lt:3}, c:6}, "
- "{a:3, b:{$gt:1,$lt:2}, c:{$gt:0,$lt:10}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {c:6}, node: {ixscan: {pattern: {a:1,b:1}, "
- "bounds: {a: [[1,5,false,false]], b: [[0,3,false,false]]}}}}}, "
- "{fetch: {filter: {$and:[{c:{$lt:10}},{c:{$gt:0}}]}, node: "
- "{ixscan: {pattern: {a:1,b:1}, "
- " bounds: {a:[[3,3,true,true]], b:[[1,2,false,false]]}}}}}]}}");
- }
-
- // SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
- TEST_F(QueryPlannerTest, OrOfAnd5) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{$or: [{a:{$gt:1,$lt:5}, c:6}, "
- "{a:3, b:{$gt:1,$lt:2}, c:{$gt:0,$lt:10}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {c:6}, node: {ixscan: {pattern: {a:1,b:1}, "
- "bounds: {a: [[1,5,false,false]], "
- "b: [['MinKey','MaxKey',true,true]]}}}}}, "
- "{fetch: {filter: {$and:[{c:{$lt:10}},{c:{$gt:0}}]}, node: "
- "{ixscan: {pattern: {a:1,b:1}, "
- " bounds: {a:[[3,3,true,true]], b:[[1,2,false,false]]}}}}}]}}");
- }
-
- // SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
- TEST_F(QueryPlannerTest, OrOfAnd6) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{$or: [{a:{$in:[1]},b:{$in:[1]}}, {a:{$in:[1,5]},b:{$in:[1,5]}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {pattern: {a:1,b:1}, bounds: "
- "{a: [[1,1,true,true]], b: [[1,1,true,true]]}}}, "
- "{ixscan: {pattern: {a:1,b:1}, bounds: "
- "{a: [[1,1,true,true], [5,5,true,true]], "
- " b: [[1,1,true,true], [5,5,true,true]]}}}]}}}}");
- }
-
- // SERVER-13960: properly handle $or with a mix of exact and inexact predicates.
- TEST_F(QueryPlannerTest, OrInexactWithExact) {
- addIndex(BSON("name" << 1));
- runQuery(fromjson("{$or: [{name: 'thomas'}, {name: /^alexand(er|ra)/}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {filter:"
- "{$or: [{name: 'thomas'}, {name: /^alexand(er|ra)/}]},"
- "pattern: {name: 1}}}}}");
- }
-
- // SERVER-13960: multiple indices, each with an inexact covered predicate.
- TEST_F(QueryPlannerTest, OrInexactWithExact2) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuery(fromjson("{$or: [{a: 'foo'}, {a: /bar/}, {b: 'foo'}, {b: /bar/}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {or: {nodes: ["
- "{ixscan: {filter: {$or:[{a:'foo'},{a:/bar/}]},"
- "pattern: {a: 1}}},"
- "{ixscan: {filter: {$or:[{b:'foo'},{b:/bar/}]},"
- "pattern: {b: 1}}}]}}}}");
- }
-
- // SERVER-13960: an exact, inexact covered, and inexact fetch predicate.
- TEST_F(QueryPlannerTest, OrAllThreeTightnesses) {
- addIndex(BSON("names" << 1));
- runQuery(fromjson("{$or: [{names: 'frank'}, {names: /^al(ice)|(ex)/},"
- "{names: {$elemMatch: {$eq: 'thomas'}}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: "
- "{$or: [{names: 'frank'}, {names: /^al(ice)|(ex)/},"
- "{names: {$elemMatch: {$eq: 'thomas'}}}]}, "
- "node: {ixscan: {filter: null, pattern: {names: 1}}}}}");
- }
-
- // SERVER-13960: two inexact fetch predicates.
- TEST_F(QueryPlannerTest, OrTwoInexactFetch) {
- // true means multikey
- addIndex(BSON("names" << 1), true);
- runQuery(fromjson("{$or: [{names: {$elemMatch: {$eq: 'alexandra'}}},"
- "{names: {$elemMatch: {$eq: 'thomas'}}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: "
- "{$or: [{names: {$elemMatch: {$eq: 'alexandra'}}},"
- "{names: {$elemMatch: {$eq: 'thomas'}}}]}, "
- "node: {ixscan: {filter: null, pattern: {names: 1}}}}}");
- }
-
- // SERVER-13960: multikey with exact and inexact covered predicates.
- TEST_F(QueryPlannerTest, OrInexactCoveredMultikey) {
- // true means multikey
- addIndex(BSON("names" << 1), true);
- runQuery(fromjson("{$or: [{names: 'dave'}, {names: /joe/}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {$or: [{names: 'dave'}, {names: /joe/}]}, "
- "node: {ixscan: {filter: null, pattern: {names: 1}}}}}");
- }
-
- // SERVER-13960: $elemMatch object with $or.
- TEST_F(QueryPlannerTest, OrElemMatchObject) {
- // true means multikey
- addIndex(BSON("a.b" << 1), true);
- runQuery(fromjson("{$or: [{a: {$elemMatch: {b: {$lte: 1}}}},"
- "{a: {$elemMatch: {b: {$gte: 4}}}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {a:{$elemMatch:{b:{$gte:4}}}}, node: "
- "{ixscan: {filter: null, pattern: {'a.b': 1}}}}},"
- "{fetch: {filter: {a:{$elemMatch:{b:{$lte:1}}}}, node: "
- "{ixscan: {filter: null, pattern: {'a.b': 1}}}}}]}}");
- }
-
- // SERVER-13960: $elemMatch object inside an $or, below an AND.
- TEST_F(QueryPlannerTest, OrElemMatchObjectBeneathAnd) {
- // true means multikey
- addIndex(BSON("a.b" << 1), true);
- runQuery(fromjson("{$or: [{'a.b': 0, a: {$elemMatch: {b: {$lte: 1}}}},"
- "{a: {$elemMatch: {b: {$gte: 4}}}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {$and:[{a:{$elemMatch:{b:{$lte:1}}}},{'a.b':0}]},"
- "node: {ixscan: {filter: null, pattern: {'a.b': 1}, "
- "bounds: {'a.b': [[-Infinity,1,true,true]]}}}}},"
- "{fetch: {filter: {a:{$elemMatch:{b:{$gte:4}}}}, node: "
- "{ixscan: {filter: null, pattern: {'a.b': 1},"
- "bounds: {'a.b': [[4,Infinity,true,true]]}}}}}]}}");
- }
-
- // SERVER-13960: $or below $elemMatch with an inexact covered predicate.
- TEST_F(QueryPlannerTest, OrBelowElemMatchInexactCovered) {
- // true means multikey
- addIndex(BSON("a.b" << 1), true);
- runQuery(fromjson("{a: {$elemMatch: {$or: [{b: 'x'}, {b: /z/}]}}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {a: {$elemMatch: {$or: [{b: 'x'}, {b: /z/}]}}},"
- "node: {ixscan: {filter: null, pattern: {'a.b': 1}}}}}");
- }
-
- // SERVER-13960: $in with exact and inexact covered predicates.
- TEST_F(QueryPlannerTest, OrWithExactAndInexact) {
- addIndex(BSON("name" << 1));
- runQuery(fromjson("{name: {$in: ['thomas', /^alexand(er|ra)/]}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: {name: {$in: ['thomas', /^alexand(er|ra)/]}}, "
- "pattern: {name: 1}}}}}");
- }
-
- // SERVER-13960: $in with exact, inexact covered, and inexact fetch predicates.
- TEST_F(QueryPlannerTest, OrWithExactAndInexact2) {
- addIndex(BSON("name" << 1));
- runQuery(fromjson("{$or: [{name: {$in: ['thomas', /^alexand(er|ra)/]}},"
- "{name: {$exists: false}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {$or: [{name: {$in: ['thomas', /^alexand(er|ra)/]}},"
- "{name: {$exists: false}}]}, "
- "node: {ixscan: {filter: null, pattern: {name: 1}}}}}");
- }
-
- // SERVER-13960: $in with exact, inexact covered, and inexact fetch predicates
- // over two indices.
- TEST_F(QueryPlannerTest, OrWithExactAndInexact3) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuery(fromjson("{$or: [{a: {$in: [/z/, /x/]}}, {a: 'w'},"
- "{b: {$exists: false}}, {b: {$in: ['p']}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {filter: {$or:[{a:{$in:[/z/, /x/]}}, {a:'w'}]}, "
- "pattern: {a: 1}}}, "
- "{fetch: {filter: {$or:[{b:{$exists:false}}, {b:{$in:['p']}}]},"
- "node: {ixscan: {filter: null, pattern: {b: 1}}}}}]}}}}");
- }
-
- //
- // Min/Max
- //
-
- TEST_F(QueryPlannerTest, MinValid) {
- addIndex(BSON("a" << 1));
- runQueryHintMinMax(BSONObj(), BSONObj(), fromjson("{a: 1}"), BSONObj());
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MinWithoutIndex) {
- runInvalidQueryHintMinMax(BSONObj(), BSONObj(), fromjson("{a: 1}"), BSONObj());
- }
-
- TEST_F(QueryPlannerTest, MinBadHint) {
- addIndex(BSON("b" << 1));
- runInvalidQueryHintMinMax(BSONObj(), fromjson("{b: 1}"), fromjson("{a: 1}"), BSONObj());
- }
-
- TEST_F(QueryPlannerTest, MaxValid) {
- addIndex(BSON("a" << 1));
- runQueryHintMinMax(BSONObj(), BSONObj(), BSONObj(), fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MinMaxSameValue) {
- addIndex(BSON("a" << 1));
- runQueryHintMinMax(BSONObj(), BSONObj(), fromjson("{a: 1}"), fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MaxWithoutIndex) {
- runInvalidQueryHintMinMax(BSONObj(), BSONObj(), BSONObj(), fromjson("{a: 1}"));
- }
-
- TEST_F(QueryPlannerTest, MaxBadHint) {
- addIndex(BSON("b" << 1));
- runInvalidQueryHintMinMax(BSONObj(), fromjson("{b: 1}"), BSONObj(), fromjson("{a: 1}"));
- }
-
- TEST_F(QueryPlannerTest, MaxMinSort) {
- addIndex(BSON("a" << 1));
-
- // Run an empty query, sort {a: 1}, max/min arguments.
- runQueryFull(BSONObj(), fromjson("{a: 1}"), BSONObj(), 0, 0, BSONObj(),
- fromjson("{a: 2}"), fromjson("{a: 8}"), false);
-
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MaxMinReverseSort) {
- addIndex(BSON("a" << 1));
-
- // Run an empty query, sort {a: -1}, max/min arguments.
- runQueryFull(BSONObj(), fromjson("{a: -1}"), BSONObj(), 0, 0, BSONObj(),
- fromjson("{a: 2}"), fromjson("{a: 8}"), false);
-
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: -1, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MaxMinReverseIndexDir) {
- addIndex(BSON("a" << -1));
-
- // Because the index is descending, the min is numerically larger than the max.
- runQueryFull(BSONObj(), fromjson("{a: -1}"), BSONObj(), 0, 0, BSONObj(),
- fromjson("{a: 8}"), fromjson("{a: 2}"), false);
-
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: 1, pattern: {a: -1}}}}}");
- }
+ runQuery(fromjson("{x: {$exists: true}}"));
- TEST_F(QueryPlannerTest, MaxMinReverseIndexDirSort) {
- addIndex(BSON("a" << -1));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsFalseSparseIndex) {
+ addIndex(BSON("x" << 1), false, true);
+
+ runQuery(fromjson("{x: {$exists: false}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsTrueOnUnindexedField) {
+ addIndex(BSON("x" << 1));
+
+ runQuery(fromjson("{x: 1, y: {$exists: true}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsFalseOnUnindexedField) {
+ addIndex(BSON("x" << 1));
+
+ runQuery(fromjson("{x: 1, y: {$exists: false}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsTrueSparseIndexOnOtherField) {
+ addIndex(BSON("x" << 1), false, true);
+
+ runQuery(fromjson("{x: 1, y: {$exists: true}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsFalseSparseIndexOnOtherField) {
+ addIndex(BSON("x" << 1), false, true);
+
+ runQuery(fromjson("{x: 1, y: {$exists: false}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsBounds) {
+ addIndex(BSON("b" << 1));
+
+ runQuery(fromjson("{b: {$exists: true}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$exists: true}}, node: "
+ "{ixscan: {pattern: {b: 1}, bounds: "
+ "{b: [['MinKey', 'MaxKey', true, true]]}}}}}");
+
+ // This ends up being a double negation, which we currently don't index.
+ runQuery(fromjson("{b: {$not: {$exists: false}}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
- // Min/max specifies a forward scan with bounds [{a: 8}, {a: 2}]. Asking for
- // an ascending sort reverses the direction of the scan to [{a: 2}, {a: 8}].
- runQueryFull(BSONObj(), fromjson("{a: 1}"), BSONObj(), 0, 0, BSONObj(),
- fromjson("{a: 8}"), fromjson("{a: 2}"), false);
+ runQuery(fromjson("{b: {$exists: false}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$exists: false}}, node: "
+ "{ixscan: {pattern: {b: 1}, bounds: "
+ "{b: [[null, null, true, true]]}}}}}");
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: -1,"
- "pattern: {a: -1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MaxMinNoMatchingIndexDir) {
- addIndex(BSON("a" << -1));
- runInvalidQueryHintMinMax(BSONObj(), fromjson("{a: 2}"), BSONObj(), fromjson("{a: 8}"));
- }
-
- TEST_F(QueryPlannerTest, MaxMinSelectCorrectlyOrderedIndex) {
- // There are both ascending and descending indices on 'a'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("a" << -1));
-
- // The ordering of min and max means that we *must* use the descending index.
- runQueryFull(BSONObj(), BSONObj(), BSONObj(), 0, 0, BSONObj(),
- fromjson("{a: 8}"), fromjson("{a: 2}"), false);
-
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: 1, pattern: {a: -1}}}}}");
-
- // If we switch the ordering, then we use the ascending index.
- // The ordering of min and max means that we *must* use the descending index.
- runQueryFull(BSONObj(), BSONObj(), BSONObj(), 0, 0, BSONObj(),
- fromjson("{a: 2}"), fromjson("{a: 8}"), false);
-
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: 1, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, MaxMinBadHintSelectsReverseIndex) {
- // There are both ascending and descending indices on 'a'.
- addIndex(BSON("a" << 1));
- addIndex(BSON("a" << -1));
-
- // A query hinting on {a: 1} is bad if min is {a: 8} and {a: 2} because this
- // min/max pairing requires a descending index.
- runInvalidQueryFull(BSONObj(), BSONObj(), BSONObj(), 0, 0, fromjson("{a: 1}"),
- fromjson("{a: 8}"), fromjson("{a: 2}"), false);
- }
-
-
- //
- // $snapshot
- //
-
- TEST_F(QueryPlannerTest, Snapshot) {
- addIndex(BSON("a" << 1));
- runQuerySnapshot(fromjson("{a: {$gt: 0}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: {a:{$gt:0}}, node: "
- "{ixscan: {filter: null, pattern: {_id: 1}}}}}");
- }
-
- //
- // Tree operations that require simple tree rewriting.
- //
-
- TEST_F(QueryPlannerTest, AndOfAnd) {
- addIndex(BSON("x" << 1));
- runQuery(fromjson("{$and: [ {$and: [ {x: 2.5}]}, {x: {$gt: 1}}, {x: {$lt: 3}} ] }"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- //
- // Logically equivalent queries
- //
-
- TEST_F(QueryPlannerTest, EquivalentAndsOne) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{$and: [{a: 1}, {b: {$all: [10, 20]}}]}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$and:[{a:1},{b:10},{b:20}]}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1, b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, EquivalentAndsTwo) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{$and: [{a: 1, b: 10}, {a: 1, b: 20}]}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$and:[{a:1},{a:1},{b:10},{b:20}]}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1, b: 1}}}}}");
- }
-
- //
- // Covering
- //
-
- TEST_F(QueryPlannerTest, BasicCovering) {
- addIndex(BSON("x" << 1));
- // query, sort, proj
- runQuerySortProj(fromjson("{ x : {$gt: 1}}"), BSONObj(), fromjson("{_id: 0, x: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, x: 1}, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, x: 1}, node: "
- "{cscan: {dir: 1, filter: {x:{$gt:1}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, DottedFieldCovering) {
- addIndex(BSON("a.b" << 1));
- runQuerySortProj(fromjson("{'a.b': 5}"), BSONObj(), fromjson("{_id: 0, 'a.b': 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, 'a.b': 1}, node: "
- "{cscan: {dir: 1, filter: {'a.b': 5}}}}}");
- // SERVER-2104
- //assertSolutionExists("{proj: {spec: {_id: 0, 'a.b': 1}, node: {'a.b': 1}}}");
- }
-
- TEST_F(QueryPlannerTest, IdCovering) {
- runQuerySortProj(fromjson("{_id: {$gt: 10}}"), BSONObj(), fromjson("{_id: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 1}, node: "
- "{cscan: {dir: 1, filter: {_id: {$gt: 10}}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 1}, node: {ixscan: "
- "{filter: null, pattern: {_id: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ProjNonCovering) {
- addIndex(BSON("x" << 1));
- runQuerySortProj(fromjson("{ x : {$gt: 1}}"), BSONObj(), fromjson("{x: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {x: 1}, node: {cscan: "
- "{dir: 1, filter: {x: {$gt: 1}}}}}}");
- assertSolutionExists("{proj: {spec: {x: 1}, node: {fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {x: 1}}}}}}}");
- }
-
- //
- // Basic sort
- //
-
- TEST_F(QueryPlannerTest, BasicSort) {
- addIndex(BSON("x" << 1));
- runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- assertSolutionExists("{sort: {pattern: {x: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CantUseHashedIndexToProvideSort) {
- addIndex(BSON("x" << "hashed"));
- runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{sort: {pattern: {x: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CantUseHashedIndexToProvideSortWithIndexablePred) {
- addIndex(BSON("x" << "hashed"));
- runQuerySortProj(BSON("x" << BSON("$in" << BSON_ARRAY(0 << 1))), BSON("x" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {x: 1}, limit: 0, node: "
- "{fetch: {node: "
- "{ixscan: {pattern: {x: 'hashed'}}}}}}}");
- assertSolutionExists("{sort: {pattern: {x: 1}, limit: 0, node: "
- "{cscan: {dir: 1, filter: {x: {$in: [0, 1]}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CantUseTextIndexToProvideSort) {
- addIndex(BSON("x" << 1 << "_fts" << "text" << "_ftsx" << 1));
- runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{sort: {pattern: {x: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {}}}}}");
- }
-
- TEST_F(QueryPlannerTest, BasicSortWithIndexablePred) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuerySortProj(fromjson("{ a : 5 }"), BSON("b" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 3U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, "
- "node: {fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}}}");
- assertSolutionExists("{fetch: {filter: {a: 5}, node: {ixscan: "
- "{filter: null, pattern: {b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, BasicSortBooleanIndexKeyPattern) {
- addIndex(BSON("a" << true));
- runQuerySortProj(fromjson("{ a : 5 }"), BSON("a" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: true}}}}}");
- }
-
- // SERVER-14070
- TEST_F(QueryPlannerTest, CompoundIndexWithEqualityPredicatesProvidesSort) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProj(fromjson("{a: 1, b: 1}"), fromjson("{b: 1}"), BSONObj());
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {filter: null,"
- "pattern: {a: 1, b: 1}, "
- "bounds: {a:[[1,1,true,true]], b:[[1,1,true,true]]}}}}}");
- }
-
- //
- // Sort with limit and/or skip
- //
-
- TEST_F(QueryPlannerTest, SortLimit) {
- // Negative limit indicates hard limit - see lite_parsed_query.cpp
- runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 0, -3);
- assertNumSolutions(1U);
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 3, "
- "node: {cscan: {dir: 1}}}}");
- }
-
- TEST_F(QueryPlannerTest, SortSkip) {
- runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 2, 0);
- assertNumSolutions(1U);
- // If only skip is provided, do not limit sort.
- assertSolutionExists("{skip: {n: 2, node: "
- "{sort: {pattern: {a: 1}, limit: 0, "
- "node: {cscan: {dir: 1}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, SortSkipLimit) {
- runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 2, -3);
- assertNumSolutions(1U);
- // Limit in sort node should be adjusted by skip count
- assertSolutionExists("{skip: {n: 2, node: "
- "{sort: {pattern: {a: 1}, limit: 5, "
- "node: {cscan: {dir: 1}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, SortSoftLimit) {
- runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 0, 3);
- assertNumSolutions(1U);
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 3, "
- "node: {cscan: {dir: 1}}}}");
- }
-
- TEST_F(QueryPlannerTest, SortSkipSoftLimit) {
- runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 2, 3);
- assertNumSolutions(1U);
- assertSolutionExists("{skip: {n: 2, node: "
- "{sort: {pattern: {a: 1}, limit: 5, "
- "node: {cscan: {dir: 1}}}}}}");
- }
-
- //
- // Sort elimination
- //
-
- TEST_F(QueryPlannerTest, BasicSortElim) {
- addIndex(BSON("x" << 1));
- // query, sort, proj
- runQuerySortProj(fromjson("{ x : {$gt: 1}}"), fromjson("{x: 1}"), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {x: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {x: {$gt: 1}}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {filter: null, pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, SortElimCompound) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProj(fromjson("{ a : 5 }"), BSON("b" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1, b: 1}}}}}");
- }
-
- // SERVER-13611: test that sort elimination still works if there are
- // trailing fields in the index.
- TEST_F(QueryPlannerTest, SortElimTrailingFields) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- runQuerySortProj(fromjson("{a: 5}"), BSON("b" << 1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1, b: 1, c: 1}}}}}");
- }
-
- // Sort elimination with trailing fields where the sort direction is descending.
- TEST_F(QueryPlannerTest, SortElimTrailingFieldsReverse) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
- runQuerySortProj(fromjson("{a: 5, b: 6}"), BSON("c" << -1), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {c: -1}, limit: 0, "
- "node: {cscan: {dir: 1, filter: {a: 5, b: 6}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, dir: -1, pattern: {a: 1, b: 1, c: 1, d: 1}}}}}");
- }
-
- //
- // Basic compound
- //
-
- TEST_F(QueryPlannerTest, BasicCompound) {
- addIndex(BSON("x" << 1 << "y" << 1));
- runQuery(fromjson("{ x : 5, y: 10}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1, y: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundMissingField) {
- addIndex(BSON("x" << 1 << "y" << 1 << "z" << 1));
- runQuery(fromjson("{ x : 5, z: 10}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {x: 1, y: 1, z: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundFieldsOrder) {
- addIndex(BSON("x" << 1 << "y" << 1 << "z" << 1));
- runQuery(fromjson("{ x : 5, z: 10, y:1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1, y: 1, z: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CantUseCompound) {
- addIndex(BSON("x" << 1 << "y" << 1));
- runQuery(fromjson("{ y: 10}"));
-
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{cscan: {dir: 1, filter: {y: 10}}}");
- }
-
- //
- // $in
- //
-
- TEST_F(QueryPlannerTest, InBasic) {
- addIndex(fromjson("{a: 1}"));
- runQuery(fromjson("{a: {$in: [1, 2]}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: {$in: [1, 2]}}}}");
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {pattern: {a: 1}}}}}");
- }
-
- // Logically equivalent to the preceding $in query.
- // Indexed solution should be the same.
- TEST_F(QueryPlannerTest, InBasicOrEquivalent) {
- addIndex(fromjson("{a: 1}"));
- runQuery(fromjson("{$or: [{a: 1}, {a: 2}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a: 1}, {a: 2}]}}}");
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, InSparseIndex) {
- addIndex(fromjson("{a: 1}"),
- false, // multikey
- true); // sparse
- runQuery(fromjson("{a: {$in: [null]}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: {$in: [null]}}}}");
- }
-
- TEST_F(QueryPlannerTest, InCompoundIndexFirst) {
- addIndex(fromjson("{a: 1, b: 1}"));
- runQuery(fromjson("{a: {$in: [1, 2]}, b: 3}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {b: 3, a: {$in: [1, 2]}}}}");
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
- }
-
- // Logically equivalent to the preceding $in query.
- // Indexed solution should be the same.
- // Currently fails - pre-requisite to SERVER-12024
- /*
- TEST_F(QueryPlannerTest, InCompoundIndexFirstOrEquivalent) {
- addIndex(fromjson("{a: 1, b: 1}"));
- runQuery(fromjson("{$and: [{$or: [{a: 1}, {a: 2}]}, {b: 3}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$and: [{$or: [{a: 1}, {a: 2}]}, {b: 3}]}}}");
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
- }
- */
-
- TEST_F(QueryPlannerTest, InCompoundIndexLast) {
- addIndex(fromjson("{a: 1, b: 1}"));
- runQuery(fromjson("{a: 3, b: {$in: [1, 2]}}"));
-
- assertNumSolutions(2U);
- // TODO: update filter in cscan solution when SERVER-12024 is implemented
- assertSolutionExists("{cscan: {dir: 1, filter: {a: 3, b: {$in: [1, 2]}}}}");
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
- }
-
- // Logically equivalent to the preceding $in query.
- // Indexed solution should be the same.
- // Currently fails - pre-requisite to SERVER-12024
- /*
- TEST_F(QueryPlannerTest, InCompoundIndexLastOrEquivalent) {
- addIndex(fromjson("{a: 1, b: 1}"));
- runQuery(fromjson("{$and: [{a: 3}, {$or: [{b: 1}, {b: 2}]}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$and: [{a: 3}, {$or: [{b: 1}, {b: 2}]}]}}}");
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
- }
- */
-
- // SERVER-1205
- TEST_F(QueryPlannerTest, InWithSort) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}}"),
- BSON("b" << 1), BSONObj(), 0, 1);
-
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 1, "
- "node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a: 1, b: 1}}}, {ixscan: {pattern: {a: 1, b: 1}}}]}}}}");
- }
-
- // SERVER-1205
- TEST_F(QueryPlannerTest, InWithoutSort) {
- addIndex(BSON("a" << 1 << "b" << 1));
- // No sort means we don't bother to blow up the bounds.
- runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}}"), BSONObj(), BSONObj(), 0, 1);
-
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
- }
-
- // SERVER-1205
- TEST_F(QueryPlannerTest, ManyInWithSort) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
- runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}, b:{$in:[1,2]}, c:{$in:[1,2]}}"),
- BSON("d" << 1), BSONObj(), 0, 1);
-
- assertSolutionExists("{sort: {pattern: {d: 1}, limit: 1, "
- "node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
- "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
- "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
- "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
- "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
- "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}}]}}}}");
- }
-
- // SERVER-1205
- TEST_F(QueryPlannerTest, TooManyToExplode) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
- runQuerySortProjSkipLimit(fromjson("{a: {$in: [1,2,3,4,5,6]},"
- "b:{$in:[1,2,3,4,5,6,7,8]},"
- "c:{$in:[1,2,3,4,5,6,7,8]}}"),
- BSON("d" << 1), BSONObj(), 0, 1);
-
- // We cap the # of ixscans we're willing to create.
- assertNumSolutions(2);
- assertSolutionExists("{sort: {pattern: {d: 1}, limit: 1, "
- "node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{sort: {pattern: {d: 1}, limit: 1, node: "
- "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CantExplodeMetaSort) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << "text"));
- runQuerySortProj(fromjson("{a: {$in: [1, 2]}, b: {$in: [3, 4]}}"),
- fromjson("{c: {$meta: 'textScore'}}"),
- fromjson("{c: {$meta: 'textScore'}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {c:{$meta:'textScore'}}, node: "
- "{sort: {pattern: {c:{$meta:'textScore'}}, limit: 0, node: "
- "{cscan: {filter: {a:{$in:[1,2]},b:{$in:[3,4]}}, dir: 1}}}}}}");
- }
-
- // SERVER-13618: test that exploding scans for sort works even
- // if we must reverse the scan direction.
- TEST_F(QueryPlannerTest, ExplodeMustReverseScans) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
- runQuerySortProj(fromjson("{a: {$in: [1, 2]}, b: {$in: [3, 4]}}"),
- BSON("c" << -1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {c: -1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a:1, b:1, c:1, d:1}}},"
- "{ixscan: {pattern: {a:1, b:1, c:1, d:1}}},"
- "{ixscan: {pattern: {a:1, b:1, c:1, d:1}}},"
- "{ixscan: {pattern: {a:1, b:1, c:1, d:1}}}]}}}}");
- }
-
- // SERVER-13618
- TEST_F(QueryPlannerTest, ExplodeMustReverseScans2) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << -1));
- runQuerySortProj(fromjson("{a: {$in: [1, 2]}, b: {$in: [3, 4]}}"),
- BSON("c" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a:1, b:1, c:-1}}},"
- "{ixscan: {pattern: {a:1, b:1, c:-1}}},"
- "{ixscan: {pattern: {a:1, b:1, c:-1}}},"
- "{ixscan: {pattern: {a:1, b:1, c:-1}}}]}}}}");
- }
-
- // SERVER-13752: don't try to explode if the ordered interval list for
- // the leading field of the compound index is empty.
- TEST_F(QueryPlannerTest, CantExplodeWithEmptyBounds) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProj(fromjson("{a: {$in: []}}"), BSON("b" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: "
- "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}}}}}}}");
- }
-
- // SERVER-13752
- TEST_F(QueryPlannerTest, CantExplodeWithEmptyBounds2) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- runQuerySortProj(fromjson("{a: {$gt: 3, $lt: 0}}"), BSON("b" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: "
- "{fetch: {node: {ixscan: {pattern: {a:1,b:1,c:1}}}}}}}");
- }
-
- // SERVER-13754: exploding an $or
- TEST_F(QueryPlannerTest, ExplodeOrForSort) {
- addIndex(BSON("a" << 1 << "c" << 1));
- addIndex(BSON("b" << 1 << "c" << 1));
-
- runQuerySortProj(fromjson("{$or: [{a: 1}, {a: 2}, {b: 2}]}"),
- BSON("c" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {bounds: {a: [[1,1,true,true]], "
- "c: [['MinKey','MaxKey',true,true]]},"
- "pattern: {a:1, c:1}}},"
- "{ixscan: {bounds: {a: [[2,2,true,true]], "
- "c: [['MinKey','MaxKey',true,true]]},"
- "pattern: {a:1, c:1}}},"
- "{ixscan: {bounds: {b: [[2,2,true,true]], "
- "c: [['MinKey','MaxKey',true,true]]},"
- "pattern: {b:1, c:1}}}]}}}}");
- }
-
- // SERVER-13754: exploding an $or
- TEST_F(QueryPlannerTest, ExplodeOrForSort2) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- addIndex(BSON("d" << 1 << "c" << 1));
-
- runQuerySortProj(fromjson("{$or: [{a: 1, b: {$in: [1, 2]}}, {d: 3}]}"),
- BSON("c" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {bounds: {a: [[1,1,true,true]], b: [[1,1,true,true]],"
- "c: [['MinKey','MaxKey',true,true]]},"
- "pattern: {a:1, b:1, c:1}}},"
- "{ixscan: {bounds: {a: [[1,1,true,true]], b: [[2,2,true,true]],"
- "c: [['MinKey','MaxKey',true,true]]},"
- "pattern: {a:1, b:1, c:1}}},"
- "{ixscan: {bounds: {d: [[3,3,true,true]], "
- "c: [['MinKey','MaxKey',true,true]]},"
- "pattern: {d:1, c:1}}}]}}}}");
- }
-
- // SERVER-13754: an $or that can't be exploded, because one clause of the
- // $or does provide the sort, even after explosion.
- TEST_F(QueryPlannerTest, CantExplodeOrForSort) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- addIndex(BSON("d" << 1 << "c" << 1));
-
- runQuerySortProj(fromjson("{$or: [{a: {$in: [1, 2]}}, {d: 3}]}"),
- BSON("c" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: "
- "{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {pattern: {a: 1, b: 1, c: 1}}},"
- "{ixscan: {pattern: {d: 1, c: 1}}}]}}}}}}");
- }
-
- // SERVER-15286: Make sure that at least the explodeForSort() path bails out
- // when it finds that there are no union of point interval fields to explode.
- // We could convert this into a MERGE_SORT plan, but we don't yet do this
- // optimization.
- TEST_F(QueryPlannerTest, CantExplodeOrForSort2) {
- addIndex(BSON("a" << 1));
-
- runQuerySortProj(fromjson("{$or: [{a: {$gt: 1, $lt: 3}}, {a: {$gt: 6, $lt: 10}}]}"),
- BSON("a" << -1),
- BSONObj());
-
- assertNumSolutions(3U);
- assertSolutionExists("{sort: {pattern: {a: -1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}}}}}");
- assertSolutionExists("{sort: {pattern: {a: -1}, limit: 0, node: "
- "{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {pattern: {a: 1}, bounds: "
- "{a: [[1,3,false,false]]}}},"
- "{ixscan: {pattern: {a: 1}, bounds: "
- "{a: [[6,10,false,false]]}}}]}}}}}}");
- }
-
- // SERVER-13754: too many scans in an $or explosion.
- TEST_F(QueryPlannerTest, TooManyToExplodeOr) {
- addIndex(BSON("a" << 1 << "e" << 1));
- addIndex(BSON("b" << 1 << "e" << 1));
- addIndex(BSON("c" << 1 << "e" << 1));
- addIndex(BSON("d" << 1 << "e" << 1));
- runQuerySortProj(fromjson("{$or: [{a: {$in: [1,2,3,4,5,6]},"
- "b: {$in: [1,2,3,4,5,6]}},"
- "{c: {$in: [1,2,3,4,5,6]},"
- "d: {$in: [1,2,3,4,5,6]}}]}"),
- BSON("e" << 1), BSONObj());
-
- // We cap the # of ixscans we're willing to create, so we don't get explosion. Instead
- // we get 5 different solutions which all use a blocking sort.
- assertNumSolutions(5U);
- assertSolutionExists("{sort: {pattern: {e: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{sort: {pattern: {e: 1}, limit: 0, node: "
- "{or: {nodes: ["
- "{fetch: {node: {ixscan: {pattern: {a: 1, e: 1}}}}},"
- "{fetch: {node: {ixscan: {pattern: {c: 1, e: 1}}}}}]}}}}");
- assertSolutionExists("{sort: {pattern: {e: 1}, limit: 0, node: "
- "{or: {nodes: ["
- "{fetch: {node: {ixscan: {pattern: {b: 1, e: 1}}}}},"
- "{fetch: {node: {ixscan: {pattern: {c: 1, e: 1}}}}}]}}}}");
- assertSolutionExists("{sort: {pattern: {e: 1}, limit: 0, node: "
- "{or: {nodes: ["
- "{fetch: {node: {ixscan: {pattern: {a: 1, e: 1}}}}},"
- "{fetch: {node: {ixscan: {pattern: {d: 1, e: 1}}}}}]}}}}");
- assertSolutionExists("{sort: {pattern: {e: 1}, limit: 0, node: "
- "{or: {nodes: ["
- "{fetch: {node: {ixscan: {pattern: {b: 1, e: 1}}}}},"
- "{fetch: {node: {ixscan: {pattern: {d: 1, e: 1}}}}}]}}}}");
- }
-
- // SERVER-15696: Make sure explodeForSort copies filters on IXSCAN stages to all of the
- // scans resulting from the explode. Regex is the easiest way to have the planner create
- // an index scan which filters using the index key.
- TEST_F(QueryPlannerTest, ExplodeIxscanWithFilter) {
- addIndex(BSON("a" << 1 << "b" << 1));
-
- runQuerySortProj(fromjson("{$and: [{b: {$regex: 'foo', $options: 'i'}},"
- "{a: {$in: [1, 2]}}]}"),
- BSON("b" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a:1, b:1},"
- "filter: {b: {$regex: 'foo', $options: 'i'}}}},"
- "{ixscan: {pattern: {a:1, b:1},"
- "filter: {b: {$regex: 'foo', $options: 'i'}}}}]}}}}");
-
- }
-
- TEST_F(QueryPlannerTest, InWithSortAndLimitTrailingField) {
- addIndex(BSON("a" << 1 << "b" << -1 << "c" << 1));
- runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}, b: {$gte: 0}}"),
- fromjson("{b: -1}"),
- BSONObj(), // no projection
- 0, // no skip
- -1); // .limit(1)
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {b:-1}, limit: 1, "
- "node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{limit: {n: 1, node: {fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a:1,b:-1,c:1}}}, "
- " {ixscan: {pattern: {a:1,b:-1,c:1}}}]}}}}}}");
- }
-
- //
- // Multiple solutions
- //
-
- TEST_F(QueryPlannerTest, TwoPlans) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("a" << 1 << "b" << 1));
-
- runQuery(fromjson("{a:1, b:{$gt:2,$lt:2}}"));
-
- // 2 indexed solns and one non-indexed
- ASSERT_EQUALS(getNumSolutions(), 3U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$and:[{b:{$lt:2}},{a:1},{b:{$gt:2}}]}}}");
- assertSolutionExists("{fetch: {filter: {$and:[{b:{$lt:2}},{b:{$gt:2}}]}, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1, b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, TwoPlansElemMatch) {
- addIndex(BSON("a" << 1 << "b" << 1));
- addIndex(BSON("arr.x" << 1 << "a" << 1));
-
- runQuery(fromjson("{arr: { $elemMatch : { x : 5 , y : 5 } },"
- " a : 55 , b : { $in : [ 1 , 5 , 8 ] } }"));
-
- // 2 indexed solns and one non-indexed
- ASSERT_EQUALS(getNumSolutions(), 3U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, bounds: "
- "{a: [[55,55,true,true]], b: [[1,1,true,true], "
- "[5,5,true,true], [8,8,true,true]]}}}}}");
- assertSolutionExists("{fetch: {filter: {$and: [{arr:{$elemMatch:{x:5,y:5}}},"
- "{b:{$in:[1,5,8]}}]}, "
- "node: {ixscan: {pattern: {'arr.x':1,a:1}, bounds: "
- "{'arr.x': [[5,5,true,true]], 'a':[[55,55,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundAndNonCompoundIndices) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("a" << 1 << "b" << 1), true);
- runQuery(fromjson("{a: 1, b: {$gt: 2, $lt: 2}}"));
-
- ASSERT_EQUALS(getNumSolutions(), 3U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {$and:[{b:{$lt:2}},{b:{$gt:2}}]}, node: "
- "{ixscan: {pattern: {a:1}, bounds: {a: [[1,1,true,true]]}}}}}");
- assertSolutionExists("{fetch: {filter: {b:{$gt:2}}, node: "
- "{ixscan: {pattern: {a:1,b:1}, bounds: "
- "{a: [[1,1,true,true]], b: [[-Infinity,2,true,false]]}}}}}");
- }
-
- //
- // Sort orders
- //
-
- // SERVER-1205.
- TEST_F(QueryPlannerTest, MergeSort) {
- addIndex(BSON("a" << 1 << "c" << 1));
- addIndex(BSON("b" << 1 << "c" << 1));
- runQuerySortProj(fromjson("{$or: [{a:1}, {b:1}]}"), fromjson("{c:1}"), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a: 1, c: 1}}}, {ixscan: {pattern: {b: 1, c: 1}}}]}}}}");
- }
-
- // SERVER-1205 as well.
- TEST_F(QueryPlannerTest, NoMergeSortIfNoSortWanted) {
- addIndex(BSON("a" << 1 << "c" << 1));
- addIndex(BSON("b" << 1 << "c" << 1));
- runQuerySortProj(fromjson("{$or: [{a:1}, {b:1}]}"), BSONObj(), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a:1}, {b:1}]}}}");
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {filter: null, pattern: {a: 1, c: 1}}}, "
- "{ixscan: {filter: null, pattern: {b: 1, c: 1}}}]}}}}");
- }
-
- // Basic "keep sort in mind with an OR"
- TEST_F(QueryPlannerTest, MergeSortEvenIfSameIndex) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProj(fromjson("{$or: [{a:1}, {a:7}]}"), fromjson("{b:1}"), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- // TODO the second solution should be mergeSort rather than just sort
- }
-
- TEST_F(QueryPlannerTest, ReverseScanForSort) {
- addIndex(BSON("_id" << 1));
- runQuerySortProj(BSONObj(), fromjson("{_id: -1}"), BSONObj());
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{sort: {pattern: {_id: -1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {_id: 1}}}}}");
- }
-
- //
- // Hint tests
- //
-
- TEST_F(QueryPlannerTest, NaturalHint) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuerySortHint(BSON("a" << 1), BSON("b" << 1), BSON("$natural" << 1));
-
- assertNumSolutions(1U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: "
- "{cscan: {filter: {a: 1}, dir: 1}}}}");
- }
-
- // Test $natural sort and its interaction with $natural hint.
- TEST_F(QueryPlannerTest, NaturalSortAndHint) {
- addIndex(BSON("x" << 1));
-
- // Non-empty query, -1 sort, no hint.
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << -1), BSONObj());
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: -1}}");
-
- // Non-empty query, 1 sort, no hint.
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << 1), BSONObj());
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- // Non-empty query, -1 sort, -1 hint.
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << -1),
- BSON("$natural" << -1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: -1}}");
-
- // Non-empty query, 1 sort, -1 hint.
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << 1),
- BSON("$natural" << -1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- // Non-empty query, -1 sort, 1 hint.
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << -1),
- BSON("$natural" << 1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: -1}}");
-
- // Non-empty query, 1 sort, 1 hint.
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << 1),
- BSON("$natural" << 1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- // Empty query, -1 sort, no hint.
- runQuerySortHint(BSONObj(), BSON("$natural" << -1), BSONObj());
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: -1}}");
-
- // Empty query, 1 sort, no hint.
- runQuerySortHint(BSONObj(), BSON("$natural" << 1), BSONObj());
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- // Empty query, -1 sort, -1 hint.
- runQuerySortHint(BSONObj(), BSON("$natural" << -1), BSON("$natural" << -1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: -1}}");
-
- // Empty query, 1 sort, -1 hint.
- runQuerySortHint(BSONObj(), BSON("$natural" << 1), BSON("$natural" << -1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
-
- // Empty query, -1 sort, 1 hint.
- runQuerySortHint(BSONObj(), BSON("$natural" << -1), BSON("$natural" << 1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: -1}}");
-
- // Empty query, 1 sort, 1 hint.
- runQuerySortHint(BSONObj(), BSON("$natural" << 1), BSON("$natural" << 1));
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- TEST_F(QueryPlannerTest, HintOverridesNaturalSort) {
- addIndex(BSON("x" << 1));
- runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << -1), BSON("x" << 1));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: {x:{$exists:true}}, node: "
- "{ixscan: {filter: null, pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, HintValid) {
- addIndex(BSON("a" << 1));
- runQueryHint(BSONObj(), fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, HintValidWithPredicate) {
- addIndex(BSON("a" << 1));
- runQueryHint(fromjson("{a: {$gt: 1}}"), fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, HintValidWithSort) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuerySortHint(fromjson("{a: 100, b: 200}"), fromjson("{b: 1}"), fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: "
- "{fetch: {filter: {b: 200}, "
- "node: {ixscan: {filter: null, pattern: {a: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, HintElemMatch) {
- // true means multikey
- addIndex(fromjson("{'a.b': 1}"), true);
- runQueryHint(fromjson("{'a.b': 1, a: {$elemMatch: {b: 2}}}"), fromjson("{'a.b': 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: {$and: [{a:{$elemMatch:{b:2}}}, {'a.b': 1}]}, "
- "node: {ixscan: {filter: null, pattern: {'a.b': 1}, bounds: "
- "{'a.b': [[2, 2, true, true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, HintInvalid) {
- addIndex(BSON("a" << 1));
- runInvalidQueryHint(BSONObj(), fromjson("{b: 1}"));
- }
-
- //
- // Sparse indices, SERVER-8067
- // Each index in this block of tests is sparse.
- //
-
- TEST_F(QueryPlannerTest, SparseIndexIgnoreForSort) {
- addIndex(fromjson("{a: 1}"), false, true);
- runQuerySortProj(BSONObj(), fromjson("{a: 1}"), BSONObj());
-
- assertNumSolutions(1U);
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- }
-
- TEST_F(QueryPlannerTest, SparseIndexHintForSort) {
- addIndex(fromjson("{a: 1}"), false, true);
- runQuerySortHint(BSONObj(), fromjson("{a: 1}"), fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, SparseIndexPreferCompoundIndexForSort) {
- addIndex(fromjson("{a: 1}"), false, true);
- addIndex(fromjson("{a: 1, b: 1}"));
- runQuerySortProj(BSONObj(), fromjson("{a: 1}"), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1, b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, SparseIndexForQuery) {
- addIndex(fromjson("{a: 1}"), false, true);
- runQuerySortProj(fromjson("{a: 1}"), BSONObj(), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: 1}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1}}}}}");
- }
-
- //
- // Regex
- //
-
- TEST_F(QueryPlannerTest, PrefixRegex) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{a: /^foo/}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: /^foo/}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, PrefixRegexCovering) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{a: /^foo/}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{cscan: {dir: 1, filter: {a: /^foo/}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegex) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{a: /foo/}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {a: /foo/}}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: {a: /foo/}, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegexCovering) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{a: /foo/}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{cscan: {dir: 1, filter: {a: /foo/}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: {a: /foo/}, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegexAnd) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{a: /foo/, b: 2}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {dir: 1, filter: {$and: [{b: 2}, {a: /foo/}]}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: {a: /foo/}, pattern: {a: 1, b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegexAndCovering) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProj(fromjson("{a: /foo/, b: 2}"), BSONObj(),
- fromjson("{_id: 0, a: 1, b: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
- "{cscan: {dir: 1, filter: {$and: [{b: 2}, {a: /foo/}]}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
- "{ixscan: {filter: {a: /foo/}, pattern: {a: 1, b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegexOrCovering) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$or: [{a: /0/}, {a: /1/}]}"), BSONObj(),
- fromjson("{_id: 0, a: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{cscan: {dir: 1, filter: {$or: [{a: /0/}, {a: /1/}]}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: {$or: [{a: /0/}, {a: /1/}]}, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegexInCovering) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{a: {$in: [/foo/, /bar/]}}"), BSONObj(),
- fromjson("{_id: 0, a: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{cscan: {dir: 1, filter: {a:{$in:[/foo/,/bar/]}}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: {a:{$in:[/foo/,/bar/]}}, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, TwoRegexCompoundIndexCovering) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProj(fromjson("{a: /0/, b: /1/}"), BSONObj(),
- fromjson("{_id: 0, a: 1, b: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
- "{cscan: {dir: 1, filter: {$and:[{a:/0/},{b:/1/}]}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
- "{ixscan: {filter: {$and:[{a:/0/},{b:/1/}]}, pattern: {a: 1, b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, TwoRegexSameFieldCovering) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$and: [{a: /0/}, {a: /1/}]}"), BSONObj(),
- fromjson("{_id: 0, a: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{cscan: {dir: 1, filter: {$and:[{a:/0/},{a:/1/}]}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: {$and:[{a:/0/},{a:/1/}]}, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ThreeRegexSameFieldCovering) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$and: [{a: /0/}, {a: /1/}, {a: /2/}]}"), BSONObj(),
- fromjson("{_id: 0, a: 1}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{cscan: {dir: 1, filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NonPrefixRegexMultikey) {
- // true means multikey
- addIndex(BSON("a" << 1), true);
- runQuery(fromjson("{a: /foo/}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {filter: {a: /foo/}, dir: 1}}");
- assertSolutionExists("{fetch: {filter: {a: /foo/}, node: {ixscan: "
- "{pattern: {a: 1}, filter: null}}}}");
- }
-
- TEST_F(QueryPlannerTest, ThreeRegexSameFieldMultikey) {
- // true means multikey
- addIndex(BSON("a" << 1), true);
- runQuery(fromjson("{$and: [{a: /0/}, {a: /1/}, {a: /2/}]}"));
-
- ASSERT_EQUALS(getNumSolutions(), 2U);
- assertSolutionExists("{cscan: {filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}, dir: 1}}");
- assertSolutionExists("{fetch: {filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}, node: {ixscan: "
- "{pattern: {a: 1}, filter: null}}}}");
- }
-
- //
- // Negation
- //
-
- TEST_F(QueryPlannerTest, NegationIndexForSort) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{a: {$ne: 1}}"), fromjson("{a: 1}"), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, "
- "bounds: {a: [['MinKey',1,true,false], "
- "[1,'MaxKey',false,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegationTopLevel) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{a: {$ne: 1}}"), BSONObj(), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [['MinKey',1,true,false], "
- "[1,'MaxKey',false,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegationOr) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$or: [{a: 1}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- TEST_F(QueryPlannerTest, NegationOrNotIn) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$or: [{a: 1}, {b: {$nin: [1]}}]}"), BSONObj(), BSONObj());
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- TEST_F(QueryPlannerTest, NegationAndIndexOnEquality) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$and: [{a: 1}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1},"
- "bounds: {a: [[1,1,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegationAndIndexOnEqualityAndNegationBranches) {
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuerySortProj(fromjson("{$and: [{a: 1}, {b: 2}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
-
- assertNumSolutions(3U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, "
- "bounds: {a: [[1,1,true,true]]}}}}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {b: 1}, "
- "bounds: {b: [[2,2,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegationAndIndexOnInequality) {
- addIndex(BSON("b" << 1));
- runQuerySortProj(fromjson("{$and: [{a: 1}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {a:1}, node: {ixscan: {pattern: {b:1}, "
- "bounds: {b: [['MinKey',1,true,false], "
- "[1,'MaxKey',false,true]]}}}}}");
- }
-
- // Negated regexes don't use the index.
- TEST_F(QueryPlannerTest, NegationRegexPrefix) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: /^a/}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // Negated mods don't use the index
- TEST_F(QueryPlannerTest, NegationMod) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$mod: [2, 1]}}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // Negated $type doesn't use the index
- TEST_F(QueryPlannerTest, NegationTypeOperator) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$type: 16}}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // Negated $elemMatch value won't use the index
- TEST_F(QueryPlannerTest, NegationElemMatchValue) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$elemMatch: {$gt: 3, $lt: 10}}}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // Negated $elemMatch object won't use the index
- TEST_F(QueryPlannerTest, NegationElemMatchObject) {
- addIndex(BSON("i.j" << 1));
- runQuery(fromjson("{i: {$not: {$elemMatch: {j: 1}}}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // Negated $elemMatch object won't use the index
- TEST_F(QueryPlannerTest, NegationElemMatchObject2) {
- addIndex(BSON("i.j" << 1));
- runQuery(fromjson("{i: {$not: {$elemMatch: {j: {$ne: 1}}}}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // If there is a negation that can't use the index,
- // ANDed with a predicate that can use the index, then
- // we can still use the index for the latter predicate.
- TEST_F(QueryPlannerTest, NegationRegexWithIndexablePred) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{$and: [{i: {$not: /o/}}, {i: 2}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {i:1}, "
- "bounds: {i: [[2,2,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegationCantUseSparseIndex) {
- // false means not multikey, true means sparse
- addIndex(BSON("i" << 1), false, true);
- runQuery(fromjson("{i: {$ne: 4}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- TEST_F(QueryPlannerTest, NegationCantUseSparseIndex2) {
- // false means not multikey, true means sparse
- addIndex(BSON("i" << 1 << "j" << 1), false, true);
- runQuery(fromjson("{i: 4, j: {$ne: 5}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {i:1,j:1}, bounds: "
- "{i: [[4,4,true,true]], j: [['MinKey','MaxKey',true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegatedRangeStrGT) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$gt: 'a'}}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
- "bounds: {i: [['MinKey','a',true,true], "
- "[{},'MaxKey',true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegatedRangeStrGTE) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$gte: 'a'}}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
- "bounds: {i: [['MinKey','a',true,false], "
- "[{},'MaxKey',true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegatedRangeIntGT) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$gt: 5}}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
- "bounds: {i: [['MinKey',5,true,true], "
- "[Infinity,'MaxKey',false,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegatedRangeIntGTE) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{i: {$not: {$gte: 5}}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
- "bounds: {i: [['MinKey',5,true,false], "
- "[Infinity,'MaxKey',false,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, TwoNegatedRanges) {
- addIndex(BSON("i" << 1));
- runQuery(fromjson("{$and: [{i: {$not: {$lte: 'b'}}}, "
- "{i: {$not: {$gte: 'f'}}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
- "bounds: {i: [['MinKey','',true,false], "
- "['b','f',false,false], "
- "[{},'MaxKey',true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, AndWithNestedNE) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{a: {$gt: -1, $lt: 1, $ne: 0}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [[-1,0,false,false], "
- "[0,1,false,false]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NegatePredOnCompoundIndex) {
- addIndex(BSON("x" << 1 << "a" << 1));
- runQuery(fromjson("{x: 1, a: {$ne: 1}, b: {$ne: 2}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {x:1,a:1}, bounds: "
- "{x: [[1,1,true,true]], "
- "a: [['MinKey',1,true,false], [1,'MaxKey',false,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NEOnMultikeyIndex) {
- // true means multikey
- addIndex(BSON("a" << 1), true);
- runQuery(fromjson("{a: {$ne: 3}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {a:{$ne:3}}, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [['MinKey',3,true,false],"
- "[3,'MaxKey',false,true]]}}}}}");
- }
-
- // In general, a negated $nin can make use of an index.
- TEST_F(QueryPlannerTest, NinUsesMultikeyIndex) {
- // true means multikey
- addIndex(BSON("a" << 1), true);
- runQuery(fromjson("{a: {$nin: [4, 10]}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {a:{$nin:[4,10]}}, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [['MinKey',4,true,false],"
- "[4,10,false,false],"
- "[10,'MaxKey',false,true]]}}}}}");
- }
-
- // But it can't if the $nin contains a regex because regex bounds can't
- // be complemented.
- TEST_F(QueryPlannerTest, NinCantUseMultikeyIndex) {
- // true means multikey
- addIndex(BSON("a" << 1), true);
- runQuery(fromjson("{a: {$nin: [4, /foobar/]}}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- //
- // Multikey indices
- //
-
- //
- // Index bounds related tests
- //
-
- TEST_F(QueryPlannerTest, CompoundIndexBoundsLastFieldMissing) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- runQuery(fromjson("{a: 5, b: {$gt: 7}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: "
- "{a: [[5,5,true,true]], b: [[7,Infinity,false,true]], "
- " c: [['MinKey','MaxKey',true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundIndexBoundsMiddleFieldMissing) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- runQuery(fromjson("{a: 1, c: {$lt: 3}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: "
- "{a: [[1,1,true,true]], b: [['MinKey','MaxKey',true,true]], "
- " c: [[-Infinity,3,true,false]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundIndexBoundsRangeAndEquality) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{a: {$gt: 8}, b: 6}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, bounds: "
- "{a: [[8,Infinity,false,true]], b:[[6,6,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundIndexBoundsEqualityThenIn) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{a: 5, b: {$in: [2,6,11]}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {filter: null, pattern: "
- "{a: 1, b: 1}, bounds: {a: [[5,5,true,true]], "
- "b:[[2,2,true,true],[6,6,true,true],[11,11,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundIndexBoundsStringBounds) {
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{a: {$gt: 'foo'}, b: {$gte: 'bar'}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {filter: null, pattern: "
- "{a: 1, b: 1}, bounds: {a: [['foo',{},false,false]], "
- "b:[['bar',{},true,false]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, IndexBoundsAndWithNestedOr) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$and: [{a: 1, $or: [{a: 2}, {a: 3}]}]}"));
-
- // Given that the index over 'a' isn't multikey, we ideally won't generate any solutions
- // since we know the query describes an empty set if 'a' isn't multikey. Any solutions
- // below are "this is how it currently works" instead of "this is how it should work."
-
- // It's kind of iffy to look for indexed solutions so we don't...
- size_t matches = 0;
- matches += numSolutionMatches("{cscan: {dir: 1, filter: "
- "{$or: [{a: 2, a:1}, {a: 3, a:1}]}}}");
- matches += numSolutionMatches("{cscan: {dir: 1, filter: "
- "{$and: [{$or: [{a: 2}, {a: 3}]}, {a: 1}]}}}");
- ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
- }
-
- TEST_F(QueryPlannerTest, IndexBoundsIndexedSort) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$or: [{a: 1}, {a: 2}]}"), BSON("a" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {a:1}, limit: 0, node: "
- "{cscan: {filter: {$or:[{a:1},{a:2}]}, dir: 1}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {filter: null, "
- "pattern: {a:1}, bounds: {a: [[1,1,true,true], [2,2,true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, IndexBoundsUnindexedSort) {
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{$or: [{a: 1}, {a: 2}]}"), BSON("b" << 1), BSONObj());
-
- assertNumSolutions(2U);
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: "
- "{cscan: {filter: {$or:[{a:1},{a:2}]}, dir: 1}}}}");
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {fetch: "
- "{filter: null, node: {ixscan: {filter: null, "
- "pattern: {a:1}, bounds: {a: [[1,1,true,true], [2,2,true,true]]}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, IndexBoundsUnindexedSortHint) {
- addIndex(BSON("a" << 1));
- runQuerySortHint(fromjson("{$or: [{a: 1}, {a: 2}]}"), BSON("b" << 1), BSON("a" << 1));
-
- assertNumSolutions(1U);
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {fetch: "
- "{filter: null, node: {ixscan: {filter: null, "
- "pattern: {a:1}, bounds: {a: [[1,1,true,true], [2,2,true,true]]}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CompoundIndexBoundsIntersectRanges) {
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- addIndex(BSON("a" << 1 << "c" << 1));
- runQuery(fromjson("{a: {$gt: 1, $lt: 10}, c: {$gt: 1, $lt: 10}}"));
-
- assertNumSolutions(3U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1,b:1,c:1}, "
- "bounds: {a: [[1,10,false,false]], "
- "b: [['MinKey','MaxKey',true,true]], "
- "c: [[1,10,false,false]]}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1,c:1}, "
- "bounds: {a: [[1,10,false,false]], "
- "c: [[1,10,false,false]]}}}}}");
- }
-
- // Test that planner properly unionizes the index bounds for two negation
- // predicates (SERVER-13890).
- TEST_F(QueryPlannerTest, IndexBoundsOrOfNegations) {
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a: {$ne: 3}}, {a: {$ne: 4}}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
- "bounds: {a: [['MinKey','MaxKey',true,true]]}}}}}");
- }
-
- TEST_F(QueryPlannerTest, BoundsTypeMinKeyMaxKey) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1));
-
- runQuery(fromjson("{a: {$type: -1}}"));
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, bounds:"
- "{a: [['MinKey','MinKey',true,true]]}}}}}");
-
- runQuery(fromjson("{a: {$type: 127}}"));
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}, bounds:"
- "{a: [['MaxKey','MaxKey',true,true]]}}}}}");
- }
-
- //
- // Tests related to building index bounds for multikey
- // indices, combined with compound and $elemMatch
- //
-
- // SERVER-12475: make sure that we compound bounds, even
- // for a multikey index.
- TEST_F(QueryPlannerTest, CompoundMultikeyBounds) {
- // true means multikey
- addIndex(BSON("a" << 1 << "b" << 1), true);
- runQuery(fromjson("{a: 1, b: 3}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {filter: {$and:[{a:1},{b:3}]}, dir: 1}}");
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: {filter: null, "
- "pattern: {a:1,b:1}, bounds: "
- "{a: [[1,1,true,true]], b: [[3,3,true,true]]}}}}}");
- }
-
- // Make sure that we compound bounds but do not intersect bounds
- // for a compound multikey index.
- TEST_F(QueryPlannerTest, CompoundMultikeyBoundsNoIntersect) {
- // true means multikey
- addIndex(BSON("a" << 1 << "b" << 1), true);
- runQuery(fromjson("{a: 1, b: {$gt: 3, $lte: 5}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{cscan: {dir: 1}}");
- assertSolutionExists("{fetch: {filter: {b:{$gt:3}}, node: {ixscan: {filter: null, "
- "pattern: {a:1,b:1}, bounds: "
- "{a: [[1,1,true,true]], b: [[-Infinity,5,true,true]]}}}}}");
- }
-
- //
- // QueryPlannerParams option tests
- //
-
- TEST_F(QueryPlannerTest, NoBlockingSortsAllowedTest) {
- params.options = QueryPlannerParams::NO_BLOCKING_SORT;
- runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
- assertNumSolutions(0U);
-
- addIndex(BSON("x" << 1));
-
- runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NoTableScanBasic) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- runQuery(BSONObj());
- assertNumSolutions(0U);
-
- addIndex(BSON("x" << 1));
-
- runQuery(BSONObj());
- assertNumSolutions(0U);
-
- runQuery(fromjson("{x: {$gte: 0}}"));
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: null, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, NoTableScanOrWithAndChild) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{$or: [{a: 20}, {$and: [{a:1}, {b:7}]}]}"));
-
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{fetch: {filter: null, node: {or: {nodes: ["
- "{ixscan: {filter: null, pattern: {a: 1}}}, "
- "{fetch: {filter: {b: 7}, node: {ixscan: "
- "{filter: null, pattern: {a: 1}}}}}]}}}}");
- }
-
- //
- // Index Intersection.
- //
- // We don't exhaustively check all plans here. Instead we check that there exists an
- // intersection plan. The blending of >1 index plans and ==1 index plans is under development
- // but we want to make sure that we create an >1 index plan when we should.
- //
-
- TEST_F(QueryPlannerTest, IntersectBasicTwoPred) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuery(fromjson("{a:1, b:{$gt: 1}}"));
-
- assertSolutionExists("{fetch: {filter: null, node: {andHash: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, IntersectBasicTwoPredCompound) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1 << "c" << 1));
- addIndex(BSON("b" << 1));
- runQuery(fromjson("{a:1, b:1, c:1}"));
-
- // There's an andSorted not andHash because the two seeks are point intervals.
- assertSolutionExists("{fetch: {filter: null, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1, c:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
- }
-
- // SERVER-12196
- TEST_F(QueryPlannerTest, IntersectBasicTwoPredCompoundMatchesIdxOrder1) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuery(fromjson("{a:1, b:1}"));
-
- assertNumSolutions(3U);
-
- assertSolutionExists("{fetch: {filter: {b:1}, node: "
- "{ixscan: {filter: null, pattern: {a:1}}}}}");
- assertSolutionExists("{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {b:1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
- }
-
- // SERVER-12196
- TEST_F(QueryPlannerTest, IntersectBasicTwoPredCompoundMatchesIdxOrder2) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("b" << 1));
- addIndex(BSON("a" << 1));
- runQuery(fromjson("{a:1, b:1}"));
-
- assertNumSolutions(3U);
-
- assertSolutionExists("{fetch: {filter: {b:1}, node: "
- "{ixscan: {filter: null, pattern: {a:1}}}}}");
- assertSolutionExists("{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {b:1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, IntersectManySelfIntersections) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- // True means multikey.
- addIndex(BSON("a" << 1), true);
-
- // This one goes to 11.
- runQuery(fromjson("{a:1, a:2, a:3, a:4, a:5, a:6, a:7, a:8, a:9, a:10, a:11}"));
-
- // But this one only goes to 10.
- assertSolutionExists("{fetch: {filter: {a:11}, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}}," // 1
- "{ixscan: {filter: null, pattern: {a:1}}}," // 2
- "{ixscan: {filter: null, pattern: {a:1}}}," // 3
- "{ixscan: {filter: null, pattern: {a:1}}}," // 4
- "{ixscan: {filter: null, pattern: {a:1}}}," // 5
- "{ixscan: {filter: null, pattern: {a:1}}}," // 6
- "{ixscan: {filter: null, pattern: {a:1}}}," // 7
- "{ixscan: {filter: null, pattern: {a:1}}}," // 8
- "{ixscan: {filter: null, pattern: {a:1}}}," // 9
- "{ixscan: {filter: null, pattern: {a:1}}}]}}}}"); // 10
- }
-
- TEST_F(QueryPlannerTest, IntersectSubtreeNodes) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- addIndex(BSON("d" << 1));
-
- runQuery(fromjson("{$or: [{a: 1}, {b: 1}], $or: [{c:1}, {d:1}]}"));
- assertSolutionExists("{fetch: {filter: null, node: {andHash: {nodes: ["
- "{or: {nodes: [{ixscan:{filter:null, pattern:{a:1}}},"
- "{ixscan:{filter:null, pattern:{b:1}}}]}},"
- "{or: {nodes: [{ixscan:{filter:null, pattern:{c:1}}},"
- "{ixscan:{filter:null, pattern:{d:1}}}]}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, IntersectSubtreeAndPred) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- runQuery(fromjson("{a: 1, $or: [{b:1}, {c:1}]}"));
-
- // This (can be) rewritten to $or:[ {a:1, b:1}, {c:1, d:1}]. We don't look for the various
- // single $or solutions as that's tested elsewhere. We look for the intersect solution,
- // where each AND inside of the root OR is an and_sorted.
- size_t matches = 0;
- matches += numSolutionMatches("{fetch: {filter: null, node: {or: {nodes: ["
- "{andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {'a':1}}},"
- "{ixscan: {filter: null, pattern: {'b':1}}}]}},"
- "{andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {'a':1}}},"
- "{ixscan: {filter: null, pattern: {'c':1}}}]}}]}}}}");
- matches += numSolutionMatches("{fetch: {filter: null, node: {andHash: {nodes:["
- "{or: {nodes: [{ixscan:{filter:null, pattern:{b:1}}},"
- "{ixscan:{filter:null, pattern:{c:1}}}]}},"
- "{ixscan:{filter: null, pattern:{a:1}}}]}}}}");
- ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
- }
-
- TEST_F(QueryPlannerTest, IntersectElemMatch) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a.b" << 1));
- addIndex(BSON("a.c" << 1));
- runQuery(fromjson("{a : {$elemMatch: {b:1, c:1}}}"));
- assertSolutionExists("{fetch: {filter: {a:{$elemMatch:{b:1, c:1}}},"
- "node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {'a.b':1}}},"
- "{ixscan: {filter: null, pattern: {'a.c':1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, IntersectSortFromAndHash) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- runQuerySortProj(fromjson("{a: 1, b:{$gt: 1}}"), fromjson("{b:1}"), BSONObj());
-
- // This provides the sort.
- assertSolutionExists("{fetch: {filter: null, node: {andHash: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
-
- // Rearrange the preds, shouldn't matter.
- runQuerySortProj(fromjson("{b: 1, a:{$lt: 7}}"), fromjson("{b:1}"), BSONObj());
- assertSolutionExists("{fetch: {filter: null, node: {andHash: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, IntersectCanBeVeryBig) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- addIndex(BSON("d" << 1));
- runQuery(fromjson("{$or: [{ 'a' : null, 'b' : 94, 'c' : null, 'd' : null },"
- "{ 'a' : null, 'b' : 98, 'c' : null, 'd' : null },"
- "{ 'a' : null, 'b' : 1, 'c' : null, 'd' : null },"
- "{ 'a' : null, 'b' : 2, 'c' : null, 'd' : null },"
- "{ 'a' : null, 'b' : 7, 'c' : null, 'd' : null },"
- "{ 'a' : null, 'b' : 9, 'c' : null, 'd' : null },"
- "{ 'a' : null, 'b' : 16, 'c' : null, 'd' : null }]}"));
-
- assertNumSolutions(internalQueryEnumerationMaxOrSolutions);
- }
-
- // Ensure that disabling AND_HASH intersection works properly.
- TEST_F(QueryPlannerTest, IntersectDisableAndHash) {
- bool oldEnableHashIntersection = internalQueryPlannerEnableHashIntersection;
-
- // Turn index intersection on but disable hash-based intersection.
- internalQueryPlannerEnableHashIntersection = false;
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
-
- runQuery(fromjson("{a: {$gt: 1}, b: 1, c: 1}"));
-
- // We should do an AND_SORT intersection of {b: 1} and {c: 1}, but no AND_HASH plans.
- assertNumSolutions(4U);
- assertSolutionExists("{fetch: {filter: {b: 1, c: 1}, node: {ixscan: "
- "{pattern: {a: 1}, bounds: {a: [[1,Infinity,false,true]]}}}}}");
- assertSolutionExists("{fetch: {filter: {a:{$gt:1},c:1}, node: {ixscan: "
- "{pattern: {b: 1}, bounds: {b: [[1,1,true,true]]}}}}}");
- assertSolutionExists("{fetch: {filter: {a:{$gt:1},b:1}, node: {ixscan: "
- "{pattern: {c: 1}, bounds: {c: [[1,1,true,true]]}}}}}");
- assertSolutionExists("{fetch: {filter: {a:{$gt:1}}, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {b:1}}},"
- "{ixscan: {filter: null, pattern: {c:1}}}]}}}}");
-
- // Restore the old value of the has intersection switch.
- internalQueryPlannerEnableHashIntersection = oldEnableHashIntersection;
- }
-
- //
- // Index intersection cases for SERVER-12825: make sure that
- // we don't generate an ixisect plan if a compound index is
- // available instead.
- //
-
- // SERVER-12825
- TEST_F(QueryPlannerTest, IntersectCompoundInsteadBasic) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuery(fromjson("{a: 1, b: 1}"));
-
- assertNumSolutions(3U);
- assertSolutionExists("{fetch: {filter: {b:1}, node: "
- "{ixscan: {filter: null, pattern: {a:1}}}}}");
- assertSolutionExists("{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {b:1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a:1,b:1}}}}}");
- }
-
- // SERVER-12825
- TEST_F(QueryPlannerTest, IntersectCompoundInsteadThreeCompoundIndices) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1 << "b" << 1));
- addIndex(BSON("c" << 1 << "d" << 1));
- addIndex(BSON("a" << 1 << "c" << -1 << "b" << -1 << "d" << 1));
- runQuery(fromjson("{a: 1, b: 1, c: 1, d: 1}"));
-
- assertNumSolutions(3U);
- assertSolutionExists("{fetch: {filter: {$and: [{c:1},{d:1}]}, node: "
- "{ixscan: {filter: null, pattern: {a:1,b:1}}}}}");
- assertSolutionExists("{fetch: {filter: {$and:[{a:1},{b:1}]}, node: "
- "{ixscan: {filter: null, pattern: {c:1,d:1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a:1,c:-1,b:-1,d:1}}}}}");
- }
-
- // SERVER-12825
- TEST_F(QueryPlannerTest, IntersectCompoundInsteadUnusedField) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- runQuery(fromjson("{a: 1, b: 1}"));
-
- assertNumSolutions(3U);
- assertSolutionExists("{fetch: {filter: {b:1}, node: "
- "{ixscan: {filter: null, pattern: {a:1}}}}}");
- assertSolutionExists("{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {b:1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a:1,b:1,c:1}}}}}");
- }
-
- // SERVER-12825
- TEST_F(QueryPlannerTest, IntersectCompoundInsteadUnusedField2) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
- addIndex(BSON("a" << 1 << "b" << 1));
- addIndex(BSON("c" << 1 << "d" << 1));
- addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
- runQuery(fromjson("{a: 1, c: 1}"));
-
- assertNumSolutions(3U);
- assertSolutionExists("{fetch: {filter: {c:1}, node: "
- "{ixscan: {filter: null, pattern: {a:1,b:1}}}}}");
- assertSolutionExists("{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {c:1,d:1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: "
- "{ixscan: {filter: null, pattern: {a:1,b:1,c:1}}}}}");
- }
-
- //
- // Test that we add a KeepMutations when we should and and we don't add one when we shouldn't.
- //
-
- // Collection scan doesn't keep any state, so it can't produce flagged data.
- TEST_F(QueryPlannerTest, NoMutationsForCollscan) {
- params.options = QueryPlannerParams::KEEP_MUTATIONS;
- runQuery(fromjson(""));
- assertSolutionExists("{cscan: {dir: 1}}");
- }
-
- // Collscan + sort doesn't produce flagged data either.
- TEST_F(QueryPlannerTest, NoMutationsForSort) {
- params.options = QueryPlannerParams::KEEP_MUTATIONS;
- runQuerySortProj(fromjson(""), fromjson("{a:1}"), BSONObj());
- assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
- }
-
- // An index scan + fetch requires a keep node as it can flag data. Also make sure we put it in
- // the right place, under the sort.
- TEST_F(QueryPlannerTest, MutationsFromFetch) {
- params.options = QueryPlannerParams::KEEP_MUTATIONS;
- addIndex(BSON("a" << 1));
- runQuerySortProj(fromjson("{a: 5}"), fromjson("{b:1}"), BSONObj());
- assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {keep: {node: "
- "{fetch: {node: {ixscan: {pattern: {a:1}}}}}}}}}");
- }
-
- // Index scan w/covering doesn't require a keep node as there's no fetch.
- TEST_F(QueryPlannerTest, NoFetchNoKeep) {
- params.options = QueryPlannerParams::KEEP_MUTATIONS;
- addIndex(BSON("x" << 1));
- // query, sort, proj
- runQuerySortProj(fromjson("{ x : {$gt: 1}}"), BSONObj(), fromjson("{_id: 0, x: 1}"));
-
- // cscan is a soln but we override the params that say to include it.
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{proj: {spec: {_id: 0, x: 1}, node: {ixscan: "
- "{filter: null, pattern: {x: 1}}}}}");
- }
-
- // No keep with geoNear.
- TEST_F(QueryPlannerTest, NoKeepWithGeoNear) {
- params.options = QueryPlannerParams::KEEP_MUTATIONS;
- addIndex(BSON("a" << "2d"));
- runQuery(fromjson("{a: {$near: [0,0], $maxDistance:0.3 }}"));
- ASSERT_EQUALS(getNumSolutions(), 1U);
- assertSolutionExists("{geoNear2d: {a: '2d'}}");
- }
-
- // No keep when we have an indexed sort.
- TEST_F(QueryPlannerTest, NoKeepWithIndexedSort) {
- params.options = QueryPlannerParams::KEEP_MUTATIONS;
- addIndex(BSON("a" << 1 << "b" << 1));
- runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}}"),
- BSON("b" << 1), BSONObj(), 0, 1);
-
- // cscan solution exists but we didn't turn on the "always include a collscan."
- assertNumSolutions(1);
- assertSolutionExists("{fetch: {node: {mergeSort: {nodes: "
- "[{ixscan: {pattern: {a: 1, b: 1}}}, {ixscan: {pattern: {a: 1, b: 1}}}]}}}}");
- }
-
- // Make sure a top-level $or hits the limiting number
- // of solutions that we are willing to consider.
- TEST_F(QueryPlannerTest, OrEnumerationLimit) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- // 6 $or clauses, each with 2 indexed predicates
- // means 2^6 = 64 possibilities. We should hit the limit.
- runQuery(fromjson("{$or: [{a: 1, b: 1},"
- "{a: 2, b: 2},"
- "{a: 3, b: 3},"
- "{a: 4, b: 4},"
- "{a: 5, b: 5},"
- "{a: 6, b: 6}]}"));
-
- assertNumSolutions(internalQueryEnumerationMaxOrSolutions);
- }
-
- TEST_F(QueryPlannerTest, OrEnumerationLimit2) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- addIndex(BSON("d" << 1));
-
- // 3 $or clauses, and a few other preds. Each $or clause can
- // generate up to the max number of allowed $or enumerations.
- runQuery(fromjson("{$or: [{a: 1, b: 1, c: 1, d: 1},"
- "{a: 2, b: 2, c: 2, d: 2},"
- "{a: 3, b: 3, c: 3, d: 3}]}"));
-
- assertNumSolutions(internalQueryEnumerationMaxOrSolutions);
- }
-
- // SERVER-13104: test that we properly enumerate all solutions for nested $or.
- TEST_F(QueryPlannerTest, EnumerateNestedOr) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
-
- runQuery(fromjson("{d: 1, $or: [{a: 1, b: 1}, {c: 1}]}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{fetch: {filter: {d: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}},"
- "{ixscan: {pattern: {c: 1}}}]}}}}");
- assertSolutionExists("{fetch: {filter: {d: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {a: 1}, node: {ixscan: {pattern: {b: 1}}}}},"
- "{ixscan: {pattern: {c: 1}}}]}}}}");
- }
-
- // SERVER-13104: test that we properly enumerate all solutions for nested $or.
- TEST_F(QueryPlannerTest, EnumerateNestedOr2) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- addIndex(BSON("d" << 1));
- addIndex(BSON("e" << 1));
- addIndex(BSON("f" << 1));
-
- runQuery(fromjson("{a: 1, b: 1, $or: [{c: 1, d: 1}, {e: 1, f: 1}]}"));
-
- assertNumSolutions(6U);
-
- // Four possibilities from indexing the $or.
- assertSolutionExists("{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {d: 1}, node: {ixscan: {pattern: {c: 1}}}}},"
- "{fetch: {filter: {f: 1}, node: {ixscan: {pattern: {e: 1}}}}}"
- "]}}}}");
- assertSolutionExists("{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}},"
- "{fetch: {filter: {f: 1}, node: {ixscan: {pattern: {e: 1}}}}}"
- "]}}}}");
- assertSolutionExists("{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {d: 1}, node: {ixscan: {pattern: {c: 1}}}}},"
- "{fetch: {filter: {e: 1}, node: {ixscan: {pattern: {f: 1}}}}}"
- "]}}}}");
- assertSolutionExists("{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}},"
- "{fetch: {filter: {e: 1}, node: {ixscan: {pattern: {f: 1}}}}}"
- "]}}}}");
-
- // Two possibilties from outside the $or.
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}}}}}");
- assertSolutionExists("{fetch: {node: {ixscan: {pattern: {b: 1}}}}}");
- }
-
- //
- // Test the "split limited sort stages" hack.
- //
-
- TEST_F(QueryPlannerTest, SplitLimitedSort) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- params.options |= QueryPlannerParams::SPLIT_LIMITED_SORT;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- runQuerySortProjSkipLimit(fromjson("{a: 1}"), fromjson("{b: 1}"),
- BSONObj(), 0, 3);
-
- assertNumSolutions(2U);
- // First solution has no blocking stage; no need to split.
- assertSolutionExists("{fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {b: 1}}}}}");
- // Second solution has a blocking sort with a limit: it gets split and
- // joined with an OR stage.
- assertSolutionExists("{or: {nodes: ["
- "{sort: {pattern: {b: 1}, limit: 3, node: "
- "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}}}, "
- "{sort: {pattern: {b: 1}, limit: 0, node: "
- "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}}}]}}");
- }
-
- // The same query run as a find command with a limit should not require the "split limited sort"
- // hack.
- TEST_F(QueryPlannerTest, NoSplitLimitedSortAsCommand) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- params.options |= QueryPlannerParams::SPLIT_LIMITED_SORT;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- runQueryAsCommand(fromjson("{find: 'testns', filter: {a: 1}, sort: {b: 1}, limit: 3}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{limit: {n: 3, node: {fetch: {filter: {a:1}, node: "
- "{ixscan: {filter: null, pattern: {b: 1}}}}}}}");
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 3, node: {fetch: {filter: null,"
- "node: {ixscan: {pattern: {a: 1}}}}}}}");
- }
-
- // Same query run as a find command with a batchSize rather than a limit should not require
- // the "split limited sort" hack, and should not have any limit represented inside the plan.
- TEST_F(QueryPlannerTest, NoSplitLimitedSortAsCommandBatchSize) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
- params.options |= QueryPlannerParams::SPLIT_LIMITED_SORT;
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- runQueryAsCommand(fromjson("{find: 'testns', filter: {a: 1}, sort: {b: 1}, batchSize: 3}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{fetch: {filter: {a: 1}, node: {ixscan: "
- "{filter: null, pattern: {b: 1}}}}}");
- assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: {fetch: {filter: null,"
- "node: {ixscan: {pattern: {a: 1}}}}}}}");
- }
-
- //
- // Test shard filter query planning
- //
-
- TEST_F(QueryPlannerTest, ShardFilterCollScan) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1);
- addIndex(BSON("a" << 1));
-
- runQuery(fromjson("{b: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{sharding_filter: {node: "
- "{cscan: {dir: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterBasicIndex) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1);
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- runQuery(fromjson("{b: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{sharding_filter: {node: "
- "{fetch: {node: "
- "{ixscan: {pattern: {b: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterBasicCovered) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1);
- addIndex(BSON("a" << 1));
-
- runQuery(fromjson("{a: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {node: "
- "{sharding_filter: {node: "
- "{ixscan: {pattern: {a: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterBasicProjCovered) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1);
- addIndex(BSON("a" << 1));
-
- runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id : 0, a : 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, type: 'coveredIndex', node: "
- "{sharding_filter: {node: "
- "{ixscan: {pattern: {a: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterCompoundProjCovered) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1 << "b" << 1);
- addIndex(BSON("a" << 1 << "b" << 1));
-
- runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id: 0, a: 1, b: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1, b: 1 }, type: 'coveredIndex', node: "
- "{sharding_filter: {node: "
- "{ixscan: {pattern: {a: 1, b: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterNestedProjNotCovered) {
- // Nested projections can't be covered currently, though the shard key filter shouldn't need
- // to fetch.
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1 << "b.c" << 1);
- addIndex(BSON("a" << 1 << "b.c" << 1));
-
- runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id: 0, a: 1, 'b.c': 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1, 'b.c': 1 }, type: 'default', node: "
- "{fetch: {node: "
- "{sharding_filter: {node: "
- "{ixscan: {pattern: {a: 1, 'b.c': 1}}}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterHashProjNotCovered) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << "hashed");
- addIndex(BSON("a" << "hashed"));
-
- runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id : 0, a : 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {_id: 0,a: 1}, type: 'simple', node: "
- "{sharding_filter : {node: "
- "{fetch: {node: "
- "{ixscan: {pattern: {a: 'hashed'}}}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterKeyPrefixIndexCovered) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << 1);
- addIndex(BSON("a" << 1 << "b" << 1 << "_id" << 1));
-
- runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{a : 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {a: 1}, type: 'coveredIndex', node: "
- "{sharding_filter : {node: "
- "{ixscan: {pattern: {a: 1, b: 1, _id: 1}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, ShardFilterNoIndexNotCovered) {
- params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
- params.shardKey = BSON("a" << "hashed");
- addIndex(BSON("b" << 1));
-
- runQuerySortProj(fromjson("{b: 1}"), BSONObj(), fromjson("{_id : 0, a : 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{proj: {spec: {_id: 0,a: 1}, type: 'simple', node: "
- "{sharding_filter : {node: "
- "{fetch: {node: "
- "{ixscan: {pattern: {b: 1}}}}}}}}}");
- }
-
- TEST_F(QueryPlannerTest, CannotTrimIxisectParam) {
- params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
- params.options |= QueryPlannerParams::INDEX_INTERSECTION;
- params.options |= QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
-
- runQuery(fromjson("{a: 1, b: 1, c: 1}"));
-
- assertNumSolutions(3U);
- assertSolutionExists("{fetch: {filter: {b: 1, c: 1}, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}");
- assertSolutionExists("{fetch: {filter: {a: 1, c: 1}, node: "
- "{ixscan: {filter: null, pattern: {b: 1}}}}}");
- assertSolutionExists("{fetch: {filter: {a:1,b:1,c:1}, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1}}},"
- "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, CannotTrimIxisectParamBeneathOr) {
- params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
- params.options |= QueryPlannerParams::INDEX_INTERSECTION;
- params.options |= QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
-
- runQuery(fromjson("{d: 1, $or: [{a: 1}, {b: 1, c: 1}]}"));
-
- assertNumSolutions(3U);
-
- assertSolutionExists("{fetch: {filter: {d: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {c: 1}, node: {ixscan: {filter: null,"
- "pattern: {b: 1}, bounds: {b: [[1,1,true,true]]}}}}},"
- "{ixscan: {filter: null, pattern: {a: 1},"
- "bounds: {a: [[1,1,true,true]]}}}]}}}}");
-
- assertSolutionExists("{fetch: {filter: {d: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {b: 1}, node: {ixscan: {filter: null,"
- "pattern: {c: 1}, bounds: {c: [[1,1,true,true]]}}}}},"
- "{ixscan: {filter: null, pattern: {a: 1},"
- "bounds: {a: [[1,1,true,true]]}}}]}}}}");
-
- assertSolutionExists("{fetch: {filter: {d: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {b: 1, c: 1}, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {b: 1}}},"
- "{ixscan: {filter: null, pattern: {c: 1}}}]}}}},"
- "{ixscan: {filter: null, pattern: {a: 1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, CannotTrimIxisectAndHashWithOrChild) {
- params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
- params.options |= QueryPlannerParams::INDEX_INTERSECTION;
- params.options |= QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
-
- runQuery(fromjson("{c: 1, $or: [{a: 1}, {b: 1, d: 1}]}"));
-
- assertNumSolutions(3U);
-
- assertSolutionExists("{fetch: {filter: {c: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {d: 1}, node: {ixscan: {filter: null,"
- "pattern: {b: 1}, bounds: {b: [[1,1,true,true]]}}}}},"
- "{ixscan: {filter: null, pattern: {a: 1},"
- "bounds: {a: [[1,1,true,true]]}}}]}}}}");
-
- assertSolutionExists("{fetch: {filter: {$or:[{b:1,d:1},{a:1}]}, node:"
- "{ixscan: {filter: null, pattern: {c: 1}}}}}");
-
- assertSolutionExists("{fetch: {filter: {c:1,$or:[{a:1},{b:1,d:1}]}, node:{andHash:{nodes:["
- "{or: {nodes: ["
- "{fetch: {filter: {d:1}, node: {ixscan: {pattern: {b: 1}}}}},"
- "{ixscan: {filter: null, pattern: {a: 1}}}]}},"
- "{ixscan: {filter: null, pattern: {c: 1}}}]}}}}");
- }
-
- TEST_F(QueryPlannerTest, CannotTrimIxisectParamSelfIntersection) {
- params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
- params.options = QueryPlannerParams::INDEX_INTERSECTION;
- params.options |= QueryPlannerParams::NO_TABLE_SCAN;
-
- // true means multikey
- addIndex(BSON("a" << 1), true);
-
- runQuery(fromjson("{a: {$all: [1, 2, 3]}}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{fetch: {filter: {$and: [{a:2}, {a:3}]}, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}");
- assertSolutionExists("{fetch: {filter: null, node: {andSorted: {nodes: ["
- "{ixscan: {filter: null, pattern: {a:1},"
- "bounds: {a: [[1,1,true,true]]}}},"
- "{ixscan: {filter: null, pattern: {a:1},"
- "bounds: {a: [[2,2,true,true]]}}},"
- "{ixscan: {filter: null, pattern: {a:1},"
- "bounds: {a: [[3,3,true,true]]}}}]}}}}");
- }
-
-
- // If a lookup against a unique index is available as a possible plan, then the planner
- // should not generate other possibilities.
- TEST_F(QueryPlannerTest, UniqueIndexLookup) {
- params.options = QueryPlannerParams::INDEX_INTERSECTION;
- params.options |= QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1),
- false, // multikey
- false, // sparse,
- true); // unique
-
- runQuery(fromjson("{a: 1, b: 1}"));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: {a: 1}, node: "
- "{ixscan: {filter: null, pattern: {b: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, HintOnNonUniqueIndex) {
- params.options = QueryPlannerParams::INDEX_INTERSECTION;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1),
- false, // multikey
- false, // sparse,
- true); // unique
-
- runQueryHint(fromjson("{a: 1, b: 1}"), BSON("a" << 1));
-
- assertNumSolutions(1U);
- assertSolutionExists("{fetch: {filter: {b: 1}, node: "
- "{ixscan: {filter: null, pattern: {a: 1}}}}}");
- }
-
- TEST_F(QueryPlannerTest, UniqueIndexLookupBelowOr) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- addIndex(BSON("d" << 1),
- false, // multikey
- false, // sparse,
- true); // unique
-
- runQuery(fromjson("{$or: [{a: 1, b: 1}, {c: 1, d: 1}]}"));
-
- // Only two plans because we throw out plans for the right branch of the $or that do not
- // use equality over the unique index.
- assertNumSolutions(2U);
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {a: 1}, node: {ixscan: {pattern: {b: 1}}}}},"
- "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}]}}");
- assertSolutionExists("{or: {nodes: ["
- "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}},"
- "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}]}}");
- }
-
- TEST_F(QueryPlannerTest, UniqueIndexLookupBelowOrBelowAnd) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1));
- addIndex(BSON("b" << 1));
- addIndex(BSON("c" << 1));
- addIndex(BSON("d" << 1),
- false, // multikey
- false, // sparse,
- true); // unique
-
- runQuery(fromjson("{e: 1, $or: [{a: 1, b: 1}, {c: 1, d: 1}]}"));
-
- // Only two plans because we throw out plans for the right branch of the $or that do not
- // use equality over the unique index.
- assertNumSolutions(2U);
- assertSolutionExists("{fetch: {filter: {e: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {a: 1}, node: {ixscan: {pattern: {b: 1}}}}},"
- "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}"
- "]}}}}");
- assertSolutionExists("{fetch: {filter: {e: 1}, node: {or: {nodes: ["
- "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}},"
- "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}"
- "]}}}}");
- }
-
- TEST_F(QueryPlannerTest, CoveredOrUniqueIndexLookup) {
- params.options = QueryPlannerParams::NO_TABLE_SCAN;
-
- addIndex(BSON("a" << 1 << "b" << 1));
- addIndex(BSON("a" << 1),
- false, // multikey
- false, // sparse,
- true); // unique
-
- runQuerySortProj(fromjson("{a: 1, b: 1}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
-
- assertNumSolutions(2U);
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}}}}");
- assertSolutionExists("{proj: {spec: {_id: 0, a: 1}, node: "
- "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}");
- }
-
- //
- // Test bad input to query planner helpers.
- //
-
- TEST(BadInputTest, CacheDataFromTaggedTree) {
- PlanCacheIndexTree* indexTree;
-
- // Null match expression.
- std::vector<IndexEntry> relevantIndices;
- Status s = QueryPlanner::cacheDataFromTaggedTree(NULL, relevantIndices, &indexTree);
- ASSERT_NOT_OK(s);
- ASSERT(NULL == indexTree);
-
- // No relevant index matching the index tag.
- relevantIndices.push_back(IndexEntry(BSON("a" << 1)));
-
- CanonicalQuery *cq;
- Status cqStatus = CanonicalQuery::canonicalize("ns", BSON("a" << 3), &cq);
- ASSERT_OK(cqStatus);
- std::unique_ptr<CanonicalQuery> scopedCq(cq);
- scopedCq->root()->setTag(new IndexTag(1));
-
- s = QueryPlanner::cacheDataFromTaggedTree(scopedCq->root(), relevantIndices, &indexTree);
- ASSERT_NOT_OK(s);
- ASSERT(NULL == indexTree);
- }
-
- TEST(BadInputTest, TagAccordingToCache) {
- CanonicalQuery *cq;
- Status cqStatus = CanonicalQuery::canonicalize("ns", BSON("a" << 3), &cq);
- ASSERT_OK(cqStatus);
- std::unique_ptr<CanonicalQuery> scopedCq(cq);
-
- std::unique_ptr<PlanCacheIndexTree> indexTree(new PlanCacheIndexTree());
- indexTree->setIndexEntry(IndexEntry(BSON("a" << 1)));
-
- std::map<BSONObj, size_t> indexMap;
-
- // Null filter.
- Status s = QueryPlanner::tagAccordingToCache(NULL, indexTree.get(), indexMap);
- ASSERT_NOT_OK(s);
-
- // Null indexTree.
- s = QueryPlanner::tagAccordingToCache(scopedCq->root(), NULL, indexMap);
- ASSERT_NOT_OK(s);
-
- // Index not found.
- s = QueryPlanner::tagAccordingToCache(scopedCq->root(), indexTree.get(), indexMap);
- ASSERT_NOT_OK(s);
-
- // Index found once added to the map.
- indexMap[BSON("a" << 1)] = 0;
- s = QueryPlanner::tagAccordingToCache(scopedCq->root(), indexTree.get(), indexMap);
- ASSERT_OK(s);
-
- // Regenerate canonical query in order to clear tags.
- cqStatus = CanonicalQuery::canonicalize("ns", BSON("a" << 3), &cq);
- ASSERT_OK(cqStatus);
- scopedCq.reset(cq);
-
- // Mismatched tree topology.
- PlanCacheIndexTree* child = new PlanCacheIndexTree();
- child->setIndexEntry(IndexEntry(BSON("a" << 1)));
- indexTree->children.push_back(child);
- s = QueryPlanner::tagAccordingToCache(scopedCq->root(), indexTree.get(), indexMap);
- ASSERT_NOT_OK(s);
- }
+ runQuery(fromjson("{b: {$not: {$exists: true}}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$exists: false}}, node: "
+ "{ixscan: {pattern: {b: 1}, bounds: "
+ "{b: [[null, null, true, true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExistsBoundsCompound) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuery(fromjson("{a: 1, b: {$exists: true}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$exists: true}}, node: "
+ "{ixscan: {pattern: {a: 1, b: 1}, bounds: "
+ "{a: [[1,1,true,true]], b: [['MinKey','MaxKey',true,true]]}}}}}");
+
+ // This ends up being a double negation, which we currently don't index.
+ runQuery(fromjson("{a: 1, b: {$not: {$exists: false}}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, bounds: "
+ "{a: [[1,1,true,true]], b: [['MinKey','MaxKey',true,true]]}}}}}");
+
+ runQuery(fromjson("{a: 1, b: {$exists: false}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$exists: false}}, node: "
+ "{ixscan: {pattern: {a: 1, b: 1}, bounds: "
+ "{a: [[1,1,true,true]], b: [[null,null,true,true]]}}}}}");
+
+ runQuery(fromjson("{a: 1, b: {$not: {$exists: true}}}"));
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$exists: false}}, node: "
+ "{ixscan: {pattern: {a: 1, b: 1}, bounds: "
+ "{a: [[1,1,true,true]], b: [[null,null,true,true]]}}}}}");
+}
+
+//
+// skip and limit
+//
+
+TEST_F(QueryPlannerTest, BasicSkipNoIndex) {
+ addIndex(BSON("a" << 1));
+
+ runQuerySkipLimit(BSON("x" << 5), 3, 0);
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists("{skip: {n: 3, node: {cscan: {dir: 1, filter: {x: 5}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicSkipWithIndex) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuerySkipLimit(BSON("a" << 5), 8, 0);
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{skip: {n: 8, node: {cscan: {dir: 1, filter: {a: 5}}}}}");
+ assertSolutionExists(
+ "{skip: {n: 8, node: {fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicLimitNoIndex) {
+ addIndex(BSON("a" << 1));
+
+ runQuerySkipLimit(BSON("x" << 5), 0, -3);
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists("{limit: {n: 3, node: {cscan: {dir: 1, filter: {x: 5}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicSoftLimitNoIndex) {
+ addIndex(BSON("a" << 1));
+
+ runQuerySkipLimit(BSON("x" << 5), 0, 3);
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {x: 5}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicLimitWithIndex) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuerySkipLimit(BSON("a" << 5), 0, -5);
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{limit: {n: 5, node: {cscan: {dir: 1, filter: {a: 5}}}}}");
+ assertSolutionExists(
+ "{limit: {n: 5, node: {fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicSoftLimitWithIndex) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuerySkipLimit(BSON("a" << 5), 0, 5);
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: 5}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SkipAndLimit) {
+ addIndex(BSON("x" << 1));
+
+ runQuerySkipLimit(BSON("x" << BSON("$lte" << 4)), 7, -2);
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{limit: {n: 2, node: {skip: {n: 7, node: "
+ "{cscan: {dir: 1, filter: {x: {$lte: 4}}}}}}}}");
+ assertSolutionExists(
+ "{limit: {n: 2, node: {skip: {n: 7, node: {fetch: "
+ "{filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SkipAndSoftLimit) {
+ addIndex(BSON("x" << 1));
+
+ runQuerySkipLimit(BSON("x" << BSON("$lte" << 4)), 7, 2);
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{skip: {n: 7, node: "
+ "{cscan: {dir: 1, filter: {x: {$lte: 4}}}}}}");
+ assertSolutionExists(
+ "{skip: {n: 7, node: {fetch: "
+ "{filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}}}");
+}
+
+//
+// tree operations
+//
+
+TEST_F(QueryPlannerTest, TwoPredicatesAnding) {
+ addIndex(BSON("x" << 1));
+
+ runQuery(fromjson("{$and: [ {x: {$gt: 1}}, {x: {$lt: 3}} ] }"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SimpleOr) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a: 20}, {a: 21}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a: 20}, {a: 21}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a:1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, OrWithoutEnoughIndices) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a: 20}, {b: 21}]}"));
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a: 20}, {b: 21}]}}}");
+}
+
+TEST_F(QueryPlannerTest, OrWithAndChild) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a: 20}, {$and: [{a:1}, {b:7}]}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a: 1}}}, "
+ "{fetch: {filter: {b: 7}, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}}}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, AndWithUnindexedOrChild) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{a:20, $or: [{b:1}, {c:7}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+
+ // Logical rewrite means we could get one of these two outcomes:
+ size_t matches = 0;
+ matches += numSolutionMatches(
+ "{fetch: {filter: {$or: [{b: 1}, {c: 7}]}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}");
+ matches += numSolutionMatches(
+ "{or: {filter: null, nodes: ["
+ "{fetch: {filter: {b:1}, node: {"
+ "ixscan: {filter: null, pattern: {a:1}}}}},"
+ "{fetch: {filter: {c:7}, node: {"
+ "ixscan: {filter: null, pattern: {a:1}}}}}]}}");
+ ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
+}
+
+
+TEST_F(QueryPlannerTest, AndWithOrWithOneIndex) {
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{b:1}, {c:7}], a:20}"));
+
+ // Logical rewrite gives us at least one of these:
+ assertSolutionExists("{cscan: {dir: 1}}");
+ size_t matches = 0;
+ matches += numSolutionMatches(
+ "{fetch: {filter: {$or: [{b: 1}, {c: 7}]}, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+ matches += numSolutionMatches(
+ "{or: {filter: null, nodes: ["
+ "{fetch: {filter: {b:1}, node: {"
+ "ixscan: {filter: null, pattern: {a:1}}}}},"
+ "{fetch: {filter: {c:7}, node: {"
+ "ixscan: {filter: null, pattern: {a:1}}}}}]}}");
+ ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
+}
+
+//
+// Additional $or tests
+//
+
+TEST_F(QueryPlannerTest, OrCollapsesToSingleScan) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a:{$gt:2}}, {a:{$gt:0}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [[0,Infinity,false,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, OrCollapsesToSingleScan2) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a:{$lt:2}}, {a:{$lt:4}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [[-Infinity,4,true,false]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, OrCollapsesToSingleScan3) {
+ addIndex(BSON("a" << 1));
+ runQueryHint(fromjson("{$or: [{a:1},{a:3}]}"), fromjson("{a:1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [[1,1,true,true], [3,3,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, OrOnlyOneBranchCanUseIndex) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a:1}, {b:2}]}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, OrOnlyOneBranchCanUseIndexHinted) {
+ addIndex(BSON("a" << 1));
+ runQueryHint(fromjson("{$or: [{a:1}, {b:2}]}"), fromjson("{a:1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {$or:[{a:1},{b:2}]}, node: {ixscan: "
+ "{pattern: {a:1}, bounds: "
+ "{a: [['MinKey','MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, OrNaturalHint) {
+ addIndex(BSON("a" << 1));
+ runQueryHint(fromjson("{$or: [{a:1}, {a:3}]}"), fromjson("{$natural:1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// SERVER-13714. A non-top-level indexable negation exposed a bug in plan enumeration.
+TEST_F(QueryPlannerTest, NonTopLevelIndexedNegation) {
+ addIndex(BSON("state" << 1));
+ addIndex(BSON("is_draft" << 1));
+ addIndex(BSON("published_date" << 1));
+ addIndex(BSON("newsroom_id" << 1));
+
+ BSONObj queryObj = fromjson(
+ "{$and:[{$or:[{is_draft:false},{creator_id:1}]},"
+ "{$or:[{state:3,is_draft:false},"
+ "{published_date:{$ne:null}}]},"
+ "{newsroom_id:{$in:[1]}}]}");
+ runQuery(queryObj);
+}
+
+TEST_F(QueryPlannerTest, NonTopLevelIndexedNegationMinQuery) {
+ addIndex(BSON("state" << 1));
+ addIndex(BSON("is_draft" << 1));
+ addIndex(BSON("published_date" << 1));
+
+ // This is the min query to reproduce SERVER-13714
+ BSONObj queryObj = fromjson("{$or:[{state:1, is_draft:1}, {published_date:{$ne: 1}}]}");
+ runQuery(queryObj);
+}
+
+// SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
+TEST_F(QueryPlannerTest, OrOfAnd) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a:{$gt:2,$lt:10}}, {a:{$gt:0,$lt:5}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {pattern: {a:1}, bounds: {a: [[2,10,false,false]]}}}, "
+ "{ixscan: {pattern: {a:1}, bounds: "
+ "{a: [[0,5,false,false]]}}}]}}}}");
+}
+
+// SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
+TEST_F(QueryPlannerTest, OrOfAnd2) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a:{$gt:2,$lt:10}}, {a:{$gt:0,$lt:15}}, {a:{$gt:20}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {pattern: {a:1}, bounds: {a: [[2,10,false,false]]}}}, "
+ "{ixscan: {pattern: {a:1}, bounds: {a: [[0,15,false,false]]}}}, "
+ "{ixscan: {pattern: {a:1}, bounds: "
+ "{a: [[20,Infinity,false,true]]}}}]}}}}");
+}
+
+// SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
+TEST_F(QueryPlannerTest, OrOfAnd3) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a:{$gt:1,$lt:5},b:6}, {a:3,b:{$gt:0,$lt:10}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {b:6}, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [[1,5,false,false]]}}}}}, "
+ "{fetch: {filter: {$and:[{b:{$lt:10}},{b:{$gt:0}}]}, node: "
+ "{ixscan: {pattern: {a:1}, bounds: {a:[[3,3,true,true]]}}}}}]}}");
+}
+
+// SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
+TEST_F(QueryPlannerTest, OrOfAnd4) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson(
+ "{$or: [{a:{$gt:1,$lt:5}, b:{$gt:0,$lt:3}, c:6}, "
+ "{a:3, b:{$gt:1,$lt:2}, c:{$gt:0,$lt:10}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {c:6}, node: {ixscan: {pattern: {a:1,b:1}, "
+ "bounds: {a: [[1,5,false,false]], b: [[0,3,false,false]]}}}}}, "
+ "{fetch: {filter: {$and:[{c:{$lt:10}},{c:{$gt:0}}]}, node: "
+ "{ixscan: {pattern: {a:1,b:1}, "
+ " bounds: {a:[[3,3,true,true]], b:[[1,2,false,false]]}}}}}]}}");
+}
+
+// SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
+TEST_F(QueryPlannerTest, OrOfAnd5) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson(
+ "{$or: [{a:{$gt:1,$lt:5}, c:6}, "
+ "{a:3, b:{$gt:1,$lt:2}, c:{$gt:0,$lt:10}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {c:6}, node: {ixscan: {pattern: {a:1,b:1}, "
+ "bounds: {a: [[1,5,false,false]], "
+ "b: [['MinKey','MaxKey',true,true]]}}}}}, "
+ "{fetch: {filter: {$and:[{c:{$lt:10}},{c:{$gt:0}}]}, node: "
+ "{ixscan: {pattern: {a:1,b:1}, "
+ " bounds: {a:[[3,3,true,true]], b:[[1,2,false,false]]}}}}}]}}");
+}
+
+// SERVER-12594: we don't yet collapse an OR of ANDs into a single ixscan.
+TEST_F(QueryPlannerTest, OrOfAnd6) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{$or: [{a:{$in:[1]},b:{$in:[1]}}, {a:{$in:[1,5]},b:{$in:[1,5]}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {pattern: {a:1,b:1}, bounds: "
+ "{a: [[1,1,true,true]], b: [[1,1,true,true]]}}}, "
+ "{ixscan: {pattern: {a:1,b:1}, bounds: "
+ "{a: [[1,1,true,true], [5,5,true,true]], "
+ " b: [[1,1,true,true], [5,5,true,true]]}}}]}}}}");
+}
+
+// SERVER-13960: properly handle $or with a mix of exact and inexact predicates.
+TEST_F(QueryPlannerTest, OrInexactWithExact) {
+ addIndex(BSON("name" << 1));
+ runQuery(fromjson("{$or: [{name: 'thomas'}, {name: /^alexand(er|ra)/}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {filter:"
+ "{$or: [{name: 'thomas'}, {name: /^alexand(er|ra)/}]},"
+ "pattern: {name: 1}}}}}");
+}
+
+// SERVER-13960: multiple indices, each with an inexact covered predicate.
+TEST_F(QueryPlannerTest, OrInexactWithExact2) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuery(fromjson("{$or: [{a: 'foo'}, {a: /bar/}, {b: 'foo'}, {b: /bar/}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {or: {nodes: ["
+ "{ixscan: {filter: {$or:[{a:'foo'},{a:/bar/}]},"
+ "pattern: {a: 1}}},"
+ "{ixscan: {filter: {$or:[{b:'foo'},{b:/bar/}]},"
+ "pattern: {b: 1}}}]}}}}");
+}
+
+// SERVER-13960: an exact, inexact covered, and inexact fetch predicate.
+TEST_F(QueryPlannerTest, OrAllThreeTightnesses) {
+ addIndex(BSON("names" << 1));
+ runQuery(fromjson(
+ "{$or: [{names: 'frank'}, {names: /^al(ice)|(ex)/},"
+ "{names: {$elemMatch: {$eq: 'thomas'}}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: "
+ "{$or: [{names: 'frank'}, {names: /^al(ice)|(ex)/},"
+ "{names: {$elemMatch: {$eq: 'thomas'}}}]}, "
+ "node: {ixscan: {filter: null, pattern: {names: 1}}}}}");
+}
+
+// SERVER-13960: two inexact fetch predicates.
+TEST_F(QueryPlannerTest, OrTwoInexactFetch) {
+ // true means multikey
+ addIndex(BSON("names" << 1), true);
+ runQuery(fromjson(
+ "{$or: [{names: {$elemMatch: {$eq: 'alexandra'}}},"
+ "{names: {$elemMatch: {$eq: 'thomas'}}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: "
+ "{$or: [{names: {$elemMatch: {$eq: 'alexandra'}}},"
+ "{names: {$elemMatch: {$eq: 'thomas'}}}]}, "
+ "node: {ixscan: {filter: null, pattern: {names: 1}}}}}");
+}
+
+// SERVER-13960: multikey with exact and inexact covered predicates.
+TEST_F(QueryPlannerTest, OrInexactCoveredMultikey) {
+ // true means multikey
+ addIndex(BSON("names" << 1), true);
+ runQuery(fromjson("{$or: [{names: 'dave'}, {names: /joe/}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$or: [{names: 'dave'}, {names: /joe/}]}, "
+ "node: {ixscan: {filter: null, pattern: {names: 1}}}}}");
+}
+
+// SERVER-13960: $elemMatch object with $or.
+TEST_F(QueryPlannerTest, OrElemMatchObject) {
+ // true means multikey
+ addIndex(BSON("a.b" << 1), true);
+ runQuery(fromjson(
+ "{$or: [{a: {$elemMatch: {b: {$lte: 1}}}},"
+ "{a: {$elemMatch: {b: {$gte: 4}}}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {a:{$elemMatch:{b:{$gte:4}}}}, node: "
+ "{ixscan: {filter: null, pattern: {'a.b': 1}}}}},"
+ "{fetch: {filter: {a:{$elemMatch:{b:{$lte:1}}}}, node: "
+ "{ixscan: {filter: null, pattern: {'a.b': 1}}}}}]}}");
+}
+
+// SERVER-13960: $elemMatch object inside an $or, below an AND.
+TEST_F(QueryPlannerTest, OrElemMatchObjectBeneathAnd) {
+ // true means multikey
+ addIndex(BSON("a.b" << 1), true);
+ runQuery(fromjson(
+ "{$or: [{'a.b': 0, a: {$elemMatch: {b: {$lte: 1}}}},"
+ "{a: {$elemMatch: {b: {$gte: 4}}}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {$and:[{a:{$elemMatch:{b:{$lte:1}}}},{'a.b':0}]},"
+ "node: {ixscan: {filter: null, pattern: {'a.b': 1}, "
+ "bounds: {'a.b': [[-Infinity,1,true,true]]}}}}},"
+ "{fetch: {filter: {a:{$elemMatch:{b:{$gte:4}}}}, node: "
+ "{ixscan: {filter: null, pattern: {'a.b': 1},"
+ "bounds: {'a.b': [[4,Infinity,true,true]]}}}}}]}}");
+}
+
+// SERVER-13960: $or below $elemMatch with an inexact covered predicate.
+TEST_F(QueryPlannerTest, OrBelowElemMatchInexactCovered) {
+ // true means multikey
+ addIndex(BSON("a.b" << 1), true);
+ runQuery(fromjson("{a: {$elemMatch: {$or: [{b: 'x'}, {b: /z/}]}}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$elemMatch: {$or: [{b: 'x'}, {b: /z/}]}}},"
+ "node: {ixscan: {filter: null, pattern: {'a.b': 1}}}}}");
+}
+
+// SERVER-13960: $in with exact and inexact covered predicates.
+TEST_F(QueryPlannerTest, OrWithExactAndInexact) {
+ addIndex(BSON("name" << 1));
+ runQuery(fromjson("{name: {$in: ['thomas', /^alexand(er|ra)/]}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: {name: {$in: ['thomas', /^alexand(er|ra)/]}}, "
+ "pattern: {name: 1}}}}}");
+}
+
+// SERVER-13960: $in with exact, inexact covered, and inexact fetch predicates.
+TEST_F(QueryPlannerTest, OrWithExactAndInexact2) {
+ addIndex(BSON("name" << 1));
+ runQuery(fromjson(
+ "{$or: [{name: {$in: ['thomas', /^alexand(er|ra)/]}},"
+ "{name: {$exists: false}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$or: [{name: {$in: ['thomas', /^alexand(er|ra)/]}},"
+ "{name: {$exists: false}}]}, "
+ "node: {ixscan: {filter: null, pattern: {name: 1}}}}}");
+}
+
+// SERVER-13960: $in with exact, inexact covered, and inexact fetch predicates
+// over two indices.
+TEST_F(QueryPlannerTest, OrWithExactAndInexact3) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuery(fromjson(
+ "{$or: [{a: {$in: [/z/, /x/]}}, {a: 'w'},"
+ "{b: {$exists: false}}, {b: {$in: ['p']}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {filter: {$or:[{a:{$in:[/z/, /x/]}}, {a:'w'}]}, "
+ "pattern: {a: 1}}}, "
+ "{fetch: {filter: {$or:[{b:{$exists:false}}, {b:{$in:['p']}}]},"
+ "node: {ixscan: {filter: null, pattern: {b: 1}}}}}]}}}}");
+}
+
+//
+// Min/Max
+//
+
+TEST_F(QueryPlannerTest, MinValid) {
+ addIndex(BSON("a" << 1));
+ runQueryHintMinMax(BSONObj(), BSONObj(), fromjson("{a: 1}"), BSONObj());
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MinWithoutIndex) {
+ runInvalidQueryHintMinMax(BSONObj(), BSONObj(), fromjson("{a: 1}"), BSONObj());
+}
+
+TEST_F(QueryPlannerTest, MinBadHint) {
+ addIndex(BSON("b" << 1));
+ runInvalidQueryHintMinMax(BSONObj(), fromjson("{b: 1}"), fromjson("{a: 1}"), BSONObj());
+}
+
+TEST_F(QueryPlannerTest, MaxValid) {
+ addIndex(BSON("a" << 1));
+ runQueryHintMinMax(BSONObj(), BSONObj(), BSONObj(), fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MinMaxSameValue) {
+ addIndex(BSON("a" << 1));
+ runQueryHintMinMax(BSONObj(), BSONObj(), fromjson("{a: 1}"), fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MaxWithoutIndex) {
+ runInvalidQueryHintMinMax(BSONObj(), BSONObj(), BSONObj(), fromjson("{a: 1}"));
+}
+
+TEST_F(QueryPlannerTest, MaxBadHint) {
+ addIndex(BSON("b" << 1));
+ runInvalidQueryHintMinMax(BSONObj(), fromjson("{b: 1}"), BSONObj(), fromjson("{a: 1}"));
+}
+
+TEST_F(QueryPlannerTest, MaxMinSort) {
+ addIndex(BSON("a" << 1));
+
+ // Run an empty query, sort {a: 1}, max/min arguments.
+ runQueryFull(BSONObj(),
+ fromjson("{a: 1}"),
+ BSONObj(),
+ 0,
+ 0,
+ BSONObj(),
+ fromjson("{a: 2}"),
+ fromjson("{a: 8}"),
+ false);
+
+ assertNumSolutions(1);
+ assertSolutionExists("{fetch: {node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MaxMinReverseSort) {
+ addIndex(BSON("a" << 1));
+
+ // Run an empty query, sort {a: -1}, max/min arguments.
+ runQueryFull(BSONObj(),
+ fromjson("{a: -1}"),
+ BSONObj(),
+ 0,
+ 0,
+ BSONObj(),
+ fromjson("{a: 2}"),
+ fromjson("{a: 8}"),
+ false);
+
+ assertNumSolutions(1);
+ assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: -1, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MaxMinReverseIndexDir) {
+ addIndex(BSON("a" << -1));
+
+ // Because the index is descending, the min is numerically larger than the max.
+ runQueryFull(BSONObj(),
+ fromjson("{a: -1}"),
+ BSONObj(),
+ 0,
+ 0,
+ BSONObj(),
+ fromjson("{a: 8}"),
+ fromjson("{a: 2}"),
+ false);
+
+ assertNumSolutions(1);
+ assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: 1, pattern: {a: -1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MaxMinReverseIndexDirSort) {
+ addIndex(BSON("a" << -1));
+
+ // Min/max specifies a forward scan with bounds [{a: 8}, {a: 2}]. Asking for
+ // an ascending sort reverses the direction of the scan to [{a: 2}, {a: 8}].
+ runQueryFull(BSONObj(),
+ fromjson("{a: 1}"),
+ BSONObj(),
+ 0,
+ 0,
+ BSONObj(),
+ fromjson("{a: 8}"),
+ fromjson("{a: 2}"),
+ false);
+
+ assertNumSolutions(1);
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {filter: null, dir: -1,"
+ "pattern: {a: -1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MaxMinNoMatchingIndexDir) {
+ addIndex(BSON("a" << -1));
+ runInvalidQueryHintMinMax(BSONObj(), fromjson("{a: 2}"), BSONObj(), fromjson("{a: 8}"));
+}
+
+TEST_F(QueryPlannerTest, MaxMinSelectCorrectlyOrderedIndex) {
+ // There are both ascending and descending indices on 'a'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("a" << -1));
+
+ // The ordering of min and max means that we *must* use the descending index.
+ runQueryFull(BSONObj(),
+ BSONObj(),
+ BSONObj(),
+ 0,
+ 0,
+ BSONObj(),
+ fromjson("{a: 8}"),
+ fromjson("{a: 2}"),
+ false);
+
+ assertNumSolutions(1);
+ assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: 1, pattern: {a: -1}}}}}");
+
+ // If we switch the ordering, then we use the ascending index.
+ // The ordering of min and max means that we *must* use the descending index.
+ runQueryFull(BSONObj(),
+ BSONObj(),
+ BSONObj(),
+ 0,
+ 0,
+ BSONObj(),
+ fromjson("{a: 2}"),
+ fromjson("{a: 8}"),
+ false);
+
+ assertNumSolutions(1);
+ assertSolutionExists("{fetch: {node: {ixscan: {filter: null, dir: 1, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, MaxMinBadHintSelectsReverseIndex) {
+ // There are both ascending and descending indices on 'a'.
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("a" << -1));
+
+ // A query hinting on {a: 1} is bad if min is {a: 8} and {a: 2} because this
+ // min/max pairing requires a descending index.
+ runInvalidQueryFull(BSONObj(),
+ BSONObj(),
+ BSONObj(),
+ 0,
+ 0,
+ fromjson("{a: 1}"),
+ fromjson("{a: 8}"),
+ fromjson("{a: 2}"),
+ false);
+}
+
+
+//
+// $snapshot
+//
+
+TEST_F(QueryPlannerTest, Snapshot) {
+ addIndex(BSON("a" << 1));
+ runQuerySnapshot(fromjson("{a: {$gt: 0}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$gt:0}}, node: "
+ "{ixscan: {filter: null, pattern: {_id: 1}}}}}");
+}
+
+//
+// Tree operations that require simple tree rewriting.
+//
+
+TEST_F(QueryPlannerTest, AndOfAnd) {
+ addIndex(BSON("x" << 1));
+ runQuery(fromjson("{$and: [ {$and: [ {x: 2.5}]}, {x: {$gt: 1}}, {x: {$lt: 3}} ] }"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
+
+//
+// Logically equivalent queries
+//
+
+TEST_F(QueryPlannerTest, EquivalentAndsOne) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{$and: [{a: 1}, {b: {$all: [10, 20]}}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$and:[{a:1},{b:10},{b:20}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, EquivalentAndsTwo) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{$and: [{a: 1, b: 10}, {a: 1, b: 20}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$and:[{a:1},{a:1},{b:10},{b:20}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+//
+// Covering
+//
+
+TEST_F(QueryPlannerTest, BasicCovering) {
+ addIndex(BSON("x" << 1));
+ // query, sort, proj
+ runQuerySortProj(fromjson("{ x : {$gt: 1}}"), BSONObj(), fromjson("{_id: 0, x: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, x: 1}, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, x: 1}, node: "
+ "{cscan: {dir: 1, filter: {x:{$gt:1}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, DottedFieldCovering) {
+ addIndex(BSON("a.b" << 1));
+ runQuerySortProj(fromjson("{'a.b': 5}"), BSONObj(), fromjson("{_id: 0, 'a.b': 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, 'a.b': 1}, node: "
+ "{cscan: {dir: 1, filter: {'a.b': 5}}}}}");
+ // SERVER-2104
+ // assertSolutionExists("{proj: {spec: {_id: 0, 'a.b': 1}, node: {'a.b': 1}}}");
+}
+
+TEST_F(QueryPlannerTest, IdCovering) {
+ runQuerySortProj(fromjson("{_id: {$gt: 10}}"), BSONObj(), fromjson("{_id: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 1}, node: "
+ "{cscan: {dir: 1, filter: {_id: {$gt: 10}}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 1}, node: {ixscan: "
+ "{filter: null, pattern: {_id: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ProjNonCovering) {
+ addIndex(BSON("x" << 1));
+ runQuerySortProj(fromjson("{ x : {$gt: 1}}"), BSONObj(), fromjson("{x: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {x: 1}, node: {cscan: "
+ "{dir: 1, filter: {x: {$gt: 1}}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {x: 1}, node: {fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {x: 1}}}}}}}");
+}
+
+//
+// Basic sort
+//
+
+TEST_F(QueryPlannerTest, BasicSort) {
+ addIndex(BSON("x" << 1));
+ runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {x: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CantUseHashedIndexToProvideSort) {
+ addIndex(BSON("x"
+ << "hashed"));
+ runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{sort: {pattern: {x: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CantUseHashedIndexToProvideSortWithIndexablePred) {
+ addIndex(BSON("x"
+ << "hashed"));
+ runQuerySortProj(BSON("x" << BSON("$in" << BSON_ARRAY(0 << 1))), BSON("x" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{sort: {pattern: {x: 1}, limit: 0, node: "
+ "{fetch: {node: "
+ "{ixscan: {pattern: {x: 'hashed'}}}}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {x: 1}, limit: 0, node: "
+ "{cscan: {dir: 1, filter: {x: {$in: [0, 1]}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CantUseTextIndexToProvideSort) {
+ addIndex(BSON("x" << 1 << "_fts"
+ << "text"
+ << "_ftsx" << 1));
+ runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{sort: {pattern: {x: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicSortWithIndexablePred) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuerySortProj(fromjson("{ a : 5 }"), BSON("b" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 3U);
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, "
+ "node: {fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: 5}, node: {ixscan: "
+ "{filter: null, pattern: {b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BasicSortBooleanIndexKeyPattern) {
+ addIndex(BSON("a" << true));
+ runQuerySortProj(fromjson("{ a : 5 }"), BSON("a" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{sort: {pattern: {a: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: true}}}}}");
+}
+
+// SERVER-14070
+TEST_F(QueryPlannerTest, CompoundIndexWithEqualityPredicatesProvidesSort) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProj(fromjson("{a: 1, b: 1}"), fromjson("{b: 1}"), BSONObj());
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {filter: null,"
+ "pattern: {a: 1, b: 1}, "
+ "bounds: {a:[[1,1,true,true]], b:[[1,1,true,true]]}}}}}");
+}
+
+//
+// Sort with limit and/or skip
+//
+
+TEST_F(QueryPlannerTest, SortLimit) {
+ // Negative limit indicates hard limit - see lite_parsed_query.cpp
+ runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 0, -3);
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sort: {pattern: {a: 1}, limit: 3, "
+ "node: {cscan: {dir: 1}}}}");
+}
+
+TEST_F(QueryPlannerTest, SortSkip) {
+ runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 2, 0);
+ assertNumSolutions(1U);
+ // If only skip is provided, do not limit sort.
+ assertSolutionExists(
+ "{skip: {n: 2, node: "
+ "{sort: {pattern: {a: 1}, limit: 0, "
+ "node: {cscan: {dir: 1}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SortSkipLimit) {
+ runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 2, -3);
+ assertNumSolutions(1U);
+ // Limit in sort node should be adjusted by skip count
+ assertSolutionExists(
+ "{skip: {n: 2, node: "
+ "{sort: {pattern: {a: 1}, limit: 5, "
+ "node: {cscan: {dir: 1}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SortSoftLimit) {
+ runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 0, 3);
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sort: {pattern: {a: 1}, limit: 3, "
+ "node: {cscan: {dir: 1}}}}");
+}
+
+TEST_F(QueryPlannerTest, SortSkipSoftLimit) {
+ runQuerySortProjSkipLimit(BSONObj(), fromjson("{a: 1}"), BSONObj(), 2, 3);
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{skip: {n: 2, node: "
+ "{sort: {pattern: {a: 1}, limit: 5, "
+ "node: {cscan: {dir: 1}}}}}}");
+}
+
+//
+// Sort elimination
+//
+
+TEST_F(QueryPlannerTest, BasicSortElim) {
+ addIndex(BSON("x" << 1));
+ // query, sort, proj
+ runQuerySortProj(fromjson("{ x : {$gt: 1}}"), fromjson("{x: 1}"), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{sort: {pattern: {x: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {x: {$gt: 1}}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {filter: null, pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SortElimCompound) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProj(fromjson("{ a : 5 }"), BSON("b" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+// SERVER-13611: test that sort elimination still works if there are
+// trailing fields in the index.
+TEST_F(QueryPlannerTest, SortElimTrailingFields) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ runQuerySortProj(fromjson("{a: 5}"), BSON("b" << 1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {a: 5}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1, b: 1, c: 1}}}}}");
+}
+
+// Sort elimination with trailing fields where the sort direction is descending.
+TEST_F(QueryPlannerTest, SortElimTrailingFieldsReverse) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
+ runQuerySortProj(fromjson("{a: 5, b: 6}"), BSON("c" << -1), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{sort: {pattern: {c: -1}, limit: 0, "
+ "node: {cscan: {dir: 1, filter: {a: 5, b: 6}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, dir: -1, pattern: {a: 1, b: 1, c: 1, d: 1}}}}}");
+}
+
+//
+// Basic compound
+//
+
+TEST_F(QueryPlannerTest, BasicCompound) {
+ addIndex(BSON("x" << 1 << "y" << 1));
+ runQuery(fromjson("{ x : 5, y: 10}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1, y: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundMissingField) {
+ addIndex(BSON("x" << 1 << "y" << 1 << "z" << 1));
+ runQuery(fromjson("{ x : 5, z: 10}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {x: 1, y: 1, z: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundFieldsOrder) {
+ addIndex(BSON("x" << 1 << "y" << 1 << "z" << 1));
+ runQuery(fromjson("{ x : 5, z: 10, y:1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1, y: 1, z: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CantUseCompound) {
+ addIndex(BSON("x" << 1 << "y" << 1));
+ runQuery(fromjson("{ y: 10}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {y: 10}}}");
+}
+
+//
+// $in
+//
+
+TEST_F(QueryPlannerTest, InBasic) {
+ addIndex(fromjson("{a: 1}"));
+ runQuery(fromjson("{a: {$in: [1, 2]}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: {$in: [1, 2]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {pattern: {a: 1}}}}}");
+}
+
+// Logically equivalent to the preceding $in query.
+// Indexed solution should be the same.
+TEST_F(QueryPlannerTest, InBasicOrEquivalent) {
+ addIndex(fromjson("{a: 1}"));
+ runQuery(fromjson("{$or: [{a: 1}, {a: 2}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a: 1}, {a: 2}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, InSparseIndex) {
+ addIndex(fromjson("{a: 1}"),
+ false, // multikey
+ true); // sparse
+ runQuery(fromjson("{a: {$in: [null]}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: {$in: [null]}}}}");
+}
+
+TEST_F(QueryPlannerTest, InCompoundIndexFirst) {
+ addIndex(fromjson("{a: 1, b: 1}"));
+ runQuery(fromjson("{a: {$in: [1, 2]}, b: 3}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {b: 3, a: {$in: [1, 2]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
+}
+
+// Logically equivalent to the preceding $in query.
+// Indexed solution should be the same.
+// Currently fails - pre-requisite to SERVER-12024
+/*
+TEST_F(QueryPlannerTest, InCompoundIndexFirstOrEquivalent) {
+ addIndex(fromjson("{a: 1, b: 1}"));
+ runQuery(fromjson("{$and: [{$or: [{a: 1}, {a: 2}]}, {b: 3}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$and: [{$or: [{a: 1}, {a: 2}]}, {b: 3}]}}}");
+ assertSolutionExists("{fetch: {filter: null, "
+ "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
+}
+*/
+
+TEST_F(QueryPlannerTest, InCompoundIndexLast) {
+ addIndex(fromjson("{a: 1, b: 1}"));
+ runQuery(fromjson("{a: 3, b: {$in: [1, 2]}}"));
+
+ assertNumSolutions(2U);
+ // TODO: update filter in cscan solution when SERVER-12024 is implemented
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: 3, b: {$in: [1, 2]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
+}
+
+// Logically equivalent to the preceding $in query.
+// Indexed solution should be the same.
+// Currently fails - pre-requisite to SERVER-12024
+/*
+TEST_F(QueryPlannerTest, InCompoundIndexLastOrEquivalent) {
+ addIndex(fromjson("{a: 1, b: 1}"));
+ runQuery(fromjson("{$and: [{a: 3}, {$or: [{b: 1}, {b: 2}]}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$and: [{a: 3}, {$or: [{b: 1}, {b: 2}]}]}}}");
+ assertSolutionExists("{fetch: {filter: null, "
+ "node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
+}
+*/
+
+// SERVER-1205
+TEST_F(QueryPlannerTest, InWithSort) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}}"), BSON("b" << 1), BSONObj(), 0, 1);
+
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 1, "
+ "node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a: 1, b: 1}}}, {ixscan: {pattern: {a: 1, b: 1}}}]}}}}");
+}
+
+// SERVER-1205
+TEST_F(QueryPlannerTest, InWithoutSort) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ // No sort means we don't bother to blow up the bounds.
+ runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}}"), BSONObj(), BSONObj(), 0, 1);
+
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}}}}}");
+}
+
+// SERVER-1205
+TEST_F(QueryPlannerTest, ManyInWithSort) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
+ runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}, b:{$in:[1,2]}, c:{$in:[1,2]}}"),
+ BSON("d" << 1),
+ BSONObj(),
+ 0,
+ 1);
+
+ assertSolutionExists(
+ "{sort: {pattern: {d: 1}, limit: 1, "
+ "node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}}]}}}}");
+}
+
+// SERVER-1205
+TEST_F(QueryPlannerTest, TooManyToExplode) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
+ runQuerySortProjSkipLimit(fromjson(
+ "{a: {$in: [1,2,3,4,5,6]},"
+ "b:{$in:[1,2,3,4,5,6,7,8]},"
+ "c:{$in:[1,2,3,4,5,6,7,8]}}"),
+ BSON("d" << 1),
+ BSONObj(),
+ 0,
+ 1);
+
+ // We cap the # of ixscans we're willing to create.
+ assertNumSolutions(2);
+ assertSolutionExists(
+ "{sort: {pattern: {d: 1}, limit: 1, "
+ "node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {d: 1}, limit: 1, node: "
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c:1, d:1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CantExplodeMetaSort) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c"
+ << "text"));
+ runQuerySortProj(fromjson("{a: {$in: [1, 2]}, b: {$in: [3, 4]}}"),
+ fromjson("{c: {$meta: 'textScore'}}"),
+ fromjson("{c: {$meta: 'textScore'}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {c:{$meta:'textScore'}}, node: "
+ "{sort: {pattern: {c:{$meta:'textScore'}}, limit: 0, node: "
+ "{cscan: {filter: {a:{$in:[1,2]},b:{$in:[3,4]}}, dir: 1}}}}}}");
+}
+
+// SERVER-13618: test that exploding scans for sort works even
+// if we must reverse the scan direction.
+TEST_F(QueryPlannerTest, ExplodeMustReverseScans) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1 << "d" << 1));
+ runQuerySortProj(fromjson("{a: {$in: [1, 2]}, b: {$in: [3, 4]}}"), BSON("c" << -1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {c: -1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a:1, b:1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a:1, b:1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a:1, b:1, c:1, d:1}}},"
+ "{ixscan: {pattern: {a:1, b:1, c:1, d:1}}}]}}}}");
+}
+
+// SERVER-13618
+TEST_F(QueryPlannerTest, ExplodeMustReverseScans2) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << -1));
+ runQuerySortProj(fromjson("{a: {$in: [1, 2]}, b: {$in: [3, 4]}}"), BSON("c" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a:1, b:1, c:-1}}},"
+ "{ixscan: {pattern: {a:1, b:1, c:-1}}},"
+ "{ixscan: {pattern: {a:1, b:1, c:-1}}},"
+ "{ixscan: {pattern: {a:1, b:1, c:-1}}}]}}}}");
+}
+
+// SERVER-13752: don't try to explode if the ordered interval list for
+// the leading field of the compound index is empty.
+TEST_F(QueryPlannerTest, CantExplodeWithEmptyBounds) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProj(fromjson("{a: {$in: []}}"), BSON("b" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {b:1}, limit: 0, node: "
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}}}}}}}");
+}
+
+// SERVER-13752
+TEST_F(QueryPlannerTest, CantExplodeWithEmptyBounds2) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ runQuerySortProj(fromjson("{a: {$gt: 3, $lt: 0}}"), BSON("b" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {b:1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {b:1}, limit: 0, node: "
+ "{fetch: {node: {ixscan: {pattern: {a:1,b:1,c:1}}}}}}}");
+}
+
+// SERVER-13754: exploding an $or
+TEST_F(QueryPlannerTest, ExplodeOrForSort) {
+ addIndex(BSON("a" << 1 << "c" << 1));
+ addIndex(BSON("b" << 1 << "c" << 1));
+
+ runQuerySortProj(fromjson("{$or: [{a: 1}, {a: 2}, {b: 2}]}"), BSON("c" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {bounds: {a: [[1,1,true,true]], "
+ "c: [['MinKey','MaxKey',true,true]]},"
+ "pattern: {a:1, c:1}}},"
+ "{ixscan: {bounds: {a: [[2,2,true,true]], "
+ "c: [['MinKey','MaxKey',true,true]]},"
+ "pattern: {a:1, c:1}}},"
+ "{ixscan: {bounds: {b: [[2,2,true,true]], "
+ "c: [['MinKey','MaxKey',true,true]]},"
+ "pattern: {b:1, c:1}}}]}}}}");
+}
+
+// SERVER-13754: exploding an $or
+TEST_F(QueryPlannerTest, ExplodeOrForSort2) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ addIndex(BSON("d" << 1 << "c" << 1));
+
+ runQuerySortProj(
+ fromjson("{$or: [{a: 1, b: {$in: [1, 2]}}, {d: 3}]}"), BSON("c" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {bounds: {a: [[1,1,true,true]], b: [[1,1,true,true]],"
+ "c: [['MinKey','MaxKey',true,true]]},"
+ "pattern: {a:1, b:1, c:1}}},"
+ "{ixscan: {bounds: {a: [[1,1,true,true]], b: [[2,2,true,true]],"
+ "c: [['MinKey','MaxKey',true,true]]},"
+ "pattern: {a:1, b:1, c:1}}},"
+ "{ixscan: {bounds: {d: [[3,3,true,true]], "
+ "c: [['MinKey','MaxKey',true,true]]},"
+ "pattern: {d:1, c:1}}}]}}}}");
+}
+
+// SERVER-13754: an $or that can't be exploded, because one clause of the
+// $or does provide the sort, even after explosion.
+TEST_F(QueryPlannerTest, CantExplodeOrForSort) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ addIndex(BSON("d" << 1 << "c" << 1));
+
+ runQuerySortProj(fromjson("{$or: [{a: {$in: [1, 2]}}, {d: 3}]}"), BSON("c" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {c: 1}, limit: 0, node: "
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {pattern: {a: 1, b: 1, c: 1}}},"
+ "{ixscan: {pattern: {d: 1, c: 1}}}]}}}}}}");
+}
+
+// SERVER-15286: Make sure that at least the explodeForSort() path bails out
+// when it finds that there are no union of point interval fields to explode.
+// We could convert this into a MERGE_SORT plan, but we don't yet do this
+// optimization.
+TEST_F(QueryPlannerTest, CantExplodeOrForSort2) {
+ addIndex(BSON("a" << 1));
+
+ runQuerySortProj(fromjson("{$or: [{a: {$gt: 1, $lt: 3}}, {a: {$gt: 6, $lt: 10}}]}"),
+ BSON("a" << -1),
+ BSONObj());
+
+ assertNumSolutions(3U);
+ assertSolutionExists("{sort: {pattern: {a: -1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {a: -1}, limit: 0, node: "
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {pattern: {a: 1}, bounds: "
+ "{a: [[1,3,false,false]]}}},"
+ "{ixscan: {pattern: {a: 1}, bounds: "
+ "{a: [[6,10,false,false]]}}}]}}}}}}");
+}
+
+// SERVER-13754: too many scans in an $or explosion.
+TEST_F(QueryPlannerTest, TooManyToExplodeOr) {
+ addIndex(BSON("a" << 1 << "e" << 1));
+ addIndex(BSON("b" << 1 << "e" << 1));
+ addIndex(BSON("c" << 1 << "e" << 1));
+ addIndex(BSON("d" << 1 << "e" << 1));
+ runQuerySortProj(fromjson(
+ "{$or: [{a: {$in: [1,2,3,4,5,6]},"
+ "b: {$in: [1,2,3,4,5,6]}},"
+ "{c: {$in: [1,2,3,4,5,6]},"
+ "d: {$in: [1,2,3,4,5,6]}}]}"),
+ BSON("e" << 1),
+ BSONObj());
+
+ // We cap the # of ixscans we're willing to create, so we don't get explosion. Instead
+ // we get 5 different solutions which all use a blocking sort.
+ assertNumSolutions(5U);
+ assertSolutionExists("{sort: {pattern: {e: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {e: 1}, limit: 0, node: "
+ "{or: {nodes: ["
+ "{fetch: {node: {ixscan: {pattern: {a: 1, e: 1}}}}},"
+ "{fetch: {node: {ixscan: {pattern: {c: 1, e: 1}}}}}]}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {e: 1}, limit: 0, node: "
+ "{or: {nodes: ["
+ "{fetch: {node: {ixscan: {pattern: {b: 1, e: 1}}}}},"
+ "{fetch: {node: {ixscan: {pattern: {c: 1, e: 1}}}}}]}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {e: 1}, limit: 0, node: "
+ "{or: {nodes: ["
+ "{fetch: {node: {ixscan: {pattern: {a: 1, e: 1}}}}},"
+ "{fetch: {node: {ixscan: {pattern: {d: 1, e: 1}}}}}]}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {e: 1}, limit: 0, node: "
+ "{or: {nodes: ["
+ "{fetch: {node: {ixscan: {pattern: {b: 1, e: 1}}}}},"
+ "{fetch: {node: {ixscan: {pattern: {d: 1, e: 1}}}}}]}}}}");
+}
+
+// SERVER-15696: Make sure explodeForSort copies filters on IXSCAN stages to all of the
+// scans resulting from the explode. Regex is the easiest way to have the planner create
+// an index scan which filters using the index key.
+TEST_F(QueryPlannerTest, ExplodeIxscanWithFilter) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuerySortProj(fromjson(
+ "{$and: [{b: {$regex: 'foo', $options: 'i'}},"
+ "{a: {$in: [1, 2]}}]}"),
+ BSON("b" << 1),
+ BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a:1, b:1},"
+ "filter: {b: {$regex: 'foo', $options: 'i'}}}},"
+ "{ixscan: {pattern: {a:1, b:1},"
+ "filter: {b: {$regex: 'foo', $options: 'i'}}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, InWithSortAndLimitTrailingField) {
+ addIndex(BSON("a" << 1 << "b" << -1 << "c" << 1));
+ runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}, b: {$gte: 0}}"),
+ fromjson("{b: -1}"),
+ BSONObj(), // no projection
+ 0, // no skip
+ -1); // .limit(1)
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{sort: {pattern: {b:-1}, limit: 1, "
+ "node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{limit: {n: 1, node: {fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a:1,b:-1,c:1}}}, "
+ " {ixscan: {pattern: {a:1,b:-1,c:1}}}]}}}}}}");
+}
+
+//
+// Multiple solutions
+//
+
+TEST_F(QueryPlannerTest, TwoPlans) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuery(fromjson("{a:1, b:{$gt:2,$lt:2}}"));
+
+ // 2 indexed solns and one non-indexed
+ ASSERT_EQUALS(getNumSolutions(), 3U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$and:[{b:{$lt:2}},{a:1},{b:{$gt:2}}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$and:[{b:{$lt:2}},{b:{$gt:2}}]}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, TwoPlansElemMatch) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ addIndex(BSON("arr.x" << 1 << "a" << 1));
+
+ runQuery(fromjson(
+ "{arr: { $elemMatch : { x : 5 , y : 5 } },"
+ " a : 55 , b : { $in : [ 1 , 5 , 8 ] } }"));
+
+ // 2 indexed solns and one non-indexed
+ ASSERT_EQUALS(getNumSolutions(), 3U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, bounds: "
+ "{a: [[55,55,true,true]], b: [[1,1,true,true], "
+ "[5,5,true,true], [8,8,true,true]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$and: [{arr:{$elemMatch:{x:5,y:5}}},"
+ "{b:{$in:[1,5,8]}}]}, "
+ "node: {ixscan: {pattern: {'arr.x':1,a:1}, bounds: "
+ "{'arr.x': [[5,5,true,true]], 'a':[[55,55,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundAndNonCompoundIndices) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1), true);
+ runQuery(fromjson("{a: 1, b: {$gt: 2, $lt: 2}}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 3U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$and:[{b:{$lt:2}},{b:{$gt:2}}]}, node: "
+ "{ixscan: {pattern: {a:1}, bounds: {a: [[1,1,true,true]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b:{$gt:2}}, node: "
+ "{ixscan: {pattern: {a:1,b:1}, bounds: "
+ "{a: [[1,1,true,true]], b: [[-Infinity,2,true,false]]}}}}}");
+}
+
+//
+// Sort orders
+//
+
+// SERVER-1205.
+TEST_F(QueryPlannerTest, MergeSort) {
+ addIndex(BSON("a" << 1 << "c" << 1));
+ addIndex(BSON("b" << 1 << "c" << 1));
+ runQuerySortProj(fromjson("{$or: [{a:1}, {b:1}]}"), fromjson("{c:1}"), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{sort: {pattern: {c: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a: 1, c: 1}}}, {ixscan: {pattern: {b: 1, c: 1}}}]}}}}");
+}
+
+// SERVER-1205 as well.
+TEST_F(QueryPlannerTest, NoMergeSortIfNoSortWanted) {
+ addIndex(BSON("a" << 1 << "c" << 1));
+ addIndex(BSON("b" << 1 << "c" << 1));
+ runQuerySortProj(fromjson("{$or: [{a:1}, {b:1}]}"), BSONObj(), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$or: [{a:1}, {b:1}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a: 1, c: 1}}}, "
+ "{ixscan: {filter: null, pattern: {b: 1, c: 1}}}]}}}}");
+}
+
+// Basic "keep sort in mind with an OR"
+TEST_F(QueryPlannerTest, MergeSortEvenIfSameIndex) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProj(fromjson("{$or: [{a:1}, {a:7}]}"), fromjson("{b:1}"), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{sort: {pattern: {b: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ // TODO the second solution should be mergeSort rather than just sort
+}
+
+TEST_F(QueryPlannerTest, ReverseScanForSort) {
+ addIndex(BSON("_id" << 1));
+ runQuerySortProj(BSONObj(), fromjson("{_id: -1}"), BSONObj());
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{sort: {pattern: {_id: -1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {_id: 1}}}}}");
+}
+
+//
+// Hint tests
+//
+
+TEST_F(QueryPlannerTest, NaturalHint) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuerySortHint(BSON("a" << 1), BSON("b" << 1), BSON("$natural" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, node: "
+ "{cscan: {filter: {a: 1}, dir: 1}}}}");
+}
+
+// Test $natural sort and its interaction with $natural hint.
+TEST_F(QueryPlannerTest, NaturalSortAndHint) {
+ addIndex(BSON("x" << 1));
+
+ // Non-empty query, -1 sort, no hint.
+ runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << -1), BSONObj());
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: -1}}");
+
+ // Non-empty query, 1 sort, no hint.
+ runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << 1), BSONObj());
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+
+ // Non-empty query, -1 sort, -1 hint.
+ runQuerySortHint(
+ fromjson("{x: {$exists: true}}"), BSON("$natural" << -1), BSON("$natural" << -1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: -1}}");
+
+ // Non-empty query, 1 sort, -1 hint.
+ runQuerySortHint(
+ fromjson("{x: {$exists: true}}"), BSON("$natural" << 1), BSON("$natural" << -1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+
+ // Non-empty query, -1 sort, 1 hint.
+ runQuerySortHint(
+ fromjson("{x: {$exists: true}}"), BSON("$natural" << -1), BSON("$natural" << 1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: -1}}");
+
+ // Non-empty query, 1 sort, 1 hint.
+ runQuerySortHint(
+ fromjson("{x: {$exists: true}}"), BSON("$natural" << 1), BSON("$natural" << 1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+
+ // Empty query, -1 sort, no hint.
+ runQuerySortHint(BSONObj(), BSON("$natural" << -1), BSONObj());
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: -1}}");
+
+ // Empty query, 1 sort, no hint.
+ runQuerySortHint(BSONObj(), BSON("$natural" << 1), BSONObj());
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+
+ // Empty query, -1 sort, -1 hint.
+ runQuerySortHint(BSONObj(), BSON("$natural" << -1), BSON("$natural" << -1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: -1}}");
+
+ // Empty query, 1 sort, -1 hint.
+ runQuerySortHint(BSONObj(), BSON("$natural" << 1), BSON("$natural" << -1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+
+ // Empty query, -1 sort, 1 hint.
+ runQuerySortHint(BSONObj(), BSON("$natural" << -1), BSON("$natural" << 1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: -1}}");
+
+ // Empty query, 1 sort, 1 hint.
+ runQuerySortHint(BSONObj(), BSON("$natural" << 1), BSON("$natural" << 1));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, HintOverridesNaturalSort) {
+ addIndex(BSON("x" << 1));
+ runQuerySortHint(fromjson("{x: {$exists: true}}"), BSON("$natural" << -1), BSON("x" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {x:{$exists:true}}, node: "
+ "{ixscan: {filter: null, pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, HintValid) {
+ addIndex(BSON("a" << 1));
+ runQueryHint(BSONObj(), fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, HintValidWithPredicate) {
+ addIndex(BSON("a" << 1));
+ runQueryHint(fromjson("{a: {$gt: 1}}"), fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, HintValidWithSort) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuerySortHint(fromjson("{a: 100, b: 200}"), fromjson("{b: 1}"), fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, node: "
+ "{fetch: {filter: {b: 200}, "
+ "node: {ixscan: {filter: null, pattern: {a: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, HintElemMatch) {
+ // true means multikey
+ addIndex(fromjson("{'a.b': 1}"), true);
+ runQueryHint(fromjson("{'a.b': 1, a: {$elemMatch: {b: 2}}}"), fromjson("{'a.b': 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {$and: [{a:{$elemMatch:{b:2}}}, {'a.b': 1}]}, "
+ "node: {ixscan: {filter: null, pattern: {'a.b': 1}, bounds: "
+ "{'a.b': [[2, 2, true, true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, HintInvalid) {
+ addIndex(BSON("a" << 1));
+ runInvalidQueryHint(BSONObj(), fromjson("{b: 1}"));
+}
+
+//
+// Sparse indices, SERVER-8067
+// Each index in this block of tests is sparse.
+//
+
+TEST_F(QueryPlannerTest, SparseIndexIgnoreForSort) {
+ addIndex(fromjson("{a: 1}"), false, true);
+ runQuerySortProj(BSONObj(), fromjson("{a: 1}"), BSONObj());
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+}
+
+TEST_F(QueryPlannerTest, SparseIndexHintForSort) {
+ addIndex(fromjson("{a: 1}"), false, true);
+ runQuerySortHint(BSONObj(), fromjson("{a: 1}"), fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SparseIndexPreferCompoundIndexForSort) {
+ addIndex(fromjson("{a: 1}"), false, true);
+ addIndex(fromjson("{a: 1, b: 1}"));
+ runQuerySortProj(BSONObj(), fromjson("{a: 1}"), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, SparseIndexForQuery) {
+ addIndex(fromjson("{a: 1}"), false, true);
+ runQuerySortProj(fromjson("{a: 1}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: 1}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}}}}}");
+}
+
+//
+// Regex
+//
+
+TEST_F(QueryPlannerTest, PrefixRegex) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{a: /^foo/}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: /^foo/}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, PrefixRegexCovering) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{a: /^foo/}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{cscan: {dir: 1, filter: {a: /^foo/}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegex) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{a: /foo/}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {a: /foo/}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: {a: /foo/}, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegexCovering) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{a: /foo/}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{cscan: {dir: 1, filter: {a: /foo/}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: {a: /foo/}, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegexAnd) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{a: /foo/, b: 2}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {$and: [{b: 2}, {a: /foo/}]}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: {a: /foo/}, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegexAndCovering) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProj(fromjson("{a: /foo/, b: 2}"), BSONObj(), fromjson("{_id: 0, a: 1, b: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
+ "{cscan: {dir: 1, filter: {$and: [{b: 2}, {a: /foo/}]}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
+ "{ixscan: {filter: {a: /foo/}, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegexOrCovering) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(
+ fromjson("{$or: [{a: /0/}, {a: /1/}]}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{cscan: {dir: 1, filter: {$or: [{a: /0/}, {a: /1/}]}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: {$or: [{a: /0/}, {a: /1/}]}, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegexInCovering) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{a: {$in: [/foo/, /bar/]}}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{cscan: {dir: 1, filter: {a:{$in:[/foo/,/bar/]}}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: {a:{$in:[/foo/,/bar/]}}, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, TwoRegexCompoundIndexCovering) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProj(fromjson("{a: /0/, b: /1/}"), BSONObj(), fromjson("{_id: 0, a: 1, b: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
+ "{cscan: {dir: 1, filter: {$and:[{a:/0/},{b:/1/}]}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1, b: 1}, node: "
+ "{ixscan: {filter: {$and:[{a:/0/},{b:/1/}]}, pattern: {a: 1, b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, TwoRegexSameFieldCovering) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(
+ fromjson("{$and: [{a: /0/}, {a: /1/}]}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{cscan: {dir: 1, filter: {$and:[{a:/0/},{a:/1/}]}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: {$and:[{a:/0/},{a:/1/}]}, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ThreeRegexSameFieldCovering) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(
+ fromjson("{$and: [{a: /0/}, {a: /1/}, {a: /2/}]}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{cscan: {dir: 1, filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NonPrefixRegexMultikey) {
+ // true means multikey
+ addIndex(BSON("a" << 1), true);
+ runQuery(fromjson("{a: /foo/}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {filter: {a: /foo/}, dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: /foo/}, node: {ixscan: "
+ "{pattern: {a: 1}, filter: null}}}}");
+}
+
+TEST_F(QueryPlannerTest, ThreeRegexSameFieldMultikey) {
+ // true means multikey
+ addIndex(BSON("a" << 1), true);
+ runQuery(fromjson("{$and: [{a: /0/}, {a: /1/}, {a: /2/}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 2U);
+ assertSolutionExists("{cscan: {filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}, dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$and:[{a:/0/},{a:/1/},{a:/2/}]}, node: {ixscan: "
+ "{pattern: {a: 1}, filter: null}}}}");
+}
+
+//
+// Negation
+//
+
+TEST_F(QueryPlannerTest, NegationIndexForSort) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{a: {$ne: 1}}"), fromjson("{a: 1}"), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1}, "
+ "bounds: {a: [['MinKey',1,true,false], "
+ "[1,'MaxKey',false,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegationTopLevel) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{a: {$ne: 1}}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [['MinKey',1,true,false], "
+ "[1,'MaxKey',false,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegationOr) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{$or: [{a: 1}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, NegationOrNotIn) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{$or: [{a: 1}, {b: {$nin: [1]}}]}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, NegationAndIndexOnEquality) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{$and: [{a: 1}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1},"
+ "bounds: {a: [[1,1,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegationAndIndexOnEqualityAndNegationBranches) {
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuerySortProj(fromjson("{$and: [{a: 1}, {b: 2}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(3U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1}, "
+ "bounds: {a: [[1,1,true,true]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {b: 1}, "
+ "bounds: {b: [[2,2,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegationAndIndexOnInequality) {
+ addIndex(BSON("b" << 1));
+ runQuerySortProj(fromjson("{$and: [{a: 1}, {b: {$ne: 1}}]}"), BSONObj(), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: {ixscan: {pattern: {b:1}, "
+ "bounds: {b: [['MinKey',1,true,false], "
+ "[1,'MaxKey',false,true]]}}}}}");
+}
+
+// Negated regexes don't use the index.
+TEST_F(QueryPlannerTest, NegationRegexPrefix) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: /^a/}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// Negated mods don't use the index
+TEST_F(QueryPlannerTest, NegationMod) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$mod: [2, 1]}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// Negated $type doesn't use the index
+TEST_F(QueryPlannerTest, NegationTypeOperator) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$type: 16}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// Negated $elemMatch value won't use the index
+TEST_F(QueryPlannerTest, NegationElemMatchValue) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$elemMatch: {$gt: 3, $lt: 10}}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// Negated $elemMatch object won't use the index
+TEST_F(QueryPlannerTest, NegationElemMatchObject) {
+ addIndex(BSON("i.j" << 1));
+ runQuery(fromjson("{i: {$not: {$elemMatch: {j: 1}}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// Negated $elemMatch object won't use the index
+TEST_F(QueryPlannerTest, NegationElemMatchObject2) {
+ addIndex(BSON("i.j" << 1));
+ runQuery(fromjson("{i: {$not: {$elemMatch: {j: {$ne: 1}}}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// If there is a negation that can't use the index,
+// ANDed with a predicate that can use the index, then
+// we can still use the index for the latter predicate.
+TEST_F(QueryPlannerTest, NegationRegexWithIndexablePred) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{$and: [{i: {$not: /o/}}, {i: 2}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {i:1}, "
+ "bounds: {i: [[2,2,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegationCantUseSparseIndex) {
+ // false means not multikey, true means sparse
+ addIndex(BSON("i" << 1), false, true);
+ runQuery(fromjson("{i: {$ne: 4}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, NegationCantUseSparseIndex2) {
+ // false means not multikey, true means sparse
+ addIndex(BSON("i" << 1 << "j" << 1), false, true);
+ runQuery(fromjson("{i: 4, j: {$ne: 5}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {i:1,j:1}, bounds: "
+ "{i: [[4,4,true,true]], j: [['MinKey','MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegatedRangeStrGT) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$gt: 'a'}}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
+ "bounds: {i: [['MinKey','a',true,true], "
+ "[{},'MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegatedRangeStrGTE) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$gte: 'a'}}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
+ "bounds: {i: [['MinKey','a',true,false], "
+ "[{},'MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegatedRangeIntGT) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$gt: 5}}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
+ "bounds: {i: [['MinKey',5,true,true], "
+ "[Infinity,'MaxKey',false,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegatedRangeIntGTE) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson("{i: {$not: {$gte: 5}}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
+ "bounds: {i: [['MinKey',5,true,false], "
+ "[Infinity,'MaxKey',false,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, TwoNegatedRanges) {
+ addIndex(BSON("i" << 1));
+ runQuery(fromjson(
+ "{$and: [{i: {$not: {$lte: 'b'}}}, "
+ "{i: {$not: {$gte: 'f'}}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {i:1}, "
+ "bounds: {i: [['MinKey','',true,false], "
+ "['b','f',false,false], "
+ "[{},'MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, AndWithNestedNE) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{a: {$gt: -1, $lt: 1, $ne: 0}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [[-1,0,false,false], "
+ "[0,1,false,false]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NegatePredOnCompoundIndex) {
+ addIndex(BSON("x" << 1 << "a" << 1));
+ runQuery(fromjson("{x: 1, a: {$ne: 1}, b: {$ne: 2}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {x:1,a:1}, bounds: "
+ "{x: [[1,1,true,true]], "
+ "a: [['MinKey',1,true,false], [1,'MaxKey',false,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NEOnMultikeyIndex) {
+ // true means multikey
+ addIndex(BSON("a" << 1), true);
+ runQuery(fromjson("{a: {$ne: 3}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$ne:3}}, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [['MinKey',3,true,false],"
+ "[3,'MaxKey',false,true]]}}}}}");
+}
+
+// In general, a negated $nin can make use of an index.
+TEST_F(QueryPlannerTest, NinUsesMultikeyIndex) {
+ // true means multikey
+ addIndex(BSON("a" << 1), true);
+ runQuery(fromjson("{a: {$nin: [4, 10]}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$nin:[4,10]}}, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [['MinKey',4,true,false],"
+ "[4,10,false,false],"
+ "[10,'MaxKey',false,true]]}}}}}");
+}
+
+// But it can't if the $nin contains a regex because regex bounds can't
+// be complemented.
+TEST_F(QueryPlannerTest, NinCantUseMultikeyIndex) {
+ // true means multikey
+ addIndex(BSON("a" << 1), true);
+ runQuery(fromjson("{a: {$nin: [4, /foobar/]}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+//
+// Multikey indices
+//
+
+//
+// Index bounds related tests
+//
+
+TEST_F(QueryPlannerTest, CompoundIndexBoundsLastFieldMissing) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ runQuery(fromjson("{a: 5, b: {$gt: 7}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: "
+ "{a: [[5,5,true,true]], b: [[7,Infinity,false,true]], "
+ " c: [['MinKey','MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundIndexBoundsMiddleFieldMissing) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ runQuery(fromjson("{a: 1, c: {$lt: 3}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1, c: 1}, bounds: "
+ "{a: [[1,1,true,true]], b: [['MinKey','MaxKey',true,true]], "
+ " c: [[-Infinity,3,true,false]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundIndexBoundsRangeAndEquality) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{a: {$gt: 8}, b: 6}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1, b: 1}, bounds: "
+ "{a: [[8,Infinity,false,true]], b:[[6,6,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundIndexBoundsEqualityThenIn) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{a: 5, b: {$in: [2,6,11]}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {filter: null, pattern: "
+ "{a: 1, b: 1}, bounds: {a: [[5,5,true,true]], "
+ "b:[[2,2,true,true],[6,6,true,true],[11,11,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundIndexBoundsStringBounds) {
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{a: {$gt: 'foo'}, b: {$gte: 'bar'}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {filter: null, pattern: "
+ "{a: 1, b: 1}, bounds: {a: [['foo',{},false,false]], "
+ "b:[['bar',{},true,false]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, IndexBoundsAndWithNestedOr) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$and: [{a: 1, $or: [{a: 2}, {a: 3}]}]}"));
+
+ // Given that the index over 'a' isn't multikey, we ideally won't generate any solutions
+ // since we know the query describes an empty set if 'a' isn't multikey. Any solutions
+ // below are "this is how it currently works" instead of "this is how it should work."
+
+ // It's kind of iffy to look for indexed solutions so we don't...
+ size_t matches = 0;
+ matches += numSolutionMatches(
+ "{cscan: {dir: 1, filter: "
+ "{$or: [{a: 2, a:1}, {a: 3, a:1}]}}}");
+ matches += numSolutionMatches(
+ "{cscan: {dir: 1, filter: "
+ "{$and: [{$or: [{a: 2}, {a: 3}]}, {a: 1}]}}}");
+ ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
+}
+
+TEST_F(QueryPlannerTest, IndexBoundsIndexedSort) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{$or: [{a: 1}, {a: 2}]}"), BSON("a" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{sort: {pattern: {a:1}, limit: 0, node: "
+ "{cscan: {filter: {$or:[{a:1},{a:2}]}, dir: 1}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {filter: null, "
+ "pattern: {a:1}, bounds: {a: [[1,1,true,true], [2,2,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, IndexBoundsUnindexedSort) {
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{$or: [{a: 1}, {a: 2}]}"), BSON("b" << 1), BSONObj());
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{sort: {pattern: {b:1}, limit: 0, node: "
+ "{cscan: {filter: {$or:[{a:1},{a:2}]}, dir: 1}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {b:1}, limit: 0, node: {fetch: "
+ "{filter: null, node: {ixscan: {filter: null, "
+ "pattern: {a:1}, bounds: {a: [[1,1,true,true], [2,2,true,true]]}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, IndexBoundsUnindexedSortHint) {
+ addIndex(BSON("a" << 1));
+ runQuerySortHint(fromjson("{$or: [{a: 1}, {a: 2}]}"), BSON("b" << 1), BSON("a" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sort: {pattern: {b:1}, limit: 0, node: {fetch: "
+ "{filter: null, node: {ixscan: {filter: null, "
+ "pattern: {a:1}, bounds: {a: [[1,1,true,true], [2,2,true,true]]}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CompoundIndexBoundsIntersectRanges) {
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ addIndex(BSON("a" << 1 << "c" << 1));
+ runQuery(fromjson("{a: {$gt: 1, $lt: 10}, c: {$gt: 1, $lt: 10}}"));
+
+ assertNumSolutions(3U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1,b:1,c:1}, "
+ "bounds: {a: [[1,10,false,false]], "
+ "b: [['MinKey','MaxKey',true,true]], "
+ "c: [[1,10,false,false]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1,c:1}, "
+ "bounds: {a: [[1,10,false,false]], "
+ "c: [[1,10,false,false]]}}}}}");
+}
+
+// Test that planner properly unionizes the index bounds for two negation
+// predicates (SERVER-13890).
+TEST_F(QueryPlannerTest, IndexBoundsOrOfNegations) {
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a: {$ne: 3}}, {a: {$ne: 4}}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a:1}, "
+ "bounds: {a: [['MinKey','MaxKey',true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, BoundsTypeMinKeyMaxKey) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1));
+
+ runQuery(fromjson("{a: {$type: -1}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1}, bounds:"
+ "{a: [['MinKey','MinKey',true,true]]}}}}}");
+
+ runQuery(fromjson("{a: {$type: 127}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {a: 1}, bounds:"
+ "{a: [['MaxKey','MaxKey',true,true]]}}}}}");
+}
+
+//
+// Tests related to building index bounds for multikey
+// indices, combined with compound and $elemMatch
+//
+
+// SERVER-12475: make sure that we compound bounds, even
+// for a multikey index.
+TEST_F(QueryPlannerTest, CompoundMultikeyBounds) {
+ // true means multikey
+ addIndex(BSON("a" << 1 << "b" << 1), true);
+ runQuery(fromjson("{a: 1, b: 3}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {filter: {$and:[{a:1},{b:3}]}, dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {filter: null, "
+ "pattern: {a:1,b:1}, bounds: "
+ "{a: [[1,1,true,true]], b: [[3,3,true,true]]}}}}}");
+}
+
+// Make sure that we compound bounds but do not intersect bounds
+// for a compound multikey index.
+TEST_F(QueryPlannerTest, CompoundMultikeyBoundsNoIntersect) {
+ // true means multikey
+ addIndex(BSON("a" << 1 << "b" << 1), true);
+ runQuery(fromjson("{a: 1, b: {$gt: 3, $lte: 5}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {filter: {b:{$gt:3}}, node: {ixscan: {filter: null, "
+ "pattern: {a:1,b:1}, bounds: "
+ "{a: [[1,1,true,true]], b: [[-Infinity,5,true,true]]}}}}}");
+}
+
+//
+// QueryPlannerParams option tests
+//
+
+TEST_F(QueryPlannerTest, NoBlockingSortsAllowedTest) {
+ params.options = QueryPlannerParams::NO_BLOCKING_SORT;
+ runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
+ assertNumSolutions(0U);
+
+ addIndex(BSON("x" << 1));
+
+ runQuerySortProj(BSONObj(), BSON("x" << 1), BSONObj());
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NoTableScanBasic) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ runQuery(BSONObj());
+ assertNumSolutions(0U);
+
+ addIndex(BSON("x" << 1));
+
+ runQuery(BSONObj());
+ assertNumSolutions(0U);
+
+ runQuery(fromjson("{x: {$gte: 0}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, NoTableScanOrWithAndChild) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{$or: [{a: 20}, {$and: [{a:1}, {b:7}]}]}"));
+
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a: 1}}}, "
+ "{fetch: {filter: {b: 7}, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}}}}}]}}}}");
+}
+
+//
+// Index Intersection.
+//
+// We don't exhaustively check all plans here. Instead we check that there exists an
+// intersection plan. The blending of >1 index plans and ==1 index plans is under development
+// but we want to make sure that we create an >1 index plan when we should.
+//
+
+TEST_F(QueryPlannerTest, IntersectBasicTwoPred) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuery(fromjson("{a:1, b:{$gt: 1}}"));
+
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andHash: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, IntersectBasicTwoPredCompound) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1 << "c" << 1));
+ addIndex(BSON("b" << 1));
+ runQuery(fromjson("{a:1, b:1, c:1}"));
+
+ // There's an andSorted not andHash because the two seeks are point intervals.
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1, c:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+}
+
+// SERVER-12196
+TEST_F(QueryPlannerTest, IntersectBasicTwoPredCompoundMatchesIdxOrder1) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuery(fromjson("{a:1, b:1}"));
+
+ assertNumSolutions(3U);
+
+ assertSolutionExists(
+ "{fetch: {filter: {b:1}, node: "
+ "{ixscan: {filter: null, pattern: {a:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {b:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+}
+
+// SERVER-12196
+TEST_F(QueryPlannerTest, IntersectBasicTwoPredCompoundMatchesIdxOrder2) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{a:1, b:1}"));
+
+ assertNumSolutions(3U);
+
+ assertSolutionExists(
+ "{fetch: {filter: {b:1}, node: "
+ "{ixscan: {filter: null, pattern: {a:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {b:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, IntersectManySelfIntersections) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ // True means multikey.
+ addIndex(BSON("a" << 1), true);
+
+ // This one goes to 11.
+ runQuery(fromjson("{a:1, a:2, a:3, a:4, a:5, a:6, a:7, a:8, a:9, a:10, a:11}"));
+
+ // But this one only goes to 10.
+ assertSolutionExists(
+ "{fetch: {filter: {a:11}, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 1
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 2
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 3
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 4
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 5
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 6
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 7
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 8
+ "{ixscan: {filter: null, pattern: {a:1}}}," // 9
+ "{ixscan: {filter: null, pattern: {a:1}}}]}}}}"); // 10
+}
+
+TEST_F(QueryPlannerTest, IntersectSubtreeNodes) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ addIndex(BSON("d" << 1));
+
+ runQuery(fromjson("{$or: [{a: 1}, {b: 1}], $or: [{c:1}, {d:1}]}"));
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andHash: {nodes: ["
+ "{or: {nodes: [{ixscan:{filter:null, pattern:{a:1}}},"
+ "{ixscan:{filter:null, pattern:{b:1}}}]}},"
+ "{or: {nodes: [{ixscan:{filter:null, pattern:{c:1}}},"
+ "{ixscan:{filter:null, pattern:{d:1}}}]}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, IntersectSubtreeAndPred) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ runQuery(fromjson("{a: 1, $or: [{b:1}, {c:1}]}"));
+
+ // This (can be) rewritten to $or:[ {a:1, b:1}, {c:1, d:1}]. We don't look for the various
+ // single $or solutions as that's tested elsewhere. We look for the intersect solution,
+ // where each AND inside of the root OR is an and_sorted.
+ size_t matches = 0;
+ matches += numSolutionMatches(
+ "{fetch: {filter: null, node: {or: {nodes: ["
+ "{andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {'a':1}}},"
+ "{ixscan: {filter: null, pattern: {'b':1}}}]}},"
+ "{andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {'a':1}}},"
+ "{ixscan: {filter: null, pattern: {'c':1}}}]}}]}}}}");
+ matches += numSolutionMatches(
+ "{fetch: {filter: null, node: {andHash: {nodes:["
+ "{or: {nodes: [{ixscan:{filter:null, pattern:{b:1}}},"
+ "{ixscan:{filter:null, pattern:{c:1}}}]}},"
+ "{ixscan:{filter: null, pattern:{a:1}}}]}}}}");
+ ASSERT_GREATER_THAN_OR_EQUALS(matches, 1U);
+}
+
+TEST_F(QueryPlannerTest, IntersectElemMatch) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a.b" << 1));
+ addIndex(BSON("a.c" << 1));
+ runQuery(fromjson("{a : {$elemMatch: {b:1, c:1}}}"));
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$elemMatch:{b:1, c:1}}},"
+ "node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {'a.b':1}}},"
+ "{ixscan: {filter: null, pattern: {'a.c':1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, IntersectSortFromAndHash) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ runQuerySortProj(fromjson("{a: 1, b:{$gt: 1}}"), fromjson("{b:1}"), BSONObj());
+
+ // This provides the sort.
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andHash: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+
+ // Rearrange the preds, shouldn't matter.
+ runQuerySortProj(fromjson("{b: 1, a:{$lt: 7}}"), fromjson("{b:1}"), BSONObj());
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andHash: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, IntersectCanBeVeryBig) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ addIndex(BSON("d" << 1));
+ runQuery(fromjson(
+ "{$or: [{ 'a' : null, 'b' : 94, 'c' : null, 'd' : null },"
+ "{ 'a' : null, 'b' : 98, 'c' : null, 'd' : null },"
+ "{ 'a' : null, 'b' : 1, 'c' : null, 'd' : null },"
+ "{ 'a' : null, 'b' : 2, 'c' : null, 'd' : null },"
+ "{ 'a' : null, 'b' : 7, 'c' : null, 'd' : null },"
+ "{ 'a' : null, 'b' : 9, 'c' : null, 'd' : null },"
+ "{ 'a' : null, 'b' : 16, 'c' : null, 'd' : null }]}"));
+
+ assertNumSolutions(internalQueryEnumerationMaxOrSolutions);
+}
+
+// Ensure that disabling AND_HASH intersection works properly.
+TEST_F(QueryPlannerTest, IntersectDisableAndHash) {
+ bool oldEnableHashIntersection = internalQueryPlannerEnableHashIntersection;
+
+ // Turn index intersection on but disable hash-based intersection.
+ internalQueryPlannerEnableHashIntersection = false;
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+
+ runQuery(fromjson("{a: {$gt: 1}, b: 1, c: 1}"));
+
+ // We should do an AND_SORT intersection of {b: 1} and {c: 1}, but no AND_HASH plans.
+ assertNumSolutions(4U);
+ assertSolutionExists(
+ "{fetch: {filter: {b: 1, c: 1}, node: {ixscan: "
+ "{pattern: {a: 1}, bounds: {a: [[1,Infinity,false,true]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$gt:1},c:1}, node: {ixscan: "
+ "{pattern: {b: 1}, bounds: {b: [[1,1,true,true]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$gt:1},b:1}, node: {ixscan: "
+ "{pattern: {c: 1}, bounds: {c: [[1,1,true,true]]}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:{$gt:1}}, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {b:1}}},"
+ "{ixscan: {filter: null, pattern: {c:1}}}]}}}}");
+
+ // Restore the old value of the has intersection switch.
+ internalQueryPlannerEnableHashIntersection = oldEnableHashIntersection;
+}
+
+//
+// Index intersection cases for SERVER-12825: make sure that
+// we don't generate an ixisect plan if a compound index is
+// available instead.
+//
+
+// SERVER-12825
+TEST_F(QueryPlannerTest, IntersectCompoundInsteadBasic) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuery(fromjson("{a: 1, b: 1}"));
+
+ assertNumSolutions(3U);
+ assertSolutionExists(
+ "{fetch: {filter: {b:1}, node: "
+ "{ixscan: {filter: null, pattern: {a:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {b:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a:1,b:1}}}}}");
+}
+
+// SERVER-12825
+TEST_F(QueryPlannerTest, IntersectCompoundInsteadThreeCompoundIndices) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1 << "b" << 1));
+ addIndex(BSON("c" << 1 << "d" << 1));
+ addIndex(BSON("a" << 1 << "c" << -1 << "b" << -1 << "d" << 1));
+ runQuery(fromjson("{a: 1, b: 1, c: 1, d: 1}"));
+
+ assertNumSolutions(3U);
+ assertSolutionExists(
+ "{fetch: {filter: {$and: [{c:1},{d:1}]}, node: "
+ "{ixscan: {filter: null, pattern: {a:1,b:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {$and:[{a:1},{b:1}]}, node: "
+ "{ixscan: {filter: null, pattern: {c:1,d:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a:1,c:-1,b:-1,d:1}}}}}");
+}
+
+// SERVER-12825
+TEST_F(QueryPlannerTest, IntersectCompoundInsteadUnusedField) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ runQuery(fromjson("{a: 1, b: 1}"));
+
+ assertNumSolutions(3U);
+ assertSolutionExists(
+ "{fetch: {filter: {b:1}, node: "
+ "{ixscan: {filter: null, pattern: {a:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {b:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a:1,b:1,c:1}}}}}");
+}
+
+// SERVER-12825
+TEST_F(QueryPlannerTest, IntersectCompoundInsteadUnusedField2) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN | QueryPlannerParams::INDEX_INTERSECTION;
+ addIndex(BSON("a" << 1 << "b" << 1));
+ addIndex(BSON("c" << 1 << "d" << 1));
+ addIndex(BSON("a" << 1 << "b" << 1 << "c" << 1));
+ runQuery(fromjson("{a: 1, c: 1}"));
+
+ assertNumSolutions(3U);
+ assertSolutionExists(
+ "{fetch: {filter: {c:1}, node: "
+ "{ixscan: {filter: null, pattern: {a:1,b:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {c:1,d:1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: "
+ "{ixscan: {filter: null, pattern: {a:1,b:1,c:1}}}}}");
+}
+
+//
+// Test that we add a KeepMutations when we should and and we don't add one when we shouldn't.
+//
+
+// Collection scan doesn't keep any state, so it can't produce flagged data.
+TEST_F(QueryPlannerTest, NoMutationsForCollscan) {
+ params.options = QueryPlannerParams::KEEP_MUTATIONS;
+ runQuery(fromjson(""));
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+// Collscan + sort doesn't produce flagged data either.
+TEST_F(QueryPlannerTest, NoMutationsForSort) {
+ params.options = QueryPlannerParams::KEEP_MUTATIONS;
+ runQuerySortProj(fromjson(""), fromjson("{a:1}"), BSONObj());
+ assertSolutionExists("{sort: {pattern: {a: 1}, limit: 0, node: {cscan: {dir: 1}}}}");
+}
+
+// An index scan + fetch requires a keep node as it can flag data. Also make sure we put it in
+// the right place, under the sort.
+TEST_F(QueryPlannerTest, MutationsFromFetch) {
+ params.options = QueryPlannerParams::KEEP_MUTATIONS;
+ addIndex(BSON("a" << 1));
+ runQuerySortProj(fromjson("{a: 5}"), fromjson("{b:1}"), BSONObj());
+ assertSolutionExists(
+ "{sort: {pattern: {b:1}, limit: 0, node: {keep: {node: "
+ "{fetch: {node: {ixscan: {pattern: {a:1}}}}}}}}}");
+}
+
+// Index scan w/covering doesn't require a keep node as there's no fetch.
+TEST_F(QueryPlannerTest, NoFetchNoKeep) {
+ params.options = QueryPlannerParams::KEEP_MUTATIONS;
+ addIndex(BSON("x" << 1));
+ // query, sort, proj
+ runQuerySortProj(fromjson("{ x : {$gt: 1}}"), BSONObj(), fromjson("{_id: 0, x: 1}"));
+
+ // cscan is a soln but we override the params that say to include it.
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, x: 1}, node: {ixscan: "
+ "{filter: null, pattern: {x: 1}}}}}");
+}
+
+// No keep with geoNear.
+TEST_F(QueryPlannerTest, NoKeepWithGeoNear) {
+ params.options = QueryPlannerParams::KEEP_MUTATIONS;
+ addIndex(BSON("a"
+ << "2d"));
+ runQuery(fromjson("{a: {$near: [0,0], $maxDistance:0.3 }}"));
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists("{geoNear2d: {a: '2d'}}");
+}
+
+// No keep when we have an indexed sort.
+TEST_F(QueryPlannerTest, NoKeepWithIndexedSort) {
+ params.options = QueryPlannerParams::KEEP_MUTATIONS;
+ addIndex(BSON("a" << 1 << "b" << 1));
+ runQuerySortProjSkipLimit(fromjson("{a: {$in: [1, 2]}}"), BSON("b" << 1), BSONObj(), 0, 1);
+
+ // cscan solution exists but we didn't turn on the "always include a collscan."
+ assertNumSolutions(1);
+ assertSolutionExists(
+ "{fetch: {node: {mergeSort: {nodes: "
+ "[{ixscan: {pattern: {a: 1, b: 1}}}, {ixscan: {pattern: {a: 1, b: 1}}}]}}}}");
+}
+
+// Make sure a top-level $or hits the limiting number
+// of solutions that we are willing to consider.
+TEST_F(QueryPlannerTest, OrEnumerationLimit) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ // 6 $or clauses, each with 2 indexed predicates
+ // means 2^6 = 64 possibilities. We should hit the limit.
+ runQuery(fromjson(
+ "{$or: [{a: 1, b: 1},"
+ "{a: 2, b: 2},"
+ "{a: 3, b: 3},"
+ "{a: 4, b: 4},"
+ "{a: 5, b: 5},"
+ "{a: 6, b: 6}]}"));
+
+ assertNumSolutions(internalQueryEnumerationMaxOrSolutions);
+}
+
+TEST_F(QueryPlannerTest, OrEnumerationLimit2) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ addIndex(BSON("d" << 1));
+
+ // 3 $or clauses, and a few other preds. Each $or clause can
+ // generate up to the max number of allowed $or enumerations.
+ runQuery(fromjson(
+ "{$or: [{a: 1, b: 1, c: 1, d: 1},"
+ "{a: 2, b: 2, c: 2, d: 2},"
+ "{a: 3, b: 3, c: 3, d: 3}]}"));
+
+ assertNumSolutions(internalQueryEnumerationMaxOrSolutions);
+}
+
+// SERVER-13104: test that we properly enumerate all solutions for nested $or.
+TEST_F(QueryPlannerTest, EnumerateNestedOr) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+
+ runQuery(fromjson("{d: 1, $or: [{a: 1, b: 1}, {c: 1}]}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{fetch: {filter: {d: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}},"
+ "{ixscan: {pattern: {c: 1}}}]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {d: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {a: 1}, node: {ixscan: {pattern: {b: 1}}}}},"
+ "{ixscan: {pattern: {c: 1}}}]}}}}");
+}
+
+// SERVER-13104: test that we properly enumerate all solutions for nested $or.
+TEST_F(QueryPlannerTest, EnumerateNestedOr2) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ addIndex(BSON("d" << 1));
+ addIndex(BSON("e" << 1));
+ addIndex(BSON("f" << 1));
+
+ runQuery(fromjson("{a: 1, b: 1, $or: [{c: 1, d: 1}, {e: 1, f: 1}]}"));
+
+ assertNumSolutions(6U);
+
+ // Four possibilities from indexing the $or.
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {d: 1}, node: {ixscan: {pattern: {c: 1}}}}},"
+ "{fetch: {filter: {f: 1}, node: {ixscan: {pattern: {e: 1}}}}}"
+ "]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}},"
+ "{fetch: {filter: {f: 1}, node: {ixscan: {pattern: {e: 1}}}}}"
+ "]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {d: 1}, node: {ixscan: {pattern: {c: 1}}}}},"
+ "{fetch: {filter: {e: 1}, node: {ixscan: {pattern: {f: 1}}}}}"
+ "]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1, b: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}},"
+ "{fetch: {filter: {e: 1}, node: {ixscan: {pattern: {f: 1}}}}}"
+ "]}}}}");
+
+ // Two possibilties from outside the $or.
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {a: 1}}}}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {b: 1}}}}}");
+}
+
+//
+// Test the "split limited sort stages" hack.
+//
+
+TEST_F(QueryPlannerTest, SplitLimitedSort) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ params.options |= QueryPlannerParams::SPLIT_LIMITED_SORT;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ runQuerySortProjSkipLimit(fromjson("{a: 1}"), fromjson("{b: 1}"), BSONObj(), 0, 3);
+
+ assertNumSolutions(2U);
+ // First solution has no blocking stage; no need to split.
+ assertSolutionExists(
+ "{fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {b: 1}}}}}");
+ // Second solution has a blocking sort with a limit: it gets split and
+ // joined with an OR stage.
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{sort: {pattern: {b: 1}, limit: 3, node: "
+ "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}}}, "
+ "{sort: {pattern: {b: 1}, limit: 0, node: "
+ "{fetch: {node: {ixscan: {pattern: {a: 1}}}}}}}]}}");
+}
+
+// The same query run as a find command with a limit should not require the "split limited sort"
+// hack.
+TEST_F(QueryPlannerTest, NoSplitLimitedSortAsCommand) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ params.options |= QueryPlannerParams::SPLIT_LIMITED_SORT;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ runQueryAsCommand(fromjson("{find: 'testns', filter: {a: 1}, sort: {b: 1}, limit: 3}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{limit: {n: 3, node: {fetch: {filter: {a:1}, node: "
+ "{ixscan: {filter: null, pattern: {b: 1}}}}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 3, node: {fetch: {filter: null,"
+ "node: {ixscan: {pattern: {a: 1}}}}}}}");
+}
+
+// Same query run as a find command with a batchSize rather than a limit should not require
+// the "split limited sort" hack, and should not have any limit represented inside the plan.
+TEST_F(QueryPlannerTest, NoSplitLimitedSortAsCommandBatchSize) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ params.options |= QueryPlannerParams::SPLIT_LIMITED_SORT;
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ runQueryAsCommand(fromjson("{find: 'testns', filter: {a: 1}, sort: {b: 1}, batchSize: 3}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1}, node: {ixscan: "
+ "{filter: null, pattern: {b: 1}}}}}");
+ assertSolutionExists(
+ "{sort: {pattern: {b: 1}, limit: 0, node: {fetch: {filter: null,"
+ "node: {ixscan: {pattern: {a: 1}}}}}}}");
+}
+
+//
+// Test shard filter query planning
+//
+
+TEST_F(QueryPlannerTest, ShardFilterCollScan) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1);
+ addIndex(BSON("a" << 1));
+
+ runQuery(fromjson("{b: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sharding_filter: {node: "
+ "{cscan: {dir: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterBasicIndex) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1);
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ runQuery(fromjson("{b: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{sharding_filter: {node: "
+ "{fetch: {node: "
+ "{ixscan: {pattern: {b: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterBasicCovered) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1);
+ addIndex(BSON("a" << 1));
+
+ runQuery(fromjson("{a: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {node: "
+ "{sharding_filter: {node: "
+ "{ixscan: {pattern: {a: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterBasicProjCovered) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1);
+ addIndex(BSON("a" << 1));
+
+ runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id : 0, a : 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, type: 'coveredIndex', node: "
+ "{sharding_filter: {node: "
+ "{ixscan: {pattern: {a: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterCompoundProjCovered) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1 << "b" << 1);
+ addIndex(BSON("a" << 1 << "b" << 1));
+
+ runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id: 0, a: 1, b: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1, b: 1 }, type: 'coveredIndex', node: "
+ "{sharding_filter: {node: "
+ "{ixscan: {pattern: {a: 1, b: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterNestedProjNotCovered) {
+ // Nested projections can't be covered currently, though the shard key filter shouldn't need
+ // to fetch.
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1 << "b.c" << 1);
+ addIndex(BSON("a" << 1 << "b.c" << 1));
+
+ runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id: 0, a: 1, 'b.c': 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1, 'b.c': 1 }, type: 'default', node: "
+ "{fetch: {node: "
+ "{sharding_filter: {node: "
+ "{ixscan: {pattern: {a: 1, 'b.c': 1}}}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterHashProjNotCovered) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a"
+ << "hashed");
+ addIndex(BSON("a"
+ << "hashed"));
+
+ runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{_id : 0, a : 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0,a: 1}, type: 'simple', node: "
+ "{sharding_filter : {node: "
+ "{fetch: {node: "
+ "{ixscan: {pattern: {a: 'hashed'}}}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterKeyPrefixIndexCovered) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a" << 1);
+ addIndex(BSON("a" << 1 << "b" << 1 << "_id" << 1));
+
+ runQuerySortProj(fromjson("{a: 1}"), BSONObj(), fromjson("{a : 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {a: 1}, type: 'coveredIndex', node: "
+ "{sharding_filter : {node: "
+ "{ixscan: {pattern: {a: 1, b: 1, _id: 1}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ShardFilterNoIndexNotCovered) {
+ params.options = QueryPlannerParams::INCLUDE_SHARD_FILTER;
+ params.shardKey = BSON("a"
+ << "hashed");
+ addIndex(BSON("b" << 1));
+
+ runQuerySortProj(fromjson("{b: 1}"), BSONObj(), fromjson("{_id : 0, a : 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0,a: 1}, type: 'simple', node: "
+ "{sharding_filter : {node: "
+ "{fetch: {node: "
+ "{ixscan: {pattern: {b: 1}}}}}}}}}");
+}
+
+TEST_F(QueryPlannerTest, CannotTrimIxisectParam) {
+ params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
+ params.options |= QueryPlannerParams::INDEX_INTERSECTION;
+ params.options |= QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+
+ runQuery(fromjson("{a: 1, b: 1, c: 1}"));
+
+ assertNumSolutions(3U);
+ assertSolutionExists(
+ "{fetch: {filter: {b: 1, c: 1}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1, c: 1}, node: "
+ "{ixscan: {filter: null, pattern: {b: 1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {a:1,b:1,c:1}, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1}}},"
+ "{ixscan: {filter: null, pattern: {b:1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, CannotTrimIxisectParamBeneathOr) {
+ params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
+ params.options |= QueryPlannerParams::INDEX_INTERSECTION;
+ params.options |= QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+
+ runQuery(fromjson("{d: 1, $or: [{a: 1}, {b: 1, c: 1}]}"));
+
+ assertNumSolutions(3U);
+
+ assertSolutionExists(
+ "{fetch: {filter: {d: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {c: 1}, node: {ixscan: {filter: null,"
+ "pattern: {b: 1}, bounds: {b: [[1,1,true,true]]}}}}},"
+ "{ixscan: {filter: null, pattern: {a: 1},"
+ "bounds: {a: [[1,1,true,true]]}}}]}}}}");
+
+ assertSolutionExists(
+ "{fetch: {filter: {d: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {b: 1}, node: {ixscan: {filter: null,"
+ "pattern: {c: 1}, bounds: {c: [[1,1,true,true]]}}}}},"
+ "{ixscan: {filter: null, pattern: {a: 1},"
+ "bounds: {a: [[1,1,true,true]]}}}]}}}}");
+
+ assertSolutionExists(
+ "{fetch: {filter: {d: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {b: 1, c: 1}, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {b: 1}}},"
+ "{ixscan: {filter: null, pattern: {c: 1}}}]}}}},"
+ "{ixscan: {filter: null, pattern: {a: 1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, CannotTrimIxisectAndHashWithOrChild) {
+ params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
+ params.options |= QueryPlannerParams::INDEX_INTERSECTION;
+ params.options |= QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+
+ runQuery(fromjson("{c: 1, $or: [{a: 1}, {b: 1, d: 1}]}"));
+
+ assertNumSolutions(3U);
+
+ assertSolutionExists(
+ "{fetch: {filter: {c: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {d: 1}, node: {ixscan: {filter: null,"
+ "pattern: {b: 1}, bounds: {b: [[1,1,true,true]]}}}}},"
+ "{ixscan: {filter: null, pattern: {a: 1},"
+ "bounds: {a: [[1,1,true,true]]}}}]}}}}");
+
+ assertSolutionExists(
+ "{fetch: {filter: {$or:[{b:1,d:1},{a:1}]}, node:"
+ "{ixscan: {filter: null, pattern: {c: 1}}}}}");
+
+ assertSolutionExists(
+ "{fetch: {filter: {c:1,$or:[{a:1},{b:1,d:1}]}, node:{andHash:{nodes:["
+ "{or: {nodes: ["
+ "{fetch: {filter: {d:1}, node: {ixscan: {pattern: {b: 1}}}}},"
+ "{ixscan: {filter: null, pattern: {a: 1}}}]}},"
+ "{ixscan: {filter: null, pattern: {c: 1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, CannotTrimIxisectParamSelfIntersection) {
+ params.options = QueryPlannerParams::CANNOT_TRIM_IXISECT;
+ params.options = QueryPlannerParams::INDEX_INTERSECTION;
+ params.options |= QueryPlannerParams::NO_TABLE_SCAN;
+
+ // true means multikey
+ addIndex(BSON("a" << 1), true);
+
+ runQuery(fromjson("{a: {$all: [1, 2, 3]}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{fetch: {filter: {$and: [{a:2}, {a:3}]}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {andSorted: {nodes: ["
+ "{ixscan: {filter: null, pattern: {a:1},"
+ "bounds: {a: [[1,1,true,true]]}}},"
+ "{ixscan: {filter: null, pattern: {a:1},"
+ "bounds: {a: [[2,2,true,true]]}}},"
+ "{ixscan: {filter: null, pattern: {a:1},"
+ "bounds: {a: [[3,3,true,true]]}}}]}}}}");
+}
+
+
+// If a lookup against a unique index is available as a possible plan, then the planner
+// should not generate other possibilities.
+TEST_F(QueryPlannerTest, UniqueIndexLookup) {
+ params.options = QueryPlannerParams::INDEX_INTERSECTION;
+ params.options |= QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1),
+ false, // multikey
+ false, // sparse,
+ true); // unique
+
+ runQuery(fromjson("{a: 1, b: 1}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: 1}, node: "
+ "{ixscan: {filter: null, pattern: {b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, HintOnNonUniqueIndex) {
+ params.options = QueryPlannerParams::INDEX_INTERSECTION;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1),
+ false, // multikey
+ false, // sparse,
+ true); // unique
+
+ runQueryHint(fromjson("{a: 1, b: 1}"), BSON("a" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {b: 1}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, UniqueIndexLookupBelowOr) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ addIndex(BSON("d" << 1),
+ false, // multikey
+ false, // sparse,
+ true); // unique
+
+ runQuery(fromjson("{$or: [{a: 1, b: 1}, {c: 1, d: 1}]}"));
+
+ // Only two plans because we throw out plans for the right branch of the $or that do not
+ // use equality over the unique index.
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {a: 1}, node: {ixscan: {pattern: {b: 1}}}}},"
+ "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}]}}");
+ assertSolutionExists(
+ "{or: {nodes: ["
+ "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}},"
+ "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}]}}");
+}
+
+TEST_F(QueryPlannerTest, UniqueIndexLookupBelowOrBelowAnd) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1));
+ addIndex(BSON("b" << 1));
+ addIndex(BSON("c" << 1));
+ addIndex(BSON("d" << 1),
+ false, // multikey
+ false, // sparse,
+ true); // unique
+
+ runQuery(fromjson("{e: 1, $or: [{a: 1, b: 1}, {c: 1, d: 1}]}"));
+
+ // Only two plans because we throw out plans for the right branch of the $or that do not
+ // use equality over the unique index.
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{fetch: {filter: {e: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {a: 1}, node: {ixscan: {pattern: {b: 1}}}}},"
+ "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}"
+ "]}}}}");
+ assertSolutionExists(
+ "{fetch: {filter: {e: 1}, node: {or: {nodes: ["
+ "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}},"
+ "{fetch: {filter: {c: 1}, node: {ixscan: {pattern: {d: 1}}}}}"
+ "]}}}}");
+}
+
+TEST_F(QueryPlannerTest, CoveredOrUniqueIndexLookup) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+
+ addIndex(BSON("a" << 1 << "b" << 1));
+ addIndex(BSON("a" << 1),
+ false, // multikey
+ false, // sparse,
+ true); // unique
+
+ runQuerySortProj(fromjson("{a: 1, b: 1}"), BSONObj(), fromjson("{_id: 0, a: 1}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{fetch: {filter: {b: 1}, node: {ixscan: {pattern: {a: 1}}}}}}}");
+ assertSolutionExists(
+ "{proj: {spec: {_id: 0, a: 1}, node: "
+ "{ixscan: {filter: null, pattern: {a: 1, b: 1}}}}}");
+}
+
+//
+// Test bad input to query planner helpers.
+//
+
+TEST(BadInputTest, CacheDataFromTaggedTree) {
+ PlanCacheIndexTree* indexTree;
+
+ // Null match expression.
+ std::vector<IndexEntry> relevantIndices;
+ Status s = QueryPlanner::cacheDataFromTaggedTree(NULL, relevantIndices, &indexTree);
+ ASSERT_NOT_OK(s);
+ ASSERT(NULL == indexTree);
+
+ // No relevant index matching the index tag.
+ relevantIndices.push_back(IndexEntry(BSON("a" << 1)));
+
+ CanonicalQuery* cq;
+ Status cqStatus = CanonicalQuery::canonicalize("ns", BSON("a" << 3), &cq);
+ ASSERT_OK(cqStatus);
+ std::unique_ptr<CanonicalQuery> scopedCq(cq);
+ scopedCq->root()->setTag(new IndexTag(1));
+
+ s = QueryPlanner::cacheDataFromTaggedTree(scopedCq->root(), relevantIndices, &indexTree);
+ ASSERT_NOT_OK(s);
+ ASSERT(NULL == indexTree);
+}
+
+TEST(BadInputTest, TagAccordingToCache) {
+ CanonicalQuery* cq;
+ Status cqStatus = CanonicalQuery::canonicalize("ns", BSON("a" << 3), &cq);
+ ASSERT_OK(cqStatus);
+ std::unique_ptr<CanonicalQuery> scopedCq(cq);
+
+ std::unique_ptr<PlanCacheIndexTree> indexTree(new PlanCacheIndexTree());
+ indexTree->setIndexEntry(IndexEntry(BSON("a" << 1)));
+
+ std::map<BSONObj, size_t> indexMap;
+
+ // Null filter.
+ Status s = QueryPlanner::tagAccordingToCache(NULL, indexTree.get(), indexMap);
+ ASSERT_NOT_OK(s);
+
+ // Null indexTree.
+ s = QueryPlanner::tagAccordingToCache(scopedCq->root(), NULL, indexMap);
+ ASSERT_NOT_OK(s);
+
+ // Index not found.
+ s = QueryPlanner::tagAccordingToCache(scopedCq->root(), indexTree.get(), indexMap);
+ ASSERT_NOT_OK(s);
+
+ // Index found once added to the map.
+ indexMap[BSON("a" << 1)] = 0;
+ s = QueryPlanner::tagAccordingToCache(scopedCq->root(), indexTree.get(), indexMap);
+ ASSERT_OK(s);
+
+ // Regenerate canonical query in order to clear tags.
+ cqStatus = CanonicalQuery::canonicalize("ns", BSON("a" << 3), &cq);
+ ASSERT_OK(cqStatus);
+ scopedCq.reset(cq);
+
+ // Mismatched tree topology.
+ PlanCacheIndexTree* child = new PlanCacheIndexTree();
+ child->setIndexEntry(IndexEntry(BSON("a" << 1)));
+ indexTree->children.push_back(child);
+ s = QueryPlanner::tagAccordingToCache(scopedCq->root(), indexTree.get(), indexMap);
+ ASSERT_NOT_OK(s);
+}
} // namespace