diff options
author | Bernard Gorman <bernard.gorman@gmail.com> | 2018-08-15 15:01:45 +0100 |
---|---|---|
committer | Bernard Gorman <bernard.gorman@gmail.com> | 2018-10-26 02:40:53 +0100 |
commit | 9bf33e2c366c15d63091b097b0393d226433b739 (patch) | |
tree | e81e124bf5d08324f07e322aa1c613546716ef67 /src | |
parent | e9fc694cd50540e36baadf73d0716a3cfd281a5c (diff) | |
download | mongo-9bf33e2c366c15d63091b097b0393d226433b739.tar.gz |
SERVER-35493 Refactor Inclusion- and ExclusionNode into ProjectionNode base class
Diffstat (limited to 'src')
17 files changed, 805 insertions, 948 deletions
diff --git a/src/mongo/db/exec/projection_exec_agg.cpp b/src/mongo/db/exec/projection_exec_agg.cpp index 9f4b6a4c1ea..905d66561e0 100644 --- a/src/mongo/db/exec/projection_exec_agg.cpp +++ b/src/mongo/db/exec/projection_exec_agg.cpp @@ -41,7 +41,8 @@ namespace mongo { class ProjectionExecAgg::ProjectionExecutor { public: using ParsedAggregationProjection = parsed_aggregation_projection::ParsedAggregationProjection; - using ProjectionParseMode = ParsedAggregationProjection::ProjectionParseMode; + using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; + using TransformerType = TransformerInterface::TransformerType; ProjectionExecutor(BSONObj projSpec, @@ -52,27 +53,34 @@ public: // ban computed fields from the projection, the ExpressionContext will never be used. boost::intrusive_ptr<ExpressionContext> expCtx(new ExpressionContext(nullptr, nullptr)); + // Create a ProjectionPolicies object, to be populated based on the passed arguments. + ParsedAggregationProjection::ProjectionPolicies projectionPolicies; + // Default projection behaviour is to include _id if the projection spec omits it. If the // caller has specified that we should *exclude* _id by default, do so here. We translate - // DefaultIdPolicy to ParsedAggregationProjection::ProjectionDefaultIdPolicy in order to - // avoid exposing internal aggregation types to the query system. - ParsedAggregationProjection::ProjectionDefaultIdPolicy idPolicy = + // DefaultIdPolicy to ProjectionPolicies::DefaultIdPolicy in order to avoid exposing + // internal aggregation types to the query system. + projectionPolicies.idPolicy = (defaultIdPolicy == ProjectionExecAgg::DefaultIdPolicy::kIncludeId - ? ParsedAggregationProjection::ProjectionDefaultIdPolicy::kIncludeId - : ParsedAggregationProjection::ProjectionDefaultIdPolicy::kExcludeId); + ? ProjectionPolicies::DefaultIdPolicy::kIncludeId + : ProjectionPolicies::DefaultIdPolicy::kExcludeId); // By default, $project will recurse through nested arrays. If the caller has specified that // it should not, we inhibit it from doing so here. We separate this class' internal enum - // ArrayRecursionPolicy from ParsedAggregationProjection::ProjectionArrayRecursionPolicy - // in order to avoid exposing aggregation types to the query system. - ParsedAggregationProjection::ProjectionArrayRecursionPolicy recursionPolicy = + // ArrayRecursionPolicy from ProjectionPolicies::ArrayRecursionPolicy in order to avoid + // exposing aggregation types to the query system. + projectionPolicies.arrayRecursionPolicy = (arrayRecursionPolicy == ArrayRecursionPolicy::kRecurseNestedArrays - ? ParsedAggregationProjection::ProjectionArrayRecursionPolicy::kRecurseNestedArrays - : ParsedAggregationProjection::ProjectionArrayRecursionPolicy:: - kDoNotRecurseNestedArrays); + ? ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays + : ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays); + + // Inclusion projections permit computed fields by default, so we must explicitly ban them. + // Computed fields are implicitly banned for exclusions. + projectionPolicies.computedFieldsPolicy = + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields; - _projection = ParsedAggregationProjection::create( - expCtx, projSpec, idPolicy, recursionPolicy, ProjectionParseMode::kBanComputedFields); + // Construct a ParsedAggregationProjection for the given projection spec and policies. + _projection = ParsedAggregationProjection::create(expCtx, projSpec, projectionPolicies); // For an inclusion, record the exhaustive set of fields retained by the projection. if (getType() == ProjectionType::kInclusionProjection) { diff --git a/src/mongo/db/exec/projection_exec_agg.h b/src/mongo/db/exec/projection_exec_agg.h index 12ab79c4b31..ea2317ee745 100644 --- a/src/mongo/db/exec/projection_exec_agg.h +++ b/src/mongo/db/exec/projection_exec_agg.h @@ -43,12 +43,6 @@ namespace mongo { */ class ProjectionExecAgg { public: - // Allows the caller to specify how the projection should handle nested arrays; that is, an - // array whose immediate parent is itself an array. For example, in the case of sample document - // {a: [1, 2, [3, 4], {b: [5, 6]}]} the array [3, 4] is a nested array. The array [5, 6] is not, - // because there is an intervening object between it and its closest array ancestor. - enum class ArrayRecursionPolicy { kRecurseNestedArrays, kDoNotRecurseNestedArrays }; - // Allows the caller to indicate whether the projection should default to including or excluding // the _id field in the event that the projection spec does not specify the desired behavior. // For instance, given a projection {a: 1}, specifying 'kExcludeId' is equivalent to projecting @@ -57,6 +51,12 @@ public: // instance, {a: 1, _id: 0} will exclude _id for both 'kExcludeId' and 'kIncludeId'. enum class DefaultIdPolicy { kIncludeId, kExcludeId }; + // Allows the caller to specify how the projection should handle nested arrays; that is, an + // array whose immediate parent is itself an array. For example, in the case of sample document + // {a: [1, 2, [3, 4], {b: [5, 6]}]} the array [3, 4] is a nested array. The array [5, 6] is not, + // because there is an intervening object between it and its closest array ancestor. + enum class ArrayRecursionPolicy { kRecurseNestedArrays, kDoNotRecurseNestedArrays }; + enum class ProjectionType { kInclusionProjection, kExclusionProjection }; static std::unique_ptr<ProjectionExecAgg> create(BSONObj projSpec, diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index c236e273752..0717e281ce1 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -495,6 +495,7 @@ env.Library( target='parsed_aggregation_projection', source=[ 'parsed_aggregation_projection.cpp', + 'parsed_aggregation_projection_node.cpp', 'parsed_exclusion_projection.cpp', 'parsed_inclusion_projection.cpp', 'parsed_add_fields.cpp', diff --git a/src/mongo/db/pipeline/document_source_project.cpp b/src/mongo/db/pipeline/document_source_project.cpp index 940055336a2..593f9e7018b 100644 --- a/src/mongo/db/pipeline/document_source_project.cpp +++ b/src/mongo/db/pipeline/document_source_project.cpp @@ -41,10 +41,8 @@ namespace mongo { using boost::intrusive_ptr; -using parsed_aggregation_projection::ParsedAggregationProjection; - -using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy; -using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy; +using ParsedAggregationProjection = parsed_aggregation_projection::ParsedAggregationProjection; +using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; REGISTER_DOCUMENT_SOURCE(project, LiteParsedDocumentSourceDefault::parse, @@ -55,10 +53,11 @@ intrusive_ptr<DocumentSource> DocumentSourceProject::create( const bool isIndependentOfAnyCollection = false; intrusive_ptr<DocumentSource> project(new DocumentSourceSingleDocumentTransformation( expCtx, - ParsedAggregationProjection::create(expCtx, - projectSpec, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays), + ParsedAggregationProjection::create( + expCtx, + projectSpec, + {ProjectionPolicies::DefaultIdPolicy::kIncludeId, + ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays}), "$project", isIndependentOfAnyCollection)); return project; diff --git a/src/mongo/db/pipeline/parsed_add_fields.cpp b/src/mongo/db/pipeline/parsed_add_fields.cpp index 587b88b71a3..e179b195e91 100644 --- a/src/mongo/db/pipeline/parsed_add_fields.cpp +++ b/src/mongo/db/pipeline/parsed_add_fields.cpp @@ -63,7 +63,7 @@ void ParsedAddFields::parse(const BSONObj& spec) { // The field name might be a dotted path. If so, we need to keep adding children // to our tree until we create a child that represents that path. auto remainingPath = FieldPath(elem.fieldName()); - auto child = _root.get(); + auto* child = _root.get(); while (remainingPath.getPathLength() > 1) { child = child->addOrGetChild(remainingPath.getFieldName(0).toString()); remainingPath = remainingPath.tail(); @@ -75,7 +75,7 @@ void ParsedAddFields::parse(const BSONObj& spec) { } } else { // This is a literal or regular value. - _root->addComputedField( + _root->addExpressionForPath( FieldPath(elem.fieldName()), Expression::parseOperand(_expCtx, elem, _expCtx->variablesParseState)); } @@ -85,7 +85,7 @@ void ParsedAddFields::parse(const BSONObj& spec) { Document ParsedAddFields::applyProjection(const Document& inputDoc) const { // The output doc is the same as the input doc, with the added fields. MutableDocument output(inputDoc); - _root->addComputedFields(&output, inputDoc); + _root->applyExpressions(inputDoc, &output); // Pass through the metadata. output.copyMetaDataFrom(inputDoc); @@ -96,11 +96,10 @@ bool ParsedAddFields::parseObjectAsExpression(StringData pathToObject, const BSONObj& objSpec, const VariablesParseState& variablesParseState) { if (objSpec.firstElementFieldName()[0] == '$') { - // This is an expression like {$add: [...]}. We have already verified that it has only one - // field. + // This is an expression like {$add: [...]}. We already verified that it has only one field. invariant(objSpec.nFields() == 1); - _root->addComputedField(pathToObject, - Expression::parseExpression(_expCtx, objSpec, variablesParseState)); + _root->addExpressionForPath( + pathToObject, Expression::parseExpression(_expCtx, objSpec, variablesParseState)); return true; } return false; @@ -123,13 +122,14 @@ void ParsedAddFields::parseSubObject(const BSONObj& subObj, elem.Obj(), variablesParseState)) { // It was a nested subobject - auto child = node->addOrGetChild(fieldName); + auto* child = node->addOrGetChild(fieldName); parseSubObject(elem.Obj(), variablesParseState, child); } } else { // This is a literal or regular value. - node->addComputedField(FieldPath(elem.fieldName()), - Expression::parseOperand(_expCtx, elem, variablesParseState)); + node->addExpressionForPath( + FieldPath(elem.fieldName()), + Expression::parseOperand(_expCtx, elem, variablesParseState)); } } } diff --git a/src/mongo/db/pipeline/parsed_add_fields.h b/src/mongo/db/pipeline/parsed_add_fields.h index be193ffa88f..641fe890b95 100644 --- a/src/mongo/db/pipeline/parsed_add_fields.h +++ b/src/mongo/db/pipeline/parsed_add_fields.h @@ -58,10 +58,12 @@ public: * applicable to the $addFields "projection" stage. We make them non-configurable here. */ ParsedAddFields(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ParsedAggregationProjection(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays), - _root(new InclusionNode(_arrayRecursionPolicy)) {} + : ParsedAggregationProjection( + expCtx, + {ProjectionPolicies::DefaultIdPolicy::kIncludeId, + ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays, + ProjectionPolicies::ComputedFieldsPolicy::kAllowComputedFields}), + _root(new InclusionNode(_policies)) {} /** * Creates the data needed to perform an AddFields. @@ -82,9 +84,7 @@ public: Document serializeTransformation( boost::optional<ExplainOptions::Verbosity> explain) const final { - MutableDocument output; - _root->serialize(&output, explain); - return output.freeze(); + return _root->serialize(explain); } /** @@ -95,14 +95,14 @@ public: } DepsTracker::State addDependencies(DepsTracker* deps) const final { - _root->addDependencies(deps); + _root->reportDependencies(deps); return DepsTracker::State::SEE_NEXT; } DocumentSource::GetModPathsReturn getModifiedPaths() const final { std::set<std::string> computedPaths; StringMap<std::string> renamedPaths; - _root->addComputedPaths(&computedPaths, &renamedPaths); + _root->reportComputedPaths(&computedPaths, &renamedPaths); return {DocumentSource::GetModPathsReturn::Type::kFiniteSet, std::move(computedPaths), std::move(renamedPaths)}; diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp index 950edbec3fc..c8fbfb19f16 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp +++ b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp @@ -151,7 +151,8 @@ void ProjectionSpecValidator::parseNestedObject(const BSONObj& thisLevelSpec, namespace { -using ProjectionParseMode = ParsedAggregationProjection::ProjectionParseMode; +using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; +using ComputedFieldsPolicy = ProjectionPolicies::ComputedFieldsPolicy; std::string makeBannedComputedFieldsErrorMessage(BSONObj projSpec) { return str::stream() << "Bad projection specification, cannot use computed fields when parsing " @@ -169,16 +170,16 @@ public: * fields (ones which are defined by an expression or a literal) are treated as inclusion * projections for in this context of the $project stage. */ - static TransformerType parse(const BSONObj& spec, ProjectionParseMode parseMode) { - ProjectTypeParser parser(spec, parseMode); + static TransformerType parse(const BSONObj& spec, ProjectionPolicies policies) { + ProjectTypeParser parser(spec, policies); parser.parse(); invariant(parser._parsedType); return *(parser._parsedType); } private: - ProjectTypeParser(const BSONObj& spec, ProjectionParseMode parseMode) - : _rawObj(spec), _parseMode(parseMode) {} + ProjectTypeParser(const BSONObj& spec, ProjectionPolicies policies) + : _rawObj(spec), _policies(policies) {} /** * Parses a single BSONElement, with 'fieldName' representing the path used for projection @@ -223,8 +224,8 @@ private: * appropriate. * * Throws a AssertionException if this element represents a mix of projection types. If we are - * parsing in ProjectionParseMode::kBanComputedFields mode, an inclusion projection which - * contains computed fields will also be rejected. + * parsing in ComputedFieldsPolicy::kBanComputedFields mode, an inclusion projection + * which contains computed fields will also be rejected. */ void parseElement(const BSONElement& elem, const FieldPath& pathToElem) { if (elem.type() == BSONType::Object) { @@ -236,7 +237,7 @@ private: uassert(ErrorCodes::FailedToParse, makeBannedComputedFieldsErrorMessage(_rawObj), elem.isBoolean() || elem.isNumber() || - _parseMode != ProjectionParseMode::kBanComputedFields); + _policies.computedFieldsPolicy != ComputedFieldsPolicy::kBanComputedFields); if (pathToElem.fullPath() == "_id") { // If the _id field is a computed value, then this must be an inclusion projection. If @@ -280,8 +281,8 @@ private: * Traverses 'thisLevelSpec', parsing each element in turn. * * Throws a AssertionException if 'thisLevelSpec' represents an invalid mix of projections. If - * we are parsing in ProjectionParseMode::kBanComputedFields mode, an inclusion projection which - * contains computed fields will also be rejected. + * we are parsing in ComputedFieldsPolicy::kBanComputedFields mode, an inclusion + * projection which contains computed fields will also be rejected. */ void parseNestedObject(const BSONObj& thisLevelSpec, const FieldPath& prefix) { @@ -293,7 +294,7 @@ private: // specified, validate that computed projections are legal, and skip it. uassert(ErrorCodes::FailedToParse, makeBannedComputedFieldsErrorMessage(_rawObj), - _parseMode != ProjectionParseMode::kBanComputedFields); + _policies.computedFieldsPolicy != ComputedFieldsPolicy::kBanComputedFields); uassert(40182, str::stream() << "Bad projection specification, cannot include fields or " "add computed fields during an exclusion projection: " @@ -314,8 +315,8 @@ private: // This will be populated during parse(). boost::optional<TransformerType> _parsedType; - // Determines whether an inclusion projection is permitted to contain computed fields. - ProjectionParseMode _parseMode; + // Policies associated with the projection which determine its runtime behaviour. + ProjectionPolicies _policies; }; } // namespace @@ -323,16 +324,14 @@ private: std::unique_ptr<ParsedAggregationProjection> ParsedAggregationProjection::create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const BSONObj& spec, - ProjectionDefaultIdPolicy defaultIdPolicy, - ProjectionArrayRecursionPolicy arrayRecursionPolicy, - ProjectionParseMode parseMode) { + ProjectionPolicies policies) { // Check that the specification was valid. Status returned is unspecific because validate() // is used by the $addFields stage as well as $project. // If there was an error, uassert with a $project-specific message. ProjectionSpecValidator::uassertValid(spec, "$project"); // Check for any conflicting specifications, and determine the type of the projection. - auto projectionType = ProjectTypeParser::parse(spec, parseMode); + auto projectionType = ProjectTypeParser::parse(spec, policies); // kComputed is a projection type reserved for $addFields, and should never be detected by the // ProjectTypeParser. invariant(projectionType != TransformerType::kComputedProjection); @@ -341,9 +340,9 @@ std::unique_ptr<ParsedAggregationProjection> ParsedAggregationProjection::create std::unique_ptr<ParsedAggregationProjection> parsedProject( projectionType == TransformerType::kInclusionProjection ? static_cast<ParsedAggregationProjection*>( - new ParsedInclusionProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy)) + new ParsedInclusionProjection(expCtx, policies)) : static_cast<ParsedAggregationProjection*>( - new ParsedExclusionProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy))); + new ParsedExclusionProjection(expCtx, policies))); // Actually parse the specification. parsedProject->parse(spec); diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.h b/src/mongo/db/pipeline/parsed_aggregation_projection.h index af0a028925f..23fd00e1f3f 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.h +++ b/src/mongo/db/pipeline/parsed_aggregation_projection.h @@ -142,25 +142,38 @@ private: */ class ParsedAggregationProjection : public TransformerInterface { public: - // Allows the caller to indicate whether the projection should default to including or excluding - // the _id field in the event that the projection spec does not specify the desired behavior. - // For instance, given a projection {a: 1}, specifying 'kExcludeId' is equivalent to projecting - // {a: 1, _id: 0} while 'kIncludeId' is equivalent to the projection {a: 1, _id: 1}. If the user - // explicitly specifies a projection on _id, then this will override the default policy; for - // instance, {a: 1, _id: 0} will exclude _id for both 'kExcludeId' and 'kIncludeId'. - enum class ProjectionDefaultIdPolicy { kIncludeId, kExcludeId }; - - // Allows the caller to specify how the projection should handle nested arrays; that is, an - // array whose immediate parent is itself an array. For example, in the case of sample document - // {a: [1, 2, [3, 4], {b: [5, 6]}]} the array [3, 4] is a nested array. The array [5, 6] is not, - // because there is an intervening object between it and its closest array ancestor. - enum class ProjectionArrayRecursionPolicy { kRecurseNestedArrays, kDoNotRecurseNestedArrays }; - - // Allows the caller to specify whether computed fields should be allowed within inclusion - // projections; they are implicitly prohibited within exclusion projections. - enum class ProjectionParseMode { - kBanComputedFields, // No computed fields are permitted in the projection spec. - kAllowComputedFields // Computed fields are permitted. + struct ProjectionPolicies { + // Allows the caller to indicate whether the projection should default to including or + // excluding the _id field in the event that the projection spec does not specify the + // desired behavior. For instance, given a projection {a: 1}, specifying 'kExcludeId' is + // equivalent to projecting {a: 1, _id: 0} while 'kIncludeId' is equivalent to the + // projection {a: 1, _id: 1}. If the user explicitly specifies a projection on _id, then + // this will override the default policy; for instance, {a: 1, _id: 0} will exclude _id for + // both 'kExcludeId' and 'kIncludeId'. + enum class DefaultIdPolicy { kIncludeId, kExcludeId }; + + // Allows the caller to specify how the projection should handle nested arrays; that is, an + // array whose immediate parent is itself an array. For example, in the case of sample + // document {a: [1, 2, [3, 4], {b: [5, 6]}]} the array [3, 4] is a nested array. The array + // [5, 6] is not, because there is an intervening object between it and its closest array + // ancestor. + enum class ArrayRecursionPolicy { kRecurseNestedArrays, kDoNotRecurseNestedArrays }; + + // Allows the caller to specify whether computed fields should be allowed within inclusion + // projections. Computed fields are implicitly prohibited by exclusion projections. + enum class ComputedFieldsPolicy { kBanComputedFields, kAllowComputedFields }; + + ProjectionPolicies( + DefaultIdPolicy idPolicy = DefaultIdPolicy::kIncludeId, + ArrayRecursionPolicy arrayRecursionPolicy = ArrayRecursionPolicy::kRecurseNestedArrays, + ComputedFieldsPolicy computedFieldsPolicy = ComputedFieldsPolicy::kAllowComputedFields) + : idPolicy(idPolicy), + arrayRecursionPolicy(arrayRecursionPolicy), + computedFieldsPolicy(computedFieldsPolicy) {} + + DefaultIdPolicy idPolicy; + ArrayRecursionPolicy arrayRecursionPolicy; + ComputedFieldsPolicy computedFieldsPolicy; }; /** @@ -171,9 +184,7 @@ public: static std::unique_ptr<ParsedAggregationProjection> create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const BSONObj& spec, - ProjectionDefaultIdPolicy defaultIdPolicy, - ProjectionArrayRecursionPolicy arrayRecursionPolicy, - ProjectionParseMode parseRules = ProjectionParseMode::kAllowComputedFields); + ProjectionPolicies policies); virtual ~ParsedAggregationProjection() = default; @@ -206,11 +217,8 @@ public: protected: ParsedAggregationProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionDefaultIdPolicy defaultIdPolicy, - ProjectionArrayRecursionPolicy arrayRecursionPolicy) - : _expCtx(expCtx), - _arrayRecursionPolicy(arrayRecursionPolicy), - _defaultIdPolicy(defaultIdPolicy){}; + ProjectionPolicies policies) + : _expCtx(expCtx), _policies(policies){}; /** * Apply the projection to 'input'. @@ -219,8 +227,7 @@ protected: boost::intrusive_ptr<ExpressionContext> _expCtx; - ProjectionArrayRecursionPolicy _arrayRecursionPolicy; - ProjectionDefaultIdPolicy _defaultIdPolicy; + ProjectionPolicies _policies; }; } // namespace parsed_aggregation_projection } // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_node.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection_node.cpp new file mode 100644 index 00000000000..65f9a36be3d --- /dev/null +++ b/src/mongo/db/pipeline/parsed_aggregation_projection_node.cpp @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/pipeline/parsed_aggregation_projection_node.h" + +namespace mongo { +namespace parsed_aggregation_projection { + +using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; +using ArrayRecursionPolicy = ProjectionPolicies::ArrayRecursionPolicy; +using ComputedFieldsPolicy = ProjectionPolicies::ComputedFieldsPolicy; +using DefaultIdPolicy = ProjectionPolicies::DefaultIdPolicy; + +ProjectionNode::ProjectionNode(ProjectionPolicies policies, std::string pathToNode) + : _policies(policies), _pathToNode(std::move(pathToNode)) {} + +void ProjectionNode::addProjectionForPath(const FieldPath& path) { + if (path.getPathLength() == 1) { + _projectedFields.insert(path.fullPath()); + return; + } + // FieldPath can't be empty, so it is safe to obtain the first path component here. + addOrGetChild(path.getFieldName(0).toString())->addProjectionForPath(path.tail()); +} + +void ProjectionNode::addExpressionForPath(const FieldPath& path, + boost::intrusive_ptr<Expression> expr) { + // If the computed fields policy is 'kBanComputedFields', we should never reach here. + invariant(_policies.computedFieldsPolicy == ComputedFieldsPolicy::kAllowComputedFields); + if (path.getPathLength() == 1) { + auto fieldName = path.fullPath(); + _expressions[fieldName] = expr; + _orderToProcessAdditionsAndChildren.push_back(fieldName); + return; + } + // FieldPath can't be empty, so it is safe to obtain the first path component here. + addOrGetChild(path.getFieldName(0).toString())->addExpressionForPath(path.tail(), expr); +} + +ProjectionNode* ProjectionNode::addOrGetChild(const std::string& field) { + auto child = getChild(field); + return child ? child : addChild(field); +} + +ProjectionNode* ProjectionNode::addChild(const std::string& field) { + invariant(!str::contains(field, ".")); + _orderToProcessAdditionsAndChildren.push_back(field); + auto insertedPair = _children.emplace(std::make_pair(field, makeChild(field))); + return insertedPair.first->second.get(); +} + +ProjectionNode* ProjectionNode::getChild(const std::string& field) const { + auto childIt = _children.find(field); + return childIt == _children.end() ? nullptr : childIt->second.get(); +} + +Document ProjectionNode::applyToDocument(const Document& inputDoc) const { + // Defer to the derived class to initialize the output document, then apply. + MutableDocument outputDoc{initializeOutputDocument(inputDoc)}; + applyProjections(inputDoc, &outputDoc); + applyExpressions(inputDoc, &outputDoc); + + // Make sure that we always pass through any metadata present in the input doc. + outputDoc.copyMetaDataFrom(inputDoc); + return outputDoc.freeze(); +} + +void ProjectionNode::applyProjections(const Document& inputDoc, MutableDocument* outputDoc) const { + // Iterate over the input document so that the projected document retains its field ordering. + auto it = inputDoc.fieldIterator(); + while (it.more()) { + auto fieldPair = it.next(); + auto fieldName = fieldPair.first.toString(); + if (_projectedFields.count(fieldName)) { + outputDoc->setField(fieldName, applyLeafProjectionToValue(fieldPair.second)); + continue; + } + + auto childIt = _children.find(fieldName); + if (childIt != _children.end()) { + outputDoc->setField(fieldName, + childIt->second->applyProjectionsToValue(fieldPair.second)); + } + } + + // Ensure we project all specified fields, including those not present in the input document. + const bool shouldProjectNonExistentFields = applyLeafProjectionToValue(Value(true)).missing(); + for (auto&& fieldName : _projectedFields) { + if (shouldProjectNonExistentFields && inputDoc[fieldName].missing()) { + outputDoc->setField(fieldName, applyLeafProjectionToValue(inputDoc[fieldName])); + } + } +} + +Value ProjectionNode::applyProjectionsToValue(Value inputValue) const { + if (inputValue.getType() == BSONType::Object) { + MutableDocument outputSubDoc{initializeOutputDocument(inputValue.getDocument())}; + applyProjections(inputValue.getDocument(), &outputSubDoc); + return outputSubDoc.freezeToValue(); + } else if (inputValue.getType() == BSONType::Array) { + std::vector<Value> values = inputValue.getArray(); + for (auto& value : values) { + // If this is a nested array and our policy is to not recurse, skip the array. + // Otherwise, descend into the array and project each element individually. + const bool shouldSkip = value.isArray() && + _policies.arrayRecursionPolicy == ArrayRecursionPolicy::kDoNotRecurseNestedArrays; + value = (shouldSkip ? transformSkippedValueForOutput(value) + : applyProjectionsToValue(value)); + } + return Value(std::move(values)); + } else { + // This represents the case where we are projecting children of a field which does not have + // any children; for instance, applying the projection {"a.b": true} to the document {a: 2}. + return transformSkippedValueForOutput(inputValue); + } +} + +void ProjectionNode::applyExpressions(const Document& root, MutableDocument* outputDoc) const { + for (auto&& field : _orderToProcessAdditionsAndChildren) { + auto childIt = _children.find(field); + if (childIt != _children.end()) { + outputDoc->setField( + field, childIt->second->applyExpressionsToValue(root, outputDoc->peek()[field])); + } else { + auto expressionIt = _expressions.find(field); + invariant(expressionIt != _expressions.end()); + outputDoc->setField(field, expressionIt->second->evaluate(root)); + } + } +} + +Value ProjectionNode::applyExpressionsToValue(const Document& root, Value inputValue) const { + if (inputValue.getType() == BSONType::Object) { + MutableDocument outputDoc(inputValue.getDocument()); + applyExpressions(root, &outputDoc); + return outputDoc.freezeToValue(); + } else if (inputValue.getType() == BSONType::Array) { + std::vector<Value> values = inputValue.getArray(); + for (auto& value : values) { + value = applyExpressionsToValue(root, value); + } + return Value(std::move(values)); + } else { + if (subtreeContainsComputedFields()) { + // Our semantics in this case are to replace whatever existing value we find with a new + // document of all the computed values. This case represents applying a projection like + // {"a.b": {$literal: 1}} to the document {a: 1}. This should yield {a: {b: 1}}. + MutableDocument outputDoc; + applyExpressions(root, &outputDoc); + return outputDoc.freezeToValue(); + } + // We didn't have any expressions, so just skip this value. + return transformSkippedValueForOutput(inputValue); + } +} + +bool ProjectionNode::subtreeContainsComputedFields() const { + return (!_expressions.empty()) || + std::any_of(_children.begin(), _children.end(), [](const auto& childPair) { + return childPair.second->subtreeContainsComputedFields(); + }); +} + +void ProjectionNode::reportProjectedPaths(std::set<std::string>* projectedPaths) const { + for (auto&& projectedField : _projectedFields) { + projectedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, projectedField)); + } + + for (auto&& childPair : _children) { + childPair.second->reportProjectedPaths(projectedPaths); + } +} + +void ProjectionNode::reportComputedPaths(std::set<std::string>* computedPaths, + StringMap<std::string>* renamedPaths) const { + for (auto&& computedPair : _expressions) { + // The expression's path is the concatenation of the path to this node, plus the field name + // associated with the expression. + auto exprPath = FieldPath::getFullyQualifiedPath(_pathToNode, computedPair.first); + auto exprComputedPaths = computedPair.second->getComputedPaths(exprPath); + computedPaths->insert(exprComputedPaths.paths.begin(), exprComputedPaths.paths.end()); + + for (auto&& rename : exprComputedPaths.renames) { + (*renamedPaths)[rename.first] = rename.second; + } + } + for (auto&& childPair : _children) { + childPair.second->reportComputedPaths(computedPaths, renamedPaths); + } +} + +void ProjectionNode::optimize() { + for (auto&& expressionIt : _expressions) { + _expressions[expressionIt.first] = expressionIt.second->optimize(); + } + for (auto&& childPair : _children) { + childPair.second->optimize(); + } +} + +Document ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + MutableDocument outputDoc; + serialize(explain, &outputDoc); + return outputDoc.freeze(); +} + +void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explain, + MutableDocument* output) const { + // Determine the boolean value for projected fields in the explain output. + const bool projVal = !applyLeafProjectionToValue(Value(true)).missing(); + + // Always put "_id" first if it was projected (implicitly or explicitly). + if (_projectedFields.find("_id") != _projectedFields.end()) { + output->addField("_id", Value(projVal)); + } + + for (auto&& projectedField : _projectedFields) { + if (projectedField != "_id") { + output->addField(projectedField, Value(projVal)); + } + } + + for (auto&& field : _orderToProcessAdditionsAndChildren) { + auto childIt = _children.find(field); + if (childIt != _children.end()) { + MutableDocument subDoc; + childIt->second->serialize(explain, &subDoc); + output->addField(field, subDoc.freezeToValue()); + } else { + invariant(_policies.computedFieldsPolicy == ComputedFieldsPolicy::kAllowComputedFields); + auto expressionIt = _expressions.find(field); + invariant(expressionIt != _expressions.end()); + output->addField(field, expressionIt->second->serialize(static_cast<bool>(explain))); + } + } +} + +} // namespace parsed_aggregation_projection +} // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_node.h b/src/mongo/db/pipeline/parsed_aggregation_projection_node.h new file mode 100644 index 00000000000..2f7e499d621 --- /dev/null +++ b/src/mongo/db/pipeline/parsed_aggregation_projection_node.h @@ -0,0 +1,190 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * 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 GNU Affero General 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. + */ + +#pragma once + +#include "mongo/db/pipeline/parsed_aggregation_projection.h" + +namespace mongo { +namespace parsed_aggregation_projection { + +/** + * A node used to define the parsed structure of a projection. Each ProjectionNode represents one + * 'level' of the parsed specification. The root ProjectionNode represents all top level projections + * or additions, with any child ProjectionNodes representing dotted or nested projections or + * additions. + * + * ProjectionNode is an abstract base class which implements all the generic construction, traversal + * and execution functionality common to different projection types. Each derived class need only + * provide a minimal set of virtual function implementations dictating, for instance, how the + * projection should behave upon reaching a leaf node. + */ +class ProjectionNode { +public: + using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; + + ProjectionNode(ProjectionPolicies policies, std::string pathToNode = ""); + + virtual ~ProjectionNode() = default; + + /** + * Recursively adds 'path' into the tree as a projected field, creating any child nodes if + * necessary. + * + * 'path' is allowed to be dotted, and is assumed not to conflict with another path already in + * the tree. For example, it is an error to add the path "a.b" from a tree which has already + * added a computed field "a". + */ + void addProjectionForPath(const FieldPath& path); + + /** + * Recursively adds 'path' into the tree as a computed field, creating any child nodes if + * necessary. + * + * 'path' is allowed to be dotted, and is assumed not to conflict with another path already in + * the tree. For example, it is an error to add the path "a.b" as a computed field to a tree + * which has already projected the field "a". + */ + void addExpressionForPath(const FieldPath& path, boost::intrusive_ptr<Expression> expr); + + /** + * Creates the child if it doesn't already exist. 'field' is not allowed to be dotted. Returns + * the child node if it already exists, or the newly-created child otherwise. + */ + ProjectionNode* addOrGetChild(const std::string& field); + + /** + * Applies all projections and expressions, if applicable, and returns the resulting document. + */ + Document applyToDocument(const Document& inputDoc) const; + + /** + * Recursively evaluates all expressions in the projection, writing the results to 'outputDoc'. + */ + void applyExpressions(const Document& root, MutableDocument* outputDoc) const; + + /** + * Reports dependencies on any fields that are required by this projection. + */ + virtual void reportDependencies(DepsTracker* deps) const = 0; + + /** + * Recursively report all paths that are referenced by this projection. + */ + void reportProjectedPaths(std::set<std::string>* preservedPaths) const; + + /** + * Recursively reports all computed paths in this projection, adding them into 'computedPaths'. + * + * Computed paths that are identified as the result of a simple rename are instead filled out in + * 'renamedPaths'. Each entry in 'renamedPaths' maps from the path's new name to its old name + * prior to application of this projection. + */ + void reportComputedPaths(std::set<std::string>* computedPaths, + StringMap<std::string>* renamedPaths) const; + + const std::string& getPath() const { + return _pathToNode; + } + + void optimize(); + + Document serialize(boost::optional<ExplainOptions::Verbosity> explain) const; + + void serialize(boost::optional<ExplainOptions::Verbosity> explain, + MutableDocument* output) const; + +protected: + // Returns a unique_ptr to a new instance of the implementing class for the given 'fieldName'. + virtual std::unique_ptr<ProjectionNode> makeChild(std::string fieldName) const = 0; + + // Returns the initial document to which the current level of the projection should be applied. + // For an inclusion projection this will be an empty document, to which we will add the fields + // we wish to retain. For an exclusion this will be the complete document, from which we will + // eliminate the fields we wish to omit. + virtual Document initializeOutputDocument(const Document& inputDoc) const = 0; + + // Given an input leaf value, returns the value that should be added to the output document. + // Depending on the projection type this will be either the value itself, or "missing". + virtual Value applyLeafProjectionToValue(const Value& value) const = 0; + + // Given an input leaf that we intend to skip over, returns the value that should be added to + // the output document. Depending on the projection this will be either the value, or "missing". + virtual Value transformSkippedValueForOutput(const Value&) const = 0; + + // TODO use StringMap once SERVER-23700 is resolved. + stdx::unordered_map<std::string, std::unique_ptr<ProjectionNode>> _children; + stdx::unordered_map<size_t, std::unique_ptr<ProjectionNode>> _arrayBranches; + + StringMap<boost::intrusive_ptr<Expression>> _expressions; + stdx::unordered_set<std::string> _projectedFields; + + ProjectionPolicies _policies; + + std::string _pathToNode; + +private: + // Iterates 'inputDoc' for each projected field, adding to or removing from 'outputDoc'. Also + // copies over enough information to preserve the structure of the incoming document for the + // fields this projection cares about. + // + // For example, given a ProjectionNode tree representing this projection: + // {a: {b: 1, c: <exp>}, "d.e": <exp>} + // Calling applyProjections() with an 'inputDoc' of + // {a: [{b: 1, d: 1}, {b: 2, d: 2}], d: [{e: 1, f: 1}, {e: 1, f: 1}]} + // and an empty 'outputDoc' will leave 'outputDoc' representing the document + // {a: [{b: 1}, {b: 2}], d: [{}, {}]} + void applyProjections(const Document& inputDoc, MutableDocument* outputDoc) const; + + // Helpers for the 'applyProjections' and 'applyExpressions' methods. Applies the transformation + // recursively to each element of any arrays, and ensures primitives are handled appropriately. + Value applyExpressionsToValue(const Document& root, Value inputVal) const; + Value applyProjectionsToValue(Value inputVal) const; + + // Adds a new ProjectionNode as a child. 'field' cannot be dotted. + ProjectionNode* addChild(const std::string& field); + + // Returns nullptr if no such child exists. + ProjectionNode* getChild(const std::string& field) const; + + // Returns true if this node or any child of this node contains a computed field. + bool subtreeContainsComputedFields() const; + + // Our projection semantics are such that all field additions need to be processed in the order + // specified. '_orderToProcessAdditionsAndChildren' tracks that order. + // + // For example, for the specification {a: <expression>, "b.c": <expression>, d: <expression>}, + // we need to add the top level fields in the order "a", then "b", then "d". This ordering + // information needs to be tracked separately, since "a" and "d" will be tracked via + // '_expressions', and "b.c" will be tracked as a child ProjectionNode in '_children'. For the + // example above, '_orderToProcessAdditionsAndChildren' would be ["a", "b", "d"]. + std::vector<std::string> _orderToProcessAdditionsAndChildren; +}; + +} // namespace parsed_aggregation_projection +} // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp index 9d224b7adbe..5a2d933246d 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp @@ -46,24 +46,27 @@ namespace mongo { namespace parsed_aggregation_projection { namespace { -using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy; -using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy; -using ProjectionParseMode = ParsedAggregationProjection::ProjectionParseMode; +using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; template <typename T> BSONObj wrapInLiteral(const T& arg) { return BSON("$literal" << arg); } -// Helper to simplify the creation of a ParsedAggregationProjection which includes _id and recurses -// nested arrays by default. -std::unique_ptr<ParsedAggregationProjection> makeProjectionWithDefaultPolicies(BSONObj projSpec) { +// Helper to simplify the creation of a ParsedAggregationProjection with default policies. +std::unique_ptr<ParsedAggregationProjection> makeProjectionWithDefaultPolicies(BSONObj spec) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - return ParsedAggregationProjection::create( - expCtx, - projSpec, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + ParsedAggregationProjection::ProjectionPolicies defaultPolicies; + return ParsedAggregationProjection::create(expCtx, spec, defaultPolicies); +} + +// Helper to simplify the creation of a ParsedAggregationProjection which bans computed fields. +std::unique_ptr<ParsedAggregationProjection> makeProjectionWithBannedComputedFields(BSONObj spec) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ParsedAggregationProjection::ProjectionPolicies banComputedFields; + banComputedFields.computedFieldsPolicy = + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields; + return ParsedAggregationProjection::create(expCtx, spec, banComputedFields); } // @@ -71,7 +74,6 @@ std::unique_ptr<ParsedAggregationProjection> makeProjectionWithDefaultPolicies(B // TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateFieldNames) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Include/exclude the same field twice. ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a" << true)), AssertionException); @@ -94,7 +96,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateFieldNames) { } TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateIds) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Include/exclude _id twice. ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id" << true)), AssertionException); @@ -116,7 +117,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateIds) { } TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldsWithSharedPrefix) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Include/exclude Fields with a shared prefix. ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a.b" << true)), AssertionException); @@ -140,7 +140,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldsWithSharedPrefix) { } TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNumericCharacters) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Include/exclude non-alphanumeric fields with a shared prefix. First assert that the non- // alphanumeric fields are accepted when no prefixes are present. ASSERT(makeProjectionWithDefaultPolicies( @@ -202,7 +201,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNum } TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfIdAndSubFieldsOfId) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Include/exclude _id twice. ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id.x" << true)), AssertionException); @@ -227,8 +225,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfIdAndSubFieldsOfId) { } TEST(ParsedAggregationProjectionErrors, ShouldAllowMixOfIdInclusionAndExclusion) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - // Mixing "_id" inclusion with exclusion. auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a" << false)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); @@ -241,7 +237,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldAllowMixOfIdInclusionAndExclusion) } TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfInclusionAndExclusion) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Simple mix. ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << false)), AssertionException); @@ -269,7 +264,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfInclusionAndExclusion) } TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfExclusionAndComputedFields) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << wrapInLiteral(1))), AssertionException); @@ -294,7 +288,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfExclusionAndComputedFie } TEST(ParsedAggregationProjectionErrors, ShouldRejectDottedFieldInSubDocument) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << true))), AssertionException); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << wrapInLiteral(1)))), @@ -302,7 +295,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectDottedFieldInSubDocument) { } TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldNamesStartingWithADollar) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 0)), AssertionException); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 1)), AssertionException); @@ -319,13 +311,11 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldNamesStartingWithADolla } TEST(ParsedAggregationProjectionErrors, ShouldRejectTopLevelExpressions) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << BSON_ARRAY(4 << 2))), AssertionException); } TEST(ParsedAggregationProjectionErrors, ShouldRejectExpressionWithMultipleFieldNames) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies( BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))), AssertionException); @@ -345,7 +335,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyProjection) { } TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyNestedObject) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSONObj())), AssertionException); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << BSONObj())), AssertionException); @@ -357,7 +346,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyNestedObject) { } TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidExpression) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS(makeProjectionWithDefaultPolicies( BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), AssertionException); @@ -367,7 +355,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidExpression) { } TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidFieldPath) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Empty field names. ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << wrapInLiteral(2))), AssertionException); @@ -401,7 +388,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidFieldPath) { } TEST(ParsedAggregationProjectionErrors, ShouldNotErrorOnTwoNestedFields) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << true)); makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a" << BSON("c" << true))); } @@ -411,7 +397,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldNotErrorOnTwoNestedFields) { // TEST(ParsedAggregationProjectionType, ShouldDefaultToInclusionProjection) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); @@ -423,7 +408,6 @@ TEST(ParsedAggregationProjectionType, ShouldDefaultToInclusionProjection) { } TEST(ParsedAggregationProjectionType, ShouldDetectExclusionProjection) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); @@ -441,7 +425,6 @@ TEST(ParsedAggregationProjectionType, ShouldDetectExclusionProjection) { } TEST(ParsedAggregationProjectionType, ShouldDetectInclusionProjection) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << true)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); @@ -462,7 +445,6 @@ TEST(ParsedAggregationProjectionType, ShouldDetectInclusionProjection) { } TEST(ParsedAggregationProjectionType, ShouldTreatOnlyComputedFieldsAsAnInclusionProjection) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); @@ -485,7 +467,6 @@ TEST(ParsedAggregationProjectionType, ShouldTreatOnlyComputedFieldsAsAnInclusion } TEST(ParsedAggregationProjectionType, ShouldAllowMixOfInclusionAndComputedFields) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << wrapInLiteral(1))); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); @@ -504,172 +485,79 @@ TEST(ParsedAggregationProjectionType, ShouldAllowMixOfInclusionAndComputedFields } TEST(ParsedAggregationProjectionType, ShouldRejectMixOfInclusionAndBannedComputedFields) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ASSERT_THROWS( - ParsedAggregationProjection::create(expCtx, - BSON("a" << true << "b" << wrapInLiteral(1)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + makeProjectionWithBannedComputedFields(BSON("a" << true << "b" << wrapInLiteral(1))), AssertionException); ASSERT_THROWS( - ParsedAggregationProjection::create(expCtx, - BSON("a.b" << true << "a.c" << wrapInLiteral(1)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + makeProjectionWithBannedComputedFields(BSON("a.b" << true << "a.c" << wrapInLiteral(1))), AssertionException); - ASSERT_THROWS(ParsedAggregationProjection::create( - expCtx, - BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1))), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))), AssertionException); - ASSERT_THROWS( - ParsedAggregationProjection::create(expCtx, - BSON("a" << BSON("b" << true << "c" - << "stringLiteral")), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), - AssertionException); + ASSERT_THROWS(makeProjectionWithBannedComputedFields(BSON("a" << BSON("b" << true << "c" + << "stringLiteral"))), + AssertionException); } TEST(ParsedAggregationProjectionType, ShouldRejectOnlyComputedFieldsWhenComputedFieldsAreBanned) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ASSERT_THROWS(ParsedAggregationProjection::create( - expCtx, - BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(2)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(2))), AssertionException); - ASSERT_THROWS(ParsedAggregationProjection::create( - expCtx, - BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(2)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(2))), AssertionException); - ASSERT_THROWS(ParsedAggregationProjection::create( - expCtx, - BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2))), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), AssertionException); - ASSERT_THROWS(ParsedAggregationProjection::create( - expCtx, - BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2))), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields), + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), AssertionException); } TEST(ParsedAggregationProjectionType, ShouldAcceptInclusionProjectionWhenComputedFieldsAreBanned) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("a" << true), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << true)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id" << false << "a" << true), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a" << true)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id" << false << "a.b.c" << true), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a.b.c" << true)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id.x" << true), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << true)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id" << BSON("x" << true)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << true))); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("x" << BSON("_id" << true)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << true))); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); } TEST(ParsedAggregationProjectionType, ShouldAcceptExclusionProjectionWhenComputedFieldsAreBanned) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("a" << false), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << false)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id.x" << false), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << false)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id" << BSON("x" << false)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << false))); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("x" << BSON("_id" << false)), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << false))); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - parsedProject = - ParsedAggregationProjection::create(expCtx, - BSON("_id" << false), - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionParseMode::kBanComputedFields); + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false)); ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); } TEST(ParsedAggregationProjectionType, ShouldCoerceNumericsToBools) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); std::vector<Value> zeros = {Value(0), Value(0LL), Value(0.0), Value(Decimal128(0))}; for (auto&& zero : zeros) { auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", zero}}.toBson()); diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection.cpp index a4e42f9561e..510abf26419 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection.cpp +++ b/src/mongo/db/pipeline/parsed_exclusion_projection.cpp @@ -41,115 +41,13 @@ namespace mongo { namespace parsed_aggregation_projection { -// -// ExclusionNode. -// - -ExclusionNode::ExclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode) - : _arrayRecursionPolicy(recursionPolicy), _pathToNode(std::move(pathToNode)) {} - -Document ExclusionNode::serialize() const { - MutableDocument output; - for (auto&& excludedField : _excludedFields) { - output.addField(excludedField, Value(false)); - } - - for (auto&& childPair : _children) { - output.addField(childPair.first, Value(childPair.second->serialize())); - } - return output.freeze(); -} - -void ExclusionNode::excludePath(FieldPath path) { - if (path.getPathLength() == 1) { - _excludedFields.insert(path.fullPath()); - return; - } - addOrGetChild(path.getFieldName(0))->excludePath(path.tail()); -} - -Document ExclusionNode::applyProjection(const Document& input) const { - MutableDocument output(input); - for (auto&& field : _excludedFields) { - output.remove(field); - } - for (auto&& childPair : _children) { - output[childPair.first] = childPair.second->applyProjectionToValue(input[childPair.first]); - } - return output.freeze(); -} - -ExclusionNode* ExclusionNode::addOrGetChild(FieldPath fieldPath) { - invariant(fieldPath.getPathLength() == 1); - auto child = getChild(fieldPath.fullPath()); - return child ? child : addChild(fieldPath.fullPath()); -} - -ExclusionNode* ExclusionNode::getChild(std::string field) const { - auto it = _children.find(field); - return it == _children.end() ? nullptr : it->second.get(); -} - -ExclusionNode* ExclusionNode::addChild(std::string field) { - auto pathToChild = _pathToNode.empty() ? field : _pathToNode + "." + field; - - auto emplacedPair = _children.emplace(std::make_pair( - std::move(field), stdx::make_unique<ExclusionNode>(_arrayRecursionPolicy, pathToChild))); - - // emplacedPair is a pair<iterator position, bool inserted>. - invariant(emplacedPair.second); - - return emplacedPair.first->second.get(); -} - -Value ExclusionNode::applyProjectionToValue(Value val) const { - switch (val.getType()) { - case BSONType::Object: - return Value(applyProjection(val.getDocument())); - case BSONType::Array: { - // Apply exclusion to each element of the array. Note that numeric paths aren't treated - // specially, and we will always apply the projection to each element in the array. - // - // For example, applying the projection {"a.1": 0} to the document - // {a: [{b: 0, "1": 0}, {b: 1, "1": 1}]} will not result in {a: [{b: 0, "1": 0}]}, but - // instead will result in {a: [{b: 0}, {b: 1}]}. - std::vector<Value> values = val.getArray(); - for (auto it = values.begin(); it != values.end(); it++) { - // If this is a nested array and our policy is to not recurse, leave the array - // as-is. Otherwise, descend into the array and project each element individually. - const bool shouldSkip = it->isArray() && - _arrayRecursionPolicy == - ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays; - *it = (shouldSkip ? *it : applyProjectionToValue(*it)); - } - return Value(std::move(values)); - } - default: - return val; - } -} - -void ExclusionNode::addModifiedPaths(std::set<std::string>* modifiedPaths) const { - for (auto&& excludedField : _excludedFields) { - modifiedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, excludedField)); - } - - for (auto&& childPair : _children) { - childPair.second->addModifiedPaths(modifiedPaths); - } -} - -// -// ParsedExclusionProjection. -// - Document ParsedExclusionProjection::serializeTransformation( boost::optional<ExplainOptions::Verbosity> explain) const { - return _root->serialize(); + return _root->serialize(explain); } Document ParsedExclusionProjection::applyProjection(const Document& inputDoc) const { - return _root->applyProjection(inputDoc); + return _root->applyToDocument(inputDoc); } void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, size_t depth) { @@ -174,7 +72,7 @@ void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, // which is permitted to be explicitly included here. invariant(!elem.trueValue() || elem.fieldNameStringData() == "_id"_sd); if (!elem.trueValue()) { - node->excludePath(FieldPath(fieldName)); + node->addProjectionForPath(FieldPath(fieldName)); } break; } @@ -184,7 +82,7 @@ void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, ExclusionNode* child; if (elem.fieldNameStringData().find('.') == std::string::npos) { - child = node->addOrGetChild(fieldName); + child = node->addOrGetChild(fieldName.toString()); } else { // A dotted field is not allowed in a sub-object, and should have been detected // in ParsedAggregationProjection's parsing before we get here. @@ -195,7 +93,7 @@ void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, child = node; auto fullPath = FieldPath(fieldName); while (fullPath.getPathLength() > 1) { - child = child->addOrGetChild(fullPath.getFieldName(0)); + child = child->addOrGetChild(fullPath.getFieldName(0).toString()); fullPath = fullPath.tail(); } // It is illegal to construct an empty FieldPath, so the above loop ends one @@ -212,8 +110,8 @@ void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, // If _id was not specified, then doing nothing will cause it to be included. If the default _id // policy is kExcludeId, we add a new entry for _id to the ExclusionNode tree here. - if (!idSpecified && _defaultIdPolicy == ProjectionDefaultIdPolicy::kExcludeId) { - _root->excludePath({FieldPath("_id")}); + if (!idSpecified && _policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kExcludeId) { + _root->addProjectionForPath({FieldPath("_id")}); } } diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.h b/src/mongo/db/pipeline/parsed_exclusion_projection.h index 52187a65a56..ee6c2aa867f 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection.h +++ b/src/mongo/db/pipeline/parsed_exclusion_projection.h @@ -34,6 +34,7 @@ #include <string> #include "mongo/db/pipeline/parsed_aggregation_projection.h" +#include "mongo/db/pipeline/parsed_aggregation_projection_node.h" #include "mongo/stdx/unordered_map.h" #include "mongo/stdx/unordered_set.h" @@ -49,50 +50,37 @@ namespace parsed_aggregation_projection { * represents one 'level' of the parsed specification. The root ExclusionNode represents all top * level exclusions, with any child ExclusionNodes representing dotted or nested exclusions. */ -class ExclusionNode { +class ExclusionNode final : public ProjectionNode { public: - using ProjectionArrayRecursionPolicy = - ParsedAggregationProjection::ProjectionArrayRecursionPolicy; - - ExclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode = ""); - - /** - * Serialize this exclusion. - */ - Document serialize() const; - - /** - * Mark this path to be excluded. 'path' is allowed to be dotted. - */ - void excludePath(FieldPath path); - - /** - * Applies this tree of exclusions to the input document. - */ - Document applyProjection(const Document& input) const; - - /** - * Creates the child if it doesn't already exist. 'field' is not allowed to be dotted. - */ - ExclusionNode* addOrGetChild(FieldPath field); - - void addModifiedPaths(std::set<std::string>* modifiedPaths) const; - -private: - // Helpers for addOrGetChild above. - ExclusionNode* getChild(std::string field) const; - ExclusionNode* addChild(std::string field); - - // Helper for applyProjection above. - Value applyProjectionToValue(Value val) const; + ExclusionNode(ProjectionPolicies policies, std::string pathToNode = "") + : ProjectionNode(policies, std::move(pathToNode)) { + // Computed fields are not supported by exclusion projections. + invariant(_policies.computedFieldsPolicy == + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields); + } - // Fields excluded at this level. - stdx::unordered_set<std::string> _excludedFields; + ExclusionNode* addOrGetChild(const std::string& field) { + return static_cast<ExclusionNode*>(ProjectionNode::addOrGetChild(field)); + } - ProjectionArrayRecursionPolicy _arrayRecursionPolicy; + void reportDependencies(DepsTracker* deps) const final { + // We have no dependencies on specific fields, since we only know the fields to be removed. + } - std::string _pathToNode; - stdx::unordered_map<std::string, std::unique_ptr<ExclusionNode>> _children; +protected: + std::unique_ptr<ProjectionNode> makeChild(std::string fieldName) const final { + return std::make_unique<ExclusionNode>( + _policies, FieldPath::getFullyQualifiedPath(_pathToNode, fieldName)); + } + Document initializeOutputDocument(const Document& inputDoc) const final { + return inputDoc; + } + Value applyLeafProjectionToValue(const Value& value) const final { + return Value(); + } + Value transformSkippedValueForOutput(const Value& value) const final { + return value; + } }; /** @@ -105,10 +93,13 @@ private: class ParsedExclusionProjection : public ParsedAggregationProjection { public: ParsedExclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionDefaultIdPolicy defaultIdPolicy, - ProjectionArrayRecursionPolicy arrayRecursionPolicy) - : ParsedAggregationProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy), - _root(new ExclusionNode(_arrayRecursionPolicy)) {} + ProjectionPolicies policies) + : ParsedAggregationProjection( + expCtx, + {policies.idPolicy, + policies.arrayRecursionPolicy, + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields}), + _root(new ExclusionNode(_policies)) {} TransformerType getType() const final { return TransformerType::kExclusionProjection; @@ -135,7 +126,7 @@ public: DocumentSource::GetModPathsReturn getModifiedPaths() const final { std::set<std::string> modifiedPaths; - _root->addModifiedPaths(&modifiedPaths); + _root->reportProjectedPaths(&modifiedPaths); return {DocumentSource::GetModPathsReturn::Type::kFiniteSet, std::move(modifiedPaths), {}}; } diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp index 6b8db5b0990..1b19d5cb664 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp @@ -51,25 +51,39 @@ namespace mongo { namespace parsed_aggregation_projection { namespace { -using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy; -using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy; +using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; using std::vector; -// Helper to simplify the creation of a ParsedExclusionProjection which includes _id and recurses -// nested arrays by default. +// Helper to simplify the creation of a ParsedExclusionProjection with default policies. ParsedExclusionProjection makeExclusionProjectionWithDefaultPolicies() { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - return ParsedExclusionProjection(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + ParsedAggregationProjection::ProjectionPolicies defaultPolicies; + return {expCtx, defaultPolicies}; +} + +// Helper to simplify the creation of a ParsedExclusionProjection which excludes _id by default. +ParsedExclusionProjection makeExclusionProjectionWithDefaultIdExclusion() { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ParsedAggregationProjection::ProjectionPolicies defaultExcludeId; + defaultExcludeId.idPolicy = ProjectionPolicies::DefaultIdPolicy::kExcludeId; + return {expCtx, defaultExcludeId}; +} + +// Helper to simplify the creation of a ParsedExclusionProjection which does not recurse arrays. +ParsedExclusionProjection makeExclusionProjectionWithNoArrayRecursion() { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ParsedAggregationProjection::ProjectionPolicies noArrayRecursion; + noArrayRecursion.arrayRecursionPolicy = + ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays; + return {expCtx, noArrayRecursion}; } // // Errors. // -DEATH_TEST(ExclusionProjection, +DEATH_TEST(ExclusionProjectionExecutionTest, ShouldRejectComputedField, "Invariant failure fieldName[0] != '$'") { // Top-level expression. @@ -77,7 +91,7 @@ DEATH_TEST(ExclusionProjection, exclusion.parse(BSON("a" << false << "b" << BSON("$literal" << 1))); } -DEATH_TEST(ExclusionProjection, +DEATH_TEST(ExclusionProjectionExecutionTest, ShouldFailWhenGivenIncludedNonIdField, "Invariant failure !elem.trueValue() || elem.fieldNameStringData() == \"_id\"_sd") { auto exclusion = makeExclusionProjectionWithDefaultPolicies(); @@ -91,12 +105,12 @@ DEATH_TEST(ExclusionProjectionExecutionTest, exclusion.parse(BSON("_id.id1" << true)); } -TEST(ExclusionProjection, ShouldAllowExplicitIdInclusionInExclusionSpec) { +TEST(ExclusionProjectionExecutionTest, ShouldAllowExplicitIdInclusionInExclusionSpec) { auto exclusion = makeExclusionProjectionWithDefaultPolicies(); exclusion.parse(BSON("_id" << true << "a" << false)); } -TEST(ExclusionProjection, ShouldSerializeToEquivalentProjection) { +TEST(ExclusionProjectionExecutionTest, ShouldSerializeToEquivalentProjection) { auto exclusion = makeExclusionProjectionWithDefaultPolicies(); exclusion.parse( fromjson("{a: 0, b: {c: NumberLong(0), d: 0.0}, 'x.y': false, _id: NumberInt(0)}")); @@ -118,7 +132,7 @@ TEST(ExclusionProjection, ShouldSerializeToEquivalentProjection) { ASSERT_VALUE_EQ(serialization["x"].getDocument()["y"], Value(false)); } -TEST(ExclusionProjection, ShouldNotAddAnyDependencies) { +TEST(ExclusionProjectionExecutionTest, ShouldNotAddAnyDependencies) { // An exclusion projection will cause the stage to return DepsTracker::State::SEE_NEXT, meaning // it doesn't strictly require any fields. // @@ -138,7 +152,7 @@ TEST(ExclusionProjection, ShouldNotAddAnyDependencies) { ASSERT_FALSE(deps.getNeedsMetadata(DepsTracker::MetadataType::TEXT_SCORE)); } -TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModified) { +TEST(ExclusionProjectionExecutionTest, ShouldReportExcludedFieldsAsModified) { auto exclusion = makeExclusionProjectionWithDefaultPolicies(); exclusion.parse(BSON("_id" << false << "a" << false << "b.c" << false)); @@ -150,7 +164,8 @@ TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModified) { ASSERT_EQ(modifiedPaths.paths.size(), 3UL); } -TEST(ExclusionProjection, ShouldReportExcludedFieldsAsModifiedWhenSpecifiedAsNestedObj) { +TEST(ExclusionProjectionExecutionTest, + ShouldReportExcludedFieldsAsModifiedWhenSpecifiedAsNestedObj) { auto exclusion = makeExclusionProjectionWithDefaultPolicies(); exclusion.parse(BSON("a" << BSON("b" << false << "c" << BSON("d" << false)))); @@ -350,24 +365,8 @@ TEST(ExclusionProjectionExecutionTest, ShouldIncludeIdByDefault) { ASSERT_DOCUMENT_EQ(result, expectedResult); } -TEST(ExclusionProjectionExecutionTest, ShouldIncludeIdWithExplicitPolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); - exclusion.parse(BSON("a" << false)); - - auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); - auto expectedResult = Document{{"_id", 2}}; - - ASSERT_DOCUMENT_EQ(result, expectedResult); -} - TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdWithExplicitPolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kExcludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); exclusion.parse(BSON("a" << false)); auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); @@ -377,10 +376,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdWithExplicitPolicy) { } TEST(ExclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitExcludeIdSpec) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(); exclusion.parse(BSON("_id" << false << "a" << false)); auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); @@ -390,10 +386,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitEx } TEST(ExclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIncludeIdSpec) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kExcludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); exclusion.parse(BSON("_id" << true << "a" << false)); auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}, {"b", 4}}); @@ -403,10 +396,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIn } TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefaultIncludePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(); exclusion.parse(BSON("_id.id1" << false << "a" << false)); auto result = exclusion.applyProjection( @@ -417,10 +407,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefau } TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefaultExcludePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kExcludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); exclusion.parse(BSON("_id.id1" << false << "a" << false)); auto result = exclusion.applyProjection( @@ -455,38 +442,12 @@ TEST(ExclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) { ASSERT_DOCUMENT_EQ(result, expectedResult); } -TEST(ExclusionProjectionExecutionTest, ShouldRecurseNestedArraysForExplicitProRecursePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); - exclusion.parse(BSON("a.b" << false)); - - // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{c: 5}], {d: 6}]} - auto result = exclusion.applyProjection( - Document{{"a", - vector<Value>{Value(1), - Value(Document{{"b", 2}, {"c", 3}}), - Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}), - Value(Document{{"d", 6}})}}}); - - auto expectedResult = Document{{"a", - vector<Value>{Value(1), - Value(Document{{"c", 3}}), - Value(vector<Value>{Value(Document{{"c", 5}})}), - Value(Document{{"d", 6}})}}}; - - ASSERT_DOCUMENT_EQ(result, expectedResult); -} - TEST(ExclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecursePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithNoArrayRecursion(); exclusion.parse(BSON("a.b" << false)); - // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{b: 4, c: 5}], {d: 6}]} + // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{b: 4, c: 5}], {d: + // 6}]} auto result = exclusion.applyProjection( Document{{"a", vector<Value>{Value(1), @@ -505,10 +466,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecurseP } TEST(ExclusionProjectionExecutionTest, ShouldNotRetainNestedArraysIfNoRecursionNeeded) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedExclusionProjection exclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays); + auto exclusion = makeExclusionProjectionWithNoArrayRecursion(); exclusion.parse(BSON("a" << false)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {} diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp index a6a5f0c4ddd..57d0df56759 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp +++ b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp @@ -38,56 +38,19 @@ namespace mongo { namespace parsed_aggregation_projection { -using std::string; -using std::unique_ptr; - // // InclusionNode // -InclusionNode::InclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode) - : _arrayRecursionPolicy(recursionPolicy), _pathToNode(std::move(pathToNode)) {} - -void InclusionNode::optimize() { - for (auto&& expressionIt : _expressions) { - _expressions[expressionIt.first] = expressionIt.second->optimize(); - } - for (auto&& childPair : _children) { - childPair.second->optimize(); - } -} - -void InclusionNode::serialize(MutableDocument* output, - boost::optional<ExplainOptions::Verbosity> explain) const { - // Always put "_id" first if it was included (implicitly or explicitly). - if (_inclusions.find("_id") != _inclusions.end()) { - output->addField("_id", Value(true)); - } - - for (auto&& includedField : _inclusions) { - if (includedField == "_id") { - // Handled above. - continue; - } - output->addField(includedField, Value(true)); - } +InclusionNode::InclusionNode(ProjectionPolicies policies, std::string pathToNode) + : ProjectionNode(policies, std::move(pathToNode)) {} - for (auto&& field : _orderToProcessAdditionsAndChildren) { - auto childIt = _children.find(field); - if (childIt != _children.end()) { - MutableDocument subDoc; - childIt->second->serialize(&subDoc, explain); - output->addField(field, subDoc.freezeToValue()); - } else { - auto expressionIt = _expressions.find(field); - invariant(expressionIt != _expressions.end()); - output->addField(field, expressionIt->second->serialize(static_cast<bool>(explain))); - } - } +InclusionNode* InclusionNode::addOrGetChild(const std::string& field) { + return static_cast<InclusionNode*>(ProjectionNode::addOrGetChild(field)); } -void InclusionNode::addDependencies(DepsTracker* deps) const { - for (auto&& includedField : _inclusions) { +void InclusionNode::reportDependencies(DepsTracker* deps) const { + for (auto&& includedField : _projectedFields) { deps->fields.insert(FieldPath::getFullyQualifiedPath(_pathToNode, includedField)); } @@ -102,166 +65,7 @@ void InclusionNode::addDependencies(DepsTracker* deps) const { expressionPair.second->addDependencies(deps); } for (auto&& childPair : _children) { - childPair.second->addDependencies(deps); - } -} - -void InclusionNode::applyInclusions(const Document& inputDoc, MutableDocument* outputDoc) const { - auto it = inputDoc.fieldIterator(); - while (it.more()) { - auto fieldPair = it.next(); - auto fieldName = fieldPair.first.toString(); - if (_inclusions.find(fieldName) != _inclusions.end()) { - outputDoc->addField(fieldName, fieldPair.second); - continue; - } - - auto childIt = _children.find(fieldName); - if (childIt != _children.end()) { - outputDoc->addField(fieldName, - childIt->second->applyInclusionsToValue(fieldPair.second)); - } - } -} - -Value InclusionNode::applyInclusionsToValue(Value inputValue) const { - if (inputValue.getType() == BSONType::Object) { - MutableDocument output; - applyInclusions(inputValue.getDocument(), &output); - return output.freezeToValue(); - } else if (inputValue.getType() == BSONType::Array) { - std::vector<Value> values = inputValue.getArray(); - for (auto it = values.begin(); it != values.end(); ++it) { - // If this is a nested array and our policy is to not recurse, remove the array. - // Otherwise, descend into the array and project each element individually. - const bool shouldSkip = it->isArray() && - _arrayRecursionPolicy == ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays; - *it = (shouldSkip ? Value() : applyInclusionsToValue(*it)); - } - return Value(std::move(values)); - } else { - // This represents the case where we are including children of a field which does not have - // any children. e.g. applying the projection {"a.b": true} to the document {a: 2}. It is - // somewhat weird, but our semantics are to return a document without the field "a". To do - // so, we return the "missing" value here. - return Value(); - } -} - -void InclusionNode::addComputedFields(MutableDocument* outputDoc, const Document& root) const { - for (auto&& field : _orderToProcessAdditionsAndChildren) { - auto childIt = _children.find(field); - if (childIt != _children.end()) { - outputDoc->setField(field, - childIt->second->addComputedFields(outputDoc->peek()[field], root)); - } else { - auto expressionIt = _expressions.find(field); - invariant(expressionIt != _expressions.end()); - outputDoc->setField(field, expressionIt->second->evaluate(root)); - } - } -} - -Value InclusionNode::addComputedFields(Value inputValue, const Document& root) const { - if (inputValue.getType() == BSONType::Object) { - MutableDocument outputDoc(inputValue.getDocument()); - addComputedFields(&outputDoc, root); - return outputDoc.freezeToValue(); - } else if (inputValue.getType() == BSONType::Array) { - std::vector<Value> values = inputValue.getArray(); - for (auto it = values.begin(); it != values.end(); ++it) { - *it = addComputedFields(*it, root); - } - return Value(std::move(values)); - } else { - if (subtreeContainsComputedFields()) { - // Our semantics in this case are to replace whatever existing value we find with a new - // document of all the computed values. This case represents applying a projection like - // {"a.b": {$literal: 1}} to the document {a: 1}. This should yield {a: {b: 1}}. - MutableDocument outputDoc; - addComputedFields(&outputDoc, root); - return outputDoc.freezeToValue(); - } - // We didn't have any expressions, so just return the missing value. - return Value(); - } -} - -bool InclusionNode::subtreeContainsComputedFields() const { - return (!_expressions.empty()) || - std::any_of( - _children.begin(), - _children.end(), - [](const std::pair<const std::string, std::unique_ptr<InclusionNode>>& childPair) { - return childPair.second->subtreeContainsComputedFields(); - }); -} - -void InclusionNode::addComputedField(const FieldPath& path, boost::intrusive_ptr<Expression> expr) { - if (path.getPathLength() == 1) { - auto fieldName = path.fullPath(); - _expressions[fieldName] = expr; - _orderToProcessAdditionsAndChildren.push_back(fieldName); - return; - } - addOrGetChild(path.getFieldName(0).toString())->addComputedField(path.tail(), expr); -} - -void InclusionNode::addIncludedField(const FieldPath& path) { - if (path.getPathLength() == 1) { - _inclusions.insert(path.fullPath()); - return; - } - addOrGetChild(path.getFieldName(0).toString())->addIncludedField(path.tail()); -} - -InclusionNode* InclusionNode::addOrGetChild(std::string field) { - auto child = getChild(field); - return child ? child : addChild(field); -} - -InclusionNode* InclusionNode::getChild(string field) const { - auto childIt = _children.find(field); - return childIt == _children.end() ? nullptr : childIt->second.get(); -} - -InclusionNode* InclusionNode::addChild(string field) { - invariant(!str::contains(field, ".")); - _orderToProcessAdditionsAndChildren.push_back(field); - auto childPath = FieldPath::getFullyQualifiedPath(_pathToNode, field); - auto insertedPair = _children.emplace(std::make_pair( - std::move(field), - stdx::make_unique<InclusionNode>(_arrayRecursionPolicy, std::move(childPath)))); - return insertedPair.first->second.get(); -} - -void InclusionNode::addPreservedPaths(std::set<std::string>* preservedPaths) const { - // Only our inclusion paths are preserved. This inclusion node may also have paths with - // associated expressions, but those paths are modified and therefore are not considered - // "preserved". - for (auto&& includedField : _inclusions) { - preservedPaths->insert(FieldPath::getFullyQualifiedPath(_pathToNode, includedField)); - } - for (auto&& childPair : _children) { - childPair.second->addPreservedPaths(preservedPaths); - } -} - -void InclusionNode::addComputedPaths(std::set<std::string>* computedPaths, - StringMap<std::string>* renamedPaths) const { - for (auto&& computedPair : _expressions) { - // The expression's path is the concatenation of the path to this inclusion node, - // plus the field name associated with the expression. - auto exprPath = FieldPath::getFullyQualifiedPath(_pathToNode, computedPair.first); - auto exprComputedPaths = computedPair.second->getComputedPaths(exprPath); - computedPaths->insert(exprComputedPaths.paths.begin(), exprComputedPaths.paths.end()); - - for (auto&& rename : exprComputedPaths.renames) { - (*renamedPaths)[rename.first] = rename.second; - } - } - for (auto&& childPair : _children) { - childPair.second->addComputedPaths(computedPaths, renamedPaths); + childPair.second->reportDependencies(deps); } } @@ -300,7 +104,7 @@ void ParsedInclusionProjection::parse(const BSONObj& spec) { case BSONType::NumberDecimal: { // This is an inclusion specification. invariant(elem.trueValue()); - _root->addIncludedField(FieldPath(elem.fieldName())); + _root->addProjectionForPath(FieldPath(elem.fieldName())); break; } case BSONType::Object: { @@ -313,7 +117,7 @@ void ParsedInclusionProjection::parse(const BSONObj& spec) { // The field name might be a dotted path. If so, we need to keep adding children // to our tree until we create a child that represents that path. auto remainingPath = FieldPath(elem.fieldName()); - auto child = _root.get(); + auto* child = _root.get(); while (remainingPath.getPathLength() > 1) { child = child->addOrGetChild(remainingPath.getFieldName(0).toString()); remainingPath = remainingPath.tail(); @@ -327,7 +131,7 @@ void ParsedInclusionProjection::parse(const BSONObj& spec) { } default: { // This is a literal value. - _root->addComputedField( + _root->addExpressionForPath( FieldPath(elem.fieldName()), Expression::parseOperand(_expCtx, elem, _expCtx->variablesParseState)); } @@ -336,11 +140,11 @@ void ParsedInclusionProjection::parse(const BSONObj& spec) { if (!idSpecified) { // _id wasn't specified, so apply the default _id projection policy here. - if (_defaultIdPolicy == ProjectionDefaultIdPolicy::kExcludeId) { + if (_policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kExcludeId) { _idExcluded = true; } else { atLeastOneFieldInOutput = true; - _root->addIncludedField(FieldPath("_id")); + _root->addProjectionForPath(FieldPath("_id")); } } @@ -352,13 +156,7 @@ void ParsedInclusionProjection::parse(const BSONObj& spec) { Document ParsedInclusionProjection::applyProjection(const Document& inputDoc) const { // All expressions will be evaluated in the context of the input document, before any // transformations have been applied. - MutableDocument output; - _root->applyInclusions(inputDoc, &output); - _root->addComputedFields(&output, inputDoc); - - // Always pass through the metadata. - output.copyMetaDataFrom(inputDoc); - return output.freeze(); + return _root->applyToDocument(inputDoc); } bool ParsedInclusionProjection::parseObjectAsExpression( @@ -369,8 +167,8 @@ bool ParsedInclusionProjection::parseObjectAsExpression( // This is an expression like {$add: [...]}. We have already verified that it has only one // field. invariant(objSpec.nFields() == 1); - _root->addComputedField(pathToObject, - Expression::parseExpression(_expCtx, objSpec, variablesParseState)); + _root->addExpressionForPath( + pathToObject, Expression::parseExpression(_expCtx, objSpec, variablesParseState)); return true; } return false; @@ -393,7 +191,7 @@ void ParsedInclusionProjection::parseSubObject(const BSONObj& subObj, case BSONType::NumberDecimal: { // This is an inclusion specification. invariant(elem.trueValue()); - node->addIncludedField(FieldPath(elem.fieldName())); + node->addProjectionForPath(FieldPath(elem.fieldName())); break; } case BSONType::Object: { @@ -405,13 +203,13 @@ void ParsedInclusionProjection::parseSubObject(const BSONObj& subObj, variablesParseState)) { break; } - auto child = node->addOrGetChild(fieldName); + auto* child = node->addOrGetChild(fieldName); parseSubObject(elem.Obj(), variablesParseState, child); break; } default: { // This is a literal value. - node->addComputedField( + node->addExpressionForPath( FieldPath(elem.fieldName()), Expression::parseOperand(_expCtx, elem, variablesParseState)); } @@ -421,7 +219,7 @@ void ParsedInclusionProjection::parseSubObject(const BSONObj& subObj, bool ParsedInclusionProjection::isSubsetOfProjection(const BSONObj& proj) const { std::set<std::string> preservedPaths; - _root->addPreservedPaths(&preservedPaths); + _root->reportProjectedPaths(&preservedPaths); for (auto&& includedField : preservedPaths) { if (!proj.hasField(includedField)) return false; @@ -430,7 +228,7 @@ bool ParsedInclusionProjection::isSubsetOfProjection(const BSONObj& proj) const // If the inclusion has any computed fields or renamed fields, then it's not a subset. std::set<std::string> computedPaths; StringMap<std::string> renamedPaths; - _root->addComputedPaths(&computedPaths, &renamedPaths); + _root->reportComputedPaths(&computedPaths, &renamedPaths); return computedPaths.empty() && renamedPaths.empty(); } diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.h b/src/mongo/db/pipeline/parsed_inclusion_projection.h index 07f7afa80d3..bd11c3e638c 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection.h +++ b/src/mongo/db/pipeline/parsed_inclusion_projection.h @@ -35,6 +35,7 @@ #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/parsed_aggregation_projection.h" +#include "mongo/db/pipeline/parsed_aggregation_projection_node.h" #include "mongo/stdx/memory.h" #include "mongo/stdx/unordered_map.h" #include "mongo/stdx/unordered_set.h" @@ -52,134 +53,28 @@ namespace parsed_aggregation_projection { * level inclusions or additions, with any child InclusionNodes representing dotted or nested * inclusions or additions. */ -class InclusionNode { +class InclusionNode final : public ProjectionNode { public: - using ProjectionArrayRecursionPolicy = - ParsedAggregationProjection::ProjectionArrayRecursionPolicy; + InclusionNode(ProjectionPolicies policies, std::string pathToNode = ""); - InclusionNode(ProjectionArrayRecursionPolicy recursionPolicy, std::string pathToNode = ""); + InclusionNode* addOrGetChild(const std::string& field); - /** - * Optimize any computed expressions. - */ - void optimize(); - - /** - * Serialize this projection. - */ - void serialize(MutableDocument* output, - boost::optional<ExplainOptions::Verbosity> explain) const; - - /** - * Adds dependencies of any fields that need to be included, or that are used by any - * expressions. - */ - void addDependencies(DepsTracker* deps) const; - - /** - * Loops over 'inputDoc', extracting and appending any included fields into 'outputDoc'. This - * will also copy over enough information to preserve the structure of the incoming document for - * all the fields this projection cares about. - * - * For example, given an InclusionNode tree representing this projection: - * {a: {b: 1, c: <exp>}, "d.e": <exp>} - * calling applyInclusions() with an 'inputDoc' of - * {a: [{b: 1, d: 1}, {b: 2, d: 2}], d: [{e: 1, f: 1}, {e: 1, f: 1}]} - * and an empty 'outputDoc' will leave 'outputDoc' representing the document - * {a: [{b: 1}, {b: 2}], d: [{}, {}]}. - */ - void applyInclusions(const Document& inputDoc, MutableDocument* outputDoc) const; - - /** - * Add computed fields to 'outputDoc'. - */ - void addComputedFields(MutableDocument* outputDoc, const Document& root) const; - - /** - * Creates the child if it doesn't already exist. 'field' is not allowed to be dotted. - */ - InclusionNode* addOrGetChild(std::string field); - - /** - * Recursively adds 'path' into the tree as a computed field, creating any child nodes if - * necessary. - * - * 'path' is allowed to be dotted, and is assumed not to conflict with another path already in - * the tree. For example, it is an error to add the path "a.b" as a computed field to a tree - * which has already included the field "a". - */ - void addComputedField(const FieldPath& path, boost::intrusive_ptr<Expression> expr); - - /** - * Recursively adds 'path' into the tree as an included field, creating any child nodes if - * necessary. - * - * 'path' is allowed to be dotted, and is assumed not to conflict with another path already in - * the tree. For example, it is an error to include the path "a.b" from a tree which has already - * added a computed field "a". - */ - void addIncludedField(const FieldPath& path); + void reportDependencies(DepsTracker* deps) const final; - std::string getPath() const { - return _pathToNode; +protected: + std::unique_ptr<ProjectionNode> makeChild(std::string fieldName) const final { + return std::make_unique<InclusionNode>( + _policies, FieldPath::getFullyQualifiedPath(_pathToNode, fieldName)); + } + Document initializeOutputDocument(const Document& inputDoc) const final { + return {}; + } + Value applyLeafProjectionToValue(const Value& value) const final { + return value; + } + Value transformSkippedValueForOutput(const Value& value) const final { + return Value(); } - - /** - * Recursively add all paths that are preserved by this inclusion projection. - */ - void addPreservedPaths(std::set<std::string>* preservedPaths) const; - - /** - * Recursively adds all paths that are purely computed in this inclusion projection to - * 'computedPaths'. - * - * Computed paths that are identified as the result of a simple rename are instead filled out in - * 'renamedPaths'. Each entry in 'renamedPaths' maps from the path's new name to its old name - * prior to application of this inclusion projection. - */ - void addComputedPaths(std::set<std::string>* computedPaths, - StringMap<std::string>* renamedPaths) const; - -private: - // Helpers for the Document versions above. These will apply the transformation recursively to - // each element of any arrays, and ensure non-documents are handled appropriately. - Value applyInclusionsToValue(Value inputVal) const; - Value addComputedFields(Value inputVal, const Document& root) const; - - /** - * Returns nullptr if no such child exists. - */ - InclusionNode* getChild(std::string field) const; - - /** - * Adds a new InclusionNode as a child. 'field' cannot be dotted. - */ - InclusionNode* addChild(std::string field); - - /** - * Returns true if this node or any child of this node contains a computed field. - */ - bool subtreeContainsComputedFields() const; - - ProjectionArrayRecursionPolicy _arrayRecursionPolicy; - - std::string _pathToNode; - - // Our projection semantics are such that all field additions need to be processed in the order - // specified. '_orderToProcessAdditionsAndChildren' tracks that order. - // - // For example, for the specification {a: <expression>, "b.c": <expression>, d: <expression>}, - // we need to add the top level fields in the order "a", then "b", then "d". This ordering - // information needs to be tracked separately, since "a" and "d" will be tracked via - // '_expressions', and "b.c" will be tracked as a child InclusionNode in '_children'. For the - // example above, '_orderToProcessAdditionsAndChildren' would be ["a", "b", "d"]. - std::vector<std::string> _orderToProcessAdditionsAndChildren; - - StringMap<boost::intrusive_ptr<Expression>> _expressions; - stdx::unordered_set<std::string> _inclusions; - - // TODO use StringMap once SERVER-23700 is resolved. - stdx::unordered_map<std::string, std::unique_ptr<InclusionNode>> _children; }; /** @@ -192,10 +87,8 @@ private: class ParsedInclusionProjection : public ParsedAggregationProjection { public: ParsedInclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionDefaultIdPolicy defaultIdPolicy, - ProjectionArrayRecursionPolicy arrayRecursionPolicy) - : ParsedAggregationProjection(expCtx, defaultIdPolicy, arrayRecursionPolicy), - _root(new InclusionNode(_arrayRecursionPolicy)) {} + ProjectionPolicies policies) + : ParsedAggregationProjection(expCtx, policies), _root(new InclusionNode(policies)) {} TransformerType getType() const final { return TransformerType::kInclusionProjection; @@ -215,7 +108,7 @@ public: if (_idExcluded) { output.addField("_id", Value(false)); } - _root->serialize(&output, explain); + _root->serialize(explain, &output); return output.freeze(); } @@ -227,17 +120,17 @@ public: } DepsTracker::State addDependencies(DepsTracker* deps) const final { - _root->addDependencies(deps); + _root->reportDependencies(deps); return DepsTracker::State::EXHAUSTIVE_FIELDS; } DocumentSource::GetModPathsReturn getModifiedPaths() const final { std::set<std::string> preservedPaths; - _root->addPreservedPaths(&preservedPaths); + _root->reportProjectedPaths(&preservedPaths); std::set<std::string> computedPaths; StringMap<std::string> renamedPaths; - _root->addComputedPaths(&computedPaths, &renamedPaths); + _root->reportComputedPaths(&computedPaths, &renamedPaths); return {DocumentSource::GetModPathsReturn::Type::kAllExcept, std::move(preservedPaths), diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp index 9327be98517..91e85cfad37 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp @@ -49,8 +49,7 @@ namespace mongo { namespace parsed_aggregation_projection { namespace { -using ProjectionArrayRecursionPolicy = ParsedAggregationProjection::ProjectionArrayRecursionPolicy; -using ProjectionDefaultIdPolicy = ParsedAggregationProjection::ProjectionDefaultIdPolicy; +using ProjectionPolicies = ParsedAggregationProjection::ProjectionPolicies; using std::vector; @@ -59,42 +58,57 @@ BSONObj wrapInLiteral(const T& arg) { return BSON("$literal" << arg); } -// Helper to simplify the creation of a ParsedInclusionProjection which includes _id and recurses -// nested arrays by default. +// Helper to simplify the creation of a ParsedInclusionProjection with default policies. ParsedInclusionProjection makeInclusionProjectionWithDefaultPolicies() { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - return ParsedInclusionProjection(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + ParsedAggregationProjection::ProjectionPolicies defaultPolicies; + return {expCtx, defaultPolicies}; } -DEATH_TEST(InclusionProjection, +// Helper to simplify the creation of a ParsedInclusionProjection which excludes _id by default. +ParsedInclusionProjection makeInclusionProjectionWithDefaultIdExclusion() { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ParsedAggregationProjection::ProjectionPolicies defaultExcludeId; + defaultExcludeId.idPolicy = ProjectionPolicies::DefaultIdPolicy::kExcludeId; + return {expCtx, defaultExcludeId}; +} + +// Helper to simplify the creation of a ParsedInclusionProjection which does not recurse arrays. +ParsedInclusionProjection makeInclusionProjectionWithNoArrayRecursion() { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ParsedAggregationProjection::ProjectionPolicies noArrayRecursion; + noArrayRecursion.arrayRecursionPolicy = + ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays; + return {expCtx, noArrayRecursion}; +} + +DEATH_TEST(InclusionProjectionExecutionTest, ShouldFailWhenGivenExcludedNonIdField, "Invariant failure elem.trueValue()") { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << false)); } -DEATH_TEST(InclusionProjection, +DEATH_TEST(InclusionProjectionExecutionTest, ShouldFailWhenGivenIncludedIdSubfield, "Invariant failure elem.trueValue()") { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("_id.id1" << false)); } -TEST(InclusionProjection, ShouldThrowWhenParsingInvalidExpression) { +TEST(InclusionProjectionExecutionTest, ShouldThrowWhenParsingInvalidExpression) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); ASSERT_THROWS(inclusion.parse(BSON("a" << BSON("$gt" << BSON("bad" << "arguments")))), AssertionException); } -TEST(InclusionProjection, ShouldRejectProjectionWithNoOutputFields) { +TEST(InclusionProjectionExecutionTest, ShouldRejectProjectionWithNoOutputFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); ASSERT_THROWS(inclusion.parse(BSON("_id" << false)), AssertionException); } -TEST(InclusionProjection, ShouldAddIncludedFieldsToDependencies) { +TEST(InclusionProjectionExecutionTest, ShouldAddIncludedFieldsToDependencies) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("_id" << false << "a" << true << "x.y" << true)); @@ -107,7 +121,7 @@ TEST(InclusionProjection, ShouldAddIncludedFieldsToDependencies) { ASSERT_EQ(deps.fields.count("x.y"), 1UL); } -TEST(InclusionProjection, ShouldAddIdToDependenciesIfNotSpecified) { +TEST(InclusionProjectionExecutionTest, ShouldAddIdToDependenciesIfNotSpecified) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true)); @@ -119,7 +133,7 @@ TEST(InclusionProjection, ShouldAddIdToDependenciesIfNotSpecified) { ASSERT_EQ(deps.fields.count("a"), 1UL); } -TEST(InclusionProjection, ShouldAddDependenciesOfComputedFields) { +TEST(InclusionProjectionExecutionTest, ShouldAddDependenciesOfComputedFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << "$a" @@ -135,7 +149,7 @@ TEST(InclusionProjection, ShouldAddDependenciesOfComputedFields) { ASSERT_EQ(deps.fields.count("z"), 1UL); } -TEST(InclusionProjection, ShouldAddPathToDependenciesForNestedComputedFields) { +TEST(InclusionProjectionExecutionTest, ShouldAddPathToDependenciesForNestedComputedFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("x.y" << "$z")); @@ -152,7 +166,7 @@ TEST(InclusionProjection, ShouldAddPathToDependenciesForNestedComputedFields) { ASSERT_EQ(deps.fields.count("x"), 1UL); } -TEST(InclusionProjection, ShouldSerializeToEquivalentProjection) { +TEST(InclusionProjectionExecutionTest, ShouldSerializeToEquivalentProjection) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(fromjson("{a: {$add: ['$a', 2]}, b: {d: 3}, 'x.y': {$literal: 4}}")); @@ -170,7 +184,7 @@ TEST(InclusionProjection, ShouldSerializeToEquivalentProjection) { inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } -TEST(InclusionProjection, ShouldSerializeExplicitExclusionOfId) { +TEST(InclusionProjectionExecutionTest, ShouldSerializeExplicitExclusionOfId) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("_id" << false << "a" << true)); @@ -188,7 +202,7 @@ TEST(InclusionProjection, ShouldSerializeExplicitExclusionOfId) { } -TEST(InclusionProjection, ShouldOptimizeTopLevelExpressions) { +TEST(InclusionProjectionExecutionTest, ShouldOptimizeTopLevelExpressions) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2)))); @@ -206,7 +220,7 @@ TEST(InclusionProjection, ShouldOptimizeTopLevelExpressions) { inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } -TEST(InclusionProjection, ShouldOptimizeNestedExpressions) { +TEST(InclusionProjectionExecutionTest, ShouldOptimizeNestedExpressions) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a.b" << BSON("$add" << BSON_ARRAY(1 << 2)))); @@ -225,7 +239,7 @@ TEST(InclusionProjection, ShouldOptimizeNestedExpressions) { inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } -TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModified) { +TEST(InclusionProjectionExecutionTest, ShouldReportThatAllExceptIncludedFieldsAreModified) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON( "a" << wrapInLiteral("computedVal") << "b.c" << wrapInLiteral("computedVal") << "d" << true @@ -244,7 +258,8 @@ TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModified) { ASSERT_EQ(modifiedPaths.paths.size(), 3UL); } -TEST(InclusionProjection, ShouldReportThatAllExceptIncludedFieldsAreModifiedWithIdExclusion) { +TEST(InclusionProjectionExecutionTest, + ShouldReportThatAllExceptIncludedFieldsAreModifiedWithIdExclusion) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("_id" << false << "a" << wrapInLiteral("computedVal") << "b.c" << wrapInLiteral("computedVal") @@ -618,6 +633,26 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldReplacingExistingShouldAppea } // +// Metadata inclusion. +// + +TEST(InclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) { + auto inclusion = makeInclusionProjectionWithDefaultPolicies(); + inclusion.parse(BSON("a" << true)); + + MutableDocument inputDocBuilder(Document{{"a", 1}}); + inputDocBuilder.setRandMetaField(1.0); + inputDocBuilder.setTextScore(10.0); + Document inputDoc = inputDocBuilder.freeze(); + + auto result = inclusion.applyProjection(inputDoc); + + MutableDocument expectedDoc(inputDoc); + expectedDoc.copyMetaDataFrom(inputDoc); + ASSERT_DOCUMENT_EQ(result, expectedDoc.freeze()); +} + +// // _id inclusion policy. // @@ -632,10 +667,7 @@ TEST(InclusionProjectionExecutionTest, ShouldIncludeIdByDefault) { } TEST(InclusionProjectionExecutionTest, ShouldIncludeIdWithIncludePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true)); auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); @@ -645,10 +677,7 @@ TEST(InclusionProjectionExecutionTest, ShouldIncludeIdWithIncludePolicy) { } TEST(InclusionProjectionExecutionTest, ShouldExcludeIdWithExcludePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kExcludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(); inclusion.parse(BSON("a" << true)); auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); @@ -658,10 +687,7 @@ TEST(InclusionProjectionExecutionTest, ShouldExcludeIdWithExcludePolicy) { } TEST(InclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitExcludeIdSpec) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("_id" << false << "a" << true)); auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); @@ -671,10 +697,7 @@ TEST(InclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitEx } TEST(InclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIncludeIdSpec) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kExcludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(); inclusion.parse(BSON("_id" << true << "a" << true)); auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); @@ -684,10 +707,7 @@ TEST(InclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIn } TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefaultIncludePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("_id.id1" << true << "a" << true)); auto result = inclusion.applyProjection( @@ -698,10 +718,7 @@ TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefau } TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefaultExcludePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kExcludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(); inclusion.parse(BSON("_id.id1" << true << "a" << true)); auto result = inclusion.applyProjection( @@ -736,35 +753,8 @@ TEST(InclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) { ASSERT_DOCUMENT_EQ(result, expectedResult); } -TEST(InclusionProjectionExecutionTest, ShouldRecurseNestedArraysForExplicitProRecursePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); - inclusion.parse(BSON("a.b" << true)); - - // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, [{b: 4}], {}]} - auto result = inclusion.applyProjection( - Document{{"a", - vector<Value>{Value(1), - Value(Document{{"b", 2}, {"c", 3}}), - Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}), - Value(Document{{"d", 6}})}}}); - - auto expectedResult = Document{{"a", - vector<Value>{Value(), - Value(Document{{"b", 2}}), - Value(vector<Value>{Value(Document{{"b", 4}})}), - Value(Document{})}}}; - - ASSERT_DOCUMENT_EQ(result, expectedResult); -} - TEST(InclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecursePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithNoArrayRecursion(); inclusion.parse(BSON("a.b" << true)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, {}]} @@ -782,10 +772,7 @@ TEST(InclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecurseP } TEST(InclusionProjectionExecutionTest, ShouldRetainNestedArraysIfNoRecursionNeeded) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithNoArrayRecursion(); inclusion.parse(BSON("a" << true)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => [output doc identical to input] @@ -803,10 +790,7 @@ TEST(InclusionProjectionExecutionTest, ShouldRetainNestedArraysIfNoRecursionNeed } TEST(InclusionProjectionExecutionTest, ComputedFieldIsAddedToNestedArrayElementsForRecursePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED"))); vector<Value> nestedValues = {Value(1), @@ -829,10 +813,7 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldIsAddedToNestedArrayElements } TEST(InclusionProjectionExecutionTest, ComputedFieldShouldReplaceNestedArrayForNoRecursePolicy) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedInclusionProjection inclusion(expCtx, - ProjectionDefaultIdPolicy::kIncludeId, - ProjectionArrayRecursionPolicy::kDoNotRecurseNestedArrays); + auto inclusion = makeInclusionProjectionWithNoArrayRecursion(); inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED"))); // For kRecurseNestedArrays, the computed field (1) replaces any scalar values in the array with @@ -859,30 +840,10 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldShouldReplaceNestedArrayForN } // -// Misc. -// - -TEST(InclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); - - MutableDocument inputDocBuilder(Document{{"a", 1}}); - inputDocBuilder.setRandMetaField(1.0); - inputDocBuilder.setTextScore(10.0); - Document inputDoc = inputDocBuilder.freeze(); - - auto result = inclusion.applyProjection(inputDoc); - - MutableDocument expectedDoc(inputDoc); - expectedDoc.copyMetaDataFrom(inputDoc); - ASSERT_DOCUMENT_EQ(result, expectedDoc.freeze()); -} - -// // Detection of subset projection. // -TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalProjection) { +TEST(InclusionProjectionExecutionTest, ShouldDetectSubsetForIdenticalProjection) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true << "b" << true)); @@ -891,7 +852,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalProjection) { ASSERT_TRUE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjection) { +TEST(InclusionProjectionExecutionTest, ShouldDetectSubsetForSupersetProjection) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true << "b" << true)); @@ -900,7 +861,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjection) { ASSERT_TRUE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalNestedProjection) { +TEST(InclusionProjectionExecutionTest, ShouldDetectSubsetForIdenticalNestedProjection) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a.b" << true)); @@ -909,7 +870,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForIdenticalNestedProjecti ASSERT_TRUE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjectionWithNestedFields) { +TEST(InclusionProjectionExecutionTest, ShouldDetectSubsetForSupersetProjectionWithNestedFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true << "c" << BSON("d" << true))); @@ -918,7 +879,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectSubsetForSupersetProjectionWithN ASSERT_TRUE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissingFields) { +TEST(InclusionProjectionExecutionTest, ShouldDetectNonSubsetForProjectionWithMissingFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true << "b" << true)); @@ -929,7 +890,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissin ASSERT_FALSE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, +TEST(InclusionProjectionExecutionTest, ShouldDetectNonSubsetForSupersetProjectionWithoutComputedFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true << "b" << true << "c" << BSON("$literal" << 1))); @@ -939,7 +900,7 @@ TEST(InclusionProjectionSubsetTest, ASSERT_FALSE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissingNestedFields) { +TEST(InclusionProjectionExecutionTest, ShouldDetectNonSubsetForProjectionWithMissingNestedFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a.b" << true << "a.c" << true)); @@ -948,7 +909,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissin ASSERT_FALSE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithRenamedFields) { +TEST(InclusionProjectionExecutionTest, ShouldDetectNonSubsetForProjectionWithRenamedFields) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << "$b")); @@ -958,7 +919,7 @@ TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithRename ASSERT_FALSE(inclusion.isSubsetOfProjection(proj)); } -TEST(InclusionProjectionSubsetTest, ShouldDetectNonSubsetForProjectionWithMissingIdField) { +TEST(InclusionProjectionExecutionTest, ShouldDetectNonSubsetForProjectionWithMissingIdField) { auto inclusion = makeInclusionProjectionWithDefaultPolicies(); inclusion.parse(BSON("a" << true)); |