From d5c17e385f543f0afdc4acd5331cc5514549981f Mon Sep 17 00:00:00 2001 From: yarai Date: Thu, 6 Sep 2018 14:23:22 -0400 Subject: SERVER-35331 Allow hinting an all paths index --- jstests/core/count_hint.js | 5 +- jstests/noPassthroughWithMongod/all_paths_hint.js | 125 ++++++++++++ src/mongo/db/query/planner_ixselect.cpp | 43 ++++- src/mongo/db/query/planner_ixselect.h | 16 +- src/mongo/db/query/query_planner.cpp | 210 +++++++++------------ .../query/query_planner_all_paths_index_test.cpp | 91 +++++++++ 6 files changed, 351 insertions(+), 139 deletions(-) create mode 100644 jstests/noPassthroughWithMongod/all_paths_hint.js 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& fields, - const std::vector& allIndices, - std::vector* out) { +std::vector QueryPlannerIXSelect::findIndexesByHint( + const BSONObj& hintedIndex, const std::vector& allIndices) { + std::vector 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 QueryPlannerIXSelect::findRelevantIndices( + const stdx::unordered_set& fields, const std::vector& allIndices) { + + std::vector 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 QueryPlannerIXSelect::expandIndexes( - const stdx::unordered_set& fields, const std::vector& allIndexes) { + const stdx::unordered_set& fields, + const std::vector& relevantIndices) { std::vector 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* 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 findIndexesByHint(const BSONObj& hintedIndex, + const std::vector& 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& fields, - const std::vector& indices, - std::vector* out); + static std::vector findRelevantIndices( + const stdx::unordered_set& fields, const std::vector& 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 expandIndexes(const stdx::unordered_set& fields, - const std::vector& allIndexes); + const std::vector& 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>> 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 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 fields; QueryPlannerIXSelect::getFields(query.root(), &fields); - for (stdx::unordered_set::const_iterator it = fields.begin(); it != fields.end(); ++it) { LOG(5) << "Predicate over field '" << *it << "'"; } - vector expandedIndexes = - QueryPlannerIXSelect::expandIndexes(fields, params.indices); - - // Filter our indices so we only look at indices that are over our predicates. - vector relevantIndices; + fullIndexList = QueryPlannerIXSelect::expandIndexes(fields, std::move(fullIndexList)); + std::vector 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 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>> 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::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>> 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 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>> 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>> 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>> 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>> 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>> 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>> QueryPlanner::plan( if (params.options & QueryPlannerParams::GENERATE_COVERED_IXSCANS && out.size() == 0 && query.getQueryObj().isEmpty() && projection && !projection->requiresDocument()) { - const auto* indicesToConsider = hintIndex.isEmpty() ? ¶ms.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>> 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. -- cgit v1.2.1