/** * Copyright (C) 2018-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, * as published by MongoDB, Inc. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Server Side Public License for more details. * * You should have received a copy of the Server Side Public License * along with this program. If not, see * . * * As a special exception, the copyright holders give permission to link the * code of portions of this program with the OpenSSL library under certain * conditions as described in each individual source file and distribute * linked combinations including the program with the OpenSSL library. You * must comply with the Server Side Public License in all respects for * all of the code used other than as permitted herein. If you modify file(s) * with this exception, you may extend this exception to your version of the * file(s), but you are not obligated to do so. If you do not wish to do so, * delete this exception statement from your version. If you delete this * exception statement from all source files in the program, then also delete * it in the license file. */ /** * This file contains tests for mongo/db/query/query_planner.cpp */ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest #include "mongo/db/query/query_planner_test_lib.h" #include #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/jsobj.h" #include "mongo/db/json.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/pipeline/document_source_group.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/collation/collator_factory_mock.h" #include "mongo/db/query/projection_ast_util.h" #include "mongo/db/query/projection_parser.h" #include "mongo/db/query/query_planner.h" #include "mongo/db/query/query_solution.h" #include "mongo/logv2/log.h" #include "mongo/unittest/unittest.h" #include "mongo/util/assert_util.h" namespace { using namespace mongo; using std::string; Status filterMatches(const BSONObj& testFilter, const BSONObj& testCollation, const QuerySolutionNode* trueFilterNode) { if (nullptr == trueFilterNode->filter) { return {ErrorCodes::Error{5619210}, "No filter found in query solution node"}; } std::unique_ptr testCollator; if (!testCollation.isEmpty()) { CollatorFactoryMock collatorFactoryMock; auto collator = collatorFactoryMock.makeFromBSON(testCollation); if (!collator.isOK()) { return collator.getStatus().withContext( "collation provided by the test did not parse successfully"); } testCollator = std::move(collator.getValue()); } boost::intrusive_ptr expCtx(new ExpressionContextForTest()); expCtx->setCollator(std::move(testCollator)); StatusWithMatchExpression statusWithMatcher = MatchExpressionParser::parse(testFilter, expCtx); if (!statusWithMatcher.isOK()) { return statusWithMatcher.getStatus().withContext( "match expression provided by the test did not parse successfully"); } const std::unique_ptr root = std::move(statusWithMatcher.getValue()); MatchExpression::sortTree(root.get()); std::unique_ptr trueFilter(trueFilterNode->filter->shallowClone()); MatchExpression::sortTree(trueFilter.get()); if (trueFilter->equivalent(root.get())) { return Status::OK(); } return { ErrorCodes::Error{5619211}, str::stream() << "Provided filter did not match filter on query solution node. Expected: " << root->toString() << ". Found: " << trueFilter->toString()}; } void appendIntervalBound(BSONObjBuilder& bob, BSONElement& el) { if (el.type() == String) { std::string data = el.String(); if (data == "MaxKey") { bob.appendMaxKey(""); } else if (data == "MinKey") { bob.appendMinKey(""); } else { bob.appendAs(el, ""); } } else { bob.appendAs(el, ""); } } Status intervalMatches(const BSONObj& testInt, const Interval trueInt) { BSONObjIterator it(testInt); if (!it.more()) { return {ErrorCodes::Error{5619212}, "Interval has no elements, expected 4 (start, end, inclusiveStart, inclusiveEnd)"}; } BSONElement low = it.next(); if (!it.more()) { return { ErrorCodes::Error{5619213}, "Interval has only 1 element, expected 4 (start, end, inclusiveStart, inclusiveEnd)"}; } BSONElement high = it.next(); if (!it.more()) { return { ErrorCodes::Error{5619214}, "Interval has only 2 elements, expected 4 (start, end, inclusiveStart, inclusiveEnd)"}; } bool startInclusive = it.next().Bool(); if (!it.more()) { return { ErrorCodes::Error{5619215}, "Interval has only 3 elements, expected 4 (start, end, inclusiveStart, inclusiveEnd)"}; } bool endInclusive = it.next().Bool(); if (it.more()) { return {ErrorCodes::Error{5619216}, "Interval has >4 elements, expected exactly 4: (start, end, inclusiveStart, " "inclusiveEnd)"}; } BSONObjBuilder bob; appendIntervalBound(bob, low); appendIntervalBound(bob, high); Interval toCompare(bob.obj(), startInclusive, endInclusive); if (trueInt.equals(toCompare)) { return Status::OK(); } return {ErrorCodes::Error{5619217}, str::stream() << "provided interval did not match. Expected: " << toCompare.toString(false) << " Found: " << trueInt.toString(false)}; } bool bsonObjFieldsAreInSet(BSONObj obj, const std::set& allowedFields) { BSONObjIterator i(obj); while (i.more()) { BSONElement child = i.next(); if (!allowedFields.count(child.fieldName())) { LOGV2_ERROR(23932, "Unexpected field", "field"_attr = child.fieldName()); return false; } } return true; } } // namespace namespace mongo { /** * Looks in the children stored in the 'nodes' field of 'testSoln' * to see if thet match the 'children' field of 'trueSoln'. * * This does an unordered comparison, i.e. childrenMatch returns * true as long as the set of subtrees in testSoln's 'nodes' matches * the set of subtrees in trueSoln's 'children' vector. */ static Status childrenMatch(const BSONObj& testSoln, const QuerySolutionNode* trueSoln, bool relaxBoundsCheck) { BSONElement children = testSoln["nodes"]; if (children.eoo() || !children.isABSONObj()) { return {ErrorCodes::Error{5619218}, "found a stage in the solution which was expected to have 'nodes' children, but no " "'nodes' object in the provided JSON"}; } // The order of the children array in testSoln might not match // the order in trueSoln, so we have to check all combos with // these nested loops. stdx::unordered_set matchedNodeIndexes; BSONObjIterator i(children.Obj()); while (i.more()) { BSONElement child = i.next(); if (child.eoo() || !child.isABSONObj()) { return { ErrorCodes::Error{5619219}, str::stream() << "found a child which was expected to be an object but was not: " << child}; } LOGV2_DEBUG( 5619201, 2, "Attempting to find matching child for {plan}", "plan"_attr = child.Obj()); // try to match against one of the QuerySolutionNode's children bool found = false; for (size_t j = 0; j < trueSoln->children.size(); ++j) { if (matchedNodeIndexes.find(j) != matchedNodeIndexes.end()) { // Do not match a child of the QuerySolutionNode more than once. continue; } auto matchStatus = QueryPlannerTestLib::solutionMatches( child.Obj(), trueSoln->children[j], relaxBoundsCheck); if (matchStatus.isOK()) { LOGV2_DEBUG(5619202, 2, "Found a matching child"); found = true; matchedNodeIndexes.insert(j); break; } LOGV2_DEBUG(5619203, 2, "Child at index {j} did not match test solution: {reason}", "j"_attr = j, "reason"_attr = matchStatus.reason()); } // we couldn't match child if (!found) { return {ErrorCodes::Error{5619220}, str::stream() << "could not find a matching plan for child: " << child}; } } // Ensure we've matched all children of the QuerySolutionNode. if (matchedNodeIndexes.size() == trueSoln->children.size()) { return Status::OK(); } return {ErrorCodes::Error{5619221}, str::stream() << "Did not match the correct number of children. Found " << matchedNodeIndexes.size() << " matching children but " << trueSoln->children.size() << " children in the observed plan"}; } Status QueryPlannerTestLib::boundsMatch(const BSONObj& testBounds, const IndexBounds trueBounds, bool relaxBoundsCheck) { if (testBounds.firstElementFieldName() == "$startKey"_sd) { if (!trueBounds.isSimpleRange) { return {ErrorCodes::Error{5920202}, str::stream() << "Expected bounds to be simple"}; } if (testBounds.nFields() != 2) { return {ErrorCodes::Error{5920203}, str::stream() << "Expected object of form {'$startKey': ..., '$endKey': ...}"}; } BSONObjIterator it(testBounds); { auto minElt = it.next(); if (minElt.type() != BSONType::Object) { return {ErrorCodes::Error{5920205}, str::stream() << "Expected min obj"}; } auto minObj = minElt.embeddedObject(); if (minObj.woCompare(trueBounds.startKey)) { return {ErrorCodes::Error{5920201}, str::stream() << "'startKey' in bounds did not match. Expected " << trueBounds.startKey << " got " << minObj}; } } { auto maxElt = it.next(); if (maxElt.type() != BSONType::Object) { return {ErrorCodes::Error{5920204}, str::stream() << "Expected max obj"}; } auto maxObj = maxElt.embeddedObject(); if (maxObj.woCompare(trueBounds.endKey)) { return {ErrorCodes::Error{5920206}, str::stream() << "'endKey' in bounds did not match. Expected " << trueBounds.endKey << " got " << maxObj}; } } return Status::OK(); } // Iterate over the fields on which we have index bounds. BSONObjIterator fieldIt(testBounds); size_t fieldItCount = 0; while (fieldIt.more()) { BSONElement arrEl = fieldIt.next(); if (arrEl.fieldNameStringData() != trueBounds.getFieldName(fieldItCount)) { return {ErrorCodes::Error{5619222}, str::stream() << "mismatching field name at index " << fieldItCount << ": expected '" << arrEl.fieldNameStringData() << "' but found '" << trueBounds.getFieldName(fieldItCount) << "'"}; } if (arrEl.type() != Array) { return {ErrorCodes::Error{5619223}, str::stream() << "bounds are expected to be arrays. Found: " << arrEl << " (type " << arrEl.type() << ")"}; } // Iterate over an ordered interval list for a particular field. BSONObjIterator oilIt(arrEl.Obj()); size_t oilItCount = 0; while (oilIt.more()) { BSONElement intervalEl = oilIt.next(); if (intervalEl.type() != Array) { return {ErrorCodes::Error{5619224}, str::stream() << "intervals within bounds are expected to be arrays. Found: " << intervalEl << " (type " << intervalEl.type() << ")"}; } Interval trueInt = trueBounds.getInterval(fieldItCount, oilItCount); if (auto matchStatus = intervalMatches(intervalEl.Obj(), trueInt); !matchStatus.isOK()) { return matchStatus.withContext( str::stream() << "mismatching interval found at index " << oilItCount << " within the bounds at index " << fieldItCount); } ++oilItCount; } if (!relaxBoundsCheck && oilItCount != trueBounds.getNumIntervals(fieldItCount)) { return { ErrorCodes::Error{5619225}, str::stream() << "true bounds have more intervals than provided (bounds at index " << fieldItCount << "). Expected: " << oilItCount << " Found: " << trueBounds.getNumIntervals(fieldItCount)}; } ++fieldItCount; } return Status::OK(); } // static Status QueryPlannerTestLib::solutionMatches(const BSONObj& testSoln, const QuerySolutionNode* trueSoln, bool relaxBoundsCheck) { // // leaf nodes // if (STAGE_COLLSCAN == trueSoln->getType()) { const CollectionScanNode* csn = static_cast(trueSoln); BSONElement el = testSoln["cscan"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619226}, "found a collection scan in the solution but no corresponding 'cscan' object " "in the provided JSON"}; } BSONObj csObj = el.Obj(); invariant(bsonObjFieldsAreInSet(csObj, {"dir", "filter", "collation"})); BSONElement dir = csObj["dir"]; if (dir.eoo() || !dir.isNumber()) { return {ErrorCodes::Error{5619227}, "found a collection scan in the solution but no numeric 'dir' in the provided " "JSON"}; } if (dir.numberInt() != csn->direction) { return {ErrorCodes::Error{5619228}, str::stream() << "Solution does not match: found a collection scan in " "the solution but in the wrong direction. Found " << csn->direction << " but was expecting " << dir.numberInt()}; } BSONElement filter = csObj["filter"]; if (filter.eoo()) { LOGV2(3155103, "Found a collection scan which was expected. No filter provided to check"); return Status::OK(); } else if (filter.isNull()) { if (csn->filter == nullptr) { return Status::OK(); } return { ErrorCodes::Error{5619209}, str::stream() << "Expected a collection scan without a filter, but found a filter: " << csn->filter->toString()}; } else if (!filter.isABSONObj()) { return {ErrorCodes::Error{5619229}, str::stream() << "Provided JSON gave a 'cscan' with a 'filter', but the filter " "was not an object." << filter}; } BSONObj collation; if (BSONElement collationElt = csObj["collation"]) { if (!collationElt.isABSONObj()) { return {ErrorCodes::Error{5619230}, str::stream() << "Provided JSON gave a 'cscan' with a 'collation', but the collation " "was not an object:" << collationElt}; } collation = collationElt.Obj(); } return filterMatches(filter.Obj(), collation, trueSoln) .withContext("mismatching 'filter' for 'cscan' node"); } else if (STAGE_IXSCAN == trueSoln->getType()) { const IndexScanNode* ixn = static_cast(trueSoln); BSONElement el = testSoln["ixscan"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619231}, "found an index scan in the solution but no corresponding 'ixscan' object in " "the provided JSON"}; } BSONObj ixscanObj = el.Obj(); invariant(bsonObjFieldsAreInSet( ixscanObj, {"pattern", "name", "bounds", "dir", "filter", "collation"})); BSONElement pattern = ixscanObj["pattern"]; if (!pattern.eoo()) { if (!pattern.isABSONObj()) { return {ErrorCodes::Error{5619232}, str::stream() << "Provided JSON gave a 'ixscan' with a 'pattern', but the pattern " "was not an object: " << pattern}; } if (SimpleBSONObjComparator::kInstance.evaluate(pattern.Obj() != ixn->index.keyPattern)) { return {ErrorCodes::Error{5619233}, str::stream() << "Provided JSON gave a 'ixscan' with a 'pattern' which did " "not match. Expected: " << pattern.Obj() << " Found: " << ixn->index.keyPattern}; } } BSONElement name = ixscanObj["name"]; if (!name.eoo()) { if (name.type() != BSONType::String) { return {ErrorCodes::Error{5619234}, str::stream() << "Provided JSON gave a 'ixscan' with a 'name', but the name " "was not an string: " << name}; } if (name.valueStringData() != ixn->index.identifier.catalogName) { return {ErrorCodes::Error{5619235}, str::stream() << "Provided JSON gave a 'ixscan' with a 'name' which did " "not match. Expected: " << name << " Found: " << ixn->index.identifier.catalogName}; } } if (name.eoo() && pattern.eoo()) { return {ErrorCodes::Error{5619236}, "Provided JSON gave a 'ixscan' without a 'name' or a 'pattern.'"}; } BSONElement bounds = ixscanObj["bounds"]; if (!bounds.eoo()) { if (!bounds.isABSONObj()) { return {ErrorCodes::Error{5619237}, str::stream() << "Provided JSON gave a 'ixscan' with a 'bounds', but the bounds " "was not an object: " << bounds}; } else if (auto boundsStatus = boundsMatch(bounds.Obj(), ixn->bounds, relaxBoundsCheck); !boundsStatus.isOK()) { return boundsStatus.withContext( "Provided JSON gave a 'ixscan' with 'bounds' which did not match"); } } BSONElement dir = ixscanObj["dir"]; if (!dir.eoo() && dir.isNumber()) { if (dir.numberInt() != ixn->direction) { return {ErrorCodes::Error{5619238}, str::stream() << "Solution does not match: found an index scan in " "the solution but in the wrong direction. Found " << ixn->direction << " but was expecting " << dir.numberInt()}; } } BSONElement filter = ixscanObj["filter"]; if (filter.eoo()) { return Status::OK(); } else if (filter.isNull()) { if (ixn->filter == nullptr) { return Status::OK(); } return {ErrorCodes::Error{5619239}, str::stream() << "Expected an index scan without a filter, but found a filter: " << ixn->filter->toString()}; } else if (!filter.isABSONObj()) { return { ErrorCodes::Error{5619240}, str::stream() << "Provided JSON gave an 'ixscan' with a 'filter', but the filter " "was not an object: " << filter}; } BSONObj collation; if (BSONElement collationElt = ixscanObj["collation"]) { if (!collationElt.isABSONObj()) { return { ErrorCodes::Error{5619241}, str::stream() << "Provided JSON gave an 'ixscan' with a 'collation', but the collation " "was not an object:" << collationElt}; } collation = collationElt.Obj(); } return filterMatches(filter.Obj(), collation, trueSoln) .withContext("mismatching 'filter' for 'ixscan' node"); } else if (STAGE_GEO_NEAR_2D == trueSoln->getType()) { const GeoNear2DNode* node = static_cast(trueSoln); BSONElement el = testSoln["geoNear2d"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619242}, "found a geoNear2d stage in the solution but no " "corresponding 'geoNear2d' object in the provided JSON"}; } BSONObj geoObj = el.Obj(); if (SimpleBSONObjComparator::kInstance.evaluate(geoObj == node->index.keyPattern)) { return Status::OK(); } return { ErrorCodes::Error{5619243}, str::stream() << "found a geoNear2d stage in the solution with mismatching keyPattern. Expected: " << geoObj << " Found: " << node->index.keyPattern}; } else if (STAGE_GEO_NEAR_2DSPHERE == trueSoln->getType()) { const GeoNear2DSphereNode* node = static_cast(trueSoln); BSONElement el = testSoln["geoNear2dsphere"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619244}, "found a geoNear2dsphere stage in the solution but no " "corresponding 'geoNear2dsphere' object in the provided JSON"}; } BSONObj geoObj = el.Obj(); invariant(bsonObjFieldsAreInSet(geoObj, {"pattern", "bounds"})); BSONElement pattern = geoObj["pattern"]; if (pattern.eoo() || !pattern.isABSONObj()) { return {ErrorCodes::Error{5619245}, "found a geoNear2dsphere stage in the solution but no 'pattern' object " "in the provided JSON"}; } if (SimpleBSONObjComparator::kInstance.evaluate(pattern.Obj() != node->index.keyPattern)) { return {ErrorCodes::Error{5619246}, str::stream() << "found a geoNear2dsphere stage in the solution with " "mismatching keyPattern. Expected: " << pattern.Obj() << " Found: " << node->index.keyPattern}; } BSONElement bounds = geoObj["bounds"]; if (!bounds.eoo()) { if (!bounds.isABSONObj()) { return { ErrorCodes::Error{5619247}, str::stream() << "Provided JSON gave a 'geoNear2dsphere' with a 'bounds', but the bounds " "was not an object: " << bounds}; } else if (auto boundsStatus = boundsMatch(bounds.Obj(), node->baseBounds, relaxBoundsCheck); !boundsStatus.isOK()) { return boundsStatus.withContext( "Provided JSON gave a 'geoNear2dsphere' with 'bounds' which did not match"); } } return Status::OK(); } else if (STAGE_TEXT_MATCH == trueSoln->getType()) { // {text: {search: "somestr", language: "something", filter: {blah: 1}}} const TextMatchNode* node = static_cast(trueSoln); BSONElement el = testSoln["text"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619248}, "found a text stage in the solution but no " "corresponding 'text' object in the provided JSON"}; } BSONObj textObj = el.Obj(); invariant(bsonObjFieldsAreInSet(textObj, {"text", "search", "language", "caseSensitive", "diacriticSensitive", "prefix", "collation", "filter"})); BSONElement searchElt = textObj["search"]; if (!searchElt.eoo()) { if (searchElt.String() != node->ftsQuery->getQuery()) { return {ErrorCodes::Error{5619249}, str::stream() << "found a text stage in the solution with " "mismatching 'search'. Expected: " << searchElt.String() << " Found: " << node->ftsQuery->getQuery()}; } } BSONElement languageElt = textObj["language"]; if (!languageElt.eoo()) { if (languageElt.String() != node->ftsQuery->getLanguage()) { return {ErrorCodes::Error{5619250}, str::stream() << "found a text stage in the solution with " "mismatching 'language'. Expected: " << languageElt.String() << " Found: " << node->ftsQuery->getLanguage()}; } } BSONElement caseSensitiveElt = textObj["caseSensitive"]; if (!caseSensitiveElt.eoo()) { if (caseSensitiveElt.trueValue() != node->ftsQuery->getCaseSensitive()) { return {ErrorCodes::Error{5619251}, str::stream() << "found a text stage in the solution with " "mismatching 'caseSensitive'. Expected: " << caseSensitiveElt.trueValue() << " Found: " << node->ftsQuery->getCaseSensitive()}; } } BSONElement diacriticSensitiveElt = textObj["diacriticSensitive"]; if (!diacriticSensitiveElt.eoo()) { if (diacriticSensitiveElt.trueValue() != node->ftsQuery->getDiacriticSensitive()) { return {ErrorCodes::Error{5619252}, str::stream() << "found a text stage in the solution with " "mismatching 'diacriticSensitive'. Expected: " << diacriticSensitiveElt.trueValue() << " Found: " << node->ftsQuery->getDiacriticSensitive()}; } } BSONElement indexPrefix = textObj["prefix"]; if (!indexPrefix.eoo()) { if (!indexPrefix.isABSONObj()) { return {ErrorCodes::Error{5619253}, str::stream() << "Provided JSON gave a 'text' with a 'prefix', but the prefix " "was not an object: " << indexPrefix}; } if (0 != indexPrefix.Obj().woCompare(node->indexPrefix)) { return {ErrorCodes::Error{5619254}, str::stream() << "found a text stage in the solution with " "mismatching 'prefix'. Expected: " << indexPrefix.Obj() << " Found: " << node->indexPrefix}; } } BSONObj collation; if (BSONElement collationElt = textObj["collation"]) { if (!collationElt.isABSONObj()) { return {ErrorCodes::Error{5619255}, str::stream() << "Provided JSON gave a 'text' stage with a 'collation', " "but the collation was not an object:" << collationElt}; } collation = collationElt.Obj(); } BSONElement filter = textObj["filter"]; if (!filter.eoo()) { if (filter.isNull()) { if (nullptr != node->filter) { return {ErrorCodes::Error{5619256}, str::stream() << "Expected a text stage without a filter, but found a filter: " << node->filter->toString()}; } } else if (!filter.isABSONObj()) { return {ErrorCodes::Error{5619257}, str::stream() << "Provided JSON gave a 'text' stage with a 'filter', but the filter " "was not an object." << filter}; } else { return filterMatches(filter.Obj(), collation, trueSoln) .withContext("mismatching 'filter' for 'text' node"); } } return Status::OK(); } // // internal nodes // if (STAGE_FETCH == trueSoln->getType()) { const FetchNode* fn = static_cast(trueSoln); BSONElement el = testSoln["fetch"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619258}, "found a fetch in the solution but no corresponding 'fetch' object in " "the provided JSON"}; } BSONObj fetchObj = el.Obj(); invariant(bsonObjFieldsAreInSet(fetchObj, {"collation", "filter", "node"})); BSONObj collation; if (BSONElement collationElt = fetchObj["collation"]) { if (!collationElt.isABSONObj()) { return {ErrorCodes::Error{5619259}, str::stream() << "Provided JSON gave a 'fetch' with a 'collation', but the collation " "was not an object:" << collationElt}; } collation = collationElt.Obj(); } BSONElement filter = fetchObj["filter"]; if (!filter.eoo()) { if (filter.isNull()) { if (nullptr != fn->filter) { return {ErrorCodes::Error{5619260}, str::stream() << "Expected a fetch stage without a filter, but found a filter: " << fn->filter->toString()}; } } else if (!filter.isABSONObj()) { return {ErrorCodes::Error{5619261}, str::stream() << "Provided JSON gave a 'fetch' stage with a 'filter', but the filter " "was not an object." << filter}; } else if (auto filterStatus = filterMatches(filter.Obj(), collation, trueSoln); !filterStatus.isOK()) { return filterStatus.withContext("mismatching 'filter' for 'fetch' node"); } } BSONElement child = fetchObj["node"]; if (child.eoo() || !child.isABSONObj()) { return {ErrorCodes::Error{5619262}, "found a fetch stage in the solution but no 'node' sub-object in the provided " "JSON"}; } return solutionMatches(child.Obj(), fn->children[0], relaxBoundsCheck) .withContext("mismatch beneath fetch node"); } else if (STAGE_OR == trueSoln->getType()) { const OrNode* orn = static_cast(trueSoln); BSONElement el = testSoln["or"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619263}, "found an OR stage in the solution but no " "corresponding 'or' object in the provided JSON"}; } BSONObj orObj = el.Obj(); return childrenMatch(orObj, orn, relaxBoundsCheck).withContext("mismatch beneath or node"); } else if (STAGE_AND_HASH == trueSoln->getType()) { const AndHashNode* ahn = static_cast(trueSoln); BSONElement el = testSoln["andHash"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619264}, "found an AND_HASH stage in the solution but no " "corresponding 'andHash' object in the provided JSON"}; } BSONObj andHashObj = el.Obj(); invariant(bsonObjFieldsAreInSet(andHashObj, {"collation", "filter", "nodes"})); BSONObj collation; if (BSONElement collationElt = andHashObj["collation"]) { if (!collationElt.isABSONObj()) { return {ErrorCodes::Error{5619265}, str::stream() << "Provided JSON gave an 'andHash' stage with a 'collation', " "but the collation was not an object:" << collationElt}; } collation = collationElt.Obj(); } BSONElement filter = andHashObj["filter"]; if (!filter.eoo()) { if (filter.isNull()) { if (nullptr != ahn->filter) { return { ErrorCodes::Error{5619266}, str::stream() << "Expected an AND_HASH stage without a filter, but found a filter: " << ahn->filter->toString()}; } } else if (!filter.isABSONObj()) { return { ErrorCodes::Error{5619267}, str::stream() << "Provided JSON gave an AND_HASH stage with a 'filter', but the filter " "was not an object." << filter}; } else if (auto matchStatus = filterMatches(filter.Obj(), collation, trueSoln); !matchStatus.isOK()) { return matchStatus.withContext("mismatching 'filter' for AND_HASH node"); } } return childrenMatch(andHashObj, ahn, relaxBoundsCheck) .withContext("mismatching children beneath AND_HASH node"); } else if (STAGE_AND_SORTED == trueSoln->getType()) { const AndSortedNode* asn = static_cast(trueSoln); BSONElement el = testSoln["andSorted"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619268}, "found an AND_SORTED stage in the solution but no " "corresponding 'andSorted' object in the provided JSON"}; } BSONObj andSortedObj = el.Obj(); invariant(bsonObjFieldsAreInSet(andSortedObj, {"collation", "filter", "nodes"})); BSONObj collation; if (BSONElement collationElt = andSortedObj["collation"]) { if (!collationElt.isABSONObj()) { return {ErrorCodes::Error{5619269}, str::stream() << "Provided JSON gave an 'andSorted' stage with a 'collation', " "but the collation was not an object:" << collationElt}; } collation = collationElt.Obj(); } BSONElement filter = andSortedObj["filter"]; if (!filter.eoo()) { if (filter.isNull()) { if (nullptr != asn->filter) { return { ErrorCodes::Error{5619270}, str::stream() << "Expected an AND_SORTED stage without a filter, but found a filter: " << asn->filter->toString()}; } } else if (!filter.isABSONObj()) { return { ErrorCodes::Error{5619271}, str::stream() << "Provided JSON gave an AND_SORTED stage with a 'filter', but the filter " "was not an object." << filter}; } else if (auto matchStatus = filterMatches(filter.Obj(), collation, trueSoln); !matchStatus.isOK()) { return matchStatus.withContext("mismatching 'filter' for AND_SORTED node"); } } return childrenMatch(andSortedObj, asn, relaxBoundsCheck) .withContext("mismatching children beneath AND_SORTED node"); } else if (isProjectionStageType(trueSoln->getType())) { const ProjectionNode* pn = static_cast(trueSoln); BSONElement el = testSoln["proj"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619272}, "found a projection stage in the solution but no " "corresponding 'proj' object in the provided JSON"}; } BSONObj projObj = el.Obj(); invariant(bsonObjFieldsAreInSet(projObj, {"type", "spec", "node"})); BSONElement projType = projObj["type"]; if (!projType.eoo()) { string projTypeStr = projType.str(); switch (pn->getType()) { case StageType::STAGE_PROJECTION_DEFAULT: if (projTypeStr != "default") return {ErrorCodes::Error{5619273}, str::stream() << "found a projection stage in the solution with " "mismatching 'type'. Expected: " << projTypeStr << " Found: 'default'"}; break; case StageType::STAGE_PROJECTION_COVERED: if (projTypeStr != "coveredIndex") return {ErrorCodes::Error{5619274}, str::stream() << "found a projection stage in the solution with " "mismatching 'type'. Expected: " << projTypeStr << " Found: 'coveredIndex'"}; break; case StageType::STAGE_PROJECTION_SIMPLE: if (projTypeStr != "simple") return {ErrorCodes::Error{5619275}, str::stream() << "found a projection stage in the solution with " "mismatching 'type'. Expected: " << projTypeStr << " Found: 'simple'"}; break; default: MONGO_UNREACHABLE; } } BSONElement spec = projObj["spec"]; if (spec.eoo() || !spec.isABSONObj()) { return {ErrorCodes::Error{5619276}, "found a projection stage in the solution but no 'spec' object in the provided " "JSON"}; } BSONElement child = projObj["node"]; if (child.eoo() || !child.isABSONObj()) { return { ErrorCodes::Error{5619277}, "found a projection stage in the solution but no 'node' sub-object in the provided " "JSON"}; } // Create an empty/dummy expression context without access to the operation context and // collator. This should be sufficient to parse a projection. auto expCtx = make_intrusive(nullptr, nullptr, NamespaceString("test.dummy")); auto projection = projection_ast::parseAndAnalyze( expCtx, spec.Obj(), ProjectionPolicies::findProjectionPolicies()); auto specProjObj = projection_ast::astToDebugBSON(projection.root()); auto solnProjObj = projection_ast::astToDebugBSON(pn->proj.root()); if (!SimpleBSONObjComparator::kInstance.evaluate(specProjObj == solnProjObj)) { return {ErrorCodes::Error{5619278}, str::stream() << "found a projection stage in the solution with " "mismatching 'spec'. Expected: " << specProjObj << " Found: " << solnProjObj}; } return solutionMatches(child.Obj(), pn->children[0], relaxBoundsCheck) .withContext("mismatch below projection stage"); } else if (isSortStageType(trueSoln->getType())) { const SortNode* sn = static_cast(trueSoln); BSONElement el = testSoln["sort"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619279}, "found a sort stage in the solution but no " "corresponding 'sort' object in the provided JSON"}; } BSONObj sortObj = el.Obj(); invariant(bsonObjFieldsAreInSet(sortObj, {"pattern", "limit", "type", "node"})); BSONElement patternEl = sortObj["pattern"]; if (patternEl.eoo() || !patternEl.isABSONObj()) { return { ErrorCodes::Error{5619280}, "found a sort stage in the solution but no 'pattern' object in the provided JSON"}; } BSONElement limitEl = sortObj["limit"]; if (limitEl.eoo()) { return {ErrorCodes::Error{5619281}, "found a sort stage in the solution but no 'limit' was provided. Specify '0' " "for no limit."}; } if (!limitEl.isNumber()) { return { ErrorCodes::Error{5619282}, str::stream() << "found a sort stage in the solution but 'limit' was not numeric: " << limitEl}; } BSONElement sortType = sortObj["type"]; if (sortType) { if (sortType.type() != BSONType::String) { return {ErrorCodes::Error{5619283}, str::stream() << "found a sort stage in the solution but 'type' was not a string: " << sortType}; } auto sortTypeString = sortType.valueStringData(); switch (sn->getType()) { case StageType::STAGE_SORT_DEFAULT: { if (sortTypeString != "default") { return {ErrorCodes::Error{5619284}, str::stream() << "found a sort stage in the solution with " "mismatching 'type'. Expected: " << sortTypeString << " Found: 'default'"}; } break; } case StageType::STAGE_SORT_SIMPLE: { if (sortTypeString != "simple") { return {ErrorCodes::Error{5619285}, str::stream() << "found a sort stage in the solution with " "mismatching 'type'. Expected: " << sortTypeString << " Found: 'simple'"}; } break; } default: { MONGO_UNREACHABLE; } } } BSONElement child = sortObj["node"]; if (child.eoo() || !child.isABSONObj()) { return { ErrorCodes::Error{5619286}, "found a sort stage in the solution but no 'node' sub-object in the provided JSON"}; } size_t expectedLimit = limitEl.numberInt(); if (!SimpleBSONObjComparator::kInstance.evaluate(patternEl.Obj() == sn->pattern)) { return {ErrorCodes::Error{5619287}, str::stream() << "found a sort stage in the solution with " "mismatching 'pattern'. Expected: " << patternEl << " Found: " << sn->pattern}; } if (expectedLimit != sn->limit) { return {ErrorCodes::Error{5619288}, str::stream() << "found a projection stage in the solution with " "mismatching 'limit'. Expected: " << expectedLimit << " Found: " << sn->limit}; } return solutionMatches(child.Obj(), sn->children[0], relaxBoundsCheck) .withContext("mismatch below sort stage"); } else if (STAGE_SORT_KEY_GENERATOR == trueSoln->getType()) { const SortKeyGeneratorNode* keyGenNode = static_cast(trueSoln); BSONElement el = testSoln["sortKeyGen"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619289}, "found a sort key generator stage in the solution but no " "corresponding 'sortKeyGen' object in the provided JSON"}; } BSONObj keyGenObj = el.Obj(); invariant(bsonObjFieldsAreInSet(keyGenObj, {"node"})); BSONElement child = keyGenObj["node"]; if (child.eoo() || !child.isABSONObj()) { return {ErrorCodes::Error{5619290}, "found a sort key generator stage in the solution but no 'node' sub-object in " "the provided JSON"}; } return solutionMatches(child.Obj(), keyGenNode->children[0], relaxBoundsCheck) .withContext("mismatch below sortKeyGen"); } else if (STAGE_SORT_MERGE == trueSoln->getType()) { const MergeSortNode* msn = static_cast(trueSoln); BSONElement el = testSoln["mergeSort"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619291}, "found a merge sort stage in the solution but no " "corresponding 'mergeSort' object in the provided JSON"}; } BSONObj mergeSortObj = el.Obj(); invariant(bsonObjFieldsAreInSet(mergeSortObj, {"nodes"})); return childrenMatch(mergeSortObj, msn, relaxBoundsCheck) .withContext("mismatching children below merge sort"); } else if (STAGE_SKIP == trueSoln->getType()) { const SkipNode* sn = static_cast(trueSoln); BSONElement el = testSoln["skip"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619292}, "found a skip stage in the solution but no " "corresponding 'skip' object in the provided JSON"}; } BSONObj skipObj = el.Obj(); invariant(bsonObjFieldsAreInSet(skipObj, {"n", "node"})); BSONElement skipEl = skipObj["n"]; if (!skipEl.isNumber()) { return {ErrorCodes::Error{5619293}, str::stream() << "found a skip stage in the solution but 'n' was not numeric: " << skipEl}; } BSONElement child = skipObj["node"]; if (child.eoo() || !child.isABSONObj()) { return {ErrorCodes::Error{5619294}, "found a skip stage in the solution but no 'node' sub-object in " "the provided JSON"}; } if (skipEl.numberInt() != sn->skip) { return {ErrorCodes::Error{5619295}, str::stream() << "found a skip stage in the solution with " "mismatching 'n'. Expected: " << skipEl.numberInt() << " Found: " << sn->skip}; } return solutionMatches(child.Obj(), sn->children[0], relaxBoundsCheck) .withContext("mismatch below skip stage"); } else if (STAGE_LIMIT == trueSoln->getType()) { const LimitNode* ln = static_cast(trueSoln); BSONElement el = testSoln["limit"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619296}, "found a limit stage in the solution but no " "corresponding 'limit' object in the provided JSON"}; } BSONObj limitObj = el.Obj(); invariant(bsonObjFieldsAreInSet(limitObj, {"n", "node"})); BSONElement limitEl = limitObj["n"]; if (!limitEl.isNumber()) { return {ErrorCodes::Error{5619297}, str::stream() << "found a limit stage in the solution but 'n' was not numeric: " << limitEl}; } BSONElement child = limitObj["node"]; if (child.eoo() || !child.isABSONObj()) { return {ErrorCodes::Error{5619298}, "found a limit stage in the solution but no 'node' sub-object in " "the provided JSON"}; } if (limitEl.numberInt() != ln->limit) { return {ErrorCodes::Error{5619299}, str::stream() << "found a limit stage in the solution with " "mismatching 'n'. Expected: " << limitEl.numberInt() << " Found: " << ln->limit}; } return solutionMatches(child.Obj(), ln->children[0], relaxBoundsCheck) .withContext("mismatch below limit stage"); } else if (STAGE_SHARDING_FILTER == trueSoln->getType()) { const ShardingFilterNode* fn = static_cast(trueSoln); BSONElement el = testSoln["sharding_filter"]; if (el.eoo() || !el.isABSONObj()) { return {ErrorCodes::Error{5619206}, "found a sharding filter stage in the solution but no " "corresponding 'sharding_filter' object in the provided JSON"}; } BSONObj keepObj = el.Obj(); invariant(bsonObjFieldsAreInSet(keepObj, {"node"})); BSONElement child = keepObj["node"]; if (child.eoo() || !child.isABSONObj()) { return {ErrorCodes::Error{5619207}, "found a sharding filter stage in the solution but no 'node' sub-object in " "the provided JSON"}; } return solutionMatches(child.Obj(), fn->children[0], relaxBoundsCheck) .withContext("mismatch below shard filter stage"); } else if (STAGE_GROUP == trueSoln->getType()) { const auto* actualGroupNode = static_cast(trueSoln); auto expectedGroupElem = testSoln["group"]; if (expectedGroupElem.eoo() || !expectedGroupElem.isABSONObj()) { return {ErrorCodes::Error{5842401}, "found a 'group' object in the test solution but no corresponding 'group' " "object " "in the expected JSON"}; } auto expectedGroupObj = expectedGroupElem.Obj(); invariant(bsonObjFieldsAreInSet(expectedGroupObj, {"key", "accs", "node"})); auto expectedGroupByElem = expectedGroupObj["key"]; if (expectedGroupByElem.eoo() || !expectedGroupByElem.isABSONObj()) { return {ErrorCodes::Error{5842402}, "found a 'key' object in the test solution but no corresponding 'key' " "object " "in the expected JSON"}; } BSONObjBuilder bob; actualGroupNode->groupByExpression->serialize(true).addToBsonObj(&bob, "_id"); auto actualGroupByObj = bob.done(); if (!SimpleBSONObjComparator::kInstance.evaluate(actualGroupByObj == expectedGroupByElem.Obj())) { return {ErrorCodes::Error{5842403}, str::stream() << "found a group stage in the solution with " "mismatching 'key' expressions. Expected: " << expectedGroupByElem.Obj() << " Found: " << actualGroupByObj}; } BSONArrayBuilder actualAccs; for (auto& acc : actualGroupNode->accumulators) { BSONObjBuilder bob; acc.expr.argument->serialize(true).addToBsonObj(&bob, acc.expr.name); actualAccs.append(BSON(acc.fieldName << bob.done())); } auto expectedAccsObj = expectedGroupObj["accs"].Obj(); auto actualAccsObj = actualAccs.done(); if (!SimpleBSONObjComparator::kInstance.evaluate(expectedAccsObj == actualAccsObj)) { return {ErrorCodes::Error{5842404}, str::stream() << "found a group stage in the solution with " "mismatching 'accs' expressions. Expected: " << expectedAccsObj << " Found: " << actualAccsObj}; } auto child = expectedGroupObj["node"]; if (child.eoo() || !child.isABSONObj()) { return {ErrorCodes::Error{5842405}, "found a group stage in the solution but no 'node' sub-object in " "the provided JSON"}; } return solutionMatches(child.Obj(), actualGroupNode->children[0], relaxBoundsCheck) .withContext("mismatch below group stage"); } else if (STAGE_SENTINEL == trueSoln->getType()) { const auto* actualSentinelNode = static_cast(trueSoln); auto expectedSentinelElem = testSoln["sentinel"]; if (expectedSentinelElem.eoo() || !expectedSentinelElem.isABSONObj()) { return { ErrorCodes::Error{5842406}, "found a 'sentinel' object in the test solution but no corresponding 'sentinel' " "object " "in the expected JSON"}; } // The sentinel node is just an empty QSN node. if (!expectedSentinelElem.Obj().isEmpty()) { return {ErrorCodes::Error{5842407}, str::stream() << "found a non-empty sentinel stage in the solution"}; } if (!actualSentinelNode->children.empty()) { return {ErrorCodes::Error{5842408}, str::stream() << "found a sentinel stage with more than zero children in the " "actual solution. Expected: " << testSoln << " Found: " << actualSentinelNode}; } return Status::OK(); } else if (STAGE_COLUMN_IXSCAN == trueSoln->getType()) { const auto* actualColumnIxScanNode = static_cast(trueSoln); auto expectedElem = testSoln["column_ixscan"]; if (expectedElem.eoo() || !expectedElem.isABSONObj()) { return {ErrorCodes::Error{5842490}, "found a 'column_ixscan' object in the test solution but no corresponding " "'column_ixscan' " "object " "in the expected JSON"}; } auto obj = expectedElem.Obj(); if (!obj["fields"].eoo()) { std::set expectedFields; for (auto& field : obj["fields"].Obj()) { expectedFields.insert(field.fieldName()); } std::set actualFields(actualColumnIxScanNode->fields.begin(), actualColumnIxScanNode->fields.end()); if (expectedFields != std::set(actualFields.begin(), actualFields.end())) { return {ErrorCodes::Error{5842491}, str::stream() << "fields mismatch."}; } } if (!actualColumnIxScanNode->children.empty()) { return { ErrorCodes::Error{5842492}, str::stream() << "found a column_ixscan stage with more than zero children in the " "actual solution:"}; } if (auto filter = obj["filter"]) { return filterMatches(filter.Obj(), BSONObj(), trueSoln) .withContext("mismatching 'filter' for 'column_ixscan' node"); } return Status::OK(); } return {ErrorCodes::Error{5698301}, str::stream() << "Unknown query solution node found: " << trueSoln->toString()}; } // namespace mongo } // namespace mongo