diff options
Diffstat (limited to 'src/mongo/db')
-rw-r--r-- | src/mongo/db/commands/find_cmd.cpp | 84 | ||||
-rw-r--r-- | src/mongo/db/matcher/expression_geo.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_geo_near.cpp | 42 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_geo_near.h | 8 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_geo_near_cursor.cpp | 8 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document_source_geo_near_cursor.h | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_context.cpp | 6 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_context.h | 7 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline.cpp | 12 | ||||
-rw-r--r-- | src/mongo/db/pipeline/pipeline.h | 8 | ||||
-rw-r--r-- | src/mongo/db/query/canonical_query.cpp | 275 | ||||
-rw-r--r-- | src/mongo/db/query/canonical_query.h | 40 | ||||
-rw-r--r-- | src/mongo/db/query/query_request_helper.cpp | 155 | ||||
-rw-r--r-- | src/mongo/db/query/query_request_helper.h | 8 | ||||
-rw-r--r-- | src/mongo/db/query/query_request_test.cpp | 177 |
15 files changed, 568 insertions, 269 deletions
diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp index 31faa7c7bb3..01bf2a8afbf 100644 --- a/src/mongo/db/commands/find_cmd.cpp +++ b/src/mongo/db/commands/find_cmd.cpp @@ -56,6 +56,7 @@ #include "mongo/db/stats/resource_consumption_metrics.h" #include "mongo/db/stats/server_read_concern_metrics.h" #include "mongo/db/storage/storage_engine.h" +#include "mongo/db/storage/storage_parameters_gen.h" #include "mongo/db/transaction_participant.h" #include "mongo/logv2/log.h" #include "mongo/rpc/get_status_from_command_result.h" @@ -119,7 +120,8 @@ std::unique_ptr<FindCommandRequest> parseCmdObjectToFindCommandRequest(Operation boost::intrusive_ptr<ExpressionContext> makeExpressionContext( OperationContext* opCtx, const FindCommandRequest& findCommand, - boost::optional<ExplainOptions::Verbosity> verbosity) { + boost::optional<ExplainOptions::Verbosity> verbosity, + bool isView) { std::unique_ptr<CollatorInterface> collator; if (!findCommand.getCollation().isEmpty()) { collator = uassertStatusOK(CollatorFactoryInterface::get(opCtx->getServiceContext()) @@ -153,9 +155,10 @@ boost::intrusive_ptr<ExpressionContext> makeExpressionContext( std::move(collator), nullptr, // mongoProcessInterface StringMap<ExpressionContext::ResolvedNamespace>{}, - boost::none, // uuid - findCommand.getLet(), // let - CurOp::get(opCtx)->dbProfileLevel() > 0 // mayDbProfile + boost::none, // uuid + findCommand.getLet(), // let + CurOp::get(opCtx)->dbProfileLevel() > 0, // mayDbProfile + isView // omitVariables ); expCtx->tempDir = storageGlobalParams.dbpath + "/_tmp"; expCtx->startExpressionCounters(); @@ -302,25 +305,24 @@ public: // Finish the parsing step by using the FindCommandRequest to create a CanonicalQuery. const ExtensionsCallbackReal extensionsCallback(opCtx, &nss); - auto expCtx = makeExpressionContext(opCtx, *findCommand, verbosity); const bool isExplain = true; - auto cq = uassertStatusOK( - CanonicalQuery::canonicalize(opCtx, - std::move(findCommand), - isExplain, - std::move(expCtx), - extensionsCallback, - MatchExpressionParser::kAllowAllSpecialFeatures)); - if (ctx->getView()) { + auto expCtx = + makeExpressionContext(opCtx, *findCommand, verbosity, true /* isView */); + auto cq = uassertStatusOK( + CanonicalQuery::canonicalize(opCtx, + std::move(findCommand), + isExplain, + std::move(expCtx), + extensionsCallback, + Pipeline::viewFindMatcherFeatures())); + // Relinquish locks. The aggregation command will re-acquire them. ctx.reset(); // Convert the find command into an aggregation using $match (and other stages, as // necessary), if possible. - const auto& findCommand = cq->getFindCommandRequest(); - auto viewAggregationCommand = - uassertStatusOK(query_request_helper::asAggregationCommand(findCommand)); + auto viewAggregationCommand = uassertStatusOK(asAggregationCommand(*cq)); auto viewAggCmd = OpMsgRequest::fromDBAndBody(_dbName, viewAggregationCommand).body; // Create the agg request equivalent of the find operation, with the explain @@ -347,6 +349,15 @@ public: return; } + auto expCtx = makeExpressionContext(opCtx, *findCommand, verbosity, false /* isView */); + auto cq = uassertStatusOK( + CanonicalQuery::canonicalize(opCtx, + std::move(findCommand), + isExplain, + std::move(expCtx), + extensionsCallback, + MatchExpressionParser::kAllowAllSpecialFeatures)); + // The collection may be NULL. If so, getExecutor() should handle it by returning an // execution tree with an EOFStage. const auto& collection = ctx->getCollection(); @@ -445,27 +456,27 @@ public: // Fill out curop information. beginQueryOp(opCtx, nss, _request.body); - // Finish the parsing step by using the FindCommandRequest to create a CanonicalQuery. const ExtensionsCallbackReal extensionsCallback(opCtx, &nss); - auto expCtx = makeExpressionContext(opCtx, *findCommand, boost::none /* verbosity */); - auto cq = uassertStatusOK( - CanonicalQuery::canonicalize(opCtx, - std::move(findCommand), - isExplain, - std::move(expCtx), - extensionsCallback, - MatchExpressionParser::kAllowAllSpecialFeatures)); if (ctx->getView()) { + auto expCtx = makeExpressionContext( + opCtx, *findCommand, boost::none /* verbosity */, true /* isView */); + + // Finish the parsing step by using the FindCommandRequest to create a + // CanonicalQuery. And then convert the find command into an aggregation using + // $match (and other stages, as necessary), if possible. + auto cq = uassertStatusOK( + CanonicalQuery::canonicalize(opCtx, + std::move(findCommand), + isExplain, + std::move(expCtx), + extensionsCallback, + Pipeline::viewFindMatcherFeatures())); + auto viewAggregationCommand = uassertStatusOK(asAggregationCommand(*cq)); + // Relinquish locks. The aggregation command will re-acquire them. ctx.reset(); - // Convert the find command into an aggregation using $match (and other stages, as - // necessary), if possible. - const auto& findCommand = cq->getFindCommandRequest(); - auto viewAggregationCommand = - uassertStatusOK(query_request_helper::asAggregationCommand(findCommand)); - BSONObj aggResult = CommandHelpers::runCommandDirectly( opCtx, OpMsgRequest::fromDBAndBody(_dbName, std::move(viewAggregationCommand))); auto status = getStatusFromCommandResult(aggResult); @@ -478,6 +489,17 @@ public: return; } + auto expCtx = makeExpressionContext( + opCtx, *findCommand, boost::none /* verbosity */, false /* isView */); + // Finish the parsing step by using the FindCommandRequest to create a CanonicalQuery. + auto cq = uassertStatusOK( + CanonicalQuery::canonicalize(opCtx, + std::move(findCommand), + isExplain, + std::move(expCtx), + extensionsCallback, + MatchExpressionParser::kAllowAllSpecialFeatures)); + const auto& collection = ctx->getCollection(); if (cq->getFindCommandRequest().getReadOnce()) { diff --git a/src/mongo/db/matcher/expression_geo.cpp b/src/mongo/db/matcher/expression_geo.cpp index 6db7f2003c5..e90ca0089d2 100644 --- a/src/mongo/db/matcher/expression_geo.cpp +++ b/src/mongo/db/matcher/expression_geo.cpp @@ -446,6 +446,7 @@ GeoNearMatchExpression::GeoNearMatchExpression(StringData path, bool GeoNearMatchExpression::matchesSingleElement(const BSONElement& e, MatchDetails* details) const { + tasserted(5844303, "GeoNearMatchExpression::matchesSingleElement() should never be called"); return true; } diff --git a/src/mongo/db/pipeline/document_source_geo_near.cpp b/src/mongo/db/pipeline/document_source_geo_near.cpp index e10b9e59b50..3c734f76b0f 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near.cpp @@ -38,6 +38,8 @@ #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/logv2/log.h" +#include "mongo/db/storage/storage_parameters_gen.h" + namespace mongo { using boost::intrusive_ptr; @@ -62,7 +64,9 @@ Value DocumentSourceGeoNear::serialize(boost::optional<ExplainOptions::Verbosity result.setField("near", Value(coords)); } - result.setField("distanceField", Value(distanceField->fullPath())); + if (distanceField) { + result.setField("distanceField", Value(distanceField->fullPath())); + } if (maxDistance) { result.setField("maxDistance", Value(*maxDistance)); @@ -120,17 +124,26 @@ void DocumentSourceGeoNear::parseOptions(BSONObj options) { !options["num"]); uassert(50856, "$geoNear no longer supports the 'start' argument.", !options["start"]); - // The "near" and "distanceField" parameters are required. + // The "near" parameter is required. uassert(16605, "$geoNear requires a 'near' option as an Array", options["near"].isABSONObj()); // Array or Object (Object is deprecated) coordsIsArray = options["near"].type() == Array; coords = options["near"].embeddedObject().getOwned(); - uassert(16606, - "$geoNear requires a 'distanceField' option as a String", - options["distanceField"].type() == String); - distanceField.reset(new FieldPath(options["distanceField"].str())); + // "distanceField" is optional. + auto distElem = options["distanceField"]; + if (!feature_flags::gTimeseriesMetricIndexes.isEnabled( + serverGlobalParams.featureCompatibility)) { + // Except without this feature flag, "distanceField" is required. + uassert(16606, + "$geoNear requires a 'distanceField' option, as a String", + distElem.type() == String); + } + if (distElem) { + uassert(5844304, "$geoNear 'distanceField' must be a String", distElem.type() == String); + distanceField = FieldPath(distElem.str()); + } // The remaining fields are optional. if (auto maxDistElem = options["maxDistance"]) { @@ -226,10 +239,10 @@ bool DocumentSourceGeoNear::needsGeoNearPoint() const { } DepsTracker::State DocumentSourceGeoNear::getDependencies(DepsTracker* deps) const { - // TODO (SERVER-35424): Implement better dependency tracking. For example, 'distanceField' is - // produced by this stage, and we could inform the query system that it need not include it in - // its response. For now, assume that we require the entire document as well as the appropriate - // geoNear metadata. + // TODO (SERVER-35424): Implement better dependency tracking. For example, 'distanceField' (if + // specified) is produced by this stage, and we could inform the query system that it need not + // include it in its response. For now, assume that we require the entire document as well as + // the appropriate geoNear metadata. deps->setNeedsMetadata(DocumentMetadataFields::kGeoNearDist, true); deps->setNeedsMetadata(DocumentMetadataFields::kGeoNearPoint, needsGeoNearPoint()); @@ -243,7 +256,14 @@ DocumentSourceGeoNear::DocumentSourceGeoNear(const intrusive_ptr<ExpressionConte boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceGeoNear::distributedPlanLogic() { // {shardsStage, mergingStage, sortPattern} - return DistributedPlanLogic{this, nullptr, BSON(distanceField->fullPath() << 1)}; + return DistributedPlanLogic{ + this, + nullptr, + // The field name here apparently doesn't matter, because we always look + // in {$meta: sortKey}, which is implicitly set from {$meta: geoNearDistance} + // when available. + BSON("field_name_does_not_matter" << 1), + }; } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_geo_near.h b/src/mongo/db/pipeline/document_source_geo_near.h index d9fbc6e669a..3e941aecb97 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.h +++ b/src/mongo/db/pipeline/document_source_geo_near.h @@ -93,8 +93,8 @@ public: /** * The field in which the computed distance will be stored. */ - FieldPath getDistanceField() const { - return *distanceField; + boost::optional<FieldPath> getDistanceField() const { + return distanceField; } /** @@ -152,10 +152,10 @@ private: void parseOptions(BSONObj options); // These fields describe the command to run. - // 'coords' and 'distanceField' are required; the rest are optional. + // 'coords' is required; the rest are optional. BSONObj coords; // "near" option, but near is a reserved keyword on windows bool coordsIsArray; - std::unique_ptr<FieldPath> distanceField; // Using unique_ptr because FieldPath can't be empty + boost::optional<FieldPath> distanceField; BSONObj query; bool spherical; boost::optional<double> maxDistance; diff --git a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp index 8e6885567d0..0d3d5ad4a4f 100644 --- a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp @@ -54,7 +54,7 @@ boost::intrusive_ptr<DocumentSourceGeoNearCursor> DocumentSourceGeoNearCursor::c const CollectionPtr& collection, std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, const boost::intrusive_ptr<ExpressionContext>& expCtx, - FieldPath distanceField, + boost::optional<FieldPath> distanceField, boost::optional<FieldPath> locationField, double distanceMultiplier) { return {new DocumentSourceGeoNearCursor(collection, @@ -69,7 +69,7 @@ DocumentSourceGeoNearCursor::DocumentSourceGeoNearCursor( const CollectionPtr& collection, std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> exec, const boost::intrusive_ptr<ExpressionContext>& expCtx, - FieldPath distanceField, + boost::optional<FieldPath> distanceField, boost::optional<FieldPath> locationField, double distanceMultiplier) : DocumentSourceCursor( @@ -94,7 +94,9 @@ Document DocumentSourceGeoNearCursor::transformDoc(Document&& objInput) const { << output.peek().toString()); const auto distance = output.peek().metadata().getGeoNearDistance() * _distanceMultiplier; - output.setNestedField(_distanceField, Value(distance)); + if (_distanceField) { + output.setNestedField(*_distanceField, Value(distance)); + } if (_locationField) { invariant( output.peek().metadata().hasGeoNearPoint(), diff --git a/src/mongo/db/pipeline/document_source_geo_near_cursor.h b/src/mongo/db/pipeline/document_source_geo_near_cursor.h index b3ddf9a834b..d0127667ac5 100644 --- a/src/mongo/db/pipeline/document_source_geo_near_cursor.h +++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.h @@ -63,7 +63,7 @@ public: const CollectionPtr&, std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>, const boost::intrusive_ptr<ExpressionContext>&, - FieldPath distanceField, + boost::optional<FieldPath> distanceField, boost::optional<FieldPath> locationField = boost::none, double distanceMultiplier = 1.0); @@ -73,7 +73,7 @@ private: DocumentSourceGeoNearCursor(const CollectionPtr&, std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>, const boost::intrusive_ptr<ExpressionContext>&, - FieldPath distanceField, + boost::optional<FieldPath> distanceField, boost::optional<FieldPath> locationField, double distanceMultiplier); @@ -85,7 +85,7 @@ private: Document transformDoc(Document&& obj) const override final; // The output field in which to store the computed distance. - FieldPath _distanceField; + boost::optional<FieldPath> _distanceField; // The output field to store the point that matched, if specified. boost::optional<FieldPath> _locationField; diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp index 69698ab0289..86a20538afc 100644 --- a/src/mongo/db/pipeline/expression_context.cpp +++ b/src/mongo/db/pipeline/expression_context.cpp @@ -92,7 +92,8 @@ ExpressionContext::ExpressionContext( StringMap<ExpressionContext::ResolvedNamespace> resolvedNamespaces, boost::optional<UUID> collUUID, const boost::optional<BSONObj>& letParameters, - bool mayDbProfile) + bool mayDbProfile, + bool omitVariables) : explain(explain), fromMongos(fromMongos), needsMerge(needsMerge), @@ -127,6 +128,9 @@ ExpressionContext::ExpressionContext( } if (letParameters) variables.seedVariablesWithLetParameters(this, *letParameters); + + if (omitVariables) + variables = Variables{}; } ExpressionContext::ExpressionContext( diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h index 398a162b283..7c8d61ea03d 100644 --- a/src/mongo/db/pipeline/expression_context.h +++ b/src/mongo/db/pipeline/expression_context.h @@ -123,6 +123,10 @@ public: * Constructs an ExpressionContext to be used for Pipeline parsing and evaluation. This version * requires finer-grained parameters but does not require an AggregateCommandRequest. * 'resolvedNamespaces' maps collection names (not full namespaces) to ResolvedNamespaces. + * + * 'omitVariables == true' means the ExpressionContext should not have any variables defined. + * This lets you parse and optimize a query without assuming that top-level variables such as + * NOW are known. */ ExpressionContext(OperationContext* opCtx, const boost::optional<ExplainOptions::Verbosity>& explain, @@ -138,7 +142,8 @@ public: StringMap<ExpressionContext::ResolvedNamespace> resolvedNamespaces, boost::optional<UUID> collUUID, const boost::optional<BSONObj>& letParameters = boost::none, - bool mayDbProfile = true); + bool mayDbProfile = true, + bool omitVariables = false); /** * Constructs an ExpressionContext suitable for use outside of the aggregation system, including diff --git a/src/mongo/db/pipeline/pipeline.cpp b/src/mongo/db/pipeline/pipeline.cpp index 90377d824f4..ae6ab01ae1b 100644 --- a/src/mongo/db/pipeline/pipeline.cpp +++ b/src/mongo/db/pipeline/pipeline.cpp @@ -45,6 +45,7 @@ #include "mongo/db/pipeline/lite_parsed_pipeline.h" #include "mongo/db/query/query_knobs_gen.h" #include "mongo/db/storage/storage_options.h" +#include "mongo/db/storage/storage_parameters_gen.h" #include "mongo/util/fail_point.h" #include "mongo/util/str.h" @@ -134,6 +135,17 @@ using StreamType = StageConstraints::StreamType; constexpr MatchExpressionParser::AllowedFeatureSet Pipeline::kAllowedMatcherFeatures; constexpr MatchExpressionParser::AllowedFeatureSet Pipeline::kGeoNearMatcherFeatures; +MatchExpressionParser::AllowedFeatureSet Pipeline::viewFindMatcherFeatures() { + if (serverGlobalParams.featureCompatibility.isVersionInitialized() && + feature_flags::gTimeseriesMetricIndexes.isEnabled( + serverGlobalParams.featureCompatibility)) { + return Pipeline::kGeoNearMatcherFeatures; + } + + return Pipeline::kAllowedMatcherFeatures; +} + + Pipeline::Pipeline(const intrusive_ptr<ExpressionContext>& pTheCtx) : pCtx(pTheCtx) {} Pipeline::Pipeline(SourceContainer stages, const intrusive_ptr<ExpressionContext>& expCtx) diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h index b67bb035fa2..61fe21cb548 100644 --- a/src/mongo/db/pipeline/pipeline.h +++ b/src/mongo/db/pipeline/pipeline.h @@ -109,6 +109,14 @@ public: MatchExpressionParser::AllowedFeatures::kGeoNear; /** + * The match expression features allowed when running .find() on a view. + * + * The result can depend on feature flags or FCV. + * If FCV is not known yet, we err on the side of disallowing newer features. + */ + static MatchExpressionParser::AllowedFeatureSet viewFindMatcherFeatures(); + + /** * Parses a Pipeline from a vector of BSONObjs then invokes the optional 'validator' callback * with a reference to the newly created Pipeline. If no validator callback is given, this * method assumes that we're parsing a top-level pipeline. Throws an exception if it failed to diff --git a/src/mongo/db/query/canonical_query.cpp b/src/mongo/db/query/canonical_query.cpp index 87efc4cc259..2cfac7c0643 100644 --- a/src/mongo/db/query/canonical_query.cpp +++ b/src/mongo/db/query/canonical_query.cpp @@ -37,7 +37,9 @@ #include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/cst/cst_parser.h" #include "mongo/db/jsobj.h" +#include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_array.h" +#include "mongo/db/matcher/expression_geo.h" #include "mongo/db/namespace_string.h" #include "mongo/db/operation_context.h" #include "mongo/db/query/canonical_query_encoder.h" @@ -45,6 +47,7 @@ #include "mongo/db/query/indexability.h" #include "mongo/db/query/projection_parser.h" #include "mongo/db/query/query_planner_common.h" +#include "mongo/db/storage/storage_parameters_gen.h" namespace mongo { namespace { @@ -210,6 +213,46 @@ Status CanonicalQuery::init(OperationContext* opCtx, return status; } + { + // If there is a geo expression, extract it. + if (auto n = countNodes(_root.get(), MatchExpression::GEO_NEAR)) { + tassert(1234501, "isValidNormalized should have caught extra GEO_NEAR", n == 1); + + if (_root->matchType() == MatchExpression::GEO_NEAR) { + std::unique_ptr<MatchExpression> nonGeoNear{ + static_cast<MatchExpression*>(new AlwaysTrueMatchExpression())}; + std::unique_ptr<GeoNearMatchExpression> geoNear{ + static_cast<GeoNearMatchExpression*>(_root->shallowClone().release())}; + _splitGeoNear = SplitGeoNear{std::move(nonGeoNear), std::move(geoNear)}; + } else { + tassert(1234502, + "GEO_NEAR can only happen in a top-level AND", + _root->matchType() == MatchExpression::AND); + + auto nonGeoNear = _root->shallowClone(); + std::unique_ptr<GeoNearMatchExpression> geoNear; + + for (size_t i = 0; i < nonGeoNear->numChildren(); ++i) { + auto& children = *nonGeoNear->getChildVector(); + if (nonGeoNear->getChild(i)->matchType() == MatchExpression::GEO_NEAR) { + // Found it. + geoNear.reset(static_cast<GeoNearMatchExpression*>(children[i].release())); + children.erase(children.begin() + i); + break; + } + } + tassert(1234503, "Expected a GEO_NEAR in here", geoNear); + + // Removing one branch of an $and may have left us with only one remaining branch. + if (nonGeoNear->numChildren() == 1) { + nonGeoNear = std::move(nonGeoNear->getChildVector()->at(0)); + } + + _splitGeoNear = SplitGeoNear{std::move(nonGeoNear), std::move(geoNear)}; + } + } + } + // Validate the projection if there is one. if (!_findCommand->getProjection().isEmpty()) { try { @@ -288,6 +331,10 @@ void CanonicalQuery::setCollator(std::unique_ptr<CollatorInterface> collator) { // The collator associated with the match expression tree is now invalid, since we have reset // the collator owned by the ExpressionContext. _root->setCollator(collatorRaw); + if (_splitGeoNear) { + _splitGeoNear->geoNear->setCollator(collatorRaw); + _splitGeoNear->nonGeoNear->setCollator(collatorRaw); + } } // static @@ -331,6 +378,14 @@ size_t CanonicalQuery::countNodes(const MatchExpression* root, MatchExpression:: return sum; } +boost::optional<GeoNearMatchExpression*> CanonicalQuery::geoNear() const { + if (_splitGeoNear) { + return static_cast<GeoNearMatchExpression*>(_splitGeoNear->geoNear.get()); + } else { + return boost::none; + } +} + /** * Does 'root' have a subtree of type 'subtreeType' with a node of type 'childType' inside? */ @@ -509,6 +564,10 @@ std::string CanonicalQuery::toString() const { // The expression tree puts an endl on for us. ss << "Tree: " << _root->debugString(); + if (_splitGeoNear) { + ss << "NonGeoNear: " << _splitGeoNear->nonGeoNear->debugString(); + ss << "GeoNear: " << _splitGeoNear->geoNear->debugString(); + } ss << "Sort: " << _findCommand->getSort().toString() << '\n'; ss << "Proj: " << _findCommand->getProjection().toString() << '\n'; if (!_findCommand->getCollation().isEmpty()) { @@ -547,4 +606,220 @@ CanonicalQuery::QueryShapeString CanonicalQuery::encodeKey() const { return canonical_query_encoder::encode(*this); } +StatusWith<BSONObj> asAggregationCommand(const CanonicalQuery& cq) { + BSONObjBuilder aggregationBuilder; + + const FindCommandRequest& findCommand = cq.getFindCommandRequest(); + + // The find command will translate away ntoreturn above this layer. + tassert(5746106, "ntoreturn should not be set in the findCommand", !findCommand.getNtoreturn()); + + // First, check if this query has options that are not supported in aggregation. + if (!findCommand.getMin().isEmpty()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kMinFieldName + << " not supported in aggregation."}; + } + if (!findCommand.getMax().isEmpty()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kMaxFieldName + << " not supported in aggregation."}; + } + if (findCommand.getReturnKey()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kReturnKeyFieldName + << " not supported in aggregation."}; + } + if (findCommand.getShowRecordId()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kShowRecordIdFieldName + << " not supported in aggregation."}; + } + if (findCommand.getTailable()) { + return {ErrorCodes::InvalidPipelineOperator, + "Tailable cursors are not supported in aggregation."}; + } + if (findCommand.getNoCursorTimeout()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kNoCursorTimeoutFieldName + << " not supported in aggregation."}; + } + if (findCommand.getAllowPartialResults()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kAllowPartialResultsFieldName + << " not supported in aggregation."}; + } + if (findCommand.getSort()[query_request_helper::kNaturalSortField]) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Sort option " << query_request_helper::kNaturalSortField + << " not supported in aggregation."}; + } + // The aggregation command normally does not support the 'singleBatch' option, but we make a + // special exception if 'limit' is set to 1. + if (findCommand.getSingleBatch() && findCommand.getLimit().value_or(0) != 1LL) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kSingleBatchFieldName + << " not supported in aggregation."}; + } + if (findCommand.getReadOnce()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kReadOnceFieldName + << " not supported in aggregation."}; + } + + if (findCommand.getAllowSpeculativeMajorityRead()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " + << FindCommandRequest::kAllowSpeculativeMajorityReadFieldName + << " not supported in aggregation."}; + } + + if (findCommand.getRequestResumeToken()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kRequestResumeTokenFieldName + << " not supported in aggregation."}; + } + + if (!findCommand.getResumeAfter().isEmpty()) { + return {ErrorCodes::InvalidPipelineOperator, + str::stream() << "Option " << FindCommandRequest::kResumeAfterFieldName + << " not supported in aggregation."}; + } + + // Now that we've successfully validated this QR, begin building the aggregation command. + aggregationBuilder.append("aggregate", + findCommand.getNamespaceOrUUID().nss() + ? findCommand.getNamespaceOrUUID().nss()->coll() + : ""); + + // Construct an aggregation pipeline that finds the equivalent documents to this query request. + BSONArrayBuilder pipelineBuilder(aggregationBuilder.subarrayStart("pipeline")); + { + if (auto geoNear = cq.geoNear()) { + const auto& args = (*geoNear)->getData(); + + BSONObjBuilder geoStage = pipelineBuilder.subobjStart(); + BSONObjBuilder geoArgs = geoStage.subobjStart("$geoNear"_sd); + + geoArgs.append("key"_sd, (*geoNear)->path()); + + // $near and $nearSphere both support two different syntaxes: + // - GeoJSON: {type: Point, coordinates: [x, y]} + // - legacy coordinate pair: [x, y] + bool spherical = [&]() { + // $nearSphere always uses spherical geometry, but $near can use either + // spherical or planar geometry, depending on how the centroid is specified. + switch (args.centroid->crs) { + case CRS::FLAT: + return false; + case CRS::SPHERE: + return true; + case CRS::STRICT_SPHERE: + tasserted(5844301, + "CRS::STRICT_SPHERE should not be possible in a " + "$near/$nearSphere query"); + case CRS::UNSET: + tasserted(5844302, + "CRS::UNSET should not be possible in a $near/$nearSphere query"); + } + tasserted(5844300, "Unhandled enum value for CRS, in $near/$nearSphere query"); + }(); + geoArgs.append("spherical"_sd, spherical); + + if (args.unitsAreRadians || !spherical) { + // Write the $geoNear as [x, y] coordinates. + BSONArrayBuilder nearBuilder = geoArgs.subarrayStart("near"_sd); + nearBuilder.append(args.centroid->oldPoint.x); + nearBuilder.append(args.centroid->oldPoint.y); + nearBuilder.doneFast(); + } else { + // Write the $geoNear as a GeoJSON point. + BSONObjBuilder nearBuilder = geoArgs.subobjStart("near"_sd); + nearBuilder.append("type"_sd, "Point"_sd); + { + BSONArrayBuilder coordinates = nearBuilder.subarrayStart("coordinates"_sd); + coordinates.append(args.centroid->oldPoint.x); + coordinates.append(args.centroid->oldPoint.y); + coordinates.doneFast(); + } + nearBuilder.doneFast(); + } + + if (args.minDistance) { + geoArgs.append("minDistance"_sd, args.minDistance); + } + if (args.maxDistance) { + geoArgs.append("maxDistance"_sd, args.maxDistance); + } + geoArgs.doneFast(); + geoStage.doneFast(); + } + if (auto filter = cq.rootWithoutGeoNear(); !filter->isTriviallyTrue()) { + BSONObjBuilder matchBuilder = pipelineBuilder.subobjStart(); + matchBuilder.append("$match"_sd, filter->serialize()); + matchBuilder.doneFast(); + } + } + if (!findCommand.getSort().isEmpty()) { + BSONObjBuilder sortBuilder(pipelineBuilder.subobjStart()); + sortBuilder.append("$sort", findCommand.getSort()); + sortBuilder.doneFast(); + } + if (findCommand.getSkip()) { + BSONObjBuilder skipBuilder(pipelineBuilder.subobjStart()); + skipBuilder.append("$skip", *findCommand.getSkip()); + skipBuilder.doneFast(); + } + if (findCommand.getLimit()) { + BSONObjBuilder limitBuilder(pipelineBuilder.subobjStart()); + limitBuilder.append("$limit", *findCommand.getLimit()); + limitBuilder.doneFast(); + } + if (!findCommand.getProjection().isEmpty()) { + BSONObjBuilder projectBuilder(pipelineBuilder.subobjStart()); + projectBuilder.append("$project", findCommand.getProjection()); + projectBuilder.doneFast(); + } + pipelineBuilder.doneFast(); + + // The aggregation 'cursor' option is always set, regardless of the presence of batchSize. + BSONObjBuilder batchSizeBuilder(aggregationBuilder.subobjStart("cursor")); + if (findCommand.getBatchSize()) { + batchSizeBuilder.append(FindCommandRequest::kBatchSizeFieldName, + *findCommand.getBatchSize()); + } + batchSizeBuilder.doneFast(); + + // Other options. + aggregationBuilder.append("collation", findCommand.getCollation()); + int maxTimeMS = findCommand.getMaxTimeMS() ? static_cast<int>(*findCommand.getMaxTimeMS()) : 0; + if (maxTimeMS > 0) { + aggregationBuilder.append(query_request_helper::cmdOptionMaxTimeMS, maxTimeMS); + } + if (!findCommand.getHint().isEmpty()) { + aggregationBuilder.append(FindCommandRequest::kHintFieldName, findCommand.getHint()); + } + if (findCommand.getReadConcern()) { + aggregationBuilder.append("readConcern", *findCommand.getReadConcern()); + } + if (!findCommand.getUnwrappedReadPref().isEmpty()) { + aggregationBuilder.append(FindCommandRequest::kUnwrappedReadPrefFieldName, + findCommand.getUnwrappedReadPref()); + } + if (findCommand.getAllowDiskUse()) { + aggregationBuilder.append(FindCommandRequest::kAllowDiskUseFieldName, + static_cast<bool>(findCommand.getAllowDiskUse())); + } + if (findCommand.getLegacyRuntimeConstants()) { + BSONObjBuilder rtcBuilder( + aggregationBuilder.subobjStart(FindCommandRequest::kLegacyRuntimeConstantsFieldName)); + findCommand.getLegacyRuntimeConstants()->serialize(&rtcBuilder); + rtcBuilder.doneFast(); + } + if (findCommand.getLet()) { + aggregationBuilder.append(FindCommandRequest::kLetFieldName, *findCommand.getLet()); + } + return StatusWith<BSONObj>(aggregationBuilder.obj()); +} + } // namespace mongo diff --git a/src/mongo/db/query/canonical_query.h b/src/mongo/db/query/canonical_query.h index 1e2a29d6def..a0eb4cfce8e 100644 --- a/src/mongo/db/query/canonical_query.h +++ b/src/mongo/db/query/canonical_query.h @@ -45,6 +45,7 @@ namespace mongo { class OperationContext; +class GeoNearMatchExpression; class CanonicalQuery { public: @@ -130,10 +131,32 @@ public: const BSONObj& getQueryObj() const { return _findCommand->getFilter(); } + + /** + * Returns the query with any $near or $nearSphere removed. + * Returns $alwaysTrue if the query contains nothing besides $near or $nearSphere. + */ + MatchExpression* rootWithoutGeoNear() const { + if (_splitGeoNear) { + return _splitGeoNear->nonGeoNear.get(); + } else { + return _root.get(); + } + } + + /** + * Returns the $near or $nearSphere expression, if the query has one. + */ + boost::optional<GeoNearMatchExpression*> geoNear() const; + const FindCommandRequest& getFindCommandRequest() const { return *_findCommand; } + std::unique_ptr<FindCommandRequest> releaseFindCommandRequest() && { + return std::move(_findCommand); + } + /** * Returns the projection, or nullptr if none. */ @@ -256,6 +279,16 @@ private: std::unique_ptr<MatchExpression> _root; + // If _root contains a GEO_NEAR, then we split it into a geo part and a non-geo part. + // GEO_NEAR is special because it also sorts, so it can't be represented as a $match; + // we split it so we can create a separate $geoNear stage instead. + // This is only supported if gTimeseriesMetricIndexes is enabled. + struct SplitGeoNear { + std::unique_ptr<MatchExpression> nonGeoNear; + std::unique_ptr<MatchExpression> geoNear; + }; + boost::optional<SplitGeoNear> _splitGeoNear; + boost::optional<projection_ast::Projection> _proj; boost::optional<SortPattern> _sortPattern; @@ -273,4 +306,11 @@ private: bool _enableSlotBasedExecutionEngine = false; }; +/** + * Converts this CanonicalQuery into an aggregation using $match. If this CanonicalQuery has + * options that cannot be satisfied by aggregation, a non-OK status is returned and 'cmdBuilder' is + * not modified. + */ +StatusWith<BSONObj> asAggregationCommand(const CanonicalQuery& cq); + } // namespace mongo diff --git a/src/mongo/db/query/query_request_helper.cpp b/src/mongo/db/query/query_request_helper.cpp index 7db63a1427b..a8405f00e69 100644 --- a/src/mongo/db/query/query_request_helper.cpp +++ b/src/mongo/db/query/query_request_helper.cpp @@ -38,6 +38,8 @@ #include "mongo/bson/simple_bsonobj_comparator.h" #include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/dbmessage.h" +#include "mongo/db/matcher/expression_geo.h" +#include "mongo/db/query/canonical_query.h" namespace mongo { @@ -414,158 +416,5 @@ StatusWith<std::unique_ptr<FindCommandRequest>> fromLegacyQuery(NamespaceStringO return std::move(findCommand); } -StatusWith<BSONObj> asAggregationCommand(const FindCommandRequest& findCommand) { - BSONObjBuilder aggregationBuilder; - - // The find command will translate away ntoreturn above this layer. - tassert(5746106, "ntoreturn should not be set in the findCommand", !findCommand.getNtoreturn()); - - // First, check if this query has options that are not supported in aggregation. - if (!findCommand.getMin().isEmpty()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kMinFieldName - << " not supported in aggregation."}; - } - if (!findCommand.getMax().isEmpty()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kMaxFieldName - << " not supported in aggregation."}; - } - if (findCommand.getReturnKey()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kReturnKeyFieldName - << " not supported in aggregation."}; - } - if (findCommand.getShowRecordId()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kShowRecordIdFieldName - << " not supported in aggregation."}; - } - if (findCommand.getTailable()) { - return {ErrorCodes::InvalidPipelineOperator, - "Tailable cursors are not supported in aggregation."}; - } - if (findCommand.getNoCursorTimeout()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kNoCursorTimeoutFieldName - << " not supported in aggregation."}; - } - if (findCommand.getAllowPartialResults()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kAllowPartialResultsFieldName - << " not supported in aggregation."}; - } - if (findCommand.getSort()[query_request_helper::kNaturalSortField]) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Sort option " << query_request_helper::kNaturalSortField - << " not supported in aggregation."}; - } - // The aggregation command normally does not support the 'singleBatch' option, but we make a - // special exception if 'limit' is set to 1. - if (findCommand.getSingleBatch() && findCommand.getLimit().value_or(0) != 1LL) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kSingleBatchFieldName - << " not supported in aggregation."}; - } - if (findCommand.getReadOnce()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kReadOnceFieldName - << " not supported in aggregation."}; - } - - if (findCommand.getAllowSpeculativeMajorityRead()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " - << FindCommandRequest::kAllowSpeculativeMajorityReadFieldName - << " not supported in aggregation."}; - } - - if (findCommand.getRequestResumeToken()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kRequestResumeTokenFieldName - << " not supported in aggregation."}; - } - - if (!findCommand.getResumeAfter().isEmpty()) { - return {ErrorCodes::InvalidPipelineOperator, - str::stream() << "Option " << FindCommandRequest::kResumeAfterFieldName - << " not supported in aggregation."}; - } - - // Now that we've successfully validated this QR, begin building the aggregation command. - aggregationBuilder.append("aggregate", - findCommand.getNamespaceOrUUID().nss() - ? findCommand.getNamespaceOrUUID().nss()->coll() - : ""); - - // Construct an aggregation pipeline that finds the equivalent documents to this query request. - BSONArrayBuilder pipelineBuilder(aggregationBuilder.subarrayStart("pipeline")); - if (!findCommand.getFilter().isEmpty()) { - BSONObjBuilder matchBuilder(pipelineBuilder.subobjStart()); - matchBuilder.append("$match", findCommand.getFilter()); - matchBuilder.doneFast(); - } - if (!findCommand.getSort().isEmpty()) { - BSONObjBuilder sortBuilder(pipelineBuilder.subobjStart()); - sortBuilder.append("$sort", findCommand.getSort()); - sortBuilder.doneFast(); - } - if (findCommand.getSkip()) { - BSONObjBuilder skipBuilder(pipelineBuilder.subobjStart()); - skipBuilder.append("$skip", *findCommand.getSkip()); - skipBuilder.doneFast(); - } - if (findCommand.getLimit()) { - BSONObjBuilder limitBuilder(pipelineBuilder.subobjStart()); - limitBuilder.append("$limit", *findCommand.getLimit()); - limitBuilder.doneFast(); - } - if (!findCommand.getProjection().isEmpty()) { - BSONObjBuilder projectBuilder(pipelineBuilder.subobjStart()); - projectBuilder.append("$project", findCommand.getProjection()); - projectBuilder.doneFast(); - } - pipelineBuilder.doneFast(); - - // The aggregation 'cursor' option is always set, regardless of the presence of batchSize. - BSONObjBuilder batchSizeBuilder(aggregationBuilder.subobjStart("cursor")); - if (findCommand.getBatchSize()) { - batchSizeBuilder.append(FindCommandRequest::kBatchSizeFieldName, - *findCommand.getBatchSize()); - } - batchSizeBuilder.doneFast(); - - // Other options. - aggregationBuilder.append("collation", findCommand.getCollation()); - int maxTimeMS = findCommand.getMaxTimeMS() ? static_cast<int>(*findCommand.getMaxTimeMS()) : 0; - if (maxTimeMS > 0) { - aggregationBuilder.append(cmdOptionMaxTimeMS, maxTimeMS); - } - if (!findCommand.getHint().isEmpty()) { - aggregationBuilder.append(FindCommandRequest::kHintFieldName, findCommand.getHint()); - } - if (findCommand.getReadConcern()) { - aggregationBuilder.append("readConcern", *findCommand.getReadConcern()); - } - if (!findCommand.getUnwrappedReadPref().isEmpty()) { - aggregationBuilder.append(FindCommandRequest::kUnwrappedReadPrefFieldName, - findCommand.getUnwrappedReadPref()); - } - if (findCommand.getAllowDiskUse()) { - aggregationBuilder.append(FindCommandRequest::kAllowDiskUseFieldName, - static_cast<bool>(findCommand.getAllowDiskUse())); - } - if (findCommand.getLegacyRuntimeConstants()) { - BSONObjBuilder rtcBuilder( - aggregationBuilder.subobjStart(FindCommandRequest::kLegacyRuntimeConstantsFieldName)); - findCommand.getLegacyRuntimeConstants()->serialize(&rtcBuilder); - rtcBuilder.doneFast(); - } - if (findCommand.getLet()) { - aggregationBuilder.append(FindCommandRequest::kLetFieldName, *findCommand.getLet()); - } - return StatusWith<BSONObj>(aggregationBuilder.obj()); -} - } // namespace query_request_helper } // namespace mongo diff --git a/src/mongo/db/query/query_request_helper.h b/src/mongo/db/query/query_request_helper.h index c925c06dd38..063bdd1bac8 100644 --- a/src/mongo/db/query/query_request_helper.h +++ b/src/mongo/db/query/query_request_helper.h @@ -41,6 +41,7 @@ namespace mongo { class QueryMessage; +class CanonicalQuery; class Status; template <typename T> class StatusWith; @@ -93,13 +94,6 @@ std::unique_ptr<FindCommandRequest> makeFromFindCommandForTests( void refreshNSS(const NamespaceString& nss, FindCommandRequest* findCommand); /** - * Converts this FindCommandRequest into an aggregation using $match. If this FindCommandRequest has - * options that cannot be satisfied by aggregation, a non-OK status is returned and 'cmdBuilder' is - * not modified. - */ -StatusWith<BSONObj> asAggregationCommand(const FindCommandRequest& findCommand); - -/** * Helper function to identify text search sort key * Example: {a: {$meta: "textScore"}} */ diff --git a/src/mongo/db/query/query_request_test.cpp b/src/mongo/db/query/query_request_test.cpp index 6eeb8704f26..46f5e716bb8 100644 --- a/src/mongo/db/query/query_request_test.cpp +++ b/src/mongo/db/query/query_request_test.cpp @@ -40,8 +40,10 @@ #include "mongo/db/json.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregation_request_helper.h" +#include "mongo/db/query/canonical_query.h" #include "mongo/db/query/query_request_helper.h" #include "mongo/db/service_context_test_fixture.h" +#include "mongo/idl/server_parameter_test_util.h" #include "mongo/unittest/unittest.h" namespace mongo { @@ -52,6 +54,29 @@ using unittest::assertGet; static const NamespaceString testns("testdb.testcoll"); +class QueryRequestTest : public ServiceContextTest { +public: + ServiceContext::UniqueOperationContext uniqueTxn = makeOperationContext(); + OperationContext* opCtx = uniqueTxn.get(); + + // Helper wrapper for this test--the real asAggregationCommand() takes a CanonicalQuery, + // but all these examples use FindCommand. For convenience this wrapper canonicalizes + // the FindCommand before calling the real function. This wrapper is limited to this test, + // because exposing it would make it easy to accidentally canonicalize a query twice. + StatusWith<BSONObj> testAsAggregationCommand(const FindCommandRequest& findCommand) { + auto cq = CanonicalQuery::canonicalize(opCtx, + std::make_unique<FindCommandRequest>(findCommand), + false /*isExplain*/, + nullptr /*expCtx*/, + ExtensionsCallbackNoop(), + Pipeline::viewFindMatcherFeatures()); + if (!cq.isOK()) + return cq.getStatus(); + + return asAggregationCommand(*cq.getValue()); + } +}; + TEST(QueryRequestTest, LimitWithNToReturn) { FindCommandRequest findCommand(testns); findCommand.setLimit(1); @@ -1264,9 +1289,9 @@ TEST(QueryRequestTest, ParseMaxTimeMSPositiveInRangeSucceeds) { ASSERT_EQ(parseMaxTimeMSForIDL(maxTimeObj[query_request_helper::cmdOptionMaxTimeMS]), 300); } -TEST(QueryRequestTest, ConvertToAggregationSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationSucceeds) { FindCommandRequest findCommand(testns); - auto agg = query_request_helper::asAggregationCommand(findCommand); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1281,9 +1306,9 @@ TEST(QueryRequestTest, ConvertToAggregationSucceeds) { ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSONObj()); } -TEST(QueryRequestTest, ConvertToAggregationOmitsExplain) { +TEST_F(QueryRequestTest, ConvertToAggregationOmitsExplain) { FindCommandRequest findCommand(testns); - auto agg = query_request_helper::asAggregationCommand(findCommand); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1295,10 +1320,10 @@ TEST(QueryRequestTest, ConvertToAggregationOmitsExplain) { ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSONObj()); } -TEST(QueryRequestTest, ConvertToAggregationWithHintSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationWithHintSucceeds) { FindCommandRequest findCommand(testns); findCommand.setHint(fromjson("{a_1: -1}")); - const auto agg = query_request_helper::asAggregationCommand(findCommand); + const auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1307,88 +1332,88 @@ TEST(QueryRequestTest, ConvertToAggregationWithHintSucceeds) { ASSERT_BSONOBJ_EQ(findCommand.getHint(), ar.getValue().getHint().value_or(BSONObj())); } -TEST(QueryRequestTest, ConvertToAggregationWithMinFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithMinFails) { FindCommandRequest findCommand(testns); findCommand.setMin(fromjson("{a: 1}")); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithMaxFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithMaxFails) { FindCommandRequest findCommand(testns); findCommand.setMax(fromjson("{a: 1}")); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithSingleBatchFieldFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithSingleBatchFieldFails) { FindCommandRequest findCommand(testns); findCommand.setSingleBatch(true); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithSingleBatchFieldAndLimitFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithSingleBatchFieldAndLimitFails) { FindCommandRequest findCommand(testns); findCommand.setSingleBatch(true); findCommand.setLimit(7); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithSingleBatchFieldLimitOneSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationWithSingleBatchFieldLimitOneSucceeds) { FindCommandRequest findCommand(testns); findCommand.setSingleBatch(true); findCommand.setLimit(1); - ASSERT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithReturnKeyFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithReturnKeyFails) { FindCommandRequest findCommand(testns); findCommand.setReturnKey(true); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithShowRecordIdFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithShowRecordIdFails) { FindCommandRequest findCommand(testns); findCommand.setShowRecordId(true); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithTailableFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithTailableFails) { FindCommandRequest findCommand(testns); query_request_helper::setTailableMode(TailableModeEnum::kTailable, &findCommand); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithNoCursorTimeoutFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithNoCursorTimeoutFails) { FindCommandRequest findCommand(testns); findCommand.setNoCursorTimeout(true); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithAwaitDataFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithAwaitDataFails) { FindCommandRequest findCommand(testns); query_request_helper::setTailableMode(TailableModeEnum::kTailableAndAwaitData, &findCommand); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithAllowPartialResultsFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithAllowPartialResultsFails) { FindCommandRequest findCommand(testns); findCommand.setAllowPartialResults(true); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithRequestResumeTokenFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithRequestResumeTokenFails) { FindCommandRequest findCommand(testns); findCommand.setRequestResumeToken(true); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithResumeAfterFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithResumeAfterFails) { FindCommandRequest findCommand(testns); BSONObj resumeAfter = BSON("$recordId" << 1LL); findCommand.setResumeAfter(resumeAfter); - ASSERT_NOT_OK(query_request_helper::asAggregationCommand(findCommand)); + ASSERT_NOT_OK(testAsAggregationCommand(findCommand)); } -TEST(QueryRequestTest, ConvertToAggregationWithPipeline) { +TEST_F(QueryRequestTest, ConvertToAggregationWithPipeline) { FindCommandRequest findCommand(testns); findCommand.setFilter(BSON("x" << 1)); findCommand.setSort(BSON("y" << -1)); @@ -1396,7 +1421,7 @@ TEST(QueryRequestTest, ConvertToAggregationWithPipeline) { findCommand.setSkip(7); findCommand.setProjection(BSON("z" << 0)); - auto agg = query_request_helper::asAggregationCommand(findCommand); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1409,7 +1434,7 @@ TEST(QueryRequestTest, ConvertToAggregationWithPipeline) { ASSERT_EQ(ar.getValue().getNamespace(), testns); ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSONObj()); - std::vector<BSONObj> expectedPipeline{BSON("$match" << BSON("x" << 1)), + std::vector<BSONObj> expectedPipeline{BSON("$match" << BSON("x" << BSON("$eq" << 1))), BSON("$sort" << BSON("y" << -1)), BSON("$skip" << 7), BSON("$limit" << 3), @@ -1417,14 +1442,55 @@ TEST(QueryRequestTest, ConvertToAggregationWithPipeline) { ASSERT(std::equal(expectedPipeline.begin(), expectedPipeline.end(), ar.getValue().getPipeline().begin(), + ar.getValue().getPipeline().end(), SimpleBSONObjComparator::kInstance.makeEqualTo())); } -TEST(QueryRequestTest, ConvertToAggregationWithBatchSize) { +TEST_F(QueryRequestTest, ConvertToAggregationWithGeoNear) { + RAIIServerParameterControllerForTest controller("featureFlagTimeseriesMetricIndexes", true); + + FindCommandRequest findCommand(testns); + findCommand.setFilter(BSON("x" << 1 << "loc" << BSON("$near" << BSON_ARRAY(0 << 0)))); + findCommand.setSort(BSON("y" << -1)); + findCommand.setLimit(3); + findCommand.setSkip(7); + findCommand.setProjection(BSON("z" << 0)); + + auto agg = testAsAggregationCommand(findCommand); + ASSERT_OK(agg); + + auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; + auto ar = aggregation_request_helper::parseFromBSONForTests(testns, aggCmd); + ASSERT_OK(ar.getStatus()); + ASSERT(!ar.getValue().getExplain()); + ASSERT_EQ(ar.getValue().getCursor().getBatchSize().value_or( + aggregation_request_helper::kDefaultBatchSize), + aggregation_request_helper::kDefaultBatchSize); + ASSERT_EQ(ar.getValue().getNamespace(), testns); + ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSONObj()); + + std::vector<BSONObj> expectedPipeline{ + BSON("$geoNear" << BSON("key" + << "loc" + << "spherical" << false << "near" << BSON_ARRAY(0.0 << 0.0) + << "maxDistance" << std::numeric_limits<double>::max())), + BSON("$match" << BSON("x" << BSON("$eq" << 1))), + BSON("$sort" << BSON("y" << -1)), + BSON("$skip" << 7), + BSON("$limit" << 3), + BSON("$project" << BSON("z" << 0))}; + ASSERT(std::equal(expectedPipeline.begin(), + expectedPipeline.end(), + ar.getValue().getPipeline().begin(), + ar.getValue().getPipeline().end(), + SimpleBSONObjComparator::kInstance.makeEqualTo())); +} + +TEST_F(QueryRequestTest, ConvertToAggregationWithBatchSize) { FindCommandRequest findCommand(testns); findCommand.setBatchSize(4); - auto agg = query_request_helper::asAggregationCommand(findCommand); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1438,11 +1504,11 @@ TEST(QueryRequestTest, ConvertToAggregationWithBatchSize) { ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSONObj()); } -TEST(QueryRequestTest, ConvertToAggregationWithMaxTimeMS) { +TEST_F(QueryRequestTest, ConvertToAggregationWithMaxTimeMS) { FindCommandRequest findCommand(testns); findCommand.setMaxTimeMS(9); - auto agg = query_request_helper::asAggregationCommand(findCommand); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); const BSONObj cmdObj = agg.getValue(); @@ -1459,10 +1525,11 @@ TEST(QueryRequestTest, ConvertToAggregationWithMaxTimeMS) { ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSONObj()); } -TEST(QueryRequestTest, ConvertToAggregationWithCollationSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationWithCollationSucceeds) { FindCommandRequest findCommand(testns); - findCommand.setCollation(BSON("f" << 1)); - auto agg = query_request_helper::asAggregationCommand(findCommand); + findCommand.setCollation(BSON("locale" + << "fr")); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1474,28 +1541,30 @@ TEST(QueryRequestTest, ConvertToAggregationWithCollationSucceeds) { aggregation_request_helper::kDefaultBatchSize), aggregation_request_helper::kDefaultBatchSize); ASSERT_EQ(ar.getValue().getNamespace(), testns); - ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), BSON("f" << 1)); + ASSERT_BSONOBJ_EQ(ar.getValue().getCollation().value_or(BSONObj()), + BSON("locale" + << "fr")); } -TEST(QueryRequestTest, ConvertToAggregationWithReadOnceFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithReadOnceFails) { FindCommandRequest findCommand(testns); findCommand.setReadOnce(true); - const auto aggCmd = query_request_helper::asAggregationCommand(findCommand); + const auto aggCmd = testAsAggregationCommand(findCommand); ASSERT_EQ(ErrorCodes::InvalidPipelineOperator, aggCmd.getStatus().code()); } -TEST(QueryRequestTest, ConvertToAggregationWithAllowSpeculativeMajorityReadFails) { +TEST_F(QueryRequestTest, ConvertToAggregationWithAllowSpeculativeMajorityReadFails) { FindCommandRequest findCommand(testns); findCommand.setAllowSpeculativeMajorityRead(true); - const auto aggCmd = query_request_helper::asAggregationCommand(findCommand); + const auto aggCmd = testAsAggregationCommand(findCommand); ASSERT_EQ(ErrorCodes::InvalidPipelineOperator, aggCmd.getStatus().code()); } -TEST(QueryRequestTest, ConvertToAggregationWithLegacyRuntimeConstantsSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationWithLegacyRuntimeConstantsSucceeds) { LegacyRuntimeConstants rtc{Date_t::now(), Timestamp(1, 1)}; FindCommandRequest findCommand(testns); findCommand.setLegacyRuntimeConstants(rtc); - auto agg = query_request_helper::asAggregationCommand(findCommand); + auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1506,10 +1575,10 @@ TEST(QueryRequestTest, ConvertToAggregationWithLegacyRuntimeConstantsSucceeds) { ASSERT_EQ(ar.getValue().getLegacyRuntimeConstants()->getClusterTime(), rtc.getClusterTime()); } -TEST(QueryRequestTest, ConvertToAggregationWithAllowDiskUseTrueSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationWithAllowDiskUseTrueSucceeds) { FindCommandRequest findCommand(testns); findCommand.setAllowDiskUse(true); - const auto agg = query_request_helper::asAggregationCommand(findCommand); + const auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg.getStatus()); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1518,10 +1587,10 @@ TEST(QueryRequestTest, ConvertToAggregationWithAllowDiskUseTrueSucceeds) { ASSERT_EQ(true, ar.getValue().getAllowDiskUse()); } -TEST(QueryRequestTest, ConvertToAggregationWithAllowDiskUseFalseSucceeds) { +TEST_F(QueryRequestTest, ConvertToAggregationWithAllowDiskUseFalseSucceeds) { FindCommandRequest findCommand(testns); findCommand.setAllowDiskUse(false); - const auto agg = query_request_helper::asAggregationCommand(findCommand); + const auto agg = testAsAggregationCommand(findCommand); ASSERT_OK(agg.getStatus()); auto aggCmd = OpMsgRequest::fromDBAndBody(testns.db(), agg.getValue()).body; @@ -1642,8 +1711,6 @@ TEST(QueryRequestTest, ParseFromLegacyQueryExplainError) { static_cast<ErrorCodes::Error>(5856600)); } -class QueryRequestTest : public ServiceContextTest {}; - TEST_F(QueryRequestTest, ParseFromUUID) { const CollectionUUID uuid = UUID::gen(); |