summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryarai <yuta.arai@10gen.com>2018-09-06 14:23:22 -0400
committeryarai <yuta.arai@10gen.com>2018-09-20 09:24:19 -0400
commitd5c17e385f543f0afdc4acd5331cc5514549981f (patch)
tree3ade8c3d608169b5a99c4bd2ff31bf8ccc927b30
parent6818230171cb12727892802c608ba9247815ef06 (diff)
downloadmongo-d5c17e385f543f0afdc4acd5331cc5514549981f.tar.gz
SERVER-35331 Allow hinting an all paths index
-rw-r--r--jstests/core/count_hint.js5
-rw-r--r--jstests/noPassthroughWithMongod/all_paths_hint.js125
-rw-r--r--src/mongo/db/query/planner_ixselect.cpp43
-rw-r--r--src/mongo/db/query/planner_ixselect.h16
-rw-r--r--src/mongo/db/query/query_planner.cpp210
-rw-r--r--src/mongo/db/query/query_planner_all_paths_index_test.cpp91
6 files changed, 351 insertions, 139 deletions
diff --git a/jstests/core/count_hint.js b/jstests/core/count_hint.js
index 7cbfc50dc0e..d508a46fd1a 100644
--- a/jstests/core/count_hint.js
+++ b/jstests/core/count_hint.js
@@ -54,10 +54,9 @@
coll.find({i: 1}).hint("BAD HINT").count();
});
- // Test that a bad hint fails with the correct error code. Also verify that the error message
- // mentions a bad hint.
+ // Test that a bad hint fails with the correct error code.
let cmdRes = db.runCommand({count: coll.getName(), hint: {bad: 1, hint: 1}});
assert.commandFailedWithCode(cmdRes, ErrorCodes.BadValue, tojson(cmdRes));
- var regex = new RegExp("bad hint");
+ var regex = new RegExp("hint provided does not correspond to an existing index");
assert(regex.test(cmdRes.errmsg));
})();
diff --git a/jstests/noPassthroughWithMongod/all_paths_hint.js b/jstests/noPassthroughWithMongod/all_paths_hint.js
new file mode 100644
index 00000000000..369488af54c
--- /dev/null
+++ b/jstests/noPassthroughWithMongod/all_paths_hint.js
@@ -0,0 +1,125 @@
+/**
+ * Tests that $** indexes obey hinting.
+ * TODO: SERVER-36198: Move this test back to jstests/core/
+ */
+(function() {
+ "use strict";
+
+ load("jstests/aggregation/extras/utils.js"); // For arrayEq.
+ load("jstests/libs/analyze_plan.js"); // For getPlanStages.
+
+ const coll = db.all_paths_hint;
+ coll.drop();
+
+ const assertArrayEq = (l, r) => assert(arrayEq(l, r), tojson(l) + " != " + tojson(r));
+
+ // Extracts the winning plan for the given query and hint from the explain output.
+ const winningPlan = (query, hint) =>
+ assert.commandWorked(coll.find(query).hint(hint).explain()).queryPlanner.winningPlan;
+
+ // Runs the given query and confirms that:
+ // (1) the expected index was used to answer the query, and
+ // (2) the results produced by the index match the given 'expectedResults'.
+ function assertExpectedIndexAnswersQueryWithHint(
+ query, hint, expectedIndexName, expectedResults) {
+ const ixScans = getPlanStages(winningPlan(query, hint), "IXSCAN");
+ assert.gt(ixScans.length, 0, tojson(coll.find(query).hint(hint).explain()));
+ ixScans.forEach((ixScan) => assert.eq(ixScan.indexName, expectedIndexName));
+
+ const allPathsResults = coll.find(query, {_id: 0}).hint(hint).toArray();
+ assertArrayEq(allPathsResults, expectedResults);
+ }
+
+ assert.commandWorked(
+ db.adminCommand({setParameter: 1, internalQueryAllowAllPathsIndexes: true}));
+
+ try {
+ assert.commandWorked(db.createCollection(coll.getName()));
+
+ // Check that error is thrown if the hinted index doesn't exist.
+ assert.commandFailedWithCode(
+ db.runCommand({find: coll.getName(), filter: {"a": 1}, hint: {"$**": 1}}),
+ ErrorCodes.BadValue);
+
+ assert.commandWorked(coll.createIndex({"$**": 1}));
+
+ assert.commandWorked(coll.insert({_id: 10, a: 1, b: 1, c: {d: 1, e: 1}}));
+ assert.commandWorked(coll.insert({a: 1, b: 2, c: {d: 2, e: 2}}));
+ assert.commandWorked(coll.insert({a: 2, b: 2, c: {d: 1, e: 2}}));
+ assert.commandWorked(coll.insert({a: 2, b: 1, c: {d: 2, e: 2}}));
+ assert.commandWorked(coll.insert({a: 2, b: 2, c: {e: 2}}));
+
+ // Hint a $** index without a competing index.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"a": 1},
+ {"$**": 1},
+ "$**_1",
+ [{a: 1, b: 1, c: {d: 1, e: 1}}, {a: 1, b: 2, c: {d: 2, e: 2}}]);
+
+ assert.commandWorked(coll.createIndex({"a": 1}));
+
+ // Hint a $** index with a competing index.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"a": 1},
+ {"$**": 1},
+ "$**_1",
+ [{a: 1, b: 1, c: {d: 1, e: 1}}, {a: 1, b: 2, c: {d: 2, e: 2}}]);
+
+ // Hint a $** index with a competing _id index.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"a": 1, "_id": 10}, {"$**": 1}, "$**_1", [{a: 1, b: 1, c: {d: 1, e: 1}}]);
+
+ // Hint a regular index with a competing $** index.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"a": 1},
+ {"a": 1},
+ "a_1",
+ [{a: 1, b: 1, c: {d: 1, e: 1}}, {a: 1, b: 2, c: {d: 2, e: 2}}]);
+
+ // Query on fields that not all documents in the collection have with $** index hint.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"c.d": 1},
+ {"$**": 1},
+ "$**_1",
+ [{a: 1, b: 1, c: {d: 1, e: 1}}, {a: 2, b: 2, c: {d: 1, e: 2}}]);
+
+ // Adding another all paths index with a path specified.
+ assert.commandWorked(coll.createIndex({"c.$**": 1}));
+
+ // Hint on path that is not in query argument.
+ assert.commandFailedWithCode(
+ db.runCommand({find: coll.getName(), filter: {"a": 1}, hint: {"c.$**": 1}}),
+ ErrorCodes.BadValue);
+
+ // Hint on a path specified $** index.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"c.d": 1},
+ {"c.$**": 1},
+ "c.$**_1",
+ [{a: 2, b: 2, c: {d: 1, e: 2}}, {a: 1, b: 1, c: {d: 1, e: 1}}]);
+
+ // Min/max with $** index hint.
+ // TODO SERVER-35335: Confirm expected $** min/max behavior when hint is specified.
+ assert.commandFailedWithCode(
+ db.runCommand(
+ {find: coll.getName(), filter: {"b": 1}, min: {"a": 1}, hint: {"$**": 1}}),
+ ErrorCodes.BadValue);
+
+ // Hint a $** index on a query with compound fields.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"a": 1, "c.e": 1}, {"$**": 1}, "$**_1", [{a: 1, b: 1, c: {d: 1, e: 1}}]);
+
+ // Hint a $** index by name.
+ assertExpectedIndexAnswersQueryWithHint(
+ {"a": 1},
+ "$**_1",
+ "$**_1",
+ [{a: 1, b: 1, c: {d: 1, e: 1}}, {a: 1, b: 2, c: {d: 2, e: 2}}]);
+ } finally {
+ // Disable $** indexes once the tests have either completed or failed.
+ db.adminCommand({setParameter: 1, internalQueryAllowAllPathsIndexes: false});
+
+ // TODO: SERVER-36444 remove calls to drop() once wildcard index validation works.
+ coll.drop();
+ }
+})(); \ No newline at end of file
diff --git a/src/mongo/db/query/planner_ixselect.cpp b/src/mongo/db/query/planner_ixselect.cpp
index 70deacf2d5b..b6b2633a944 100644
--- a/src/mongo/db/query/planner_ixselect.cpp
+++ b/src/mongo/db/query/planner_ixselect.cpp
@@ -313,22 +313,53 @@ void QueryPlannerIXSelect::getFields(const MatchExpression* node,
}
// static
-void QueryPlannerIXSelect::findRelevantIndices(const stdx::unordered_set<std::string>& fields,
- const std::vector<IndexEntry>& allIndices,
- std::vector<IndexEntry>* out) {
+std::vector<IndexEntry> QueryPlannerIXSelect::findIndexesByHint(
+ const BSONObj& hintedIndex, const std::vector<IndexEntry>& allIndices) {
+ std::vector<IndexEntry> out;
+ BSONElement firstHintElt = hintedIndex.firstElement();
+ if (firstHintElt.fieldNameStringData() == "$hint"_sd &&
+ firstHintElt.type() == BSONType::String) {
+ auto hintName = firstHintElt.valueStringData();
+ for (auto&& entry : allIndices) {
+ if (entry.identifier.catalogName == hintName) {
+ LOG(5) << "Hint by name specified, restricting indices to "
+ << entry.keyPattern.toString();
+ out.push_back(entry);
+ }
+ }
+ } else {
+ for (auto&& entry : allIndices) {
+ if (SimpleBSONObjComparator::kInstance.evaluate(entry.keyPattern == hintedIndex)) {
+ LOG(5) << "Hint specified, restricting indices to " << hintedIndex.toString();
+ out.push_back(entry);
+ }
+ }
+ }
+
+ return out;
+}
+
+// static
+std::vector<IndexEntry> QueryPlannerIXSelect::findRelevantIndices(
+ const stdx::unordered_set<std::string>& fields, const std::vector<IndexEntry>& allIndices) {
+
+ std::vector<IndexEntry> out;
for (auto&& entry : allIndices) {
BSONObjIterator it(entry.keyPattern);
BSONElement elt = it.next();
if (fields.end() != fields.find(elt.fieldName())) {
- out->push_back(entry);
+ out.push_back(entry);
}
}
+
+ return out;
}
std::vector<IndexEntry> QueryPlannerIXSelect::expandIndexes(
- const stdx::unordered_set<std::string>& fields, const std::vector<IndexEntry>& allIndexes) {
+ const stdx::unordered_set<std::string>& fields,
+ const std::vector<IndexEntry>& relevantIndices) {
std::vector<IndexEntry> out;
- for (auto&& entry : allIndexes) {
+ for (auto&& entry : relevantIndices) {
if (entry.type == IndexType::INDEX_ALLPATHS) {
expandIndex(entry, fields, &out);
} else {
diff --git a/src/mongo/db/query/planner_ixselect.h b/src/mongo/db/query/planner_ixselect.h
index b0181fab150..e21e88b838a 100644
--- a/src/mongo/db/query/planner_ixselect.h
+++ b/src/mongo/db/query/planner_ixselect.h
@@ -60,12 +60,18 @@ public:
stdx::unordered_set<std::string>* out);
/**
- * Find all indices prefixed by fields we have predicates over. Only these indices are
+ * Finds all indices that correspond to the hinted index. Matches the index both by name and by
+ * key pattern.
+ */
+ static std::vector<IndexEntry> findIndexesByHint(const BSONObj& hintedIndex,
+ const std::vector<IndexEntry>& allIndices);
+
+ /**
+ * Finds all indices prefixed by fields we have predicates over. Only these indices are
* useful in answering the query.
*/
- static void findRelevantIndices(const stdx::unordered_set<std::string>& fields,
- const std::vector<IndexEntry>& indices,
- std::vector<IndexEntry>* out);
+ static std::vector<IndexEntry> findRelevantIndices(
+ const stdx::unordered_set<std::string>& fields, const std::vector<IndexEntry>& allIndices);
/**
* Return true if the index key pattern field 'keyPatternElt' (which belongs to 'index' and is
@@ -143,7 +149,7 @@ public:
* "expanded" indexes (where the $** indexes in the given list have been expanded).
*/
static std::vector<IndexEntry> expandIndexes(const stdx::unordered_set<std::string>& fields,
- const std::vector<IndexEntry>& allIndexes);
+ const std::vector<IndexEntry>& relevantIndices);
private:
/**
diff --git a/src/mongo/db/query/query_planner.cpp b/src/mongo/db/query/query_planner.cpp
index 7f867dc025e..6ce7d6fa7cc 100644
--- a/src/mongo/db/query/query_planner.cpp
+++ b/src/mongo/db/query/query_planner.cpp
@@ -39,6 +39,7 @@
#include "mongo/bson/simple_bsonelement_comparator.h"
#include "mongo/db/bson/dotted_path_support.h"
#include "mongo/db/index/all_paths_key_generator.h"
+#include "mongo/db/index_names.h"
#include "mongo/db/matcher/expression_algo.h"
#include "mongo/db/matcher/expression_geo.h"
#include "mongo/db/matcher/expression_text.h"
@@ -574,74 +575,64 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
}
}
+ // Hints require us to only consider the hinted index. If index filters in the query settings
+ // were used to override the allowed indices for planning, we should not use the hinted index
+ // requested in the query.
+ BSONObj hintedIndex;
+ if (!params.indexFiltersApplied) {
+ hintedIndex = query.getQueryRequest().getHint();
+ }
+
+ // Either the list of indices passed in by the caller, or the list of indices filtered according
+ // to the hint. This list is later expanded in order to allow the planner to handle wildcard
+ // indexes.
+ std::vector<IndexEntry> fullIndexList;
+
+ if (hintedIndex.isEmpty()) {
+ fullIndexList = params.indices;
+ } else {
+ fullIndexList = QueryPlannerIXSelect::findIndexesByHint(hintedIndex, params.indices);
+
+ if (fullIndexList.empty()) {
+ return Status(ErrorCodes::BadValue,
+ "hint provided does not correspond to an existing index");
+ }
+ if (fullIndexList.size() > 1) {
+ return Status(ErrorCodes::IndexNotFound,
+ str::stream() << "Hint matched multiple indexes, "
+ << "must hint by index name. Matched: "
+ << fullIndexList[0].toString()
+ << " and "
+ << fullIndexList[1].toString());
+ }
+ }
+
// Figure out what fields we care about.
stdx::unordered_set<string> fields;
QueryPlannerIXSelect::getFields(query.root(), &fields);
-
for (stdx::unordered_set<string>::const_iterator it = fields.begin(); it != fields.end();
++it) {
LOG(5) << "Predicate over field '" << *it << "'";
}
- vector<IndexEntry> expandedIndexes =
- QueryPlannerIXSelect::expandIndexes(fields, params.indices);
-
- // Filter our indices so we only look at indices that are over our predicates.
- vector<IndexEntry> relevantIndices;
+ fullIndexList = QueryPlannerIXSelect::expandIndexes(fields, std::move(fullIndexList));
+ std::vector<IndexEntry> relevantIndices;
- // Hints require us to only consider the hinted index.
- // If index filters in the query settings were used to override
- // the allowed indices for planning, we should not use the hinted index
- // requested in the query.
- BSONObj hintIndex;
- if (!params.indexFiltersApplied) {
- hintIndex = query.getQueryRequest().getHint();
- }
-
- boost::optional<size_t> hintIndexNumber;
-
- if (hintIndex.isEmpty()) {
- QueryPlannerIXSelect::findRelevantIndices(fields, expandedIndexes, &relevantIndices);
+ if (hintedIndex.isEmpty()) {
+ relevantIndices = QueryPlannerIXSelect::findRelevantIndices(fields, fullIndexList);
} else {
- // Sigh. If the hint is specified it might be using the index name.
- BSONElement firstHintElt = hintIndex.firstElement();
- if (str::equals("$hint", firstHintElt.fieldName()) && String == firstHintElt.type()) {
- string hintName = firstHintElt.String();
- for (size_t i = 0; i < params.indices.size(); ++i) {
- if (params.indices[i].identifier.catalogName == hintName) {
- LOG(5) << "Hint by name specified, restricting indices to "
- << params.indices[i].keyPattern.toString();
- relevantIndices.clear();
- relevantIndices.push_back(params.indices[i]);
- hintIndexNumber = i;
- hintIndex = params.indices[i].keyPattern;
- break;
- }
- }
- } else {
- for (size_t i = 0; i < params.indices.size(); ++i) {
- if (0 == params.indices[i].keyPattern.woCompare(hintIndex)) {
- relevantIndices.clear();
- relevantIndices.push_back(params.indices[i]);
- LOG(5) << "Hint specified, restricting indices to " << hintIndex.toString();
- if (hintIndexNumber) {
- return Status(ErrorCodes::IndexNotFound,
- str::stream() << "Hint matched multiple indexes, "
- << "must hint by index name. Matched: "
- << params.indices[i].toString()
- << " and "
- << params.indices[*hintIndexNumber].toString());
- }
- hintIndexNumber = i;
- }
- }
- }
+ relevantIndices = fullIndexList;
- if (!hintIndexNumber) {
- return Status(ErrorCodes::BadValue, "bad hint");
+ // Relevant indices should only ever exceed a size of 1 when there is a hint in the case of
+ // $** index.
+ if (relevantIndices.size() > 1) {
+ for (auto&& entry : relevantIndices) {
+ invariant(entry.type == IndexType::INDEX_ALLPATHS);
+ }
}
}
+ // TODO SERVER-35335 Ensure min/max can generate bounds over $** index.
// Deal with the .min() and .max() query options. If either exist we can only use an index
// that matches the object inside.
if (!query.getQueryRequest().getMin().isEmpty() ||
@@ -656,63 +647,31 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
BSONObj finishedMinObj;
BSONObj finishedMaxObj;
- // This is the index into params.indices[...] that we use.
+ // Index into the 'fulledIndexList' vector indicating the index that we will use to answer
+ // this min/max query.
size_t idxNo = numeric_limits<size_t>::max();
- // If there's an index hinted we need to be able to use it.
- if (!hintIndex.isEmpty()) {
- invariant(hintIndexNumber);
- const auto& hintedIndexEntry = params.indices[*hintIndexNumber];
-
- if (!minObj.isEmpty() &&
- !indexCompatibleMaxMin(minObj, query.getCollator(), hintedIndexEntry)) {
- LOG(5) << "Minobj doesn't work with hint";
- return Status(ErrorCodes::BadValue, "hint provided does not work with min query");
- }
-
- if (!maxObj.isEmpty() &&
- !indexCompatibleMaxMin(maxObj, query.getCollator(), hintedIndexEntry)) {
- LOG(5) << "Maxobj doesn't work with hint";
- return Status(ErrorCodes::BadValue, "hint provided does not work with max query");
- }
-
- finishedMinObj = finishMinObj(hintedIndexEntry, minObj, maxObj);
- finishedMaxObj = finishMaxObj(hintedIndexEntry, minObj, maxObj);
-
- // The min must be less than the max for the hinted index ordering.
- if (0 <= finishedMinObj.woCompare(finishedMaxObj, hintedIndexEntry.keyPattern, false)) {
- LOG(5) << "Minobj/Maxobj don't work with hint";
- return Status(ErrorCodes::BadValue,
- "hint provided does not work with min/max query");
- }
-
- idxNo = *hintIndexNumber;
- } else {
- // No hinted index, look for one that is compatible (has same field names and
- // ordering thereof).
- for (size_t i = 0; i < params.indices.size(); ++i) {
- const auto& indexEntry = params.indices[i];
-
- BSONObj toUse = minObj.isEmpty() ? maxObj : minObj;
- if (indexCompatibleMaxMin(toUse, query.getCollator(), indexEntry)) {
- // In order to be fully compatible, the min has to be less than the max
- // according to the index key pattern ordering. The first step in verifying
- // this is "finish" the min and max by replacing empty objects and stripping
- // field names.
- finishedMinObj = finishMinObj(indexEntry, minObj, maxObj);
- finishedMaxObj = finishMaxObj(indexEntry, minObj, maxObj);
-
- // Now we have the final min and max. This index is only relevant for
- // the min/max query if min < max.
- if (0 >
- finishedMinObj.woCompare(finishedMaxObj, indexEntry.keyPattern, false)) {
- // Found a relevant index.
- idxNo = i;
- break;
- }
-
- // This index is not relevant; move on to the next.
+ for (size_t i = 0; i < fullIndexList.size(); ++i) {
+ const auto& indexEntry = fullIndexList[i];
+
+ const BSONObj toUse = minObj.isEmpty() ? maxObj : minObj;
+ if (indexCompatibleMaxMin(toUse, query.getCollator(), indexEntry)) {
+ // In order to be fully compatible, the min has to be less than the max
+ // according to the index key pattern ordering. The first step in verifying
+ // this is "finish" the min and max by replacing empty objects and stripping
+ // field names.
+ finishedMinObj = finishMinObj(indexEntry, minObj, maxObj);
+ finishedMaxObj = finishMaxObj(indexEntry, minObj, maxObj);
+
+ // Now we have the final min and max. This index is only relevant for
+ // the min/max query if min < max.
+ if (0 > finishedMinObj.woCompare(finishedMaxObj, indexEntry.keyPattern, false)) {
+ // Found a relevant index.
+ idxNo = i;
+ break;
}
+
+ // This index is not relevant; move on to the next.
}
}
@@ -722,11 +681,11 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
return Status(ErrorCodes::BadValue, "unable to find relevant index for max/min query");
}
- LOG(5) << "Max/min query using index " << params.indices[idxNo].toString();
+ LOG(5) << "Max/min query using index " << fullIndexList[idxNo].toString();
// Make our scan and output.
std::unique_ptr<QuerySolutionNode> solnRoot(QueryPlannerAccess::makeIndexScan(
- params.indices[idxNo], query, params, finishedMinObj, finishedMaxObj));
+ fullIndexList[idxNo], query, params, finishedMinObj, finishedMaxObj));
invariant(solnRoot);
auto soln = QueryPlannerAnalysis::analyzeDataAccess(query, params, std::move(solnRoot));
@@ -786,8 +745,8 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
// the text stage can't be built if no text index exists or there is an ambiguity as to
// which one to use.
size_t textIndexCount = 0;
- for (size_t i = 0; i < params.indices.size(); i++) {
- if (INDEX_TEXT == params.indices[i].type) {
+ for (size_t i = 0; i < fullIndexList.size(); i++) {
+ if (INDEX_TEXT == fullIndexList[i].type) {
textIndexCount++;
}
}
@@ -885,15 +844,16 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
}
}
- // An index was hinted. If there are any solutions, they use the hinted index. If not, we
+ // An index was hinted. If there are any solutions, they use the hinted index. If not, we
// scan the entire index to provide results and output that as our plan. This is the
- // desired behavior when an index is hinted that is not relevant to the query.
- if (!hintIndex.isEmpty()) {
- if (0 == out.size()) {
+ // desired behavior when an index is hinted that is not relevant to the query. In the case that
+ // $** index is hinted, we do not want this behavior.
+ if (!hintedIndex.isEmpty() && relevantIndices.size() == 1) {
+ if (0 == out.size() && relevantIndices.front().type != IndexType::INDEX_ALLPATHS) {
// Push hinted index solution to output list if found. It is possible to end up without
// a solution in the case where a filtering QueryPlannerParams argument, such as
// NO_BLOCKING_SORT, leads to its exclusion.
- auto soln = buildWholeIXSoln(params.indices[*hintIndexNumber], query, params);
+ auto soln = buildWholeIXSoln(relevantIndices.front(), query, params);
if (soln) {
LOG(5) << "Planner: outputting soln that uses hinted index as scan.";
out.push_back(std::move(soln));
@@ -920,8 +880,8 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
}
if (!usingIndexToSort) {
- for (size_t i = 0; i < params.indices.size(); ++i) {
- const IndexEntry& index = params.indices[i];
+ for (size_t i = 0; i < fullIndexList.size(); ++i) {
+ const IndexEntry& index = fullIndexList[i];
// Only regular (non-plugin) indexes can be used to provide a sort, and only
// non-sparse indexes can be used to provide a sort.
//
@@ -958,10 +918,10 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
const BSONObj kp = QueryPlannerAnalysis::getSortPattern(index.keyPattern);
if (providesSort(query, kp)) {
LOG(5) << "Planner: outputting soln that uses index to provide sort.";
- auto soln = buildWholeIXSoln(params.indices[i], query, params);
+ auto soln = buildWholeIXSoln(fullIndexList[i], query, params);
if (soln) {
PlanCacheIndexTree* indexTree = new PlanCacheIndexTree();
- indexTree->setIndexEntry(params.indices[i]);
+ indexTree->setIndexEntry(fullIndexList[i]);
SolutionCacheData* scd = new SolutionCacheData();
scd->tree.reset(indexTree);
scd->solnType = SolutionCacheData::WHOLE_IXSCAN_SOLN;
@@ -975,10 +935,10 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
if (providesSort(query, QueryPlannerCommon::reverseSortObj(kp))) {
LOG(5) << "Planner: outputting soln that uses (reverse) index "
<< "to provide sort.";
- auto soln = buildWholeIXSoln(params.indices[i], query, params, -1);
+ auto soln = buildWholeIXSoln(fullIndexList[i], query, params, -1);
if (soln) {
PlanCacheIndexTree* indexTree = new PlanCacheIndexTree();
- indexTree->setIndexEntry(params.indices[i]);
+ indexTree->setIndexEntry(fullIndexList[i]);
SolutionCacheData* scd = new SolutionCacheData();
scd->tree.reset(indexTree);
scd->solnType = SolutionCacheData::WHOLE_IXSCAN_SOLN;
@@ -999,7 +959,7 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
if (params.options & QueryPlannerParams::GENERATE_COVERED_IXSCANS && out.size() == 0 &&
query.getQueryObj().isEmpty() && projection && !projection->requiresDocument()) {
- const auto* indicesToConsider = hintIndex.isEmpty() ? &params.indices : &relevantIndices;
+ const auto* indicesToConsider = hintedIndex.isEmpty() ? &fullIndexList : &relevantIndices;
for (auto&& index : *indicesToConsider) {
if (index.type != INDEX_BTREE || index.multikey || index.sparse || index.filterExpr ||
!CollatorInterface::collatorsMatch(index.collator, query.getCollator())) {
@@ -1031,7 +991,7 @@ StatusWith<std::vector<std::unique_ptr<QuerySolution>>> QueryPlanner::plan(
// Also, if a hint is specified it indicates that we MUST use it.
bool possibleToCollscan =
!QueryPlannerCommon::hasNode(query.root(), MatchExpression::GEO_NEAR) &&
- !QueryPlannerCommon::hasNode(query.root(), MatchExpression::TEXT) && hintIndex.isEmpty();
+ !QueryPlannerCommon::hasNode(query.root(), MatchExpression::TEXT) && hintedIndex.isEmpty();
// The caller can explicitly ask for a collscan.
bool collscanRequested = (params.options & QueryPlannerParams::INCLUDE_COLLSCAN);
diff --git a/src/mongo/db/query/query_planner_all_paths_index_test.cpp b/src/mongo/db/query/query_planner_all_paths_index_test.cpp
index bed49a69514..405b81c52a4 100644
--- a/src/mongo/db/query/query_planner_all_paths_index_test.cpp
+++ b/src/mongo/db/query/query_planner_all_paths_index_test.cpp
@@ -802,6 +802,97 @@ TEST_F(QueryPlannerAllPathsTest, AllPathsDoesNotSupportNegationPredicateInsideEl
assertHasOnlyCollscan();
}
+//
+// Hinting with all paths index tests.
+//
+
+TEST_F(QueryPlannerTest, ChooseAllPathsIndexHint) {
+ addIndex(BSON("$**" << 1));
+ addIndex(BSON("x" << 1));
+
+ runQueryHint(fromjson("{x: {$eq: 1}}"), BSON("$**" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {$_path: 1, x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ChooseAllPathsIndexHintByName) {
+ addIndex(BSON("$**" << 1), nullptr, "allPaths");
+ addIndex(BSON("x" << 1));
+
+ runQueryHint(fromjson("{x: {$eq: 1}}"),
+ BSON("$hint"
+ << "allPaths"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {$_path: 1, x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ChooseAllPathsIndexHintWithPath) {
+ addIndex(BSON("x.$**" << 1));
+ addIndex(BSON("x" << 1));
+
+ runQueryHint(fromjson("{x: {$eq: 1}}"), BSON("x.$**" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {$_path: 1, x: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ChooseAllPathsIndexHintWithOr) {
+ addIndex(BSON("$**" << 1));
+ addIndex(BSON("x" << 1 << "y" << 1));
+
+ runQueryHint(fromjson("{$or: [{x: 1}, {y: 1}]}"), BSON("$**" << 1));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {node: {or: {nodes: [{ixscan: {pattern: {$_path: 1, x: 1}}},"
+ " {ixscan: {pattern: {$_path: 1, y: 1}}}]}}}}");
+}
+
+TEST_F(QueryPlannerTest, ChooseAllPathsIndexHintWithCompoundIndex) {
+ addIndex(BSON("$**" << 1));
+ addIndex(BSON("x" << 1 << "y" << 1));
+
+ runQueryHint(fromjson("{x: 1, y: 1}"), BSON("$**" << 1));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {$_path: 1, x: 1}}}}}");
+ assertSolutionExists("{fetch: {node: {ixscan: {pattern: {$_path: 1, y: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, QueryNotInAllPathsIndexHint) {
+ addIndex(BSON("a.$**" << 1));
+ addIndex(BSON("x" << 1));
+
+ runQueryHint(fromjson("{x: {$eq: 1}}"), BSON("a.$**" << 1));
+ assertNumSolutions(0U);
+}
+
+TEST_F(QueryPlannerTest, AllPathsIndexDoesNotExist) {
+ addIndex(BSON("x" << 1));
+
+ runInvalidQueryHint(fromjson("{x: {$eq: 1}}"), BSON("$**" << 1));
+}
+
+TEST_F(QueryPlannerTest, AllPathsIndexHintWithPartialFilter) {
+ auto filterObj = fromjson("{a: {$gt: 100}}");
+ auto filterExpr = QueryPlannerTest::parseMatchExpression(filterObj);
+ addIndex(BSON("$**" << 1), filterExpr.get());
+
+ runQueryHint(fromjson("{a: {$eq: 1}}"), BSON("$**" << 1));
+ assertNumSolutions(0U);
+}
+
+TEST_F(QueryPlannerTest, MultipleAllPathsIndexesHintWithPartialFilter) {
+ auto filterObj = fromjson("{a: {$gt: 100}, b: {$gt: 100}}");
+ auto filterExpr = QueryPlannerTest::parseMatchExpression(filterObj);
+ addIndex(BSON("$**" << 1), filterExpr.get());
+
+ runQueryHint(fromjson("{a: {$eq: 1}, b: {$eq: 1}}"), BSON("$**" << 1));
+ assertNumSolutions(0U);
+}
+
// TODO SERVER-35335: Add testing for Min/Max.
// TODO SERVER-36517: Add testing for DISTINCT_SCAN.
// TODO SERVER-35331: Add testing for hints.