diff options
author | Anton Korshunov <anton.korshunov@mongodb.com> | 2019-11-15 10:19:14 +0000 |
---|---|---|
committer | evergreen <evergreen@mongodb.com> | 2019-11-15 10:19:14 +0000 |
commit | 73b456d5c059b17d1c7f0f8badb7c72391ee2173 (patch) | |
tree | 89451cae5a7cfbedb3a2faccd307bc870f7b0a40 | |
parent | cc3f2c8ba06e9e8c248a0d91a9efd5351311ca37 (diff) | |
download | mongo-73b456d5c059b17d1c7f0f8badb7c72391ee2173.tar.gz |
SERVER-43291 Consolidate projection execution interfaces
56 files changed, 2110 insertions, 2347 deletions
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 7dcdda1cbf1..77eb23e934e 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -970,29 +970,6 @@ env.Library( ) env.Library( - target='projection_executor', - source=[ - 'exec/find_projection_executor.cpp', - 'exec/projection_executor.cpp', - ], - LIBDEPS=[ - 'matcher/expressions', - 'pipeline/parsed_aggregation_projection', - ], -) - -env.Library( - target='projection_exec_agg', - source=[ - 'exec/projection_exec_agg.cpp' - ], - LIBDEPS=[ - 'projection_executor', - 'query/projection_ast', - ], -) - -env.Library( target='query_exec', source=[ 'clientcursor.cpp', @@ -1078,6 +1055,7 @@ env.Library( 'cursor_server_params', 'db_raii', 'dbdirectclient', + 'exec/projection_executor', 'exec/scoped_timer', 'exec/sort_executor', 'exec/working_set', @@ -1088,7 +1066,6 @@ env.Library( 'matcher/expressions_mongod_only', 'ops/parsed_update', 'pipeline/pipeline', - 'projection_executor', 'query/query_common', 'query/query_planner', 'repl/repl_coordinator_interface', diff --git a/src/mongo/db/catalog/index_key_validate.cpp b/src/mongo/db/catalog/index_key_validate.cpp index 1c5c0f180de..c25fe90e159 100644 --- a/src/mongo/db/catalog/index_key_validate.cpp +++ b/src/mongo/db/catalog/index_key_validate.cpp @@ -430,7 +430,7 @@ StatusWith<BSONObj> validateIndexSpec( try { // We use WildcardKeyGenerator::createProjectionExec to parse and validate the path // projection spec. - WildcardKeyGenerator::createProjectionExec(key, indexSpecElem.embeddedObject()); + WildcardKeyGenerator::createProjectionExecutor(key, indexSpecElem.embeddedObject()); } catch (const DBException& ex) { return ex.toStatus(str::stream() << "Failed to parse: " << IndexDescriptor::kPathProjectionFieldName); diff --git a/src/mongo/db/commands/mr_common.cpp b/src/mongo/db/commands/mr_common.cpp index 7ac5056e5ac..10cb66b3543 100644 --- a/src/mongo/db/commands/mr_common.cpp +++ b/src/mongo/db/commands/mr_common.cpp @@ -39,6 +39,8 @@ #include "mongo/db/auth/privilege.h" #include "mongo/db/catalog/document_validation.h" #include "mongo/db/commands.h" +#include "mongo/db/exec/inclusion_projection_executor.h" +#include "mongo/db/exec/projection_node.h" #include "mongo/db/jsobj.h" #include "mongo/db/pipeline/accumulator_js_reduce.h" #include "mongo/db/pipeline/document_source.h" @@ -51,8 +53,6 @@ #include "mongo/db/pipeline/document_source_sort.h" #include "mongo/db/pipeline/document_source_unwind.h" #include "mongo/db/pipeline/expression_javascript.h" -#include "mongo/db/pipeline/parsed_aggregation_projection_node.h" -#include "mongo/db/pipeline/parsed_inclusion_projection.h" #include "mongo/db/query/util/make_data_structure.h" #include "mongo/util/intrusive_counter.h" #include "mongo/util/log.h" @@ -110,11 +110,11 @@ auto translateSort(boost::intrusive_ptr<ExpressionContext> expCtx, const BSONObj auto translateMap(boost::intrusive_ptr<ExpressionContext> expCtx, std::string code) { auto emitExpression = ExpressionInternalJsEmit::create( expCtx, ExpressionFieldPath::parse(expCtx, "$$ROOT", expCtx->variablesParseState), code); - auto node = std::make_unique<parsed_aggregation_projection::InclusionNode>( + auto node = std::make_unique<projection_executor::InclusionNode>( ProjectionPolicies{ProjectionPolicies::DefaultIdPolicy::kExcludeId}); node->addExpressionForPath(FieldPath{"emits"s}, std::move(emitExpression)); auto inclusion = std::unique_ptr<TransformerInterface>{ - std::make_unique<parsed_aggregation_projection::ParsedInclusionProjection>( + std::make_unique<projection_executor::InclusionProjectionExecutor>( expCtx, ProjectionPolicies{ProjectionPolicies::DefaultIdPolicy::kExcludeId}, std::move(node))}; @@ -145,12 +145,12 @@ auto translateFinalize(boost::intrusive_ptr<ExpressionContext> expCtx, std::stri ExpressionFieldPath::parse(expCtx, "$_id", expCtx->variablesParseState), ExpressionFieldPath::parse(expCtx, "$value", expCtx->variablesParseState))), code); - auto node = std::make_unique<parsed_aggregation_projection::InclusionNode>( + auto node = std::make_unique<projection_executor::InclusionNode>( ProjectionPolicies{ProjectionPolicies::DefaultIdPolicy::kIncludeId}); node->addProjectionForPath(FieldPath{"_id"s}); node->addExpressionForPath(FieldPath{"value"s}, std::move(jsExpression)); auto inclusion = std::unique_ptr<TransformerInterface>{ - std::make_unique<parsed_aggregation_projection::ParsedInclusionProjection>( + std::make_unique<projection_executor::InclusionProjectionExecutor>( expCtx, ProjectionPolicies{ProjectionPolicies::DefaultIdPolicy::kIncludeId}, std::move(node))}; diff --git a/src/mongo/db/exec/SConscript b/src/mongo/db/exec/SConscript index d8c2926a43d..27dd9c4fee9 100644 --- a/src/mongo/db/exec/SConscript +++ b/src/mongo/db/exec/SConscript @@ -56,6 +56,19 @@ sortExecutorEnv.Library( ) env.Library( + target='projection_executor', + source=[ + 'add_fields_projection_executor.cpp', + 'projection_executor_builder.cpp', + 'projection_executor_utils.cpp', + 'projection_node.cpp' + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/db/matcher/expressions' + ], +) + +env.Library( target='stagedebug_cmd', source=[ 'stagedebug_cmd.cpp' @@ -77,9 +90,14 @@ env.CppUnitTest( "document_value/document_value_test.cpp", "document_value/document_value_test_util_self_test.cpp", "document_value/value_comparator_test.cpp", + "add_fields_projection_executor_test.cpp", + "exclusion_projection_executor_test.cpp", "find_projection_executor_test.cpp", - "projection_exec_agg_test.cpp", + "inclusion_projection_executor_test.cpp", + "projection_executor_builder_test.cpp", "projection_executor_test.cpp", + "projection_executor_utils_test.cpp", + "projection_executor_wildcard_access_test.cpp", "queued_data_stage_test.cpp", "sort_test.cpp", "working_set_test.cpp", @@ -87,7 +105,6 @@ env.CppUnitTest( LIBDEPS=[ "$BUILD_DIR/mongo/base", "$BUILD_DIR/mongo/db/auth/authmocks", - "$BUILD_DIR/mongo/db/projection_exec_agg", "$BUILD_DIR/mongo/db/query/collation/collator_factory_mock", "$BUILD_DIR/mongo/db/query/collation/collator_interface_mock", "$BUILD_DIR/mongo/db/query/query_test_service_context", @@ -98,6 +115,7 @@ env.CppUnitTest( "$BUILD_DIR/mongo/util/clock_source_mock", "document_value/document_value", "document_value/document_value_test_util", + "projection_executor", "working_set", ], ) diff --git a/src/mongo/db/pipeline/parsed_add_fields.cpp b/src/mongo/db/exec/add_fields_projection_executor.cpp index a4b9677da84..ec3acc136bc 100644 --- a/src/mongo/db/pipeline/parsed_add_fields.cpp +++ b/src/mongo/db/exec/add_fields_projection_executor.cpp @@ -29,23 +29,104 @@ #include "mongo/platform/basic.h" -#include "mongo/db/pipeline/parsed_add_fields.h" +#include "mongo/db/exec/add_fields_projection_executor.h" #include <algorithm> #include "mongo/db/matcher/expression_algo.h" -#include "mongo/db/pipeline/parsed_aggregation_projection.h" - -namespace mongo { -namespace parsed_aggregation_projection { +namespace mongo::projection_executor { +namespace { using TransformerType = TransformerInterface::TransformerType; - using expression::isPathPrefixOf; -// -// ProjectionSpecValidator -// +/** + * This class ensures that the specification was valid: that none of the paths specified conflict + * with one another, that there is at least one field, etc. Here "projection" includes $addFields + * specifications. + */ +class ProjectionSpecValidator { +public: + /** + * Throws if the specification is not valid for a projection. Because this validator is meant to + * be generic, the error thrown is generic. Callers at the DocumentSource level should modify + * the error message if they want to include information specific to the stage name used. + */ + static void uassertValid(const BSONObj& spec); + +private: + ProjectionSpecValidator(const BSONObj& spec) : _rawObj(spec) {} + + /** + * Uses '_seenPaths' to see if 'path' conflicts with any paths that have already been specified. + * + * For example, a user is not allowed to specify {'a': 1, 'a.b': 1}, or some similar conflicting + * paths. + */ + void ensurePathDoesNotConflictOrThrow(const std::string& path); + + /** + * Throws if an invalid projection specification is detected. + */ + void validate(); + + /** + * Parses a single BSONElement. 'pathToElem' should include the field name of 'elem'. + * + * Delegates to parseSubObject() if 'elem' is an object. Otherwise adds the full path to 'elem' + * to '_seenPaths'. + * + * Calls ensurePathDoesNotConflictOrThrow with the path to this element, throws on conflicting + * path specifications. + */ + void parseElement(const BSONElement& elem, const FieldPath& pathToElem); + + /** + * Traverses 'thisLevelSpec', parsing each element in turn. + * + * Throws if any paths conflict with each other or existing paths, 'thisLevelSpec' contains a + * dotted path, or if 'thisLevelSpec' represents an invalid expression. + */ + void parseNestedObject(const BSONObj& thisLevelSpec, const FieldPath& prefix); + + // The original object. Used to generate more helpful error messages. + const BSONObj& _rawObj; + + // Custom comparator that orders fieldpath strings by path prefix first, then by field. + struct PathPrefixComparator { + static constexpr char dot = '.'; + + // Returns true if the lhs value should sort before the rhs, false otherwise. + bool operator()(const std::string& lhs, const std::string& rhs) const { + for (size_t pos = 0, len = std::min(lhs.size(), rhs.size()); pos < len; ++pos) { + auto &lchar = lhs[pos], &rchar = rhs[pos]; + if (lchar == rchar) { + continue; + } + + // Consider the path delimiter '.' as being less than all other characters, so that + // paths sort directly before any paths they prefix and directly after any paths + // which prefix them. + if (lchar == dot) { + return true; + } else if (rchar == dot) { + return false; + } + + // Otherwise, default to normal character comparison. + return lchar < rchar; + } + + // If we get here, then we have reached the end of lhs and/or rhs and all of their path + // segments up to this point match. If lhs is shorter than rhs, then lhs prefixes rhs + // and should sort before it. + return lhs.size() < rhs.size(); + } + }; + + // Tracks which paths we've seen to ensure no two paths conflict with each other. + std::set<std::string, PathPrefixComparator> _seenPaths; +}; void ProjectionSpecValidator::uassertValid(const BSONObj& spec) { ProjectionSpecValidator(spec).validate(); @@ -127,19 +208,20 @@ void ProjectionSpecValidator::parseNestedObject(const BSONObj& thisLevelSpec, parseElement(elem, FieldPath::getFullyQualifiedPath(prefix.fullPath(), fieldName)); } } +} // namespace -std::unique_ptr<ParsedAddFields> ParsedAddFields::create( +std::unique_ptr<AddFieldsProjectionExecutor> AddFieldsProjectionExecutor::create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const BSONObj& spec) { // Verify that we don't have conflicting field paths, etc. ProjectionSpecValidator::uassertValid(spec); - std::unique_ptr<ParsedAddFields> parsedAddFields = std::make_unique<ParsedAddFields>(expCtx); + auto executor = std::make_unique<AddFieldsProjectionExecutor>(expCtx); // Actually parse the specification. - parsedAddFields->parse(spec); - return parsedAddFields; + executor->parse(spec); + return executor; } -void ParsedAddFields::parse(const BSONObj& spec) { +void AddFieldsProjectionExecutor::parse(const BSONObj& spec) { for (auto elem : spec) { auto fieldName = elem.fieldNameStringData(); @@ -170,7 +252,7 @@ void ParsedAddFields::parse(const BSONObj& spec) { } } -Document ParsedAddFields::applyProjection(const Document& inputDoc) const { +Document AddFieldsProjectionExecutor::applyProjection(const Document& inputDoc) const { // The output doc is the same as the input doc, with the added fields. MutableDocument output(inputDoc); _root->applyExpressions(inputDoc, &output); @@ -180,9 +262,10 @@ Document ParsedAddFields::applyProjection(const Document& inputDoc) const { return output.freeze(); } -bool ParsedAddFields::parseObjectAsExpression(StringData pathToObject, - const BSONObj& objSpec, - const VariablesParseState& variablesParseState) { +bool AddFieldsProjectionExecutor::parseObjectAsExpression( + StringData pathToObject, + const BSONObj& objSpec, + const VariablesParseState& variablesParseState) { if (objSpec.firstElementFieldName()[0] == '$') { // This is an expression like {$add: [...]}. We already verified that it has only one field. invariant(objSpec.nFields() == 1); @@ -193,9 +276,9 @@ bool ParsedAddFields::parseObjectAsExpression(StringData pathToObject, return false; } -void ParsedAddFields::parseSubObject(const BSONObj& subObj, - const VariablesParseState& variablesParseState, - InclusionNode* node) { +void AddFieldsProjectionExecutor::parseSubObject(const BSONObj& subObj, + const VariablesParseState& variablesParseState, + InclusionNode* node) { for (auto&& elem : subObj) { invariant(elem.fieldName()[0] != '$'); // Dotted paths in a sub-object have already been detected and disallowed by the function @@ -221,6 +304,4 @@ void ParsedAddFields::parseSubObject(const BSONObj& subObj, } } } - -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_add_fields.h b/src/mongo/db/exec/add_fields_projection_executor.h index 5ea10e55c4b..e17ce25e494 100644 --- a/src/mongo/db/pipeline/parsed_add_fields.h +++ b/src/mongo/db/exec/add_fields_projection_executor.h @@ -31,130 +31,37 @@ #include <memory> +#include "mongo/db/exec/inclusion_projection_executor.h" #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_inclusion_projection.h" -namespace mongo { -namespace parsed_aggregation_projection { +namespace mongo::projection_executor { /** - * This class ensures that the specification was valid: that none of the paths specified conflict - * with one another, that there is at least one field, etc. Here "projection" includes $addFields - * specifications. - */ -class ProjectionSpecValidator { -public: - /** - * Throws if the specification is not valid for a projection. Because this validator is meant to - * be generic, the error thrown is generic. Callers at the DocumentSource level should modify - * the error message if they want to include information specific to the stage name used. - */ - static void uassertValid(const BSONObj& spec); - -private: - ProjectionSpecValidator(const BSONObj& spec) : _rawObj(spec) {} - - /** - * Uses '_seenPaths' to see if 'path' conflicts with any paths that have already been specified. - * - * For example, a user is not allowed to specify {'a': 1, 'a.b': 1}, or some similar conflicting - * paths. - */ - void ensurePathDoesNotConflictOrThrow(const std::string& path); - - /** - * Throws if an invalid projection specification is detected. - */ - void validate(); - - /** - * Parses a single BSONElement. 'pathToElem' should include the field name of 'elem'. - * - * Delegates to parseSubObject() if 'elem' is an object. Otherwise adds the full path to 'elem' - * to '_seenPaths'. - * - * Calls ensurePathDoesNotConflictOrThrow with the path to this element, throws on conflicting - * path specifications. - */ - void parseElement(const BSONElement& elem, const FieldPath& pathToElem); - - /** - * Traverses 'thisLevelSpec', parsing each element in turn. - * - * Throws if any paths conflict with each other or existing paths, 'thisLevelSpec' contains a - * dotted path, or if 'thisLevelSpec' represents an invalid expression. - */ - void parseNestedObject(const BSONObj& thisLevelSpec, const FieldPath& prefix); - - // The original object. Used to generate more helpful error messages. - const BSONObj& _rawObj; - - // Custom comparator that orders fieldpath strings by path prefix first, then by field. - struct PathPrefixComparator { - static constexpr char dot = '.'; - - // Returns true if the lhs value should sort before the rhs, false otherwise. - bool operator()(const std::string& lhs, const std::string& rhs) const { - for (size_t pos = 0, len = std::min(lhs.size(), rhs.size()); pos < len; ++pos) { - auto &lchar = lhs[pos], &rchar = rhs[pos]; - if (lchar == rchar) { - continue; - } - - // Consider the path delimiter '.' as being less than all other characters, so that - // paths sort directly before any paths they prefix and directly after any paths - // which prefix them. - if (lchar == dot) { - return true; - } else if (rchar == dot) { - return false; - } - - // Otherwise, default to normal character comparison. - return lchar < rchar; - } - - // If we get here, then we have reached the end of lhs and/or rhs and all of their path - // segments up to this point match. If lhs is shorter than rhs, then lhs prefixes rhs - // and should sort before it. - return lhs.size() < rhs.size(); - } - }; - - // Tracks which paths we've seen to ensure no two paths conflict with each other. - std::set<std::string, PathPrefixComparator> _seenPaths; -}; - -/** - * A ParsedAddFields represents a parsed form of the raw BSON specification for the AddFields - * stage. + * A AddFieldsProjectionExecutor represents a projection execution tree for the $addFields stage. * * This class is mostly a wrapper around an InclusionNode tree. It contains logic to parse a * specification object into the corresponding InclusionNode tree, but defers most execution logic - * to the underlying tree. In this way it is similar to ParsedInclusionProjection, but it differs + * to the underlying tree. In this way it is similar to InclusionProjectionExecutor, but it differs * by not applying inclusions before adding computed fields, thus keeping all existing fields. */ -class ParsedAddFields : public ParsedAggregationProjection { +class AddFieldsProjectionExecutor : public ProjectionExecutor { public: /** - * TODO SERVER-25510: The ParsedAggregationProjection _id and array-recursion policies are not + * TODO SERVER-25510: The ProjectionExecutor _id and array-recursion policies are not * applicable to the $addFields "projection" stage. We make them non-configurable here. */ - ParsedAddFields(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ParsedAggregationProjection( - expCtx, - {ProjectionPolicies::DefaultIdPolicy::kIncludeId, - ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays, - ProjectionPolicies::ComputedFieldsPolicy::kAllowComputedFields}), + AddFieldsProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx) + : ProjectionExecutor(expCtx, + {ProjectionPolicies::DefaultIdPolicy::kIncludeId, + ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays, + ProjectionPolicies::ComputedFieldsPolicy::kAllowComputedFields}), _root(new InclusionNode(_policies)) {} /** * Creates the data needed to perform an AddFields. * Verifies that there are no conflicting paths in the specification. - * Overrides the ParsedAggregationProjection's create method. */ - static std::unique_ptr<ParsedAddFields> create( + static std::unique_ptr<AddFieldsProjectionExecutor> create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const BSONObj& spec); TransformerType getType() const final { @@ -234,5 +141,4 @@ private: // The InclusionNode tree does most of the execution work once constructed. std::unique_ptr<InclusionNode> _root; }; -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_add_fields_test.cpp b/src/mongo/db/exec/add_fields_projection_executor_test.cpp index 318eb84b9ef..047bbbbc42e 100644 --- a/src/mongo/db/pipeline/parsed_add_fields_test.cpp +++ b/src/mongo/db/exec/add_fields_projection_executor_test.cpp @@ -29,13 +29,12 @@ #include "mongo/platform/basic.h" -#include "mongo/db/pipeline/parsed_add_fields.h" - #include <vector> #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/json.h" +#include "mongo/db/exec/add_fields_projection_executor.h" #include "mongo/db/exec/document_value/document.h" #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/document_value/value.h" @@ -43,88 +42,96 @@ #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/unittest/unittest.h" -namespace mongo { -namespace parsed_aggregation_projection { +namespace mongo::projection_executor { namespace { using std::vector; -// These ParsedAddFields spec tests are a subset of the ParsedAggregationProjection creation tests. -// ParsedAddFields should behave the same way, but does not use the same creation, so we include -// an abbreviation of the same tests here. +// These AddFieldsProjectionExecutor spec tests are a subset of the ProjectionExecutor creation +// tests. AddFieldsProjectionExecutor should behave the same way, but does not use the same +// creation, so we include an abbreviation of the same tests here. -// Verify that ParsedAddFields rejects specifications with conflicting field paths. -TEST(ParsedAddFieldsSpec, ThrowsOnCreationWithConflictingFieldPaths) { +// Verify that AddFieldsProjectionExecutor rejects specifications with conflicting field paths. +TEST(AddFieldsProjectionExecutorSpec, ThrowsOnCreationWithConflictingFieldPaths) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // These specs contain the same exact path. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a" << 1 << "a" << 2)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a" << BSON("b" << 1 << "b" << 2))), + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << 1 << "a" << 2)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("_id" << 3 << "_id" << true)), + ASSERT_THROWS( + AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSON("b" << 1 << "b" << 2))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("_id" << 3 << "_id" << true)), AssertionException); // These specs contain overlapping paths. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a" << 1 << "a.b" << 2)), - AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a.b.c" << 1 << "a" << 2)), + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << 1 << "a.b" << 2)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("_id" << true << "_id.x" << true)), + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a.b.c" << 1 << "a" << 2)), AssertionException); + ASSERT_THROWS( + AddFieldsProjectionExecutor::create(expCtx, BSON("_id" << true << "_id.x" << true)), + AssertionException); } -// Verify that ParsedAddFields rejects specifications that contain invalid field paths. -TEST(ParsedAddFieldsSpec, ThrowsOnCreationWithInvalidFieldPath) { +// Verify that AddFieldsProjectionExecutor rejects specifications that contain invalid field paths. +TEST(AddFieldsProjectionExecutorSpec, ThrowsOnCreationWithInvalidFieldPath) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Dotted subfields are not allowed. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a" << BSON("b.c" << true))), + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSON("b.c" << true))), AssertionException); // The user cannot start a field with $. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("$dollar" << 0)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("c.$d" << true)), AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("$dollar" << 0)), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("c.$d" << true)), + AssertionException); // Empty field names should throw an error. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("" << 2)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a" << BSON("" << true))), + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("" << 2)), AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSON("" << true))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("" << BSON("a" << true))), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("" << BSON("a" << true))), + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a." << true)), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON(".a" << true)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a." << true)), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON(".a" << true)), AssertionException); } -// Verify that ParsedAddFields rejects specifications that contain empty objects or invalid -// expressions. -TEST(ParsedAddFieldsSpec, ThrowsOnCreationWithInvalidObjectsOrExpressions) { +// Verify that AddFieldsProjectionExecutor rejects specifications that contain empty objects or +// invalid expressions. +TEST(AddFieldsProjectionExecutorSpec, ThrowsOnCreationWithInvalidObjectsOrExpressions) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); // Invalid expressions should be rejected. - ASSERT_THROWS(ParsedAddFields::create( + ASSERT_THROWS(AddFieldsProjectionExecutor::create( expCtx, BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))), AssertionException); - ASSERT_THROWS(ParsedAddFields::create(expCtx, - BSON("a" << BSON("$gt" << BSON("bad" - << "arguments")))), - AssertionException); - ASSERT_THROWS(ParsedAddFields::create( + ASSERT_THROWS( + AddFieldsProjectionExecutor::create(expCtx, + BSON("a" << BSON("$gt" << BSON("bad" + << "arguments")))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create( expCtx, BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), AssertionException); // Empty specifications are not allowed. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSONObj()), AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSONObj()), AssertionException); // Empty nested objects are not allowed. - ASSERT_THROWS(ParsedAddFields::create(expCtx, BSON("a" << BSONObj())), AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSONObj())), + AssertionException); } -TEST(ParsedAddFields, DoesNotErrorOnTwoNestedFields) { +TEST(AddFieldsProjectionExecutor, DoesNotErrorOnTwoNestedFields) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields::create(expCtx, BSON("a.b" << true << "a.c" << true)); - ParsedAddFields::create(expCtx, BSON("a.b" << true << "a" << BSON("c" << true))); + AddFieldsProjectionExecutor::create(expCtx, BSON("a.b" << true << "a.c" << true)); + AddFieldsProjectionExecutor::create(expCtx, BSON("a.b" << true << "a" << BSON("c" << true))); } // Verify that replaced fields are not included as dependencies. -TEST(ParsedAddFieldsDeps, RemovesReplaceFieldsFromDependencies) { +TEST(AddFieldsProjectionExecutorDeps, RemovesReplaceFieldsFromDependencies) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a" << true)); DepsTracker deps; @@ -136,9 +143,9 @@ TEST(ParsedAddFieldsDeps, RemovesReplaceFieldsFromDependencies) { } // Verify that adding nested fields keeps the top-level field as a dependency. -TEST(ParsedAddFieldsDeps, IncludesTopLevelFieldInDependenciesWhenAddingNestedFields) { +TEST(AddFieldsProjectionExecutorDeps, IncludesTopLevelFieldInDependenciesWhenAddingNestedFields) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("x.y" << true)); DepsTracker deps; @@ -151,9 +158,9 @@ TEST(ParsedAddFieldsDeps, IncludesTopLevelFieldInDependenciesWhenAddingNestedFie } // Verify that fields that an expression depends on are added to the dependencies. -TEST(ParsedAddFieldsDeps, AddsDependenciesForComputedFields) { +TEST(AddFieldsProjectionExecutorDeps, AddsDependenciesForComputedFields) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("x.y" << "$z" << "a" @@ -172,9 +179,9 @@ TEST(ParsedAddFieldsDeps, AddsDependenciesForComputedFields) { // Verify that the serialization produces the correct output: converting numbers and literals to // their corresponding $const form. -TEST(ParsedAddFieldsSerialize, SerializesToCorrectForm) { +TEST(AddFieldsProjectionExecutorSerialize, SerializesToCorrectForm) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(fromjson("{a: {$add: ['$a', 2]}, b: {d: 3}, 'x.y': {$literal: 4}}")); auto expectedSerialization = Document( @@ -191,9 +198,9 @@ TEST(ParsedAddFieldsSerialize, SerializesToCorrectForm) { } // Verify that serialize treats the _id field as any other field: including when explicity included. -TEST(ParsedAddFieldsSerialize, AddsIdToSerializeWhenExplicitlyIncluded) { +TEST(AddFieldsProjectionExecutorSerialize, AddsIdToSerializeWhenExplicitlyIncluded) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("_id" << false)); // Adds explicit "_id" setting field, serializes expressions. @@ -213,9 +220,9 @@ TEST(ParsedAddFieldsSerialize, AddsIdToSerializeWhenExplicitlyIncluded) { // listed in the specification. We add this check because it is different behavior from $project, // yet they derive from the same parent class. If the parent class were to change, this test would // fail. -TEST(ParsedAddFieldsSerialize, OmitsIdFromSerializeWhenNotIncluded) { +TEST(AddFieldsProjectionExecutorSerialize, OmitsIdFromSerializeWhenNotIncluded) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a" << true)); // Does not implicitly include "_id" field. @@ -232,9 +239,9 @@ TEST(ParsedAddFieldsSerialize, OmitsIdFromSerializeWhenNotIncluded) { } // Verify that the $addFields stage optimizes expressions into simpler forms when possible. -TEST(ParsedAddFieldsOptimize, OptimizesTopLevelExpressions) { +TEST(AddFieldsProjectionExecutorOptimize, OptimizesTopLevelExpressions) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2)))); addition.optimize(); auto expectedSerialization = Document{{"a", Document{{"$const", 3}}}}; @@ -250,9 +257,9 @@ TEST(ParsedAddFieldsOptimize, OptimizesTopLevelExpressions) { } // Verify that the $addFields stage optimizes expressions even when they are nested. -TEST(ParsedAddFieldsOptimize, ShouldOptimizeNestedExpressions) { +TEST(AddFieldsProjectionExecutorOptimize, ShouldOptimizeNestedExpressions) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a.b" << BSON("$add" << BSON_ARRAY(1 << 2)))); addition.optimize(); auto expectedSerialization = Document{{"a", Document{{"b", Document{{"$const", 3}}}}}}; @@ -272,9 +279,9 @@ TEST(ParsedAddFieldsOptimize, ShouldOptimizeNestedExpressions) { // // Verify that a new field is added to the end of the document. -TEST(ParsedAddFieldsExecutionTest, AddsNewFieldToEndOfDocument) { +TEST(AddFieldsProjectionExecutorExecutionTest, AddsNewFieldToEndOfDocument) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("c" << 3)); // There are no fields in the document. @@ -289,9 +296,9 @@ TEST(ParsedAddFieldsExecutionTest, AddsNewFieldToEndOfDocument) { } // Verify that an existing field is replaced and stays in the same order in the document. -TEST(ParsedAddFieldsExecutionTest, ReplacesFieldThatAlreadyExistsInDocument) { +TEST(AddFieldsProjectionExecutorExecutionTest, ReplacesFieldThatAlreadyExistsInDocument) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("c" << 3)); // Specified field is the only field in the document, and is replaced. @@ -306,9 +313,10 @@ TEST(ParsedAddFieldsExecutionTest, ReplacesFieldThatAlreadyExistsInDocument) { } // Verify that replacing multiple fields preserves the original field order in the document. -TEST(ParsedAddFieldsExecutionTest, ReplacesMultipleFieldsWhilePreservingInputFieldOrder) { +TEST(AddFieldsProjectionExecutorExecutionTest, + ReplacesMultipleFieldsWhilePreservingInputFieldOrder) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("second" << "SECOND" << "first" @@ -319,9 +327,9 @@ TEST(ParsedAddFieldsExecutionTest, ReplacesMultipleFieldsWhilePreservingInputFie } // Verify that adding multiple fields adds the fields in the order specified. -TEST(ParsedAddFieldsExecutionTest, AddsNewFieldsAfterExistingFieldsInOrderSpecified) { +TEST(AddFieldsProjectionExecutorExecutionTest, AddsNewFieldsAfterExistingFieldsInOrderSpecified) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("firstComputed" << "FIRST" << "secondComputed" @@ -337,9 +345,10 @@ TEST(ParsedAddFieldsExecutionTest, AddsNewFieldsAfterExistingFieldsInOrderSpecif // Verify that both adding and replacing fields at the same time follows the same rules as doing // each independently. -TEST(ParsedAddFieldsExecutionTest, ReplacesAndAddsNewFieldsWithSameOrderingRulesAsSeparately) { +TEST(AddFieldsProjectionExecutorExecutionTest, + ReplacesAndAddsNewFieldsWithSameOrderingRulesAsSeparately) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("firstComputed" << "FIRST" << "second" @@ -352,9 +361,9 @@ TEST(ParsedAddFieldsExecutionTest, ReplacesAndAddsNewFieldsWithSameOrderingRules // Verify that _id is included just like a regular field, in whatever order it appears in the // input document, when adding new fields. -TEST(ParsedAddFieldsExecutionTest, IdFieldIsKeptInOrderItAppearsInInputDocument) { +TEST(AddFieldsProjectionExecutorExecutionTest, IdFieldIsKeptInOrderItAppearsInInputDocument) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("newField" << "computedVal")); auto result = addition.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}}); @@ -367,9 +376,9 @@ TEST(ParsedAddFieldsExecutionTest, IdFieldIsKeptInOrderItAppearsInInputDocument) } // Verify that replacing or adding _id works just like any other field. -TEST(ParsedAddFieldsExecutionTest, ShouldReplaceIdWithComputedId) { +TEST(AddFieldsProjectionExecutorExecutionTest, ShouldReplaceIdWithComputedId) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("_id" << "newId")); auto result = addition.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}}); @@ -390,9 +399,10 @@ TEST(ParsedAddFieldsExecutionTest, ShouldReplaceIdWithComputedId) { // // Verify that adding a dotted field keeps the other fields in the subdocument. -TEST(ParsedAddFieldsExecutionTest, KeepsExistingSubFieldsWhenAddingSimpleDottedFieldToSubDoc) { +TEST(AddFieldsProjectionExecutorExecutionTest, + KeepsExistingSubFieldsWhenAddingSimpleDottedFieldToSubDoc) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a.b" << true)); // More than one field in sub document. @@ -417,9 +427,9 @@ TEST(ParsedAddFieldsExecutionTest, KeepsExistingSubFieldsWhenAddingSimpleDottedF } // Verify that creating a dotted field creates the subdocument structure necessary. -TEST(ParsedAddFieldsExecutionTest, CreatesSubDocIfDottedAddedFieldDoesNotExist) { +TEST(AddFieldsProjectionExecutorExecutionTest, CreatesSubDocIfDottedAddedFieldDoesNotExist) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("sub.target" << true)); // Should add the path if it doesn't exist. @@ -435,9 +445,9 @@ TEST(ParsedAddFieldsExecutionTest, CreatesSubDocIfDottedAddedFieldDoesNotExist) // Verify that adding a dotted value to an array field sets the field in every element of the array. // SERVER-25200: make this agree with $set. -TEST(ParsedAddFieldsExecutionTest, AppliesDottedAdditionToEachElementInArray) { +TEST(AddFieldsProjectionExecutorExecutionTest, AppliesDottedAdditionToEachElementInArray) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a.b" << true)); vector<Value> nestedValues = {Value(1), @@ -461,9 +471,9 @@ TEST(ParsedAddFieldsExecutionTest, AppliesDottedAdditionToEachElementInArray) { } // Verify that creation of the subdocument structure works for many layers of nesting. -TEST(ParsedAddFieldsExecutionTest, CreatesNestedSubDocumentsAllTheWayToAddedField) { +TEST(AddFieldsProjectionExecutorExecutionTest, CreatesNestedSubDocumentsAllTheWayToAddedField) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a.b.c.d" << "computedVal")); @@ -479,9 +489,9 @@ TEST(ParsedAddFieldsExecutionTest, CreatesNestedSubDocumentsAllTheWayToAddedFiel } // Verify that _id is not special: we can add subfields to it as well. -TEST(ParsedAddFieldsExecutionTest, AddsSubFieldsOfId) { +TEST(AddFieldsProjectionExecutorExecutionTest, AddsSubFieldsOfId) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("_id.X" << true << "_id.Z" << "NEW")); auto result = addition.applyProjection(Document{{"_id", Document{{"X", 1}, {"Y", 2}}}}); @@ -491,9 +501,9 @@ TEST(ParsedAddFieldsExecutionTest, AddsSubFieldsOfId) { // Verify that both ways of specifying nested fields -- both dotted notation and nesting -- // can be used together in the same specification. -TEST(ParsedAddFieldsExecutionTest, ShouldAllowMixedNestedAndDottedFields) { +TEST(AddFieldsProjectionExecutorExecutionTest, ShouldAllowMixedNestedAndDottedFields) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); // Include all of "a.b", "a.c", "a.d", and "a.e". // Add new computed fields "a.W", "a.X", "a.Y", and "a.Z". addition.parse(BSON("a.b" << true << "a.c" << true << "a.W" @@ -522,9 +532,9 @@ TEST(ParsedAddFieldsExecutionTest, ShouldAllowMixedNestedAndDottedFields) { } // Verify that adding nested fields preserves the addition order in the spec. -TEST(ParsedAddFieldsExecutionTest, AddsNestedAddedFieldsInOrderSpecified) { +TEST(AddFieldsProjectionExecutorExecutionTest, AddsNestedAddedFieldsInOrderSpecified) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("b.d" << "FIRST" << "b.c" @@ -539,9 +549,9 @@ TEST(ParsedAddFieldsExecutionTest, AddsNestedAddedFieldsInOrderSpecified) { // // Verify that the metadata is kept from the original input document. -TEST(ParsedAddFieldsExecutionTest, AlwaysKeepsMetadataFromOriginalDoc) { +TEST(AddFieldsProjectionExecutorExecutionTest, AlwaysKeepsMetadataFromOriginalDoc) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ParsedAddFields addition(expCtx); + AddFieldsProjectionExecutor addition(expCtx); addition.parse(BSON("a" << true)); MutableDocument inputDocBuilder(Document{{"a", 1}}); @@ -555,7 +565,5 @@ TEST(ParsedAddFieldsExecutionTest, AlwaysKeepsMetadataFromOriginalDoc) { expectedDoc.copyMetaDataFrom(inputDoc); ASSERT_DOCUMENT_EQ(result, expectedDoc.freeze()); } - } // namespace -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.h b/src/mongo/db/exec/exclusion_projection_executor.h index 65592682f33..a025d693e26 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection.h +++ b/src/mongo/db/exec/exclusion_projection_executor.h @@ -32,18 +32,10 @@ #include <memory> #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" - -namespace mongo { - -class FieldPath; -class Value; - -namespace parsed_aggregation_projection { +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_node.h" +namespace mongo::projection_executor { /** * A node used to define the parsed structure of an exclusion projection. Each ExclusionNode * represents one 'level' of the parsed specification. The root ExclusionNode represents all top @@ -79,17 +71,16 @@ protected: }; /** - * A ParsedExclusionProjection represents a parsed form of the raw BSON specification. + * A ExclusionProjectionExecutor represents an execution tree for an exclusion projection. * - * This class is mostly a wrapper around an ExclusionNode tree. It contains logic to parse a - * specification object into the corresponding ExclusionNode tree, but defers most execution logic - * to the underlying tree. + * This class is mostly a wrapper around an ExclusionNode tree and defers most execution logic to + * the underlying tree. */ -class ParsedExclusionProjection : public ParsedAggregationProjection { +class ExclusionProjectionExecutor : public ProjectionExecutor { public: - ParsedExclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies) - : ParsedAggregationProjection(expCtx, policies), _root(new ExclusionNode(_policies)) {} + ExclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies) + : ProjectionExecutor(expCtx, policies), _root(new ExclusionNode(_policies)) {} TransformerType getType() const final { return TransformerType::kExclusionProjection; @@ -138,6 +129,4 @@ private: // The ExclusionNode tree does most of the execution work once constructed. std::unique_ptr<ExclusionNode> _root; }; - -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp b/src/mongo/db/exec/exclusion_projection_executor_test.cpp index 72a9276d7c1..59d1748d5fe 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp +++ b/src/mongo/db/exec/exclusion_projection_executor_test.cpp @@ -29,7 +29,7 @@ #include "mongo/platform/basic.h" -#include "mongo/db/pipeline/parsed_exclusion_projection.h" +#include "mongo/db/exec/exclusion_projection_executor.h" #include <iostream> #include <iterator> @@ -42,33 +42,32 @@ #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/document_value/value.h" #include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/pipeline/dependencies.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/projection_parser.h" #include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" -namespace mongo { -namespace parsed_aggregation_projection { +namespace mongo::projection_executor { namespace { - using std::vector; auto createProjectionExecutor(const BSONObj& spec, const ProjectionPolicies& policies) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto projection = projection_ast::parse(expCtx, spec, policies); - auto executor = projection_executor::buildProjectionExecutor( - expCtx, &projection, policies, true /* optimizeExecutor */); + auto executor = + buildProjectionExecutor(expCtx, &projection, policies, true /* optimizeExecutor */); invariant(executor->getType() == TransformerInterface::TransformerType::kExclusionProjection); return executor; } -// Helper to simplify the creation of a ParsedExclusionProjection with default policies. +// Helper to simplify the creation of a ExclusionProjectionExecutor with default policies. auto makeExclusionProjectionWithDefaultPolicies(const BSONObj& spec) { return createProjectionExecutor(spec, {}); } -// Helper to simplify the creation of a ParsedExclusionProjection which excludes _id by default. +// Helper to simplify the creation of a ExclusionProjectionExecutor which excludes _id by default. auto makeExclusionProjectionWithDefaultIdExclusion(const BSONObj& spec) { ProjectionPolicies defaultExcludeId{ProjectionPolicies::DefaultIdPolicy::kExcludeId, ProjectionPolicies::kArrayRecursionPolicyDefault, @@ -76,7 +75,7 @@ auto makeExclusionProjectionWithDefaultIdExclusion(const BSONObj& spec) { return createProjectionExecutor(spec, defaultExcludeId); } -// Helper to simplify the creation of a ParsedExclusionProjection which does not recurse arrays. +// Helper to simplify the creation of a ExclusionProjectionExecutor which does not recurse arrays. auto makeExclusionProjectionWithNoArrayRecursion(const BSONObj& spec) { ProjectionPolicies noArrayRecursion{ ProjectionPolicies::kDefaultIdPolicyDefault, @@ -454,5 +453,4 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotRetainNestedArraysIfNoRecursionN ASSERT_DOCUMENT_EQ(result, expectedResult); } } // namespace -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/find_projection_executor_test.cpp b/src/mongo/db/exec/find_projection_executor_test.cpp index 39194fbdd93..e3e7d98ff97 100644 --- a/src/mongo/db/exec/find_projection_executor_test.cpp +++ b/src/mongo/db/exec/find_projection_executor_test.cpp @@ -30,289 +30,253 @@ #include "mongo/platform/basic.h" #include "mongo/db/exec/document_value/document_value_test_util.h" -#include "mongo/db/exec/find_projection_executor.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/unittest/death_test.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" +#include "mongo/db/pipeline/aggregation_context_fixture.h" +#include "mongo/db/pipeline/expression_find_internal.h" +#include "mongo/db/query/projection_parser.h" #include "mongo/unittest/unittest.h" -namespace mongo { -namespace projection_executor { -namespace positional_projection_tests { -/** - * Applies a find()-style positional projection at the given 'path' using 'matchSpec' to create - * a 'MatchExpression' to match an element on the first array in the 'path'. If no value for - * 'postImage' is provided, then the post-image used will be the value passed for the 'preImage'. - */ -auto applyPositional(const BSONObj& matchSpec, - const std::string& path, - const Document& preImage, - boost::optional<Document> postImage = boost::none) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchSpec, expCtx)); - return projection_executor::applyPositionalProjection( - preImage, postImage.value_or(preImage), *matchExpr, path); -} - -TEST(PositionalProjection, CorrectlyProjectsSimplePath) { - ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1, foo: [6]}")}, - applyPositional(fromjson("{bar: 1, foo: {$gte: 5}}"), - "foo", - Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); -} - -TEST(PositionalProjection, CorrectlyProjectsDottedPath) { - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1, x: {y: [6]}}")}, - applyPositional(fromjson("{a: 1, 'x.y': {$gte: 5}}"), - "x.y", - Document{fromjson("{a: 1, x: {y: [1,2,6,10]}}")})); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1, x: {y: {z: [6]}}}")}, - applyPositional(fromjson("{a: 1, 'x.y.z': {$gte: 5}}"), - "x.y.z", - Document{fromjson("{a: 1, x: {y: {z: [1,2,6,10]}}}")})); -} - -TEST(PositionalProjection, ProjectsValueUnmodifiedIfFieldIsNotArray) { - auto doc = Document{fromjson("{foo: 3}")}; - ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc)); -} +namespace mongo::projection_executor { +constexpr auto kProjectionPostImageVarName = + projection_executor::ProjectionExecutor::kProjectionPostImageVarName; -TEST(PositionalProjection, FailsToProjectPositionalPathComponentsForNestedArrays) { - ASSERT_THROWS_CODE(applyPositional(fromjson("{'x.0.y': 42}"), - "x.0.y", - Document{fromjson("{x: [{y: [11, 42]}]}")}), - AssertionException, - 51247); +auto createProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const BSONObj& projSpec, + ProjectionPolicies policies) { + auto projection = projection_ast::parse(expCtx, projSpec, policies); + return projection_executor::buildProjectionExecutor( + expCtx, &projection, policies, true /* optimizeExecutor */); } -TEST(PositionalProjection, CorrectlyProjectsNestedArrays) { - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [1,2]}]}")}, - applyPositional(fromjson("{'a.b': 1}"), - "a", - Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3,4]}]}")}, - applyPositional(fromjson("{'a.b': 3}"), - "a", - Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3,4]}]}")}, - applyPositional(fromjson("{'a.b': 3}"), - "a.b", - Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [['d','e','f']]}")}, - applyPositional(fromjson("{a: {$gt: ['a','b','c']}}"), - "a", - Document{fromjson("{a: [['a','b','c'],['d','e','f']]}")})); -} - -TEST(PositionalProjection, FailsToProjectWithMultipleConditionsOnArray) { - ASSERT_THROWS_CODE(applyPositional(fromjson("{$or: [{'x.y': 1}, {'x.y': 2}]}"), - "x", - Document{fromjson("{x: [{y: [1,2]}]}")}), - AssertionException, - 51246); -} - -TEST(PositionalProjection, CanMergeWithExistingFieldsInOutputDocument) { - auto doc = Document{fromjson("{foo: {bar: [1,2,6,10]}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: {bar: [6]}}")}, - applyPositional(fromjson("{'foo.bar': {$gte: 5}}"), "foo.bar", doc)); - - doc = Document{fromjson("{bar: 1, foo: {bar: [1,2,6,10]}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1, foo: {bar: [6]}}")}, - applyPositional(fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"), "foo.bar", doc)); - - doc = Document{fromjson("{bar: 1, foo: 3}")}; - ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc)); -} - -TEST(PositionalProjection, AppliesMatchExpressionToPreImageAndStoresResultInPostImage) { - auto preImage = Document{fromjson("{foo: 1, bar: [1,2,6,10]}")}; - auto postImage = Document{fromjson("{bar: [1,2,6,10]}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: [6]}")}, - applyPositional(fromjson("{foo: 1, bar: 6}"), "bar", preImage, postImage)); -} -} // namespace positional_projection_tests - -namespace elem_match_projection_tests { -auto applyElemMatch(const BSONObj& match, const std::string& path, const Document& input) { - boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto matchObj = BSON(path << BSON("$elemMatch" << match)); - auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchObj, expCtx)); - return projection_executor::applyElemMatchProjection(input, *matchExpr, path); -} +class PositionalProjectionExecutionTest : public AggregationContextFixture { +protected: + auto applyPositional(const BSONObj& projSpec, + const BSONObj& matchSpec, + const std::string& path, + const Document& input) { + auto executor = createProjectionExecutor(getExpCtx(), projSpec, {}); + auto matchExpr = CopyableMatchExpression{matchSpec, + getExpCtx(), + std::make_unique<ExtensionsCallbackNoop>(), + MatchExpressionParser::kBanAllSpecialFeatures}; + auto expr = make_intrusive<ExpressionInternalFindPositional>( + getExpCtx(), + ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + path, + std::move(matchExpr)); + executor->setRootReplacementExpression(expr); + return executor->applyTransformation(input); + } +}; + +class SliceProjectionExecutionTest : public AggregationContextFixture { +protected: + auto applySlice(const BSONObj& projSpec, + const std::string& path, + boost::optional<int> skip, + int limit, + const Document& input) { + auto executor = createProjectionExecutor(getExpCtx(), projSpec, {}); + auto expr = make_intrusive<ExpressionInternalFindSlice>( + getExpCtx(), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + path, + skip, + limit); + executor->setRootReplacementExpression(expr); + return executor->applyTransformation(input); + } +}; + +TEST_F(PositionalProjectionExecutionTest, CanApplyPositionalWithInclusionProjection) { + ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: [6]}")}, + applyPositional(fromjson("{foo: 1}"), + fromjson("{foo: {$gte: 5}}"), + "foo", + Document{fromjson("{foo: [1,2,6,10]}")})); -TEST(ElemMatchProjection, CorrectlyProjectsNonObjectElement) { - ASSERT_VALUE_EQ( - Document{fromjson("{foo: [4]}")}["foo"], - applyElemMatch(fromjson("{$in: [4]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")})); - ASSERT_VALUE_EQ( - Document{fromjson("{foo: [4]}")}["foo"], - applyElemMatch(fromjson("{$nin: [1,2,3]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")})); + ASSERT_DOCUMENT_EQ(Document{fromjson("{bar:1, foo: [6]}")}, + applyPositional(fromjson("{bar: 1, foo: 1}"), + fromjson("{bar: 1, foo: {$gte: 5}}"), + "foo", + Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); } -TEST(ElemMatchProjection, CorrectlyProjectsObjectElement) { - ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], - applyElemMatch(fromjson("{bar: {$gte: 5}}"), - "foo", - Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " - "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +TEST_F(PositionalProjectionExecutionTest, AppliesProjectionToPreImage) { + ASSERT_DOCUMENT_EQ(Document{fromjson("{b: [6], c: 'abc'}")}, + applyPositional(fromjson("{b: 1, c: 1}"), + fromjson("{a: 1, b: {$gte: 5}}"), + "b", + Document{fromjson("{a: 1, b: [1,2,6,10], c: 'abc'}")})); } -TEST(ElemMatchProjection, CorrectlyProjectsArrayElement) { - ASSERT_VALUE_EQ(Document{fromjson("{foo: [[3,4]]}")}["foo"], - applyElemMatch(fromjson("{$gt: [1,2]}"), - "foo", - Document{fromjson("{foo: [[1,2], [3,4]]}")})); +TEST_F(PositionalProjectionExecutionTest, ShouldAddInclusionFieldsAndWholeDocumentToDependencies) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); + auto matchSpec = fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"); + auto matchExpr = CopyableMatchExpression{matchSpec, + getExpCtx(), + std::make_unique<ExtensionsCallbackNoop>(), + MatchExpressionParser::kBanAllSpecialFeatures}; + auto expr = make_intrusive<ExpressionInternalFindPositional>( + getExpCtx(), + ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo.bar", + std::move(matchExpr)); + executor->setRootReplacementExpression(expr); + + DepsTracker deps; + executor->addDependencies(&deps); + + ASSERT_EQ(deps.fields.size(), 2UL); + ASSERT_EQ(deps.fields.count("bar"), 1UL); + ASSERT_EQ(deps.fields.count("foo.bar"), 1UL); + ASSERT(deps.needWholeDocument); } -TEST(ElemMatchProjection, ProjectsAsEmptyDocumentIfInputIsEmpty) { - ASSERT_VALUE_EQ({}, applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", {})); +TEST_F(PositionalProjectionExecutionTest, ShouldConsiderAllPathsAsModified) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); + auto matchSpec = fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"); + auto matchExpr = CopyableMatchExpression{matchSpec, + getExpCtx(), + std::make_unique<ExtensionsCallbackNoop>(), + MatchExpressionParser::kBanAllSpecialFeatures}; + auto expr = make_intrusive<ExpressionInternalFindPositional>( + getExpCtx(), + ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo.bar", + std::move(matchExpr)); + executor->setRootReplacementExpression(expr); + + auto modifiedPaths = executor->getModifiedPaths(); + ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); } -TEST(ElemMatchProjection, RemovesFieldFromOutputDocumentIfUnableToMatchArrayElement) { - ASSERT_VALUE_EQ({}, - applyElemMatch(fromjson("{bar: {$gte: 5}}"), - "foo", - Document{fromjson("{foo: [{bar: 1, z: 1}, " - "{bar: 2, z: 2}]}")})); - ASSERT_VALUE_EQ( - {}, - applyElemMatch(fromjson("{bar: {$gte: 20}}"), - "foo", - Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " - "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +TEST_F(SliceProjectionExecutionTest, CanApplySliceWithInclusionProjection) { + ASSERT_DOCUMENT_EQ( + Document{fromjson("{foo: [1,2]}")}, + applySlice( + fromjson("{foo: 1}"), "foo", boost::none, 2, Document{fromjson("{foo: [1,2,6,10]}")})); + + ASSERT_DOCUMENT_EQ(Document{fromjson("{bar:1, foo: [6]}")}, + applySlice(fromjson("{bar: 1, foo: 1}"), + "foo", + 2, + 1, + Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); } -TEST(ElemMatchProjection, CorrectlyProjectsWithMultipleCriteriaInMatchExpression) { - ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 2, z: 2}]}")}["foo"], - applyElemMatch(fromjson("{bar: {$gt: 1, $lt: 6}}"), - "foo", - Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " - "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +TEST_F(SliceProjectionExecutionTest, AppliesProjectionToPostImage) { + ASSERT_DOCUMENT_EQ(Document{fromjson("{b: [1,2], c: 'abc'}")}, + applySlice(fromjson("{b: 1, c: 1}"), + "b", + boost::none, + 2, + Document{fromjson("{a: 1, b: [1,2,6,10], c: 'abc'}")})); } -TEST(ElemMatchProjection, CanMergeWithExistingFieldsInInputDocument) { - ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], - applyElemMatch(fromjson("{bar: {$gte: 5}}"), - "foo", - Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " - "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +TEST_F(SliceProjectionExecutionTest, CanApplySliceAndPositionalProjectionsTogether) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{foo: 1, bar: 1}"), {}); + auto matchSpec = fromjson("{foo: {$gte: 3}}"); + auto matchExpr = CopyableMatchExpression{matchSpec, + getExpCtx(), + std::make_unique<ExtensionsCallbackNoop>(), + MatchExpressionParser::kBanAllSpecialFeatures}; + auto positionalExpr = make_intrusive<ExpressionInternalFindPositional>( + getExpCtx(), + ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo", + std::move(matchExpr)); + auto sliceExpr = + make_intrusive<ExpressionInternalFindSlice>(getExpCtx(), positionalExpr, "bar", 1, 1); + executor->setRootReplacementExpression(sliceExpr); - ASSERT_VALUE_EQ( - Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], - applyElemMatch(fromjson("{bar: {$gte: 5}}"), - "foo", - Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " - "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{foo: [3], bar: [6]}")}, + executor->applyTransformation(Document{fromjson("{foo: [1,2,3,4], bar: [5,6,7,8]}")})); } -TEST(ElemMatchProjection, RertursEmptyValuefItContainsNumericSubfield) { - ASSERT_VALUE_EQ( - {}, applyElemMatch(fromjson("{$gt: 2}"), "foo", Document{BSON("foo" << BSON(0 << 3))})); - - ASSERT_VALUE_EQ({}, - applyElemMatch(fromjson("{$gt: 2}"), - "foo", - Document{BSON("bar" << 1 << "foo" << BSON(0 << 3))})); +TEST_F(SliceProjectionExecutionTest, CanApplySliceWithExclusionProjection) { + ASSERT_DOCUMENT_EQ( + Document{fromjson("{foo: [6]}")}, + applySlice( + fromjson("{bar: 0}"), "foo", 2, 1, Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); } -} // namespace elem_match_projection_tests -namespace slice_projection_tests { -DEATH_TEST(SliceProjection, - ShouldFailIfNegativeLimitSpecifiedWithPositiveSkip, - "Invariant failure limit >= 0") { - auto doc = Document{fromjson("{a: [1,2,3,4]}")}; - applySliceProjection(doc, "a", 1, -1); +TEST_F(SliceProjectionExecutionTest, + ShouldAddFieldsAndWholeDocumentToDependenciesWithInclusionProjection) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); + auto expr = make_intrusive<ExpressionInternalFindSlice>( + getExpCtx(), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo.bar", + 1, + 1); + executor->setRootReplacementExpression(expr); + + DepsTracker deps; + executor->addDependencies(&deps); + + ASSERT_EQ(deps.fields.size(), 1UL); + ASSERT_EQ(deps.fields.count("bar"), 1UL); + ASSERT(deps.needWholeDocument); } -DEATH_TEST(SliceProjection, - ShouldFailIfNegativeLimitSpecifiedWithNegativeSkip, - "Invariant failure limit >= 0") { - auto doc = Document{fromjson("{a: [1,2,3,4]}")}; - applySliceProjection(doc, "a", -1, -1); +TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithInclusionProjection) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1}"), {}); + auto expr = make_intrusive<ExpressionInternalFindSlice>( + getExpCtx(), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo.bar", + 1, + 1); + executor->setRootReplacementExpression(expr); + + auto modifiedPaths = executor->getModifiedPaths(); + ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); } -TEST(SliceProjection, CorrectlyProjectsSimplePath) { - auto doc = Document{fromjson("{a: [1,2,3,4]}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3]}")}, - applySliceProjection(doc, "a", boost::none, 3)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2,3,4]}")}, - applySliceProjection(doc, "a", boost::none, -3)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2]}")}, applySliceProjection(doc, "a", -3, 1)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2,3,4]}")}, applySliceProjection(doc, "a", -3, 4)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [4]}")}, applySliceProjection(doc, "a", 3, 1)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, applySliceProjection(doc, "a", -5, 5)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: []}")}, applySliceProjection(doc, "a", 5, 2)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2]}")}, applySliceProjection(doc, "a", -5, 2)); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: [1,2,3,4]}")}, - applySliceProjection(doc, "a", boost::none, std::numeric_limits<int>::max())); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: [1,2,3,4]}")}, - applySliceProjection(doc, "a", boost::none, std::numeric_limits<int>::min())); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: [1,2,3,4]}")}, - applySliceProjection( - doc, "a", std::numeric_limits<int>::min(), std::numeric_limits<int>::max())); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: []}")}, - applySliceProjection( - doc, "a", std::numeric_limits<int>::max(), std::numeric_limits<int>::max())); - - doc = Document{fromjson("{a: [{b: 1, c: 1}, {b: 2, c: 2}, {b: 3, c: 3}], d: 2}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 1}], d: 2}")}, - applySliceProjection(doc, "a", boost::none, 1)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 3, c: 3}], d: 2}")}, - applySliceProjection(doc, "a", boost::none, -1)); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 2, c: 2}], d: 2}")}, - applySliceProjection(doc, "a", 1, 1)); - - doc = Document{fromjson("{a: 1}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1}")}, - applySliceProjection(doc, "a", boost::none, 2)); - - doc = Document{fromjson("{a: {b: 1}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: 1}}")}, - applySliceProjection(doc, "a", boost::none, 2)); +TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithExclusionProjection) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 0}"), {}); + auto expr = make_intrusive<ExpressionInternalFindSlice>( + getExpCtx(), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo.bar", + 1, + 1); + executor->setRootReplacementExpression(expr); + + auto modifiedPaths = executor->getModifiedPaths(); + ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); } -TEST(SliceProjection, CorrectlyProjectsDottedPath) { - auto doc = Document{fromjson("{a: {b: [1,2,3], c: 1}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [1,2], c: 1}}")}, - applySliceProjection(doc, "a.b", boost::none, 2)); - - doc = Document{fromjson("{a: {b: [1,2,3], c: 1}, d: 1}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [1,2], c: 1}, d: 1}")}, - applySliceProjection(doc, "a.b", boost::none, 2)); - - doc = Document{fromjson("{a: {b: [[1,2], [3,4], [5,6]]}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [[1,2], [3,4]]}}")}, - applySliceProjection(doc, "a.b", boost::none, 2)); - - doc = Document{fromjson("{a: [{b: {c: [1,2,3,4]}}, {b: {c: [5,6,7,8]}}], d: 1}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: {c: [4]}}, {b: {c: [8]}}], d: 1}")}, - applySliceProjection(doc, "a.b.c", -1, 2)); - - doc = Document{fromjson("{a: {b: 1}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: 1}}")}, - applySliceProjection(doc, "a.b", boost::none, 2)); - - doc = Document{fromjson("{a: {b: {c: 1}}}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: {c: 1}}}")}, - applySliceProjection(doc, "a.b", boost::none, 2)); - - doc = Document{fromjson("{a: [{b: [1,2,3], c: 1}]}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3], c: 1}]}")}, - applySliceProjection(doc, "a.b", boost::none, -1)); - - doc = Document{fromjson("{a: [{b: [1,2,3], c: 4}, {b: [5,6,7], c: 8}]}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3], c: 4}, {b: [7], c: 8}]}")}, - applySliceProjection(doc, "a.b", boost::none, -1)); - - doc = Document{fromjson("{a: [{b: [{x:1, c: [1, 2]}, {y: 1, c: [3, 4]}]}], z: 1}")}; - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [{x:1, c: [1]}, {y: 1, c: [3]}]}], z: 1}")}, - applySliceProjection(doc, "a.b.c", boost::none, 1)); +TEST_F(SliceProjectionExecutionTest, ShouldAddWholeDocumentToDependenciesWithExclusionProjection) { + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 0}"), {}); + auto expr = make_intrusive<ExpressionInternalFindSlice>( + getExpCtx(), + ExpressionFieldPath::parse( + getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), + "foo.bar", + 1, + 1); + executor->setRootReplacementExpression(expr); + + DepsTracker deps; + executor->addDependencies(&deps); + + ASSERT_EQ(deps.fields.size(), 0UL); + ASSERT(deps.needWholeDocument); } -} // namespace slice_projection_tests -} // namespace projection_executor -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.h b/src/mongo/db/exec/inclusion_projection_executor.h index 7bc69dd8cad..4b8769b44f2 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection.h +++ b/src/mongo/db/exec/inclusion_projection_executor.h @@ -31,20 +31,10 @@ #include <memory> -#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/unordered_map.h" -#include "mongo/stdx/unordered_set.h" - -namespace mongo { - -class FieldPath; -class Value; - -namespace parsed_aggregation_projection { +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_node.h" +namespace mongo::projection_executor { /** * A node used to define the parsed structure of an inclusion projection. Each InclusionNode * represents one 'level' of the parsed specification. The root InclusionNode represents all top @@ -103,22 +93,22 @@ protected: }; /** - * A ParsedInclusionProjection represents a parsed form of the raw BSON specification. + * A InclusionProjectionExecutor represents an execution tree for an inclusion projection. * - * This class is mostly a wrapper around an InclusionNode tree. It contains logic to parse a - * specification object into the corresponding InclusionNode tree, but defers most execution logic - * to the underlying tree. + * This class is mostly a wrapper around an InclusionNode tree and defers most execution logic to + * the underlying tree. */ -class ParsedInclusionProjection : public ParsedAggregationProjection { +class InclusionProjectionExecutor : public ProjectionExecutor { public: - ParsedInclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies, - std::unique_ptr<InclusionNode> root) - : ParsedAggregationProjection(expCtx, policies), _root(std::move(root)) {} - - ParsedInclusionProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies) - : ParsedInclusionProjection(expCtx, policies, std::make_unique<InclusionNode>(policies)) {} + InclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies, + std::unique_ptr<InclusionNode> root) + : ProjectionExecutor(expCtx, policies), _root(std::move(root)) {} + + InclusionProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies) + : InclusionProjectionExecutor(expCtx, policies, std::make_unique<InclusionNode>(policies)) { + } TransformerType getType() const final { return TransformerType::kInclusionProjection; @@ -151,7 +141,7 @@ public: * Optimize any computed expressions. */ void optimize() final { - ParsedAggregationProjection::optimize(); + ProjectionExecutor::optimize(); _root->optimize(); } @@ -199,5 +189,4 @@ private: // The InclusionNode tree does most of the execution work once constructed. std::unique_ptr<InclusionNode> _root; }; -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp b/src/mongo/db/exec/inclusion_projection_executor_test.cpp index c7dcf9f080b..b9df51e21ba 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp +++ b/src/mongo/db/exec/inclusion_projection_executor_test.cpp @@ -29,7 +29,7 @@ #include "mongo/platform/basic.h" -#include "mongo/db/pipeline/parsed_inclusion_projection.h" +#include "mongo/db/exec/inclusion_projection_executor.h" #include <vector> @@ -40,16 +40,15 @@ #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/document_value/value.h" #include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/pipeline/dependencies.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/projection_parser.h" #include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" -namespace mongo { -namespace parsed_aggregation_projection { +namespace mongo::projection_executor { namespace { - using std::vector; template <typename T> @@ -60,18 +59,18 @@ BSONObj wrapInLiteral(const T& arg) { auto createProjectionExecutor(const BSONObj& spec, const ProjectionPolicies& policies) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); auto projection = projection_ast::parse(expCtx, spec, policies); - auto executor = projection_executor::buildProjectionExecutor( - expCtx, &projection, policies, true /* optimizeExecutor */); + auto executor = + buildProjectionExecutor(expCtx, &projection, policies, true /* optimizeExecutor */); invariant(executor->getType() == TransformerInterface::TransformerType::kInclusionProjection); return executor; } -// Helper to simplify the creation of a ParsedInclusionProjection with default policies. +// Helper to simplify the creation of a InclusionProjectionExecutor with default policies. auto makeInclusionProjectionWithDefaultPolicies(BSONObj spec) { return createProjectionExecutor(spec, {}); } -// Helper to simplify the creation of a ParsedInclusionProjection which excludes _id by default. +// Helper to simplify the creation of a InclusionProjectionExecutor which excludes _id by default. auto makeInclusionProjectionWithDefaultIdExclusion(BSONObj spec) { ProjectionPolicies defaultExcludeId{ProjectionPolicies::DefaultIdPolicy::kExcludeId, ProjectionPolicies::kArrayRecursionPolicyDefault, @@ -79,7 +78,7 @@ auto makeInclusionProjectionWithDefaultIdExclusion(BSONObj spec) { return createProjectionExecutor(spec, defaultExcludeId); } -// Helper to simplify the creation of a ParsedInclusionProjection which does not recurse arrays. +// Helper to simplify the creation of a InclusionProjectionExecutor which does not recurse arrays. auto makeInclusionProjectionWithNoArrayRecursion(BSONObj spec) { ProjectionPolicies noArrayRecursion{ ProjectionPolicies::kDefaultIdPolicyDefault, @@ -805,5 +804,4 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldShouldReplaceNestedArrayForN ASSERT_DOCUMENT_EQ(result, expectedResult); } } // namespace -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/projection.cpp b/src/mongo/db/exec/projection.cpp index 13e48a44c4c..cc46b688dbc 100644 --- a/src/mongo/db/exec/projection.cpp +++ b/src/mongo/db/exec/projection.cpp @@ -36,6 +36,7 @@ #include "mongo/db/exec/document_value/document.h" #include "mongo/db/exec/plan_stage.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/exec/scoped_timer.h" #include "mongo/db/exec/working_set_common.h" #include "mongo/db/jsobj.h" diff --git a/src/mongo/db/exec/projection.h b/src/mongo/db/exec/projection.h index 0866dbdd150..3e69e7f6e1e 100644 --- a/src/mongo/db/exec/projection.h +++ b/src/mongo/db/exec/projection.h @@ -104,7 +104,7 @@ private: // True, if the projection contains a recordId $meta expression. const bool _wantRecordId; const projection_ast::ProjectType _projectType; - std::unique_ptr<parsed_aggregation_projection::ParsedAggregationProjection> _executor; + std::unique_ptr<projection_executor::ProjectionExecutor> _executor; }; /** diff --git a/src/mongo/db/exec/projection_exec_agg.cpp b/src/mongo/db/exec/projection_exec_agg.cpp deleted file mode 100644 index 94eafdafb7f..00000000000 --- a/src/mongo/db/exec/projection_exec_agg.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#include "mongo/platform/basic.h" - -#include "mongo/db/exec/projection_exec_agg.h" - -#include "mongo/db/exec/document_value/document.h" -#include "mongo/db/exec/projection_executor.h" -#include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/pipeline/parsed_aggregation_projection.h" -#include "mongo/db/query/projection_parser.h" -#include "mongo/db/query/projection_policies.h" - -namespace mongo { - -class ProjectionExecAgg::ProjectionExecutor { -public: - using ParsedAggregationProjection = parsed_aggregation_projection::ParsedAggregationProjection; - - using TransformerType = TransformerInterface::TransformerType; - - ProjectionExecutor(BSONObj projSpec, - DefaultIdPolicy defaultIdPolicy, - ArrayRecursionPolicy arrayRecursionPolicy) { - // Construct a dummy ExpressionContext for ParsedAggregationProjection. It's OK to set the - // ExpressionContext's OperationContext and CollatorInterface to 'nullptr' here; since we - // ban computed fields from the projection, the ExpressionContext will never be used. - boost::intrusive_ptr<ExpressionContext> expCtx(new ExpressionContext(nullptr, nullptr)); - - // 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 ProjectionPolicies::DefaultIdPolicy in order to avoid exposing - // internal aggregation types to the query system. - const auto idPolicy = (defaultIdPolicy == ProjectionExecAgg::DefaultIdPolicy::kIncludeId - ? 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 ProjectionPolicies::ArrayRecursionPolicy in order to avoid - // exposing aggregation types to the query system. - const auto recursionPolicy = - (arrayRecursionPolicy == ArrayRecursionPolicy::kRecurseNestedArrays - ? 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. - const auto computedFieldsPolicy = - ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields; - - // Create a ProjectionPolicies object, to be populated based on the passed arguments. - ProjectionPolicies projectionPolicies{idPolicy, recursionPolicy, computedFieldsPolicy}; - - // Construct a ParsedAggregationProjection for the given projection spec and policies. - const auto proj = projection_ast::parse(expCtx, projSpec, projectionPolicies); - _projection = projection_executor::buildProjectionExecutor( - expCtx, &proj, projectionPolicies, true /* optimizeExecutor */); - - // For an inclusion, record the exhaustive set of fields retained by the projection. - if (getType() == ProjectionType::kInclusionProjection) { - DepsTracker depsTracker; - _projection->addDependencies(&depsTracker); - for (auto&& field : depsTracker.fields) - _exhaustivePaths.insert(FieldRef{field}); - } - } - - const std::set<FieldRef>& getExhaustivePaths() const { - return _exhaustivePaths; - } - - ProjectionType getType() const { - return (_projection->getType() == TransformerType::kInclusionProjection - ? ProjectionType::kInclusionProjection - : ProjectionType::kExclusionProjection); - } - - BSONObj applyProjection(BSONObj inputDoc) const { - return applyTransformation(Document{inputDoc}).toBson(); - } - - bool applyProjectionToOneField(StringData field) const { - MutableDocument doc; - const FieldPath f{field}; - doc.setNestedField(f, Value(1.0)); - const Document transformedDoc = applyTransformation(doc.freeze()); - return !transformedDoc.getNestedField(f).missing(); - } - - stdx::unordered_set<std::string> applyProjectionToFields( - const stdx::unordered_set<std::string>& fields) const { - stdx::unordered_set<std::string> out; - - for (const auto& field : fields) { - if (applyProjectionToOneField(field)) { - out.insert(field); - } - } - - return out; - } - -private: - Document applyTransformation(Document inputDoc) const { - return _projection->applyTransformation(inputDoc); - } - - std::unique_ptr<ParsedAggregationProjection> _projection; - std::set<FieldRef> _exhaustivePaths; -}; - -// ProjectionExecAgg's constructor and destructor are defined here, at a point where the -// implementation of ProjectionExecutor is known, so that std::unique_ptr can be used with the -// forward-declared ProjectionExecutor class. -ProjectionExecAgg::ProjectionExecAgg(BSONObj projSpec, std::unique_ptr<ProjectionExecutor> exec) - : _exec(std::move(exec)), _projSpec(std::move(projSpec)){}; - -ProjectionExecAgg::~ProjectionExecAgg() = default; - -std::unique_ptr<ProjectionExecAgg> ProjectionExecAgg::create(BSONObj projSpec, - DefaultIdPolicy defaultIdPolicy, - ArrayRecursionPolicy recursionPolicy) { - return std::unique_ptr<ProjectionExecAgg>(new ProjectionExecAgg( - projSpec, - std::make_unique<ProjectionExecutor>(projSpec, defaultIdPolicy, recursionPolicy))); -} - -ProjectionExecAgg::ProjectionType ProjectionExecAgg::getType() const { - return _exec->getType(); -} - -BSONObj ProjectionExecAgg::applyProjection(BSONObj inputDoc) const { - return _exec->applyProjection(inputDoc); -} - -bool ProjectionExecAgg::applyProjectionToOneField(StringData field) const { - return _exec->applyProjectionToOneField(field); -} - -stdx::unordered_set<std::string> ProjectionExecAgg::applyProjectionToFields( - const stdx::unordered_set<std::string>& fields) const { - return _exec->applyProjectionToFields(fields); -} - -const std::set<FieldRef>& ProjectionExecAgg::getExhaustivePaths() const { - return _exec->getExhaustivePaths(); -} -} // namespace mongo diff --git a/src/mongo/db/exec/projection_exec_agg.h b/src/mongo/db/exec/projection_exec_agg.h deleted file mode 100644 index f15c71f7c40..00000000000 --- a/src/mongo/db/exec/projection_exec_agg.h +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#pragma once - -#include <memory> - -#include "mongo/bson/bsonobj.h" -#include "mongo/db/field_ref.h" - -namespace mongo { - -/** - * This class provides the query system with the ability to perform projections using the - * aggregation system's projection semantics. - */ -class ProjectionExecAgg { -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 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, - DefaultIdPolicy defaultIdPolicy, - ArrayRecursionPolicy recursionPolicy); - - ~ProjectionExecAgg(); - - BSONObj applyProjection(BSONObj inputDoc) const; - - stdx::unordered_set<std::string> applyProjectionToFields( - const stdx::unordered_set<std::string>& fields) const; - - /** - * Apply the projection to a single field name. Returns whether or not the projection would - * allow that field to remain in a document. - **/ - bool applyProjectionToOneField(StringData field) const; - - /** - * Returns the exhaustive set of all paths that will be preserved by this projection, or an - * empty set if the exhaustive set cannot be determined. An inclusion will always produce an - * exhaustive set; an exclusion will always produce an empty set. - */ - const std::set<FieldRef>& getExhaustivePaths() const; - - ProjectionType getType() const; - - BSONObj getProjectionSpec() const { - return _projSpec; - } - -private: - /** - * ProjectionExecAgg::ProjectionExecutor wraps all agg-specific calls, and is forward-declared - * here to avoid exposing any types from ParsedAggregationProjection to the query system. - */ - class ProjectionExecutor; - - ProjectionExecAgg(BSONObj projSpec, std::unique_ptr<ProjectionExecutor> exec); - - std::unique_ptr<ProjectionExecutor> _exec; - const BSONObj _projSpec; -}; -} // namespace mongo diff --git a/src/mongo/db/exec/projection_executor.h b/src/mongo/db/exec/projection_executor.h index 3045e4ca710..7921d4510e8 100644 --- a/src/mongo/db/exec/projection_executor.h +++ b/src/mongo/db/exec/projection_executor.h @@ -1,5 +1,5 @@ /** - * Copyright (C) 2019-present MongoDB, Inc. + * Copyright (C) 2018-present MongoDB, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the Server Side Public License, version 1, @@ -29,20 +29,103 @@ #pragma once -#include "mongo/db/pipeline/parsed_aggregation_projection.h" -#include "mongo/db/query/projection_ast.h" -#include "mongo/db/query/projection_ast_walker.h" +#include "mongo/platform/basic.h" + +#include <boost/intrusive_ptr.hpp> +#include <memory> + +#include "mongo/bson/bsonelement.h" +#include "mongo/db/pipeline/expression_context.h" +#include "mongo/db/pipeline/field_path.h" +#include "mongo/db/pipeline/transformer_interface.h" +#include "mongo/db/query/projection_policies.h" namespace mongo::projection_executor { /** - * Builds a projection execution tree from the given 'projection' and using the given projection - * 'policies' by walking an AST tree starting at the root node stored within the 'projection'. - * Set 'optimizeExecutor' to 'true' when the 'optimize()' method needs to be called on the newly - * created executor before returning it to the caller. + * A ProjectionExecutor is responsible for parsing and executing a $project. It represents either an + * inclusion or exclusion projection. This is the common interface between the two types of + * projections. */ -std::unique_ptr<parsed_aggregation_projection::ParsedAggregationProjection> buildProjectionExecutor( - boost::intrusive_ptr<ExpressionContext> expCtx, - const projection_ast::Projection* projection, - ProjectionPolicies policies, - bool optimizeExecutor); +class ProjectionExecutor : public TransformerInterface { +public: + /** + * The name of an internal variable to bind a projection post image to, which is used by the + * '_rootReplacementExpression' to replace the content of the transformed document. + */ + static constexpr StringData kProjectionPostImageVarName{"INTERNAL_PROJ_POST_IMAGE"_sd}; + + /** + * Optimize any expressions contained within this projection. + */ + void optimize() override { + if (_rootReplacementExpression) { + _rootReplacementExpression->optimize(); + } + } + + /** + * Add any dependencies needed by this projection or any sub-expressions to 'deps'. + */ + DepsTracker::State addDependencies(DepsTracker* deps) const override { + return DepsTracker::State::NOT_SUPPORTED; + } + + /** + * Apply the projection transformation. + */ + Document applyTransformation(const Document& input) override { + auto output = applyProjection(input); + if (_rootReplacementExpression) { + return _applyRootReplacementExpression(input, output); + } + return output; + } + + /** + * Sets 'expr' as a root-replacement expression to this tree. A root-replacement expression, + * once evaluated, will replace an entire output document. A projection post image document + * will be accessible via the special variable, whose name is stored in + * 'kProjectionPostImageVarName', if this expression needs access to it. + */ + void setRootReplacementExpression(boost::intrusive_ptr<Expression> expr) { + _rootReplacementExpression = expr; + } + +protected: + ProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, + ProjectionPolicies policies) + : _expCtx(expCtx), + _policies(policies), + _projectionPostImageVarId{ + _expCtx->variablesParseState.defineVariable(kProjectionPostImageVarName)} {} + + /** + * Apply the projection to 'input'. + */ + virtual Document applyProjection(const Document& input) const = 0; + + boost::intrusive_ptr<ExpressionContext> _expCtx; + + ProjectionPolicies _policies; + + boost::intrusive_ptr<Expression> _rootReplacementExpression; + +private: + Document _applyRootReplacementExpression(const Document& input, const Document& output) { + using namespace fmt::literals; + + _expCtx->variables.setValue(_projectionPostImageVarId, Value{output}); + auto val = _rootReplacementExpression->evaluate(input, &_expCtx->variables); + uassert(51254, + "Root-replacement expression must return a document, but got {}"_format( + typeName(val.getType())), + val.getType() == BSONType::Object); + return val.getDocument(); + } + + // This variable id is used to bind a projection post-image so that it can be accessed by + // root-replacement expressions which apply projection to the entire post-image document, rather + // than to a specific field. + Variables::Id _projectionPostImageVarId; +}; } // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/projection_executor.cpp b/src/mongo/db/exec/projection_executor_builder.cpp index cecc5a468d4..9bb5eb46cd9 100644 --- a/src/mongo/db/exec/projection_executor.cpp +++ b/src/mongo/db/exec/projection_executor_builder.cpp @@ -30,23 +30,19 @@ #include "mongo/platform/basic.h" #include "mongo/base/exact_cast.h" +#include "mongo/db/exec/exclusion_projection_executor.h" +#include "mongo/db/exec/inclusion_projection_executor.h" #include "mongo/db/exec/projection_executor.h" #include "mongo/db/pipeline/expression_find_internal.h" -#include "mongo/db/pipeline/parsed_exclusion_projection.h" -#include "mongo/db/pipeline/parsed_inclusion_projection.h" #include "mongo/db/query/projection_ast_path_tracking_visitor.h" +#include "mongo/db/query/projection_ast_walker.h" #include "mongo/db/query/util/make_data_structure.h" namespace mongo::projection_executor { namespace { -using ParsedAggregationProjection = parsed_aggregation_projection::ParsedAggregationProjection; -using ParsedInclusionProjection = parsed_aggregation_projection::ParsedInclusionProjection; -using ParsedExclusionProjection = parsed_aggregation_projection::ParsedExclusionProjection; - constexpr auto kInclusion = projection_ast::ProjectType::kInclusion; constexpr auto kExclusion = projection_ast::ProjectType::kExclusion; -constexpr auto kProjectionPostImageVarName = - parsed_aggregation_projection::ParsedAggregationProjection::kProjectionPostImageVarName; +constexpr auto kProjectionPostImageVarName = ProjectionExecutor::kProjectionPostImageVarName; /** * Holds data used to built a projection executor while walking an AST tree. This struct is attached @@ -174,7 +170,7 @@ public: } void visit(const projection_ast::ProjectionPositionalASTNode* node) final { - constexpr auto isInclusion = std::is_same_v<Executor, ParsedInclusionProjection>; + constexpr auto isInclusion = std::is_same_v<Executor, InclusionProjectionExecutor>; invariant(isInclusion); const auto& path = _context->fullPath(); @@ -191,7 +187,7 @@ public: // A $slice expression can be applied to an exclusion projection. In this case we don't need // to project out the path to which $slice is applied, since it will already be included // into the output document. - if constexpr (std::is_same_v<Executor, ParsedInclusionProjection>) { + if constexpr (std::is_same_v<Executor, InclusionProjectionExecutor>) { userData.rootNode()->addProjectionForPath(path.fullPath()); } @@ -219,7 +215,7 @@ public: // In an inclusion projection only the _id field can be excluded from the result document. // If this is the case, then we don't need to include the field into the projection. - if constexpr (std::is_same_v<Executor, ParsedInclusionProjection>) { + if constexpr (std::is_same_v<Executor, InclusionProjectionExecutor>) { const auto isIdField = path == "_id"; if (isIdField && !node->value()) { return; @@ -260,7 +256,7 @@ auto buildProjectionExecutor(boost::intrusive_ptr<ExpressionContext> expCtx, } } // namespace -std::unique_ptr<ParsedAggregationProjection> buildProjectionExecutor( +std::unique_ptr<ProjectionExecutor> buildProjectionExecutor( boost::intrusive_ptr<ExpressionContext> expCtx, const projection_ast::Projection* projection, const ProjectionPolicies policies, @@ -269,10 +265,10 @@ std::unique_ptr<ParsedAggregationProjection> buildProjectionExecutor( switch (projection->type()) { case kInclusion: - return buildProjectionExecutor<ParsedInclusionProjection>( + return buildProjectionExecutor<InclusionProjectionExecutor>( expCtx, projection->root(), policies, optimizeExecutor); case kExclusion: - return buildProjectionExecutor<ParsedExclusionProjection>( + return buildProjectionExecutor<ExclusionProjectionExecutor>( expCtx, projection->root(), policies, optimizeExecutor); default: MONGO_UNREACHABLE; diff --git a/src/mongo/db/exec/projection_executor_builder.h b/src/mongo/db/exec/projection_executor_builder.h new file mode 100644 index 00000000000..9d934894c17 --- /dev/null +++ b/src/mongo/db/exec/projection_executor_builder.h @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/query/projection_ast.h" +#include "mongo/db/query/projection_ast_walker.h" + +namespace mongo::projection_executor { +/** + * Builds a projection execution tree from the given 'projection' and using the given projection + * 'policies' by walking an AST tree starting at the root node stored within the 'projection'. + * Set 'optimizeExecutor' to 'true' when the 'optimize()' method needs to be called on the newly + * created executor before returning it to the caller. + */ +std::unique_ptr<ProjectionExecutor> buildProjectionExecutor( + boost::intrusive_ptr<ExpressionContext> expCtx, + const projection_ast::Projection* projection, + ProjectionPolicies policies, + bool optimizeExecutor); +} // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/projection_executor_builder_test.cpp b/src/mongo/db/exec/projection_executor_builder_test.cpp new file mode 100644 index 00000000000..f12e1353743 --- /dev/null +++ b/src/mongo/db/exec/projection_executor_builder_test.cpp @@ -0,0 +1,241 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/exec/document_value/document_value_test_util.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" +#include "mongo/db/pipeline/aggregation_context_fixture.h" +#include "mongo/db/query/collation/collator_interface_mock.h" +#include "mongo/db/query/projection_ast_util.h" +#include "mongo/db/query/projection_parser.h" +#include "mongo/unittest/unittest.h" + +namespace mongo::projection_executor { +namespace { +constexpr auto kOptimzeExecutor = true; + +class ProjectionExecutorTest : public AggregationContextFixture { +public: + projection_ast::Projection parseWithDefaultPolicies( + const BSONObj& projectionBson, boost::optional<BSONObj> matchExprBson = boost::none) { + return parseWithPolicies(projectionBson, matchExprBson, ProjectionPolicies{}); + } + + projection_ast::Projection parseWithFindFeaturesEnabled( + const BSONObj& projectionBson, boost::optional<BSONObj> matchExprBson = boost::none) { + auto policy = ProjectionPolicies::findProjectionPolicies(); + return parseWithPolicies(projectionBson, matchExprBson, policy); + } + + projection_ast::Projection parseWithPolicies(const BSONObj& projectionBson, + boost::optional<BSONObj> matchExprBson, + ProjectionPolicies policies) { + StatusWith<std::unique_ptr<MatchExpression>> swMatchExpression(nullptr); + if (matchExprBson) { + swMatchExpression = MatchExpressionParser::parse(*matchExprBson, getExpCtx()); + uassertStatusOK(swMatchExpression.getStatus()); + } + + return projection_ast::parse(getExpCtx(), + projectionBson, + swMatchExpression.getValue().get(), + matchExprBson.get_value_or(BSONObj()), + policies); + } +}; + +TEST_F(ProjectionExecutorTest, CanProjectInclusionWithIdPath) { + auto projWithId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &projWithId, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{_id: 123, a: 'abc'}")}, + executor->applyTransformation( + Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); + + auto projWithoutId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 0}")); + executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 'abc'}")}, + executor->applyTransformation( + Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectInclusionUndottedPath) { + auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: 'abc', b: 'def'}")}, + executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectInclusionDottedPath) { + auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 1, 'a.d': 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: 'abc', d: 'ghi'}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: 'abc', c: 'def', d: 'ghi'}}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectExpression) { + auto proj = parseWithDefaultPolicies(fromjson("{c: {$add: ['$a', '$b']}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{c: 3}")}, + executor->applyTransformation(Document{fromjson("{a: 1, b: 2}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectExclusionWithIdPath) { + auto projWithoutId = parseWithDefaultPolicies(fromjson("{a: 0, _id: 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{b: 'def', c: 'ghi'}")}, + executor->applyTransformation( + Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectExclusionUndottedPath) { + auto proj = parseWithDefaultPolicies(fromjson("{a: 0, b: 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{c: 'ghi'}")}, + executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectExclusionDottedPath) { + auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 0, 'a.d': 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {c: 'def'}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: 'abc', c: 'def', d: 'ghi'}}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindPositional) { + auto proj = + parseWithFindFeaturesEnabled(fromjson("{'a.b.$': 1}"), fromjson("{'a.b': {$gte: 3}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [3]}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}}")})); + + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [4]}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [4, 3, 2]}}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithInclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [{b: 3}]}")}, + executor->applyTransformation(Document{fromjson("{a: [{b: 1}, {b: 2}, {b: 3}]}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindElemMatch) { + + const BSONObj obj = fromjson("{a: [{b: 3, c: 1}, {b: 1, c: 2}, {b: 1, c: 3}]}"); + { + auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: 1}}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 2}]}")}, + executor->applyTransformation(Document{obj})); + } + + { + auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: 1, c: 3}}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 3}]}")}, + executor->applyTransformation(Document{obj})); + } +} + +TEST_F(ProjectionExecutorTest, ElemMatchRespectsCollator) { + CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); + getExpCtx()->setCollator(&collator); + + auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {$gte: 'abc'}}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + + ASSERT_DOCUMENT_EQ( + Document{fromjson("{ a: [ \"zdd\" ] }")}, + executor->applyTransformation(Document{fromjson("{a: ['zaa', 'zbb', 'zdd', 'zee']}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithExclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 3}], d: 'def'}")}, + executor->applyTransformation(Document{ + fromjson("{a: [{b: 1}, {b: 2}, {b: 3}], c: 'abc', d: 'def'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithInclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3]}, c: 'abc'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithInclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceBasicWithExclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: 3}, c: 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [1,2,3]}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithExclusion) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 0}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [2,3]}}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +} + +TEST_F(ProjectionExecutorTest, CanProjectFindSliceAndPositional) { + auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, 'c.$': 1}"), + fromjson("{c: {$gte: 6}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [2,3]}, c: [6]}")}, + executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: [5,6,7]}")})); +} + +TEST_F(ProjectionExecutorTest, ExecutorOptimizesExpression) { + auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: {$add: [1, 2]}}")); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + ASSERT_DOCUMENT_EQ(Document{fromjson("{_id: true, a: true, b: {$const: 3}}")}, + executor->serializeTransformation(boost::none)); +} +} // namespace +} // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/projection_executor_test.cpp b/src/mongo/db/exec/projection_executor_test.cpp index 0d97b093b1b..6d0d06a84d4 100644 --- a/src/mongo/db/exec/projection_executor_test.cpp +++ b/src/mongo/db/exec/projection_executor_test.cpp @@ -29,212 +29,582 @@ #include "mongo/platform/basic.h" -#include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/projection_executor.h" -#include "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/query/collation/collator_interface_mock.h" -#include "mongo/db/query/projection_ast_util.h" + +#include <vector> + +#include "mongo/bson/bsonmisc.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/exec/document_value/document.h" +#include "mongo/db/exec/document_value/value.h" +#include "mongo/db/exec/inclusion_projection_executor.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" +#include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/projection_parser.h" #include "mongo/unittest/unittest.h" namespace mongo::projection_executor { namespace { -constexpr auto kOptimzeExecutor = true; +template <typename T> +BSONObj wrapInLiteral(const T& arg) { + return BSON("$literal" << arg); +} -class ProjectionExecutorTest : public AggregationContextFixture { -public: - projection_ast::Projection parseWithDefaultPolicies( - const BSONObj& projectionBson, boost::optional<BSONObj> matchExprBson = boost::none) { - return parseWithPolicies(projectionBson, matchExprBson, ProjectionPolicies{}); - } +// Helper to simplify the creation of a ProjectionExecutor with default policies. +auto makeProjectionWithDefaultPolicies(BSONObj spec) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ProjectionPolicies defaultPolicies; + auto projection = projection_ast::parse(expCtx, spec, defaultPolicies); + return buildProjectionExecutor( + expCtx, &projection, defaultPolicies, true /* optimizeExecutor */); +} - projection_ast::Projection parseWithFindFeaturesEnabled( - const BSONObj& projectionBson, boost::optional<BSONObj> matchExprBson = boost::none) { - auto policy = ProjectionPolicies::findProjectionPolicies(); - return parseWithPolicies(projectionBson, matchExprBson, policy); - } +// Helper to simplify the creation of a ProjectionExecutor which bans computed fields. +auto makeProjectionWithBannedComputedFields(BSONObj spec) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ProjectionPolicies banComputedFields{ + ProjectionPolicies::kDefaultIdPolicyDefault, + ProjectionPolicies::kArrayRecursionPolicyDefault, + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields}; + auto projection = projection_ast::parse(expCtx, spec, banComputedFields); + return buildProjectionExecutor( + expCtx, &projection, banComputedFields, true /* optimizeExecutor */); +} - projection_ast::Projection parseWithPolicies(const BSONObj& projectionBson, - boost::optional<BSONObj> matchExprBson, - ProjectionPolicies policies) { - StatusWith<std::unique_ptr<MatchExpression>> swMatchExpression(nullptr); - if (matchExprBson) { - swMatchExpression = MatchExpressionParser::parse(*matchExprBson, getExpCtx()); - uassertStatusOK(swMatchExpression.getStatus()); - } - - return projection_ast::parse(getExpCtx(), - projectionBson, - swMatchExpression.getValue().get(), - matchExprBson.get_value_or(BSONObj()), - policies); - } -}; +// +// Error cases. +// + +TEST(ProjectionExecutorErrors, ShouldRejectDuplicateFieldNames) { + // Include/exclude the same field twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << false)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "b" << false))), + AssertionException); + + // Mix of include/exclude and adding a field. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << wrapInLiteral(0))), + AssertionException); + + // Adding the same field twice. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << wrapInLiteral(0))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectDuplicateIds) { + // Include/exclude _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << false)), + AssertionException); + + // Mix of including/excluding and adding _id. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id" << true)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << wrapInLiteral(0))), + AssertionException); + + // Adding _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("_id" << wrapInLiteral(1) << "_id" << wrapInLiteral(0))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectFieldsWithSharedPrefix) { + // Include/exclude Fields with a shared prefix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a.b" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << false)), + AssertionException); + + // Mix of include/exclude and adding a shared prefix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a.b" << true)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << wrapInLiteral(0))), + AssertionException); + + // Adding a shared prefix twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << wrapInLiteral(1) << "a.b" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b.c.d" << wrapInLiteral(1) << "a.b.c" << wrapInLiteral(0))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectPathConflictsWithNonAlphaNumericCharacters) { + // 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( + BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true))); + ASSERT(makeProjectionWithDefaultPolicies( + BSON("a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c" << false))); + + // Then assert that we throw when we introduce a prefixed field. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true + << "a.b c" << true << "a.b.d" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b.d" << false << "a.b c" << false + << "a.b?c" << false << "a.b" + << false << "a.b-c" << false)), + AssertionException); + + // Adding the same field twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b?c" << wrapInLiteral(1) << "a.b?c" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b c" << wrapInLiteral(0) << "a.b c" << wrapInLiteral(1))), + AssertionException); + + // Mix of include/exclude and adding a shared prefix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b-c" << true << "a.b" << wrapInLiteral(1) << "a.b?c" << true + << "a.b c" << true << "a.b.d" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" + << wrapInLiteral(0) << "a.b-c" << false)), + AssertionException); -TEST_F(ProjectionExecutorTest, CanProjectInclusionWithIdPath) { - auto projWithId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &projWithId, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{_id: 123, a: 'abc'}")}, - executor->applyTransformation( - Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); + // Adding a shared prefix twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b-c" << wrapInLiteral(1) << "a.b" << wrapInLiteral(1) << "a.b?c" + << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b.d" + << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b.d" << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b?c" + << wrapInLiteral(1) << "a.b" << wrapInLiteral(0) << "a.b-c" + << wrapInLiteral(1))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectMixOfIdAndSubFieldsOfId) { + // Include/exclude _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id.x" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << false)), + AssertionException); - auto projWithoutId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 0}")); - executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 'abc'}")}, - executor->applyTransformation( - Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); + // Mix of including/excluding and adding _id. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id.x" << true)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << wrapInLiteral(0))), + AssertionException); + + // Adding _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("_id" << wrapInLiteral(1) << "_id.x" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("_id.b.c.d" << wrapInLiteral(1) << "_id.b.c" << wrapInLiteral(0))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectInclusionUndottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: 'abc', b: 'def'}")}, - executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); +TEST(ProjectionExecutorErrors, ShouldAllowMixOfIdInclusionAndExclusion) { + // Mixing "_id" inclusion with exclusion. + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false << "_id" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a.b.c" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); } -TEST_F(ProjectionExecutorTest, CanProjectInclusionDottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 1, 'a.d': 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: 'abc', d: 'ghi'}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: 'abc', c: 'def', d: 'ghi'}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectMixOfInclusionAndExclusion) { + // Simple mix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << false)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "c" << true))), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << BSON("b" << false << "c" << true))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.b" << false << "a.c" << true)), + AssertionException); + + // Mix while also adding a field. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << true << "b" << wrapInLiteral(1) << "c" << false)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << false << "b" << wrapInLiteral(1) << "c" << true)), + AssertionException); + + // Mix of "_id" subfield inclusion and exclusion. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << true << "a.b.c" << false)), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExpression) { - auto proj = parseWithDefaultPolicies(fromjson("{c: {$add: ['$a', '$b']}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{c: 3}")}, - executor->applyTransformation(Document{fromjson("{a: 1, b: 2}")})); +TEST(ProjectionExecutorErrors, ShouldRejectMixOfExclusionAndComputedFields) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "b" << false)), + AssertionException); + + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a.c" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b" << wrapInLiteral(1) << "a.c" << false)), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExclusionWithIdPath) { - auto projWithoutId = parseWithDefaultPolicies(fromjson("{a: 0, _id: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{b: 'def', c: 'ghi'}")}, - executor->applyTransformation( - Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); +TEST(ProjectionExecutorErrors, ShouldRejectFieldNamesStartingWithADollar) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 0)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 1)), AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 0)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 1)), AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 1))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 0)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 1)), AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExclusionUndottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{a: 0, b: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{c: 'ghi'}")}, - executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); +TEST(ProjectionExecutorErrors, ShouldRejectTopLevelExpressions) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << BSON_ARRAY(4 << 2))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExclusionDottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 0, 'a.d': 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {c: 'def'}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: 'abc', c: 'def', d: 'ghi'}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectExpressionWithMultipleFieldNames) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << 1 << "$add" << BSON_ARRAY(4 << 2)))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << BSON("c" << 1 << "$add" << BSON_ARRAY(4 << 2))))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << BSON("$add" << BSON_ARRAY(4 << 2) << "c" << 1)))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindPositional) { - auto proj = - parseWithFindFeaturesEnabled(fromjson("{'a.b.$': 1}"), fromjson("{'a.b': {$gte: 3}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [3]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectEmptyProjection) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSONObj()), AssertionException); +} - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [4]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [4, 3, 2]}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectEmptyNestedObject) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSONObj())), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << BSONObj())), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << BSONObj())), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << BSONObj())), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << BSONObj()))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithInclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: [{b: 3}]}")}, - executor->applyTransformation(Document{fromjson("{a: [{b: 1}, {b: 2}, {b: 3}]}")})); +TEST(ProjectionExecutorErrors, ShouldErrorOnInvalidExpression) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << true << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindElemMatch) { +TEST(ProjectionExecutorErrors, ShouldErrorOnInvalidFieldPath) { + // Empty field names. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << wrapInLiteral(2))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << false)), AssertionException); - const BSONObj obj = fromjson("{a: [{b: 3, c: 1}, {b: 1, c: 2}, {b: 1, c: 3}]}"); - { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: 1}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 2}]}")}, - executor->applyTransformation(Document{obj})); - } + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << true))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << false))), + AssertionException); - { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: 1, c: 3}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 3}]}")}, - executor->applyTransformation(Document{obj})); - } + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << true))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << false))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << false)), AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << false)), AssertionException); + + // Not testing field names with null bytes, since that is invalid BSON, and won't make it to the + // $project stage without a previous error. + + // Field names starting with '$'. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$x" << wrapInLiteral(2))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << false)), AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldNotErrorOnTwoNestedFields) { + makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << true)); + makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a" << BSON("c" << true))); +} + +// +// Determining exclusion vs. inclusion. +// + +TEST(ProjectionExecutorType, ShouldAllowDottedFieldInSubDocument) { + auto proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << true))); + ASSERT(proj->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << wrapInLiteral(1)))); + ASSERT(proj->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << false))); + ASSERT(proj->getType() == TransformerInterface::TransformerType::kExclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldDefaultToInclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldDetectExclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldDetectInclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldTreatOnlyComputedFieldsAsAnInclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = + makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = + makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << wrapInLiteral(1)))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << wrapInLiteral(1)))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); } -TEST_F(ProjectionExecutorTest, ElemMatchRespectsCollator) { - CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); - getExpCtx()->setCollator(&collator); +TEST(ProjectionExecutorType, ShouldAllowMixOfInclusionAndComputedFields) { + auto parsedProject = + makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = + makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {$gte: 'abc'}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + parsedProject = makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{ a: [ \"zdd\" ] }")}, - executor->applyTransformation(Document{fromjson("{a: ['zaa', 'zbb', 'zdd', 'zee']}")})); + parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << true << "c" + << "stringLiteral"))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); } -TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithExclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 3}], d: 'def'}")}, - executor->applyTransformation(Document{ - fromjson("{a: [{b: 1}, {b: 2}, {b: 3}], c: 'abc', d: 'def'}")})); +TEST(ProjectionExecutorType, ShouldRejectMixOfInclusionAndBannedComputedFields) { + ASSERT_THROWS( + makeProjectionWithBannedComputedFields(BSON("a" << true << "b" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS( + makeProjectionWithBannedComputedFields(BSON("a.b" << true << "a.c" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields(BSON("a" << BSON("b" << true << "c" + << "stringLiteral"))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithInclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, ShouldRejectOnlyComputedFieldsWhenComputedFieldsAreBanned) { + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(2))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(2))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithInclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, ShouldAcceptInclusionProjectionWhenComputedFieldsAreBanned) { + auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a.b.c" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldAcceptExclusionProjectionWhenComputedFieldsAreBanned) { + auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceBasicWithExclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: 3}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [1,2,3]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, ShouldCoerceNumericsToBools) { + 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()); + ASSERT(parsedProject->getType() == + TransformerInterface::TransformerType::kExclusionProjection); + } + + std::vector<Value> nonZeroes = { + Value(1), Value(-1), Value(3), Value(1LL), Value(1.0), Value(Decimal128(1))}; + for (auto&& nonZero : nonZeroes) { + auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", nonZero}}.toBson()); + ASSERT(parsedProject->getType() == + TransformerInterface::TransformerType::kInclusionProjection); + } } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithExclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, GetExpressionForPathGetsTopLevelExpression) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto projectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); + auto expr = Expression::parseObject(expCtx, projectObj, expCtx->variablesParseState); + ProjectionPolicies defaultPolicies; + auto node = InclusionNode(defaultPolicies); + node.addExpressionForPath(FieldPath("key"), expr); + BSONObjBuilder bob; + ASSERT_EQ(expr, node.getExpressionForPath(FieldPath("key"))); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceAndPositional) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, 'c.$': 1}"), - fromjson("{c: {$gte: 6}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}, c: [6]}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: [5,6,7]}")})); +TEST(ProjectionExecutorType, GetExpressionForPathGetsCorrectTopLevelExpression) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto correctObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); + auto incorrectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 2) << BSON("$const" << 4))); + auto correctExpr = Expression::parseObject(expCtx, correctObj, expCtx->variablesParseState); + auto incorrectExpr = Expression::parseObject(expCtx, incorrectObj, expCtx->variablesParseState); + ProjectionPolicies defaultPolicies; + auto node = InclusionNode(defaultPolicies); + node.addExpressionForPath(FieldPath("key"), correctExpr); + node.addExpressionForPath(FieldPath("other"), incorrectExpr); + BSONObjBuilder bob; + ASSERT_EQ(correctExpr, node.getExpressionForPath(FieldPath("key"))); } -TEST_F(ProjectionExecutorTest, ExecutorOptimizesExpression) { - auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: {$add: [1, 2]}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{_id: true, a: true, b: {$const: 3}}")}, - executor->serializeTransformation(boost::none)); +TEST(ProjectionExecutorType, GetExpressionForPathGetsNonTopLevelExpression) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto projectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); + auto expr = Expression::parseObject(expCtx, projectObj, expCtx->variablesParseState); + ProjectionPolicies defaultPolicies; + auto node = InclusionNode(defaultPolicies); + node.addExpressionForPath(FieldPath("key.second"), expr); + BSONObjBuilder bob; + ASSERT_EQ(expr, node.getExpressionForPath(FieldPath("key.second"))); } + } // namespace } // namespace mongo::projection_executor diff --git a/src/mongo/db/exec/find_projection_executor.cpp b/src/mongo/db/exec/projection_executor_utils.cpp index 9ff1bcd4602..0923e2e2fab 100644 --- a/src/mongo/db/exec/find_projection_executor.cpp +++ b/src/mongo/db/exec/projection_executor_utils.cpp @@ -27,12 +27,47 @@ * it in the license file. */ -#include "mongo/platform/basic.h" +#include "mongo/db/exec/projection_executor.h" -#include "mongo/db/exec/find_projection_executor.h" +namespace mongo::projection_executor_utils { +bool applyProjectionToOneField(projection_executor::ProjectionExecutor* executor, + StringData field) { + const FieldPath fp{field}; + MutableDocument md; + md.setNestedField(fp, Value{1.0}); + auto output = executor->applyTransformation(md.freeze()); + return !output.getNestedField(fp).missing(); + return false; +} + +stdx::unordered_set<std::string> applyProjectionToFields( + projection_executor::ProjectionExecutor* executor, + const stdx::unordered_set<std::string>& fields) { + stdx::unordered_set<std::string> out; + + for (const auto& field : fields) { + if (applyProjectionToOneField(executor, field)) { + out.insert(field); + } + } + + return out; +} + +std::set<FieldRef> extractExhaustivePaths(const projection_executor::ProjectionExecutor* executor) { + std::set<FieldRef> exhaustivePaths; + + if (executor->getType() == TransformerInterface::TransformerType::kInclusionProjection) { + DepsTracker depsTracker; + executor->addDependencies(&depsTracker); + for (auto&& field : depsTracker.fields) { + exhaustivePaths.insert(FieldRef{field}); + } + } + + return exhaustivePaths; +} -namespace mongo { -namespace projection_executor { namespace { /** * Holds various parameters required to apply a $slice projection. Populated from the arguments @@ -44,9 +79,21 @@ struct SliceParams { const int limit; }; -Value applySliceProjectionHelper(const Document& input, - const SliceParams& params, - size_t fieldPathIndex); +/** + * Extracts an element from the array 'arr' at position 'elemIndex'. The 'elemIndex' string + * parameter must hold a value which can be converted to an unsigned integer. If 'elemIndex' is not + * within array boundaries, an empty Value is returned. + */ +Value extractArrayElement(const Value& arr, const std::string& elemIndex) { + auto index = str::parseUnsignedBase10Integer(elemIndex); + invariant(index); + return arr[*index]; +} + + +Value applyFindSliceProjectionHelper(const Document& input, + const SliceParams& params, + size_t fieldPathIndex); /** * Returns a portion of the 'array', skipping a number of elements as indicated by the 'skip' @@ -95,16 +142,16 @@ Value sliceArray(const std::vector<Value>& array, boost::optional<int> skip, int * on the path "a.b", but {a: [{b: 1}]} does, so nested arrays are stored within the output array * as regular values. */ -Value applySliceProjectionToArray(const std::vector<Value>& array, - const SliceParams& params, - size_t fieldPathIndex) { +Value applyFindSliceProjectionToArray(const std::vector<Value>& array, + const SliceParams& params, + size_t fieldPathIndex) { std::vector<Value> output; output.reserve(array.size()); for (const auto& elem : array) { output.push_back( elem.getType() == BSONType::Object - ? applySliceProjectionHelper(elem.getDocument(), params, fieldPathIndex) + ? applyFindSliceProjectionHelper(elem.getDocument(), params, fieldPathIndex) : elem); } @@ -123,9 +170,9 @@ Value applySliceProjectionToArray(const std::vector<Value>& array, * the result in 'val'. * * Store the computed 'val' in the 'output' document under the current field name. */ -Value applySliceProjectionHelper(const Document& input, - const SliceParams& params, - size_t fieldPathIndex) { +Value applyFindSliceProjectionHelper(const Document& input, + const SliceParams& params, + size_t fieldPathIndex) { invariant(fieldPathIndex < params.path.getPathLength()); auto fieldName = params.path.getFieldName(fieldPathIndex++); @@ -135,11 +182,11 @@ Value applySliceProjectionHelper(const Document& input, case BSONType::Array: val = (fieldPathIndex == params.path.getPathLength()) ? sliceArray(val.getArray(), params.skip, params.limit) - : applySliceProjectionToArray(val.getArray(), params, fieldPathIndex); + : applyFindSliceProjectionToArray(val.getArray(), params, fieldPathIndex); break; case BSONType::Object: if (fieldPathIndex < params.path.getPathLength()) { - val = applySliceProjectionHelper(val.getDocument(), params, fieldPathIndex); + val = applyFindSliceProjectionHelper(val.getDocument(), params, fieldPathIndex); } break; default: @@ -152,16 +199,10 @@ Value applySliceProjectionHelper(const Document& input, } } // namespace -Value extractArrayElement(const Value& arr, const std::string& elemIndex) { - auto index = str::parseUnsignedBase10Integer(elemIndex); - invariant(index); - return arr[*index]; -} - -Document applyPositionalProjection(const Document& preImage, - const Document& postImage, - const MatchExpression& matchExpr, - const FieldPath& path) { +Document applyFindPositionalProjection(const Document& preImage, + const Document& postImage, + const MatchExpression& matchExpr, + const FieldPath& path) { MutableDocument output(postImage); // Try to find the first matching array element from the 'input' document based on the condition @@ -217,9 +258,9 @@ Document applyPositionalProjection(const Document& preImage, return output.freeze(); } -Value applyElemMatchProjection(const Document& input, - const MatchExpression& matchExpr, - const FieldPath& path) { +Value applyFindElemMatchProjection(const Document& input, + const MatchExpression& matchExpr, + const FieldPath& path) { invariant(path.getPathLength() == 1); // Try to find the first matching array element from the 'input' document based on the condition @@ -241,14 +282,13 @@ Value applyElemMatchProjection(const Document& input, return Value{std::vector<Value>{matchingElem}}; } -Document applySliceProjection(const Document& input, - const FieldPath& path, - boost::optional<int> skip, - int limit) { +Document applyFindSliceProjection(const Document& input, + const FieldPath& path, + boost::optional<int> skip, + int limit) { auto params = SliceParams{path, skip, limit}; - auto val = applySliceProjectionHelper(input, params, 0); + auto val = applyFindSliceProjectionHelper(input, params, 0); invariant(val.getType() == BSONType::Object); return val.getDocument(); } -} // namespace projection_executor -} // namespace mongo +} // namespace mongo::projection_executor_utils diff --git a/src/mongo/db/exec/find_projection_executor.h b/src/mongo/db/exec/projection_executor_utils.h index 695da74cbec..338d12d39d3 100644 --- a/src/mongo/db/exec/find_projection_executor.h +++ b/src/mongo/db/exec/projection_executor_utils.h @@ -30,17 +30,31 @@ #pragma once #include "mongo/db/exec/document_value/document.h" +#include "mongo/db/exec/projection_executor.h" #include "mongo/db/matcher/expression.h" #include "mongo/db/pipeline/field_path.h" -namespace mongo { -namespace projection_executor { +namespace mongo::projection_executor_utils { /** - * Extracts an element from the array 'arr' at position 'elemIndex'. The 'elemIndex' string - * parameter must hold a value which can be converted to an unsigned integer. If 'elemIndex' is not - * within array boundaries, an empty Value is returned. + * Applies the projection to a single field name. Returns whether or not the projection would + * allow that field to remain in a document. + **/ +bool applyProjectionToOneField(projection_executor::ProjectionExecutor* executor, StringData field); + +/** + * Applies the projection to each field from the 'fields' set and stores it in the returned set + * if the projection would allow that field to remain in a document. + **/ +stdx::unordered_set<std::string> applyProjectionToFields( + projection_executor::ProjectionExecutor* executor, + const stdx::unordered_set<std::string>& fields); + +/** + * Returns the exhaustive set of all paths that will be preserved by this projection, or an + * empty set if the exhaustive set cannot be determined. An inclusion will always produce an + * exhaustive set; an exclusion will always produce an empty set. */ -Value extractArrayElement(const Value& arr, const std::string& elemIndex); +std::set<FieldRef> extractExhaustivePaths(const projection_executor::ProjectionExecutor* executor); /** * Applies a positional projection on the first array found in the 'path' on a projection @@ -63,10 +77,10 @@ Value extractArrayElement(const Value& arr, const std::string& elemIndex); * Throws an AssertionException if 'matchExpr' matches the input document, but an array element * satisfying positional projection requirements cannot be found. */ -Document applyPositionalProjection(const Document& preImage, - const Document& postImage, - const MatchExpression& matchExpr, - const FieldPath& path); +Document applyFindPositionalProjection(const Document& preImage, + const Document& postImage, + const MatchExpression& matchExpr, + const FieldPath& path); /** * Applies an $elemMatch projection on the array at the given 'path' on the 'input' document. The * applied projection is stored in the output Value. The 'matchExpr' specifies a condition to @@ -85,9 +99,9 @@ Document applyPositionalProjection(const Document& preImage, * Since the $elemMatch projection cannot be used with a nested field, the 'path' value must not * be a dotted path, otherwise an invariant will be triggered. */ -Value applyElemMatchProjection(const Document& input, - const MatchExpression& matchExpr, - const FieldPath& path); +Value applyFindElemMatchProjection(const Document& input, + const MatchExpression& matchExpr, + const FieldPath& path); /** * Applies a $slice projection on the array at the given 'path' on the 'input' document. The applied * projection is returned as a Document. The 'skip' parameter indicates the number of items in the @@ -107,10 +121,8 @@ Value applyElemMatchProjection(const Document& input, * * The resulting document will contain the following element: {foo: [{bar: 2}, {bar: 3}]}. */ -Document applySliceProjection(const Document& input, - const FieldPath& path, - boost::optional<int> skip, - int limit); - -} // namespace projection_executor -} // namespace mongo +Document applyFindSliceProjection(const Document& input, + const FieldPath& path, + boost::optional<int> skip, + int limit); +} // namespace mongo::projection_executor_utils diff --git a/src/mongo/db/exec/projection_executor_utils_test.cpp b/src/mongo/db/exec/projection_executor_utils_test.cpp new file mode 100644 index 00000000000..867a1de7704 --- /dev/null +++ b/src/mongo/db/exec/projection_executor_utils_test.cpp @@ -0,0 +1,336 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/exec/document_value/document_value_test_util.h" +#include "mongo/db/exec/projection_executor_utils.h" +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/death_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo::projection_executor_utils { +namespace positional_projection_tests { +/** + * Applies a find()-style positional projection at the given 'path' using 'matchSpec' to create + * a 'MatchExpression' to match an element on the first array in the 'path'. If no value for + * 'postImage' is provided, then the post-image used will be the value passed for the 'preImage'. + */ +auto applyPositional(const BSONObj& matchSpec, + const std::string& path, + const Document& preImage, + boost::optional<Document> postImage = boost::none) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchSpec, expCtx)); + return projection_executor_utils::applyFindPositionalProjection( + preImage, postImage.value_or(preImage), *matchExpr, path); +} + +TEST(PositionalProjection, CorrectlyProjectsSimplePath) { + ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1, foo: [6]}")}, + applyPositional(fromjson("{bar: 1, foo: {$gte: 5}}"), + "foo", + Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); +} + +TEST(PositionalProjection, CorrectlyProjectsDottedPath) { + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1, x: {y: [6]}}")}, + applyPositional(fromjson("{a: 1, 'x.y': {$gte: 5}}"), + "x.y", + Document{fromjson("{a: 1, x: {y: [1,2,6,10]}}")})); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1, x: {y: {z: [6]}}}")}, + applyPositional(fromjson("{a: 1, 'x.y.z': {$gte: 5}}"), + "x.y.z", + Document{fromjson("{a: 1, x: {y: {z: [1,2,6,10]}}}")})); +} + +TEST(PositionalProjection, ProjectsValueUnmodifiedIfFieldIsNotArray) { + auto doc = Document{fromjson("{foo: 3}")}; + ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc)); +} + +TEST(PositionalProjection, FailsToProjectPositionalPathComponentsForNestedArrays) { + ASSERT_THROWS_CODE(applyPositional(fromjson("{'x.0.y': 42}"), + "x.0.y", + Document{fromjson("{x: [{y: [11, 42]}]}")}), + AssertionException, + 51247); +} + +TEST(PositionalProjection, CorrectlyProjectsNestedArrays) { + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [1,2]}]}")}, + applyPositional(fromjson("{'a.b': 1}"), + "a", + Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3,4]}]}")}, + applyPositional(fromjson("{'a.b': 3}"), + "a", + Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3,4]}]}")}, + applyPositional(fromjson("{'a.b': 3}"), + "a.b", + Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [['d','e','f']]}")}, + applyPositional(fromjson("{a: {$gt: ['a','b','c']}}"), + "a", + Document{fromjson("{a: [['a','b','c'],['d','e','f']]}")})); +} + +TEST(PositionalProjection, FailsToProjectWithMultipleConditionsOnArray) { + ASSERT_THROWS_CODE(applyPositional(fromjson("{$or: [{'x.y': 1}, {'x.y': 2}]}"), + "x", + Document{fromjson("{x: [{y: [1,2]}]}")}), + AssertionException, + 51246); +} + +TEST(PositionalProjection, CanMergeWithExistingFieldsInOutputDocument) { + auto doc = Document{fromjson("{foo: {bar: [1,2,6,10]}}")}; + ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: {bar: [6]}}")}, + applyPositional(fromjson("{'foo.bar': {$gte: 5}}"), "foo.bar", doc)); + + doc = Document{fromjson("{bar: 1, foo: {bar: [1,2,6,10]}}")}; + ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1, foo: {bar: [6]}}")}, + applyPositional(fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"), "foo.bar", doc)); + + doc = Document{fromjson("{bar: 1, foo: 3}")}; + ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc)); +} + +TEST(PositionalProjection, AppliesMatchExpressionToPreImageAndStoresResultInPostImage) { + auto preImage = Document{fromjson("{foo: 1, bar: [1,2,6,10]}")}; + auto postImage = Document{fromjson("{bar: [1,2,6,10]}")}; + ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: [6]}")}, + applyPositional(fromjson("{foo: 1, bar: 6}"), "bar", preImage, postImage)); +} +} // namespace positional_projection_tests + +namespace elem_match_projection_tests { +auto applyElemMatch(const BSONObj& match, const std::string& path, const Document& input) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto matchObj = BSON(path << BSON("$elemMatch" << match)); + auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchObj, expCtx)); + return projection_executor_utils::applyFindElemMatchProjection(input, *matchExpr, path); +} + +TEST(ElemMatchProjection, CorrectlyProjectsNonObjectElement) { + ASSERT_VALUE_EQ( + Document{fromjson("{foo: [4]}")}["foo"], + applyElemMatch(fromjson("{$in: [4]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")})); + ASSERT_VALUE_EQ( + Document{fromjson("{foo: [4]}")}["foo"], + applyElemMatch(fromjson("{$nin: [1,2,3]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")})); +} + +TEST(ElemMatchProjection, CorrectlyProjectsObjectElement) { + ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], + applyElemMatch(fromjson("{bar: {$gte: 5}}"), + "foo", + Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " + "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +} + +TEST(ElemMatchProjection, CorrectlyProjectsArrayElement) { + ASSERT_VALUE_EQ(Document{fromjson("{foo: [[3,4]]}")}["foo"], + applyElemMatch(fromjson("{$gt: [1,2]}"), + "foo", + Document{fromjson("{foo: [[1,2], [3,4]]}")})); +} + +TEST(ElemMatchProjection, ProjectsAsEmptyDocumentIfInputIsEmpty) { + ASSERT_VALUE_EQ({}, applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", {})); +} + +TEST(ElemMatchProjection, RemovesFieldFromOutputDocumentIfUnableToMatchArrayElement) { + ASSERT_VALUE_EQ({}, + applyElemMatch(fromjson("{bar: {$gte: 5}}"), + "foo", + Document{fromjson("{foo: [{bar: 1, z: 1}, " + "{bar: 2, z: 2}]}")})); + ASSERT_VALUE_EQ( + {}, + applyElemMatch(fromjson("{bar: {$gte: 20}}"), + "foo", + Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " + "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +} + +TEST(ElemMatchProjection, CorrectlyProjectsWithMultipleCriteriaInMatchExpression) { + ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 2, z: 2}]}")}["foo"], + applyElemMatch(fromjson("{bar: {$gt: 1, $lt: 6}}"), + "foo", + Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " + "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +} + +TEST(ElemMatchProjection, CanMergeWithExistingFieldsInInputDocument) { + ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], + applyElemMatch(fromjson("{bar: {$gte: 5}}"), + "foo", + Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " + "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); + + ASSERT_VALUE_EQ( + Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], + applyElemMatch(fromjson("{bar: {$gte: 5}}"), + "foo", + Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " + "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); +} + +TEST(ElemMatchProjection, RertursEmptyValuefItContainsNumericSubfield) { + ASSERT_VALUE_EQ( + {}, applyElemMatch(fromjson("{$gt: 2}"), "foo", Document{BSON("foo" << BSON(0 << 3))})); + + ASSERT_VALUE_EQ({}, + applyElemMatch(fromjson("{$gt: 2}"), + "foo", + Document{BSON("bar" << 1 << "foo" << BSON(0 << 3))})); +} +} // namespace elem_match_projection_tests + +namespace slice_projection_tests { +DEATH_TEST(SliceProjection, + ShouldFailIfNegativeLimitSpecifiedWithPositiveSkip, + "Invariant failure limit >= 0") { + auto doc = Document{fromjson("{a: [1,2,3,4]}")}; + projection_executor_utils::applyFindSliceProjection(doc, "a", 1, -1); +} + +DEATH_TEST(SliceProjection, + ShouldFailIfNegativeLimitSpecifiedWithNegativeSkip, + "Invariant failure limit >= 0") { + auto doc = Document{fromjson("{a: [1,2,3,4]}")}; + projection_executor_utils::applyFindSliceProjection(doc, "a", -1, -1); +} + +TEST(SliceProjection, CorrectlyProjectsSimplePath) { + auto doc = Document{fromjson("{a: [1,2,3,4]}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [1,2,3]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 3)); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [2,3,4]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, -3)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", -3, 1)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2,3,4]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", -3, 4)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [4]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", 3, 1)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", -5, 5)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: []}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", 5, 2)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", -5, 2)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, + projection_executor_utils::applyFindSliceProjection( + doc, "a", boost::none, std::numeric_limits<int>::max())); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, + projection_executor_utils::applyFindSliceProjection( + doc, "a", boost::none, std::numeric_limits<int>::min())); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [1,2,3,4]}")}, + projection_executor_utils::applyFindSliceProjection( + doc, "a", std::numeric_limits<int>::min(), std::numeric_limits<int>::max())); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: []}")}, + projection_executor_utils::applyFindSliceProjection( + doc, "a", std::numeric_limits<int>::max(), std::numeric_limits<int>::max())); + + doc = Document{fromjson("{a: [{b: 1, c: 1}, {b: 2, c: 2}, {b: 3, c: 3}], d: 2}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [{b: 1, c: 1}], d: 2}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 1)); + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [{b: 3, c: 3}], d: 2}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, -1)); + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 2, c: 2}], d: 2}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", 1, 1)); + + doc = Document{fromjson("{a: 1}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: 1}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 2)); + + doc = Document{fromjson("{a: {b: 1}}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: 1}}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 2)); +} + +TEST(SliceProjection, CorrectlyProjectsDottedPath) { + auto doc = Document{fromjson("{a: {b: [1,2,3], c: 1}}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [1,2], c: 1}}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); + + doc = Document{fromjson("{a: {b: [1,2,3], c: 1}, d: 1}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [1,2], c: 1}, d: 1}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); + + doc = Document{fromjson("{a: {b: [[1,2], [3,4], [5,6]]}}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: [[1,2], [3,4]]}}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); + + doc = Document{fromjson("{a: [{b: {c: [1,2,3,4]}}, {b: {c: [5,6,7,8]}}], d: 1}")}; + ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: {c: [4]}}, {b: {c: [8]}}], d: 1}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b.c", -1, 2)); + + doc = Document{fromjson("{a: {b: 1}}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: 1}}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); + + doc = Document{fromjson("{a: {b: {c: 1}}}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: {b: {c: 1}}}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); + + doc = Document{fromjson("{a: [{b: [1,2,3], c: 1}]}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [{b: [3], c: 1}]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, -1)); + + doc = Document{fromjson("{a: [{b: [1,2,3], c: 4}, {b: [5,6,7], c: 8}]}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [{b: [3], c: 4}, {b: [7], c: 8}]}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, -1)); + + doc = Document{fromjson("{a: [{b: [{x:1, c: [1, 2]}, {y: 1, c: [3, 4]}]}], z: 1}")}; + ASSERT_DOCUMENT_EQ( + Document{fromjson("{a: [{b: [{x:1, c: [1]}, {y: 1, c: [3]}]}], z: 1}")}, + projection_executor_utils::applyFindSliceProjection(doc, "a.b.c", boost::none, 1)); +} +} // namespace slice_projection_tests +} // namespace mongo::projection_executor_utils diff --git a/src/mongo/db/exec/projection_exec_agg_test.cpp b/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp index 1a5c11ecc88..4624a3b9b92 100644 --- a/src/mongo/db/exec/projection_exec_agg_test.cpp +++ b/src/mongo/db/exec/projection_executor_wildcard_access_test.cpp @@ -29,37 +29,50 @@ #include "mongo/platform/basic.h" -#include "mongo/db/exec/projection_exec_agg.h" - #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/bson/json.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" +#include "mongo/db/exec/projection_executor_utils.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/db/query/projection_parser.h" #include "mongo/unittest/unittest.h" #include "mongo/util/assert_util.h" -namespace mongo { +namespace mongo::projection_executor { namespace { - -using ArrayRecursionPolicy = ProjectionExecAgg::ArrayRecursionPolicy; -using DefaultIdPolicy = ProjectionExecAgg::DefaultIdPolicy; - template <typename T> BSONObj wrapInLiteral(const T& arg) { return BSON("$literal" << arg); } -// Helper to simplify the creation of a ProjectionExecAgg which includes _id and recurses arrays. -std::unique_ptr<ProjectionExecAgg> makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( +auto createProjectionExecutor(const BSONObj& spec, const ProjectionPolicies& policies) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto projection = projection_ast::parse(expCtx, spec, policies); + auto executor = + buildProjectionExecutor(expCtx, &projection, policies, true /* optimizeExecutor */); + return executor; +} + +// Helper to simplify the creation of a ProjectionExecutor which includes _id and recurses arrays. +std::unique_ptr<ProjectionExecutor> makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSONObj projSpec) { - return ProjectionExecAgg::create( - projSpec, DefaultIdPolicy::kIncludeId, ArrayRecursionPolicy::kRecurseNestedArrays); + ProjectionPolicies policies{ProjectionPolicies::DefaultIdPolicy::kIncludeId, + ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays, + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields, + ProjectionPolicies::FindOnlyFeaturesPolicy::kBanFindOnlyFeatures}; + return createProjectionExecutor(projSpec, policies); } -// Helper to simplify the creation of a ProjectionExecAgg which excludes _id and recurses arrays. -std::unique_ptr<ProjectionExecAgg> makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion( +// Helper to simplify the creation of a ProjectionExecutor which excludes _id and recurses arrays. +std::unique_ptr<ProjectionExecutor> makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion( BSONObj projSpec) { - return ProjectionExecAgg::create( - projSpec, DefaultIdPolicy::kExcludeId, ArrayRecursionPolicy::kRecurseNestedArrays); + ProjectionPolicies policies{ProjectionPolicies::DefaultIdPolicy::kExcludeId, + ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays, + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields, + ProjectionPolicies::FindOnlyFeaturesPolicy::kBanFindOnlyFeatures}; + return createProjectionExecutor(projSpec, policies); } std::set<FieldRef> toFieldRefs(const std::set<std::string>& stringPaths) { @@ -75,7 +88,7 @@ std::set<FieldRef> toFieldRefs(const std::set<std::string>& stringPaths) { // Error cases. // -TEST(ProjectionExecAggErrors, ShouldRejectMixOfInclusionAndComputedFields) { +TEST(ProjectionExecutorErrors, ShouldRejectMixOfInclusionAndComputedFields) { ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("a" << true << "b" << wrapInLiteral(1))), AssertionException); @@ -101,7 +114,7 @@ TEST(ProjectionExecAggErrors, ShouldRejectMixOfInclusionAndComputedFields) { AssertionException); } -TEST(ProjectionExecAggErrors, ShouldRejectMixOfExclusionAndComputedFields) { +TEST(ProjectionExecutorErrors, ShouldRejectMixOfExclusionAndComputedFields) { ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("a" << false << "b" << wrapInLiteral(1))), AssertionException); @@ -127,7 +140,7 @@ TEST(ProjectionExecAggErrors, ShouldRejectMixOfExclusionAndComputedFields) { AssertionException); } -TEST(ProjectionExecAggErrors, ShouldRejectOnlyComputedFields) { +TEST(ProjectionExecutorErrors, ShouldRejectOnlyComputedFields) { ASSERT_THROWS(makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(1))), AssertionException); @@ -143,180 +156,180 @@ TEST(ProjectionExecAggErrors, ShouldRejectOnlyComputedFields) { // Valid projections. -TEST(ProjectionExecAggType, ShouldAcceptInclusionProjection) { +TEST(ProjectionExecutorType, ShouldAcceptInclusionProjection) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("a" << true)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("_id" << false << "a" << true)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("_id" << false << "a.b.c" << true)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("_id.x" << true)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("_id" << BSON("x" << true))); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("x" << BSON("_id" << true))); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); } -TEST(ProjectionExecAggType, ShouldAcceptExclusionProjection) { +TEST(ProjectionExecutorType, ShouldAcceptExclusionProjection) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("a" << false)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("_id.x" << false)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("_id" << BSON("x" << false))); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( BSON("x" << BSON("_id" << false))); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(BSON("_id" << false)); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); } // Misc tests. -TEST(ProjectionExecAggTests, InclusionFieldPathsWithImplicitIdInclusion) { +TEST(ProjectionExecutorTests, InclusionFieldPathsWithImplicitIdInclusion) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( fromjson("{a: {b: {c: 1}}, d: 1}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); std::set<FieldRef> expectedPaths = toFieldRefs({"_id", "a.b.c", "d"}); // Verify that the exhaustive set of paths is as expected. ASSERT(exhaustivePaths == expectedPaths); } -TEST(ProjectionExecAggTests, InclusionFieldPathsWithExplicitIdInclusion) { +TEST(ProjectionExecutorTests, InclusionFieldPathsWithExplicitIdInclusion) { auto parsedProject = makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion( fromjson("{_id: 1, a: {b: {c: 1}}, d: 1}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); std::set<FieldRef> expectedPaths = toFieldRefs({"_id", "a.b.c", "d"}); // Verify that the exhaustive set of paths is as expected. ASSERT(exhaustivePaths == expectedPaths); } -TEST(ProjectionExecAggTests, InclusionFieldPathsWithExplicitIdInclusionIdOnly) { +TEST(ProjectionExecutorTests, InclusionFieldPathsWithExplicitIdInclusionIdOnly) { auto parsedProject = makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion(fromjson("{_id: 1}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); std::set<FieldRef> expectedPaths = toFieldRefs({"_id"}); // Verify that the exhaustive set of paths is as expected. ASSERT(exhaustivePaths == expectedPaths); } -TEST(ProjectionExecAggTests, InclusionFieldPathsWithImplicitIdExclusion) { +TEST(ProjectionExecutorTests, InclusionFieldPathsWithImplicitIdExclusion) { auto parsedProject = makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion( fromjson("{a: {b: {c: 1}}, d: 1}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); std::set<FieldRef> expectedPaths = toFieldRefs({"a.b.c", "d"}); // Verify that the exhaustive set of paths is as expected. ASSERT(exhaustivePaths == expectedPaths); } -TEST(ProjectionExecAggTests, InclusionFieldPathsWithExplicitIdExclusion) { +TEST(ProjectionExecutorTests, InclusionFieldPathsWithExplicitIdExclusion) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( fromjson("{_id: 0, a: {b: {c: 1}}, d: 1}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kInclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); std::set<FieldRef> expectedPaths = toFieldRefs({"a.b.c", "d"}); // Verify that the exhaustive set of paths is as expected. ASSERT(exhaustivePaths == expectedPaths); } -TEST(ProjectionExecAggTests, ExclusionFieldPathsWithImplicitIdInclusion) { +TEST(ProjectionExecutorTests, ExclusionFieldPathsWithImplicitIdInclusion) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( fromjson("{a: {b: {c: 0}}, d: 0}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); // Verify that the exhaustive set is empty, despite the implicit inclusion of _id. ASSERT(exhaustivePaths.empty()); } -TEST(ProjectionExecAggTests, ExclusionFieldPathsWithExplicitIdInclusion) { +TEST(ProjectionExecutorTests, ExclusionFieldPathsWithExplicitIdInclusion) { auto parsedProject = makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion( fromjson("{_id: 1, a: {b: {c: 0}}, d: 0}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); // Verify that the exhaustive set is empty, despite the explicit inclusion of _id. ASSERT(exhaustivePaths.empty()); } -TEST(ProjectionExecAggTests, ExclusionFieldPathsWithImplicitIdExclusion) { +TEST(ProjectionExecutorTests, ExclusionFieldPathsWithImplicitIdExclusion) { auto parsedProject = makeProjectionWithDefaultIdExclusionAndNestedArrayRecursion( fromjson("{a: {b: {c: 0}}, d: 0}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); // Verify that the exhaustive set is empty. ASSERT(exhaustivePaths.empty()); } -TEST(ProjectionExecAggTests, ExclusionFieldPathsWithExplicitIdExclusion) { +TEST(ProjectionExecutorTests, ExclusionFieldPathsWithExplicitIdExclusion) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion( fromjson("{_id: 1, a: {b: {c: 0}}, d: 0}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); // Verify that the exhaustive set is empty. ASSERT(exhaustivePaths.empty()); } -TEST(ProjectionExecAggTests, ExclusionFieldPathsWithExplicitIdExclusionIdOnly) { +TEST(ProjectionExecutorTests, ExclusionFieldPathsWithExplicitIdExclusionIdOnly) { auto parsedProject = makeProjectionWithDefaultIdInclusionAndNestedArrayRecursion(fromjson("{_id: 0}")); - ASSERT(parsedProject->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); // Extract the exhaustive set of paths that will be preserved by the projection. - auto exhaustivePaths = parsedProject->getExhaustivePaths(); + auto exhaustivePaths = projection_executor_utils::extractExhaustivePaths(parsedProject.get()); // Verify that the exhaustive set is empty. ASSERT(exhaustivePaths.empty()); } } // namespace -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_node.cpp b/src/mongo/db/exec/projection_node.cpp index 941a6f2deb8..b2fb949cea9 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection_node.cpp +++ b/src/mongo/db/exec/projection_node.cpp @@ -29,11 +29,9 @@ #include "mongo/platform/basic.h" -#include "mongo/db/pipeline/parsed_aggregation_projection_node.h" - -namespace mongo { -namespace parsed_aggregation_projection { +#include "mongo/db/exec/projection_node.h" +namespace mongo::projection_executor { using ArrayRecursionPolicy = ProjectionPolicies::ArrayRecursionPolicy; using ComputedFieldsPolicy = ProjectionPolicies::ComputedFieldsPolicy; using DefaultIdPolicy = ProjectionPolicies::DefaultIdPolicy; @@ -284,6 +282,4 @@ void ProjectionNode::serialize(boost::optional<ExplainOptions::Verbosity> explai } } } - -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_node.h b/src/mongo/db/exec/projection_node.h index 3e77720858a..2a3f739cb9b 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection_node.h +++ b/src/mongo/db/exec/projection_node.h @@ -29,13 +29,11 @@ #pragma once -#include "mongo/db/pipeline/parsed_aggregation_projection.h" +#include "mongo/db/exec/projection_executor.h" #include "mongo/db/query/projection_policies.h" -namespace mongo { -namespace parsed_aggregation_projection { - +namespace mongo::projection_executor { /** * 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 @@ -197,6 +195,4 @@ private: // example above, '_orderToProcessAdditionsAndChildren' would be ["a", "b", "d"]. std::vector<std::string> _orderToProcessAdditionsAndChildren; }; - -} // namespace parsed_aggregation_projection -} // namespace mongo +} // namespace mongo::projection_executor diff --git a/src/mongo/db/index/SConscript b/src/mongo/db/index/SConscript index 9dbc5affd53..c681e21fa3c 100644 --- a/src/mongo/db/index/SConscript +++ b/src/mongo/db/index/SConscript @@ -46,14 +46,15 @@ env.Library( LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/bson/dotted_path_support', + '$BUILD_DIR/mongo/db/exec/projection_executor', '$BUILD_DIR/mongo/db/exec/working_set', '$BUILD_DIR/mongo/db/fts/base_fts', '$BUILD_DIR/mongo/db/geo/geoparser', '$BUILD_DIR/mongo/db/index_names', '$BUILD_DIR/mongo/db/mongohasher', '$BUILD_DIR/mongo/db/pipeline/document_path_support', - '$BUILD_DIR/mongo/db/projection_exec_agg', '$BUILD_DIR/mongo/db/query/collation/collator_interface', + '$BUILD_DIR/mongo/db/query/projection_ast', '$BUILD_DIR/mongo/db/query/sort_pattern', '$BUILD_DIR/third_party/s2/s2', 'expression_params', diff --git a/src/mongo/db/index/wildcard_access_method.h b/src/mongo/db/index/wildcard_access_method.h index 69ca0ad80dd..09a3f5dc530 100644 --- a/src/mongo/db/index/wildcard_access_method.h +++ b/src/mongo/db/index/wildcard_access_method.h @@ -71,10 +71,10 @@ public: const MultikeyPaths& multikeyPaths) const final; /** - * Returns a pointer to the ProjectionExecAgg owned by the underlying WildcardKeyGenerator. + * Returns a pointer to the ProjectionExecutor owned by the underlying WildcardKeyGenerator. */ - const ProjectionExecAgg* getProjectionExec() const { - return _keyGen.getProjectionExec(); + projection_executor::ProjectionExecutor* getProjectionExecutor() const { + return _keyGen.getProjectionExecutor(); } /** diff --git a/src/mongo/db/index/wildcard_key_generator.cpp b/src/mongo/db/index/wildcard_key_generator.cpp index 63607683c60..116043c2fe4 100644 --- a/src/mongo/db/index/wildcard_key_generator.cpp +++ b/src/mongo/db/index/wildcard_key_generator.cpp @@ -31,8 +31,11 @@ #include "mongo/db/index/wildcard_key_generator.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/jsobj.h" #include "mongo/db/query/collation/collation_index_key.h" +#include "mongo/db/query/projection_parser.h" namespace mongo { namespace { @@ -62,8 +65,8 @@ void popPathComponent(BSONElement elem, bool enclosingObjIsArray, FieldRef* path constexpr StringData WildcardKeyGenerator::kSubtreeSuffix; -std::unique_ptr<ProjectionExecAgg> WildcardKeyGenerator::createProjectionExec( - BSONObj keyPattern, BSONObj pathProjection) { +std::unique_ptr<projection_executor::ProjectionExecutor> +WildcardKeyGenerator::createProjectionExecutor(BSONObj keyPattern, BSONObj pathProjection) { // We should never have a key pattern that contains more than a single element. invariant(keyPattern.nFields() == 1); @@ -82,13 +85,14 @@ std::unique_ptr<ProjectionExecAgg> WildcardKeyGenerator::createProjectionExec( ? BSON(indexRoot.substr(0, suffixPos) << 1) : pathProjection.isEmpty() ? kDefaultProjection : pathProjection); - // If the projection spec does not explicitly specify _id, we exclude it by default. We also - // prevent the projection from recursing through nested arrays, in order to ensure that the - // output document aligns with the match system's expectations. - return ProjectionExecAgg::create( - projSpec, - ProjectionExecAgg::DefaultIdPolicy::kExcludeId, - ProjectionExecAgg::ArrayRecursionPolicy::kDoNotRecurseNestedArrays); + // Construct a dummy ExpressionContext for ProjectionExecutor. It's OK to set the + // ExpressionContext's OperationContext and CollatorInterface to 'nullptr' here; since we + // ban computed fields from the projection, the ExpressionContext will never be used. + auto expCtx = make_intrusive<ExpressionContext>(nullptr, nullptr); + auto policies = ProjectionPolicies::wildcardIndexSpecProjectionPolicies(); + auto projection = projection_ast::parse(expCtx, projSpec, policies); + return projection_executor::buildProjectionExecutor( + expCtx, &projection, policies, true /* optimizeExecutor */); } WildcardKeyGenerator::WildcardKeyGenerator(BSONObj keyPattern, @@ -96,20 +100,23 @@ WildcardKeyGenerator::WildcardKeyGenerator(BSONObj keyPattern, const CollatorInterface* collator, KeyString::Version keyStringVersion, Ordering ordering) - : _collator(collator), + : _projExec(createProjectionExecutor(keyPattern, pathProjection)), + _collator(collator), _keyPattern(keyPattern), _keyStringVersion(keyStringVersion), - _ordering(ordering) { - _projExec = createProjectionExec(keyPattern, pathProjection); -} + _ordering(ordering) {} void WildcardKeyGenerator::generateKeys(BSONObj inputDoc, KeyStringSet* keys, KeyStringSet* multikeyPaths, boost::optional<RecordId> id) const { FieldRef rootPath; - _traverseWildcard( - _projExec->applyProjection(inputDoc), false, &rootPath, keys, multikeyPaths, id); + _traverseWildcard(_projExec->applyTransformation(Document{inputDoc}).toBson(), + false, + &rootPath, + keys, + multikeyPaths, + id); } void WildcardKeyGenerator::_traverseWildcard(BSONObj obj, diff --git a/src/mongo/db/index/wildcard_key_generator.h b/src/mongo/db/index/wildcard_key_generator.h index f5c8524c751..cf99187e35f 100644 --- a/src/mongo/db/index/wildcard_key_generator.h +++ b/src/mongo/db/index/wildcard_key_generator.h @@ -29,7 +29,7 @@ #pragma once -#include "mongo/db/exec/projection_exec_agg.h" +#include "mongo/db/exec/projection_executor.h" #include "mongo/db/field_ref.h" #include "mongo/db/query/collation/collator_interface.h" #include "mongo/db/storage/key_string.h" @@ -47,12 +47,12 @@ public: static constexpr StringData kSubtreeSuffix = ".$**"_sd; /** - * Returns an owned ProjectionExecAgg identical to the one that WildcardKeyGenerator will use + * Returns an owned ProjectionExecutor identical to the one that WildcardKeyGenerator will use * internally when generating the keys for the $** index, as defined by the 'keyPattern' and * 'pathProjection' arguments. */ - static std::unique_ptr<ProjectionExecAgg> createProjectionExec(BSONObj keyPattern, - BSONObj pathProjection); + static std::unique_ptr<projection_executor::ProjectionExecutor> createProjectionExecutor( + BSONObj keyPattern, BSONObj pathProjection); WildcardKeyGenerator(BSONObj keyPattern, BSONObj pathProjection, @@ -61,9 +61,9 @@ public: Ordering ordering); /** - * Returns a pointer to the key generator's underlying ProjectionExecAgg. + * Returns a pointer to the key generator's underlying ProjectionExecutor. */ - const ProjectionExecAgg* getProjectionExec() const { + projection_executor::ProjectionExecutor* getProjectionExecutor() const { return _projExec.get(); } @@ -107,7 +107,7 @@ private: KeyStringSet* keys, boost::optional<RecordId> id) const; - std::unique_ptr<ProjectionExecAgg> _projExec; + std::unique_ptr<projection_executor::ProjectionExecutor> _projExec; const CollatorInterface* _collator; const BSONObj _keyPattern; const KeyString::Version _keyStringVersion; diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index ee2fb4ac34e..13da4edb4aa 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -334,6 +334,7 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/db/curop', '$BUILD_DIR/mongo/db/curop_failpoint_helpers', '$BUILD_DIR/mongo/db/exec/document_value/document_value', + '$BUILD_DIR/mongo/db/exec/projection_executor', '$BUILD_DIR/mongo/db/exec/scoped_timer', '$BUILD_DIR/mongo/db/exec/sort_executor', '$BUILD_DIR/mongo/db/generic_cursor', @@ -342,7 +343,6 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/db/logical_session_id_helpers', '$BUILD_DIR/mongo/db/matcher/expressions', '$BUILD_DIR/mongo/db/pipeline/lite_parsed_document_source', - '$BUILD_DIR/mongo/db/projection_executor', '$BUILD_DIR/mongo/db/query/collation/collator_factory_interface', '$BUILD_DIR/mongo/db/query/collation/collator_interface', '$BUILD_DIR/mongo/db/query/sort_pattern', @@ -364,7 +364,6 @@ pipelineEnv.Library( 'expression_context', 'expression_javascript', 'granularity_rounder', - 'parsed_aggregation_projection', ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/commands/test_commands_enabled', @@ -373,19 +372,6 @@ pipelineEnv.Library( ) env.Library( - target='parsed_aggregation_projection', - source=[ - 'parsed_aggregation_projection_node.cpp', - 'parsed_add_fields.cpp', - ], - LIBDEPS=[ - 'expression', - 'field_path', - '$BUILD_DIR/mongo/db/matcher/expressions', - ] -) - -env.Library( target='runtime_constants_idl', source=[ env.Idlc('runtime_constants.idl')[0] @@ -477,11 +463,6 @@ env.CppUnitTest( 'granularity_rounder_preferred_numbers_test.cpp', 'lookup_set_cache_test.cpp', 'mongos_process_interface_test.cpp', - 'parsed_add_fields_test.cpp', - 'parsed_aggregation_projection_test.cpp', - 'parsed_exclusion_projection_test.cpp', - 'parsed_find_projection_test.cpp', - 'parsed_inclusion_projection_test.cpp', 'pipeline_metadata_tree_test.cpp', 'pipeline_test.cpp', 'process_interface_standalone_test.cpp', @@ -497,7 +478,6 @@ env.CppUnitTest( '$BUILD_DIR/mongo/db/auth/authmocks', '$BUILD_DIR/mongo/db/exec/document_value/document_value', '$BUILD_DIR/mongo/db/exec/document_value/document_value_test_util', - '$BUILD_DIR/mongo/db/projection_executor', '$BUILD_DIR/mongo/db/query/collation/collator_interface_mock', '$BUILD_DIR/mongo/db/query/query_test_service_context', '$BUILD_DIR/mongo/db/repl/oplog_entry', @@ -520,7 +500,6 @@ env.CppUnitTest( 'mongo_process_common', 'mongo_process_interface', 'mongos_process_interface', - 'parsed_aggregation_projection', 'pipeline', 'process_interface_shardsvr', 'process_interface_standalone', diff --git a/src/mongo/db/pipeline/document_source_add_fields.cpp b/src/mongo/db/pipeline/document_source_add_fields.cpp index a362f38495d..2e2b4a5a53d 100644 --- a/src/mongo/db/pipeline/document_source_add_fields.cpp +++ b/src/mongo/db/pipeline/document_source_add_fields.cpp @@ -34,13 +34,11 @@ #include <boost/optional.hpp> #include <boost/smart_ptr/intrusive_ptr.hpp> +#include "mongo/db/exec/add_fields_projection_executor.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/pipeline/parsed_add_fields.h" namespace mongo { - using boost::intrusive_ptr; -using parsed_aggregation_projection::ParsedAddFields; REGISTER_DOCUMENT_SOURCE(addFields, LiteParsedDocumentSourceDefault::parse, @@ -60,7 +58,8 @@ intrusive_ptr<DocumentSource> DocumentSourceAddFields::create( expCtx, [&]() { try { - return ParsedAddFields::create(expCtx, addFieldsSpec); + return projection_executor::AddFieldsProjectionExecutor::create(expCtx, + addFieldsSpec); } catch (DBException& ex) { ex.addContext("Invalid " + userSpecifiedName.toString()); throw; diff --git a/src/mongo/db/pipeline/document_source_add_fields_test.cpp b/src/mongo/db/pipeline/document_source_add_fields_test.cpp index e20820feb0d..b5a1dd85baa 100644 --- a/src/mongo/db/pipeline/document_source_add_fields_test.cpp +++ b/src/mongo/db/pipeline/document_source_add_fields_test.cpp @@ -47,10 +47,9 @@ namespace { using std::vector; // -// DocumentSourceAddFields delegates much of its responsibilities to the ParsedAddFields, which -// derives from ParsedAggregationProjection. -// Most of the functional tests are testing ParsedAddFields directly. These are meant as -// simpler integration tests. +// DocumentSourceAddFields delegates much of its responsibilities to the +// AddFieldsProjectionExecutor. Most of the functional tests are testing +// AddFieldsProjectionExecutor. directly. These are meant as simpler integration tests. // // This provides access to getExpCtx(), but we'll use a different name for this test suite. diff --git a/src/mongo/db/pipeline/document_source_project.cpp b/src/mongo/db/pipeline/document_source_project.cpp index a626eb2b41f..b19eba77aec 100644 --- a/src/mongo/db/pipeline/document_source_project.cpp +++ b/src/mongo/db/pipeline/document_source_project.cpp @@ -35,14 +35,13 @@ #include <boost/smart_ptr/intrusive_ptr.hpp> #include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" -#include "mongo/db/pipeline/parsed_aggregation_projection.h" #include "mongo/db/query/projection_parser.h" namespace mongo { using boost::intrusive_ptr; -using ParsedAggregationProjection = parsed_aggregation_projection::ParsedAggregationProjection; REGISTER_DOCUMENT_SOURCE(project, LiteParsedDocumentSourceDefault::parse, @@ -68,10 +67,10 @@ intrusive_ptr<DocumentSource> DocumentSourceProject::create( intrusive_ptr<DocumentSource> project(new DocumentSourceSingleDocumentTransformation( expCtx, [&]() { - // The ParsedAggregationProjection will internally perform a check to see if the - // provided specification is valid, and throw an exception if it was not. The exception - // is caught here so we can add the name that was actually specified by the user, be it - // $project or an alias. + // The ProjectionExecutor will internally perform a check to see if the provided + // specification is valid, and throw an exception if it was not. The exception is caught + // here so we can add the name that was actually specified by the user, be it $project + // or an alias. try { auto policies = ProjectionPolicies::aggregateProjectionPolicies(); auto projection = projection_ast::parse(expCtx, projectSpec, policies); diff --git a/src/mongo/db/pipeline/document_source_project_test.cpp b/src/mongo/db/pipeline/document_source_project_test.cpp index 9c04482340e..ff4fa9d6b5f 100644 --- a/src/mongo/db/pipeline/document_source_project_test.cpp +++ b/src/mongo/db/pipeline/document_source_project_test.cpp @@ -51,9 +51,9 @@ using boost::intrusive_ptr; using std::vector; // -// DocumentSourceProject delegates much of its responsibilities to the ParsedAggregationProjection. -// Most of the functional tests are testing ParsedAggregationProjection directly. These are meant as -// simpler integration tests. +// DocumentSourceProject delegates much of its responsibilities to the ProjectionExecutor. Most of +// the functional tests are testing ProjectionExecutor directly. These are meant as simpler +// integration tests. // // This provides access to getExpCtx(), but we'll use a different name for this test suite. diff --git a/src/mongo/db/pipeline/expression_find_internal.h b/src/mongo/db/pipeline/expression_find_internal.h index 794d5ebc912..fe9f4162c89 100644 --- a/src/mongo/db/pipeline/expression_find_internal.h +++ b/src/mongo/db/pipeline/expression_find_internal.h @@ -31,7 +31,7 @@ #include <fmt/format.h> -#include "mongo/db/exec/find_projection_executor.h" +#include "mongo/db/exec/projection_executor_utils.h" #include "mongo/db/matcher/copyable_match_expression.h" #include "mongo/db/pipeline/expression.h" @@ -66,7 +66,7 @@ public: "Positional operator post-image can only be an object, but got {}"_format( typeName(postImage.getType())), postImage.getType() == BSONType::Object); - return Value{projection_executor::applyPositionalProjection( + return Value{projection_executor_utils::applyFindPositionalProjection( preImage.getDocument(), postImage.getDocument(), *_matchExpr, _path)}; } @@ -137,7 +137,7 @@ public: "$slice operator can only be applied to an object, but got {}"_format( typeName(postImage.getType())), postImage.getType() == BSONType::Object); - return Value{projection_executor::applySliceProjection( + return Value{projection_executor_utils::applyFindSliceProjection( postImage.getDocument(), _path, _skip, _limit)}; } @@ -197,7 +197,7 @@ public: "$elemMatch operator can only be applied to an object, but got {}"_format( typeName(input.getType())), input.getType() == BSONType::Object); - return projection_executor::applyElemMatchProjection( + return projection_executor_utils::applyFindElemMatchProjection( input.getDocument(), *_matchExpr, _path); } diff --git a/src/mongo/db/pipeline/expression_find_internal_test.cpp b/src/mongo/db/pipeline/expression_find_internal_test.cpp index 0fe1e34e9c5..62b79968731 100644 --- a/src/mongo/db/pipeline/expression_find_internal_test.cpp +++ b/src/mongo/db/pipeline/expression_find_internal_test.cpp @@ -30,14 +30,14 @@ #include "mongo/platform/basic.h" #include "mongo/db/exec/document_value/document_value_test_util.h" +#include "mongo/db/exec/projection_executor.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/expression_find_internal.h" -#include "mongo/db/pipeline/parsed_aggregation_projection.h" #include "mongo/unittest/unittest.h" namespace mongo::expression_internal_tests { constexpr auto kProjectionPostImageVarName = - parsed_aggregation_projection::ParsedAggregationProjection::kProjectionPostImageVarName; + projection_executor::ProjectionExecutor::kProjectionPostImageVarName; auto defineAndSetProjectionPostImageVariable(boost::intrusive_ptr<ExpressionContext> expCtx, Value postImage) { diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.h b/src/mongo/db/pipeline/parsed_aggregation_projection.h deleted file mode 100644 index 5c7e2a24b44..00000000000 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.h +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#pragma once - -#include "mongo/platform/basic.h" - -#include <boost/intrusive_ptr.hpp> -#include <memory> - -#include "mongo/bson/bsonelement.h" -#include "mongo/db/pipeline/expression_context.h" -#include "mongo/db/pipeline/field_path.h" -#include "mongo/db/pipeline/transformer_interface.h" -#include "mongo/db/query/projection_policies.h" - -namespace mongo { - -class BSONObj; -class Document; -class ExpressionContext; - -namespace parsed_aggregation_projection { -/** - * A ParsedAggregationProjection is responsible for parsing and executing a $project. It - * represents either an inclusion or exclusion projection. This is the common interface between the - * two types of projections. - */ -class ParsedAggregationProjection : public TransformerInterface { -public: - /** - * The name of an internal variable to bind a projection post image to, which is used by the - * '_rootReplacementExpression' to replace the content of the transformed document. - */ - static constexpr StringData kProjectionPostImageVarName{"INTERNAL_PROJ_POST_IMAGE"_sd}; - - /** - * Optimize any expressions contained within this projection. - */ - virtual void optimize() { - if (_rootReplacementExpression) { - _rootReplacementExpression->optimize(); - } - } - - /** - * Add any dependencies needed by this projection or any sub-expressions to 'deps'. - */ - virtual DepsTracker::State addDependencies(DepsTracker* deps) const { - return DepsTracker::State::NOT_SUPPORTED; - } - - /** - * Apply the projection transformation. - */ - Document applyTransformation(const Document& input) { - auto output = applyProjection(input); - if (_rootReplacementExpression) { - return _applyRootReplacementExpression(input, output); - } - return output; - } - - /** - * Sets 'expr' as a root-replacement expression to this tree. A root-replacement expression, - * once evaluated, will replace an entire output document. A projection post image document - * will be accessible via the special variable, whose name is stored in - * 'kProjectionPostImageVarName', if this expression needs access to it. - */ - void setRootReplacementExpression(boost::intrusive_ptr<Expression> expr) { - _rootReplacementExpression = expr; - } - -protected: - ParsedAggregationProjection(const boost::intrusive_ptr<ExpressionContext>& expCtx, - ProjectionPolicies policies) - : _expCtx(expCtx), - _policies(policies), - _projectionPostImageVarId{ - _expCtx->variablesParseState.defineVariable(kProjectionPostImageVarName)} {} - - /** - * Apply the projection to 'input'. - */ - virtual Document applyProjection(const Document& input) const = 0; - - boost::intrusive_ptr<ExpressionContext> _expCtx; - - ProjectionPolicies _policies; - - boost::intrusive_ptr<Expression> _rootReplacementExpression; - -private: - Document _applyRootReplacementExpression(const Document& input, const Document& output) { - using namespace fmt::literals; - - _expCtx->variables.setValue(_projectionPostImageVarId, Value{output}); - auto val = _rootReplacementExpression->evaluate(input, &_expCtx->variables); - uassert(51254, - "Root-replacement expression must return a document, but got {}"_format( - typeName(val.getType())), - val.getType() == BSONType::Object); - return val.getDocument(); - } - - // This variable id is used to bind a projection post-image so that it can be accessed by - // root-replacement expressions which apply projection to the entire post-image document, rather - // than to a specific field. - Variables::Id _projectionPostImageVarId; -}; -} // 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 deleted file mode 100644 index 337d9f74307..00000000000 --- a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp +++ /dev/null @@ -1,612 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#include "mongo/platform/basic.h" - -#include "mongo/db/pipeline/parsed_aggregation_projection.h" - -#include <vector> - -#include "mongo/bson/bsonmisc.h" -#include "mongo/bson/bsonobjbuilder.h" -#include "mongo/bson/json.h" -#include "mongo/db/exec/document_value/document.h" -#include "mongo/db/exec/document_value/value.h" -#include "mongo/db/exec/projection_executor.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/db/pipeline/parsed_inclusion_projection.h" -#include "mongo/db/query/projection_parser.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace parsed_aggregation_projection { -namespace { - -template <typename T> -BSONObj wrapInLiteral(const T& arg) { - return BSON("$literal" << arg); -} - -// Helper to simplify the creation of a ParsedAggregationProjection with default policies. -auto makeProjectionWithDefaultPolicies(BSONObj spec) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ProjectionPolicies defaultPolicies; - auto projection = projection_ast::parse(expCtx, spec, defaultPolicies); - return projection_executor::buildProjectionExecutor( - expCtx, &projection, defaultPolicies, true /* optimizeExecutor */); -} - -// Helper to simplify the creation of a ParsedAggregationProjection which bans computed fields. -auto makeProjectionWithBannedComputedFields(BSONObj spec) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ProjectionPolicies banComputedFields{ - ProjectionPolicies::kDefaultIdPolicyDefault, - ProjectionPolicies::kArrayRecursionPolicyDefault, - ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields}; - auto projection = projection_ast::parse(expCtx, spec, banComputedFields); - return projection_executor::buildProjectionExecutor( - expCtx, &projection, banComputedFields, true /* optimizeExecutor */); -} - -// -// Error cases. -// - -TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateFieldNames) { - // Include/exclude the same field twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << false)), - AssertionException); - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "b" << false))), - AssertionException); - - // Mix of include/exclude and adding a field. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << wrapInLiteral(0))), - AssertionException); - - // Adding the same field twice. - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << wrapInLiteral(0))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectDuplicateIds) { - // Include/exclude _id twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << false)), - AssertionException); - - // Mix of including/excluding and adding _id. - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id" << true)), - AssertionException); - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << wrapInLiteral(0))), - AssertionException); - - // Adding _id twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("_id" << wrapInLiteral(1) << "_id" << wrapInLiteral(0))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldsWithSharedPrefix) { - // Include/exclude Fields with a shared prefix. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a.b" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << false)), - AssertionException); - - // Mix of include/exclude and adding a shared prefix. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a.b" << true)), - AssertionException); - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << wrapInLiteral(0))), - AssertionException); - - // Adding a shared prefix twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << wrapInLiteral(1) << "a.b" << wrapInLiteral(0))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b.c.d" << wrapInLiteral(1) << "a.b.c" << wrapInLiteral(0))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectPathConflictsWithNonAlphaNumericCharacters) { - // 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( - BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true))); - ASSERT(makeProjectionWithDefaultPolicies( - BSON("a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c" << false))); - - // Then assert that we throw when we introduce a prefixed field. - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true - << "a.b c" << true << "a.b.d" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b.d" << false << "a.b c" << false - << "a.b?c" << false << "a.b" - << false << "a.b-c" << false)), - AssertionException); - - // Adding the same field twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b?c" << wrapInLiteral(1) << "a.b?c" << wrapInLiteral(0))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b c" << wrapInLiteral(0) << "a.b c" << wrapInLiteral(1))), - AssertionException); - - // Mix of include/exclude and adding a shared prefix. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b-c" << true << "a.b" << wrapInLiteral(1) << "a.b?c" << true - << "a.b c" << true << "a.b.d" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" - << wrapInLiteral(0) << "a.b-c" << false)), - AssertionException); - - // Adding a shared prefix twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b-c" << wrapInLiteral(1) << "a.b" << wrapInLiteral(1) << "a.b?c" - << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b.d" - << wrapInLiteral(0))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a.b.d" << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b?c" - << wrapInLiteral(1) << "a.b" << wrapInLiteral(0) << "a.b-c" - << wrapInLiteral(1))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfIdAndSubFieldsOfId) { - // Include/exclude _id twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id.x" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << false)), - AssertionException); - - // Mix of including/excluding and adding _id. - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id.x" << true)), - AssertionException); - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << wrapInLiteral(0))), - AssertionException); - - // Adding _id twice. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("_id" << wrapInLiteral(1) << "_id.x" << wrapInLiteral(0))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("_id.b.c.d" << wrapInLiteral(1) << "_id.b.c" << wrapInLiteral(0))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldAllowMixOfIdInclusionAndExclusion) { - // Mixing "_id" inclusion with exclusion. - auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false << "_id" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a.b.c" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfInclusionAndExclusion) { - // Simple mix. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << false)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << true)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "c" << true))), - AssertionException); - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("_id" << BSON("b" << false << "c" << true))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.b" << false << "a.c" << true)), - AssertionException); - - // Mix while also adding a field. - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << true << "b" << wrapInLiteral(1) << "c" << false)), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << false << "b" << wrapInLiteral(1) << "c" << true)), - AssertionException); - - // Mix of "_id" subfield inclusion and exclusion. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << true << "a.b.c" << false)), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfExclusionAndComputedFields) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << wrapInLiteral(1))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "b" << false)), - AssertionException); - - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a.c" << wrapInLiteral(1))), - AssertionException); - - ASSERT_THROWS( - makeProjectionWithDefaultPolicies(BSON("a.b" << wrapInLiteral(1) << "a.c" << false)), - AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldNamesStartingWithADollar) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 0)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 1)), AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 0)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 1)), AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 0))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 1))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 0)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 1)), AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectTopLevelExpressions) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << BSON_ARRAY(4 << 2))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectExpressionWithMultipleFieldNames) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << BSON("b" << 1 << "$add" << BSON_ARRAY(4 << 2)))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << BSON("b" << BSON("c" << 1 << "$add" << BSON_ARRAY(4 << 2))))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << BSON("b" << BSON("$add" << BSON_ARRAY(4 << 2) << "c" << 1)))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyProjection) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSONObj()), AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldRejectEmptyNestedObject) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSONObj())), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << BSONObj())), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << BSONObj())), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << BSONObj())), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << BSONObj()))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidExpression) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies( - BSON("a" << true << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), - AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldErrorOnInvalidFieldPath) { - // Empty field names. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << wrapInLiteral(2))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << true)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << false)), AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << true))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << false))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << true))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << false))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << true)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << false)), AssertionException); - - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << true)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << false)), AssertionException); - - // Not testing field names with null bytes, since that is invalid BSON, and won't make it to the - // $project stage without a previous error. - - // Field names starting with '$'. - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$x" << wrapInLiteral(2))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << true)), AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << false)), AssertionException); -} - -TEST(ParsedAggregationProjectionErrors, ShouldNotErrorOnTwoNestedFields) { - makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << true)); - makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a" << BSON("c" << true))); -} - -// -// Determining exclusion vs. inclusion. -// - -TEST(ParsedAggregationProjectionType, ShouldAllowDottedFieldInSubDocument) { - auto proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << true))); - ASSERT(proj->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << wrapInLiteral(1)))); - ASSERT(proj->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << false))); - ASSERT(proj->getType() == TransformerInterface::TransformerType::kExclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldDefaultToInclusionProjection) { - auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldDetectExclusionProjection) { - auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << false))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << false))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldDetectInclusionProjection) { - auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << true))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << true))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldTreatOnlyComputedFieldsAsAnInclusionProjection) { - auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = - makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = - makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << wrapInLiteral(1)))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << wrapInLiteral(1)))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldAllowMixOfInclusionAndComputedFields) { - auto parsedProject = - makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = - makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << wrapInLiteral(1))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies( - BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << true << "c" - << "stringLiteral"))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldRejectMixOfInclusionAndBannedComputedFields) { - ASSERT_THROWS( - makeProjectionWithBannedComputedFields(BSON("a" << true << "b" << wrapInLiteral(1))), - AssertionException); - - ASSERT_THROWS( - makeProjectionWithBannedComputedFields(BSON("a.b" << true << "a.c" << wrapInLiteral(1))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithBannedComputedFields( - BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithBannedComputedFields(BSON("a" << BSON("b" << true << "c" - << "stringLiteral"))), - AssertionException); -} - -TEST(ParsedAggregationProjectionType, ShouldRejectOnlyComputedFieldsWhenComputedFieldsAreBanned) { - ASSERT_THROWS(makeProjectionWithBannedComputedFields( - BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(2))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithBannedComputedFields( - BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(2))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithBannedComputedFields( - BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), - AssertionException); - - ASSERT_THROWS(makeProjectionWithBannedComputedFields( - BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), - AssertionException); -} - -TEST(ParsedAggregationProjectionType, ShouldAcceptInclusionProjectionWhenComputedFieldsAreBanned) { - auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a.b.c" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << true)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << true))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << true))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldAcceptExclusionProjectionWhenComputedFieldsAreBanned) { - auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << false))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << false))); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); - - parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false)); - ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); -} - -TEST(ParsedAggregationProjectionType, ShouldCoerceNumericsToBools) { - 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()); - ASSERT(parsedProject->getType() == - TransformerInterface::TransformerType::kExclusionProjection); - } - - std::vector<Value> nonZeroes = { - Value(1), Value(-1), Value(3), Value(1LL), Value(1.0), Value(Decimal128(1))}; - for (auto&& nonZero : nonZeroes) { - auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", nonZero}}.toBson()); - ASSERT(parsedProject->getType() == - TransformerInterface::TransformerType::kInclusionProjection); - } -} - -TEST(ParsedAggregationProjectionType, GetExpressionForPathGetsTopLevelExpression) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto projectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); - auto expr = Expression::parseObject(expCtx, projectObj, expCtx->variablesParseState); - ProjectionPolicies defaultPolicies; - auto node = InclusionNode(defaultPolicies); - node.addExpressionForPath(FieldPath("key"), expr); - BSONObjBuilder bob; - ASSERT_EQ(expr, node.getExpressionForPath(FieldPath("key"))); -} - -TEST(ParsedAggregationProjectionType, GetExpressionForPathGetsCorrectTopLevelExpression) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto correctObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); - auto incorrectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 2) << BSON("$const" << 4))); - auto correctExpr = Expression::parseObject(expCtx, correctObj, expCtx->variablesParseState); - auto incorrectExpr = Expression::parseObject(expCtx, incorrectObj, expCtx->variablesParseState); - ProjectionPolicies defaultPolicies; - auto node = InclusionNode(defaultPolicies); - node.addExpressionForPath(FieldPath("key"), correctExpr); - node.addExpressionForPath(FieldPath("other"), incorrectExpr); - BSONObjBuilder bob; - ASSERT_EQ(correctExpr, node.getExpressionForPath(FieldPath("key"))); -} - -TEST(ParsedAggregationProjectionType, GetExpressionForPathGetsNonTopLevelExpression) { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - auto projectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); - auto expr = Expression::parseObject(expCtx, projectObj, expCtx->variablesParseState); - ProjectionPolicies defaultPolicies; - auto node = InclusionNode(defaultPolicies); - node.addExpressionForPath(FieldPath("key.second"), expr); - BSONObjBuilder bob; - ASSERT_EQ(expr, node.getExpressionForPath(FieldPath("key.second"))); -} - -} // namespace -} // namespace parsed_aggregation_projection -} // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_find_projection_test.cpp b/src/mongo/db/pipeline/parsed_find_projection_test.cpp deleted file mode 100644 index 27e2ef8c3f8..00000000000 --- a/src/mongo/db/pipeline/parsed_find_projection_test.cpp +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Copyright (C) 2019-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#include "mongo/platform/basic.h" - -#include "mongo/db/exec/document_value/document_value_test_util.h" -#include "mongo/db/exec/projection_executor.h" -#include "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/pipeline/expression_find_internal.h" -#include "mongo/db/pipeline/parsed_aggregation_projection.h" -#include "mongo/db/query/projection_parser.h" -#include "mongo/unittest/unittest.h" - -namespace mongo::parsed_aggregation_projection { -constexpr auto kProjectionPostImageVarName = - parsed_aggregation_projection::ParsedAggregationProjection::kProjectionPostImageVarName; - -auto createProjectionExecutor(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const BSONObj& projSpec, - ProjectionPolicies policies) { - auto projection = projection_ast::parse(expCtx, projSpec, policies); - return projection_executor::buildProjectionExecutor( - expCtx, &projection, policies, true /* optimizeExecutor */); -} - -class PositionalProjectionExecutionTest : public AggregationContextFixture { -protected: - auto applyPositional(const BSONObj& projSpec, - const BSONObj& matchSpec, - const std::string& path, - const Document& input) { - auto executor = createProjectionExecutor(getExpCtx(), projSpec, {}); - auto matchExpr = CopyableMatchExpression{matchSpec, - getExpCtx(), - std::make_unique<ExtensionsCallbackNoop>(), - MatchExpressionParser::kBanAllSpecialFeatures}; - auto expr = make_intrusive<ExpressionInternalFindPositional>( - getExpCtx(), - ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - path, - std::move(matchExpr)); - executor->setRootReplacementExpression(expr); - return executor->applyTransformation(input); - } -}; - -class SliceProjectionExecutionTest : public AggregationContextFixture { -protected: - auto applySlice(const BSONObj& projSpec, - const std::string& path, - boost::optional<int> skip, - int limit, - const Document& input) { - auto executor = createProjectionExecutor(getExpCtx(), projSpec, {}); - auto expr = make_intrusive<ExpressionInternalFindSlice>( - getExpCtx(), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - path, - skip, - limit); - executor->setRootReplacementExpression(expr); - return executor->applyTransformation(input); - } -}; - -TEST_F(PositionalProjectionExecutionTest, CanApplyPositionalWithInclusionProjection) { - ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: [6]}")}, - applyPositional(fromjson("{foo: 1}"), - fromjson("{foo: {$gte: 5}}"), - "foo", - Document{fromjson("{foo: [1,2,6,10]}")})); - - ASSERT_DOCUMENT_EQ(Document{fromjson("{bar:1, foo: [6]}")}, - applyPositional(fromjson("{bar: 1, foo: 1}"), - fromjson("{bar: 1, foo: {$gte: 5}}"), - "foo", - Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); -} - -TEST_F(PositionalProjectionExecutionTest, AppliesProjectionToPreImage) { - ASSERT_DOCUMENT_EQ(Document{fromjson("{b: [6], c: 'abc'}")}, - applyPositional(fromjson("{b: 1, c: 1}"), - fromjson("{a: 1, b: {$gte: 5}}"), - "b", - Document{fromjson("{a: 1, b: [1,2,6,10], c: 'abc'}")})); -} - -TEST_F(PositionalProjectionExecutionTest, ShouldAddInclusionFieldsAndWholeDocumentToDependencies) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); - auto matchSpec = fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"); - auto matchExpr = CopyableMatchExpression{matchSpec, - getExpCtx(), - std::make_unique<ExtensionsCallbackNoop>(), - MatchExpressionParser::kBanAllSpecialFeatures}; - auto expr = make_intrusive<ExpressionInternalFindPositional>( - getExpCtx(), - ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo.bar", - std::move(matchExpr)); - executor->setRootReplacementExpression(expr); - - DepsTracker deps; - executor->addDependencies(&deps); - - ASSERT_EQ(deps.fields.size(), 2UL); - ASSERT_EQ(deps.fields.count("bar"), 1UL); - ASSERT_EQ(deps.fields.count("foo.bar"), 1UL); - ASSERT(deps.needWholeDocument); -} - -TEST_F(PositionalProjectionExecutionTest, ShouldConsiderAllPathsAsModified) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); - auto matchSpec = fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"); - auto matchExpr = CopyableMatchExpression{matchSpec, - getExpCtx(), - std::make_unique<ExtensionsCallbackNoop>(), - MatchExpressionParser::kBanAllSpecialFeatures}; - auto expr = make_intrusive<ExpressionInternalFindPositional>( - getExpCtx(), - ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo.bar", - std::move(matchExpr)); - executor->setRootReplacementExpression(expr); - - auto modifiedPaths = executor->getModifiedPaths(); - ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); -} - -TEST_F(SliceProjectionExecutionTest, CanApplySliceWithInclusionProjection) { - ASSERT_DOCUMENT_EQ( - Document{fromjson("{foo: [1,2]}")}, - applySlice( - fromjson("{foo: 1}"), "foo", boost::none, 2, Document{fromjson("{foo: [1,2,6,10]}")})); - - ASSERT_DOCUMENT_EQ(Document{fromjson("{bar:1, foo: [6]}")}, - applySlice(fromjson("{bar: 1, foo: 1}"), - "foo", - 2, - 1, - Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); -} - -TEST_F(SliceProjectionExecutionTest, AppliesProjectionToPostImage) { - ASSERT_DOCUMENT_EQ(Document{fromjson("{b: [1,2], c: 'abc'}")}, - applySlice(fromjson("{b: 1, c: 1}"), - "b", - boost::none, - 2, - Document{fromjson("{a: 1, b: [1,2,6,10], c: 'abc'}")})); -} - -TEST_F(SliceProjectionExecutionTest, CanApplySliceAndPositionalProjectionsTogether) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{foo: 1, bar: 1}"), {}); - auto matchSpec = fromjson("{foo: {$gte: 3}}"); - auto matchExpr = CopyableMatchExpression{matchSpec, - getExpCtx(), - std::make_unique<ExtensionsCallbackNoop>(), - MatchExpressionParser::kBanAllSpecialFeatures}; - auto positionalExpr = make_intrusive<ExpressionInternalFindPositional>( - getExpCtx(), - ExpressionFieldPath::parse(getExpCtx(), "$$ROOT", getExpCtx()->variablesParseState), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo", - std::move(matchExpr)); - auto sliceExpr = - make_intrusive<ExpressionInternalFindSlice>(getExpCtx(), positionalExpr, "bar", 1, 1); - executor->setRootReplacementExpression(sliceExpr); - - ASSERT_DOCUMENT_EQ( - Document{fromjson("{foo: [3], bar: [6]}")}, - executor->applyTransformation(Document{fromjson("{foo: [1,2,3,4], bar: [5,6,7,8]}")})); -} - -TEST_F(SliceProjectionExecutionTest, CanApplySliceWithExclusionProjection) { - ASSERT_DOCUMENT_EQ( - Document{fromjson("{foo: [6]}")}, - applySlice( - fromjson("{bar: 0}"), "foo", 2, 1, Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); -} - -TEST_F(SliceProjectionExecutionTest, - ShouldAddFieldsAndWholeDocumentToDependenciesWithInclusionProjection) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); - auto expr = make_intrusive<ExpressionInternalFindSlice>( - getExpCtx(), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo.bar", - 1, - 1); - executor->setRootReplacementExpression(expr); - - DepsTracker deps; - executor->addDependencies(&deps); - - ASSERT_EQ(deps.fields.size(), 1UL); - ASSERT_EQ(deps.fields.count("bar"), 1UL); - ASSERT(deps.needWholeDocument); -} - -TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithInclusionProjection) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1}"), {}); - auto expr = make_intrusive<ExpressionInternalFindSlice>( - getExpCtx(), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo.bar", - 1, - 1); - executor->setRootReplacementExpression(expr); - - auto modifiedPaths = executor->getModifiedPaths(); - ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); -} - -TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithExclusionProjection) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 0}"), {}); - auto expr = make_intrusive<ExpressionInternalFindSlice>( - getExpCtx(), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo.bar", - 1, - 1); - executor->setRootReplacementExpression(expr); - - auto modifiedPaths = executor->getModifiedPaths(); - ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); -} - -TEST_F(SliceProjectionExecutionTest, ShouldAddWholeDocumentToDependenciesWithExclusionProjection) { - auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 0}"), {}); - auto expr = make_intrusive<ExpressionInternalFindSlice>( - getExpCtx(), - ExpressionFieldPath::parse( - getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), - "foo.bar", - 1, - 1); - executor->setRootReplacementExpression(expr); - - DepsTracker deps; - executor->addDependencies(&deps); - - ASSERT_EQ(deps.fields.size(), 0UL); - ASSERT(deps.needWholeDocument); -} -} // namespace mongo::parsed_aggregation_projection diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp index a20a8382e4a..4f645f91dfa 100644 --- a/src/mongo/db/pipeline/pipeline_d.cpp +++ b/src/mongo/db/pipeline/pipeline_d.cpp @@ -67,7 +67,6 @@ #include "mongo/db/pipeline/document_source_sample_from_random_cursor.h" #include "mongo/db/pipeline/document_source_single_document_transformation.h" #include "mongo/db/pipeline/document_source_sort.h" -#include "mongo/db/pipeline/parsed_inclusion_projection.h" #include "mongo/db/pipeline/pipeline.h" #include "mongo/db/query/collation/collator_interface.h" #include "mongo/db/query/get_executor.h" diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript index b39bf5d553d..876ab2b8c77 100644 --- a/src/mongo/db/query/SConscript +++ b/src/mongo/db/query/SConscript @@ -42,6 +42,7 @@ env.Library( "$BUILD_DIR/mongo/base", "$BUILD_DIR/mongo/db/bson/dotted_path_support", '$BUILD_DIR/mongo/db/commands/server_status_core', + "$BUILD_DIR/mongo/db/exec/projection_executor", "$BUILD_DIR/mongo/db/index/expression_params", "$BUILD_DIR/mongo/db/index/key_generator", "$BUILD_DIR/mongo/db/index_names", diff --git a/src/mongo/db/query/collection_query_info.cpp b/src/mongo/db/query/collection_query_info.cpp index b0e5685d7ad..ccf86e6a759 100644 --- a/src/mongo/db/query/collection_query_info.cpp +++ b/src/mongo/db/query/collection_query_info.cpp @@ -39,6 +39,8 @@ #include "mongo/db/catalog/index_catalog.h" #include "mongo/db/concurrency/d_concurrency.h" #include "mongo/db/curop_metrics.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_utils.h" #include "mongo/db/fts/fts_spec.h" #include "mongo/db/index/index_descriptor.h" #include "mongo/db/index/wildcard_access_method.h" @@ -60,9 +62,9 @@ CoreIndexInfo indexInfoFromIndexCatalogEntry(const IndexCatalogEntry& ice) { auto accessMethod = ice.accessMethod(); invariant(accessMethod); - const ProjectionExecAgg* projExec = nullptr; + projection_executor::ProjectionExecutor* projExec = nullptr; if (desc->getIndexType() == IndexType::INDEX_WILDCARD) - projExec = static_cast<const WildcardAccessMethod*>(accessMethod)->getProjectionExec(); + projExec = static_cast<const WildcardAccessMethod*>(accessMethod)->getProjectionExecutor(); return {desc->keyPattern(), desc->getIndexType(), @@ -102,15 +104,17 @@ void CollectionQueryInfo::computeIndexKeys(OperationContext* opCtx) { if (descriptor->getAccessMethodName() == IndexNames::WILDCARD) { // Obtain the projection used by the $** index's key generator. const auto* pathProj = - static_cast<const WildcardAccessMethod*>(iam)->getProjectionExec(); + static_cast<const WildcardAccessMethod*>(iam)->getProjectionExecutor(); // If the projection is an exclusion, then we must check the new document's keys on all // updates, since we do not exhaustively know the set of paths to be indexed. - if (pathProj->getType() == ProjectionExecAgg::ProjectionType::kExclusionProjection) { + if (pathProj->getType() == + TransformerInterface::TransformerType::kExclusionProjection) { _indexedPaths.allPathsIndexed(); } else { // If a subtree was specified in the keyPattern, or if an inclusion projection is // present, then we need only index the path(s) preserved by the projection. - for (const auto& path : pathProj->getExhaustivePaths()) { + for (const auto& path : + projection_executor_utils::extractExhaustivePaths(pathProj)) { _indexedPaths.addPath(path); } } diff --git a/src/mongo/db/query/get_executor.cpp b/src/mongo/db/query/get_executor.cpp index e7f0513fbbc..df61af88f4e 100644 --- a/src/mongo/db/query/get_executor.cpp +++ b/src/mongo/db/query/get_executor.cpp @@ -48,6 +48,7 @@ #include "mongo/db/exec/idhack.h" #include "mongo/db/exec/multi_plan.h" #include "mongo/db/exec/projection.h" +#include "mongo/db/exec/projection_executor_utils.h" #include "mongo/db/exec/record_store_fast_count.h" #include "mongo/db/exec/return_key.h" #include "mongo/db/exec/shard_filter.h" @@ -165,17 +166,18 @@ IndexEntry indexEntryFromIndexCatalogEntry(OperationContext* opCtx, const bool isMultikey = desc->isMultikey(); - const ProjectionExecAgg* projExec = nullptr; + projection_executor::ProjectionExecutor* projExec = nullptr; std::set<FieldRef> multikeyPathSet; if (desc->getIndexType() == IndexType::INDEX_WILDCARD) { - projExec = static_cast<const WildcardAccessMethod*>(accessMethod)->getProjectionExec(); + projExec = static_cast<const WildcardAccessMethod*>(accessMethod)->getProjectionExecutor(); if (isMultikey) { MultikeyMetadataAccessStats mkAccessStats; if (canonicalQuery) { stdx::unordered_set<std::string> fields; QueryPlannerIXSelect::getFields(canonicalQuery->root(), &fields); - const auto projectedFields = projExec->applyProjectionToFields(fields); + const auto projectedFields = + projection_executor_utils::applyProjectionToFields(projExec, fields); multikeyPathSet = accessMethod->getMultikeyPathSet(opCtx, projectedFields, &mkAccessStats); @@ -1389,9 +1391,10 @@ QueryPlannerParams fillOutPlannerParamsForDistinct(OperationContext* opCtx, indexEntryFromIndexCatalogEntry(opCtx, *ice, parsedDistinct.getQuery())); } else if (desc->getIndexType() == IndexType::INDEX_WILDCARD && !query.isEmpty()) { // Check whether the $** projection captures the field over which we are distinct-ing. - const auto* proj = - static_cast<const WildcardAccessMethod*>(ice->accessMethod())->getProjectionExec(); - if (proj->applyProjectionToOneField(parsedDistinct.getKey())) { + auto* proj = static_cast<const WildcardAccessMethod*>(ice->accessMethod()) + ->getProjectionExecutor(); + if (projection_executor_utils::applyProjectionToOneField(proj, + parsedDistinct.getKey())) { plannerParams.indices.push_back( indexEntryFromIndexCatalogEntry(opCtx, *ice, parsedDistinct.getQuery())); } diff --git a/src/mongo/db/query/get_executor_test.cpp b/src/mongo/db/query/get_executor_test.cpp index d54080debef..9b36fcab408 100644 --- a/src/mongo/db/query/get_executor_test.cpp +++ b/src/mongo/db/query/get_executor_test.cpp @@ -37,7 +37,12 @@ #include <string> #include "mongo/bson/simple_bsonobj_comparator.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/json.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/db/query/projection_parser.h" +#include "mongo/db/query/projection_policies.h" #include "mongo/db/query/query_settings.h" #include "mongo/db/query/query_test_service_context.h" #include "mongo/stdx/unordered_set.h" @@ -47,6 +52,13 @@ using namespace mongo; namespace { +auto createProjectionExecutor(const BSONObj& spec, const ProjectionPolicies& policies) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto projection = projection_ast::parse(expCtx, spec, policies); + auto executor = projection_executor::buildProjectionExecutor( + expCtx, &projection, policies, true /* optimizeExecutor */); + return executor; +} using std::unique_ptr; @@ -138,7 +150,7 @@ IndexEntry buildSimpleIndexEntry(const BSONObj& kp, const std::string& indexName * is neccesary for wildcard indicies. */ IndexEntry buildWildcardIndexEntry(const BSONObj& kp, - const ProjectionExecAgg* projExec, + projection_executor::ProjectionExecutor* projExec, const std::string& indexName) { return {kp, IndexNames::nameToType(IndexNames::findPluginName(kp)), @@ -207,10 +219,10 @@ TEST(GetExecutorTest, GetAllowedIndicesMatchesMultipleIndexesByKey) { } TEST(GetExecutorTest, GetAllowedWildcardIndicesByKey) { - auto projExec = ProjectionExecAgg::create( + auto projExec = createProjectionExecutor( fromjson("{_id: 0}"), - ProjectionExecAgg::DefaultIdPolicy::kExcludeId, - ProjectionExecAgg::ArrayRecursionPolicy::kDoNotRecurseNestedArrays); + {ProjectionPolicies::DefaultIdPolicy::kExcludeId, + ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays}); testAllowedIndices({buildWildcardIndexEntry(BSON("$**" << 1), projExec.get(), "$**_1"), buildSimpleIndexEntry(fromjson("{a: 1}"), "a_1"), buildSimpleIndexEntry(fromjson("{a: 1, b: 1}"), "a_1_b_1"), @@ -221,10 +233,10 @@ TEST(GetExecutorTest, GetAllowedWildcardIndicesByKey) { } TEST(GetExecutorTest, GetAllowedWildcardIndicesByName) { - auto projExec = ProjectionExecAgg::create( + auto projExec = createProjectionExecutor( fromjson("{_id: 0}"), - ProjectionExecAgg::DefaultIdPolicy::kExcludeId, - ProjectionExecAgg::ArrayRecursionPolicy::kDoNotRecurseNestedArrays); + {ProjectionPolicies::DefaultIdPolicy::kExcludeId, + ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays}); testAllowedIndices({buildWildcardIndexEntry(BSON("$**" << 1), projExec.get(), "$**_1"), buildSimpleIndexEntry(fromjson("{a: 1}"), "a_1"), buildSimpleIndexEntry(fromjson("{a: 1, b: 1}"), "a_1_b_1"), @@ -235,10 +247,10 @@ TEST(GetExecutorTest, GetAllowedWildcardIndicesByName) { } TEST(GetExecutorTest, GetAllowedPathSpecifiedWildcardIndicesByKey) { - auto projExec = ProjectionExecAgg::create( + auto projExec = createProjectionExecutor( fromjson("{_id: 0}"), - ProjectionExecAgg::DefaultIdPolicy::kExcludeId, - ProjectionExecAgg::ArrayRecursionPolicy::kDoNotRecurseNestedArrays); + {ProjectionPolicies::DefaultIdPolicy::kExcludeId, + ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays}); testAllowedIndices({buildWildcardIndexEntry(BSON("a.$**" << 1), projExec.get(), "a.$**_1"), buildSimpleIndexEntry(fromjson("{a: 1}"), "a_1"), buildSimpleIndexEntry(fromjson("{a: 1, b: 1}"), "a_1_b_1"), @@ -249,10 +261,10 @@ TEST(GetExecutorTest, GetAllowedPathSpecifiedWildcardIndicesByKey) { } TEST(GetExecutorTest, GetAllowedPathSpecifiedWildcardIndicesByName) { - auto projExec = ProjectionExecAgg::create( + auto projExec = createProjectionExecutor( fromjson("{_id: 0}"), - ProjectionExecAgg::DefaultIdPolicy::kExcludeId, - ProjectionExecAgg::ArrayRecursionPolicy::kDoNotRecurseNestedArrays); + {ProjectionPolicies::DefaultIdPolicy::kExcludeId, + ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays}); testAllowedIndices({buildWildcardIndexEntry(BSON("a.$**" << 1), projExec.get(), "a.$**_1"), buildSimpleIndexEntry(fromjson("{a: 1}"), "a_1"), buildSimpleIndexEntry(fromjson("{a: 1, b: 1}"), "a_1_b_1"), diff --git a/src/mongo/db/query/index_entry.h b/src/mongo/db/query/index_entry.h index efe9179bc1d..d9eaf0ecdd7 100644 --- a/src/mongo/db/query/index_entry.h +++ b/src/mongo/db/query/index_entry.h @@ -32,7 +32,6 @@ #include <set> #include <string> -#include "mongo/db/exec/projection_exec_agg.h" #include "mongo/db/field_ref.h" #include "mongo/db/index/multikey_paths.h" #include "mongo/db/index_names.h" @@ -41,9 +40,11 @@ #include "mongo/util/str.h" namespace mongo { - class CollatorInterface; class MatchExpression; +namespace projection_executor { +class ProjectionExecutor; +} /** * A CoreIndexInfo is a representation of an index in the catalog with parsed information which is @@ -60,7 +61,7 @@ struct CoreIndexInfo { Identifier ident, const MatchExpression* fe = nullptr, const CollatorInterface* ci = nullptr, - const ProjectionExecAgg* projExec = nullptr) + projection_executor::ProjectionExecutor* projExec = nullptr) : identifier(std::move(ident)), keyPattern(kp), filterExpr(fe), @@ -137,7 +138,7 @@ struct CoreIndexInfo { // For $** indexes, a pointer to the projection executor owned by the index access method. Null // unless this IndexEntry represents a wildcard index, in which case this is always non-null. - const ProjectionExecAgg* wildcardProjection = nullptr; + projection_executor::ProjectionExecutor* wildcardProjection = nullptr; }; /** @@ -159,7 +160,7 @@ struct IndexEntry : CoreIndexInfo { const MatchExpression* fe, const BSONObj& io, const CollatorInterface* ci, - const ProjectionExecAgg* projExec) + projection_executor::ProjectionExecutor* projExec) : CoreIndexInfo(kp, type, sp, std::move(ident), fe, ci, projExec), multikey(mk), multikeyPaths(mkp), diff --git a/src/mongo/db/query/plan_cache_indexability.cpp b/src/mongo/db/query/plan_cache_indexability.cpp index 71d1fa456ce..96f7697e06c 100644 --- a/src/mongo/db/query/plan_cache_indexability.cpp +++ b/src/mongo/db/query/plan_cache_indexability.cpp @@ -35,6 +35,7 @@ #include "mongo/base/init.h" #include "mongo/base/owned_pointer_vector.h" +#include "mongo/db/exec/projection_executor_utils.h" #include "mongo/db/index/wildcard_key_generator.h" #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_algo.h" @@ -148,7 +149,8 @@ IndexToDiscriminatorMap PlanCacheIndexabilityState::buildWildcardDiscriminators( IndexToDiscriminatorMap ret; for (auto&& wildcardDiscriminator : _wildcardIndexDiscriminators) { - if (wildcardDiscriminator.projectionExec->applyProjectionToOneField(path)) { + if (projection_executor_utils::applyProjectionToOneField( + wildcardDiscriminator.projectionExec, path)) { CompositeIndexabilityDiscriminator& cid = ret[wildcardDiscriminator.catalogName]; // We can use these 'shallow' functions because the code building the plan cache key diff --git a/src/mongo/db/query/plan_cache_indexability.h b/src/mongo/db/query/plan_cache_indexability.h index 426cec2fb25..e98e7101fc8 100644 --- a/src/mongo/db/query/plan_cache_indexability.h +++ b/src/mongo/db/query/plan_cache_indexability.h @@ -32,16 +32,17 @@ #include <functional> #include <vector> -#include "mongo/db/exec/projection_exec_agg.h" #include "mongo/util/string_map.h" namespace mongo { - class BSONObj; class CollatorInterface; class CompositeIndexabilityDiscriminator; class MatchExpression; struct CoreIndexInfo; +namespace projection_executor { +class ProjectionExecutor; +} using IndexabilityDiscriminator = std::function<bool(const MatchExpression* me)>; using IndexabilityDiscriminators = std::vector<IndexabilityDiscriminator>; @@ -116,7 +117,7 @@ private: * index. */ struct WildcardIndexDiscriminatorContext { - WildcardIndexDiscriminatorContext(const ProjectionExecAgg* proj, + WildcardIndexDiscriminatorContext(projection_executor::ProjectionExecutor* proj, std::string name, const MatchExpression* filter, const CollatorInterface* coll) @@ -126,7 +127,7 @@ private: catalogName(std::move(name)) {} // These are owned by the catalog. - const ProjectionExecAgg* projectionExec; + projection_executor::ProjectionExecutor* projectionExec; const MatchExpression* filterExpr; // For partial indexes. const CollatorInterface* collator; diff --git a/src/mongo/db/query/plan_cache_indexability_test.cpp b/src/mongo/db/query/plan_cache_indexability_test.cpp index 48116f58416..eb2978ef6db 100644 --- a/src/mongo/db/query/plan_cache_indexability_test.cpp +++ b/src/mongo/db/query/plan_cache_indexability_test.cpp @@ -53,12 +53,12 @@ std::unique_ptr<MatchExpression> parseMatchExpression(const BSONObj& obj, return std::move(status.getValue()); } -// Helper which constructs a $** IndexEntry and returns it along with an owned ProjectionExecAgg. -// The latter simulates the ProjectionExecAgg which, during normal operation, is owned and +// Helper which constructs a $** IndexEntry and returns it along with an owned ProjectionExecutor. +// The latter simulates the ProjectionExecutor which, during normal operation, is owned and // maintained by the $** index's IndexAccessMethod, and is required because the plan cache will // obtain unowned pointers to it. auto makeWildcardEntry(BSONObj keyPattern, const MatchExpression* filterExpr = nullptr) { - auto projExec = WildcardKeyGenerator::createProjectionExec(keyPattern, {}); + auto projExec = WildcardKeyGenerator::createProjectionExecutor(keyPattern, {}); return std::make_pair(IndexEntry(keyPattern, IndexNames::nameToType(IndexNames::findPluginName(keyPattern)), false, // multikey diff --git a/src/mongo/db/query/plan_cache_test.cpp b/src/mongo/db/query/plan_cache_test.cpp index a4ee72710d8..ce580870ca9 100644 --- a/src/mongo/db/query/plan_cache_test.cpp +++ b/src/mongo/db/query/plan_cache_test.cpp @@ -239,12 +239,13 @@ void assertEquivalent(const char* queryStr, FAIL(ss); } -// Helper which constructs a $** IndexEntry and returns it along with an owned ProjectionExecAgg. -// The latter simulates the ProjectionExecAgg which, during normal operation, is owned and +// Helper which constructs a $** IndexEntry and returns it along with an owned ProjectionExecutor. +// The latter simulates the ProjectionExecutor which, during normal operation, is owned and // maintained by the $** index's IndexAccessMethod, and is required because the plan cache will // obtain unowned pointers to it. -std::pair<IndexEntry, std::unique_ptr<ProjectionExecAgg>> makeWildcardEntry(BSONObj keyPattern) { - auto projExec = WildcardKeyGenerator::createProjectionExec(keyPattern, {}); +std::pair<IndexEntry, std::unique_ptr<projection_executor::ProjectionExecutor>> makeWildcardEntry( + BSONObj keyPattern) { + auto projExec = WildcardKeyGenerator::createProjectionExecutor(keyPattern, {}); return {IndexEntry(keyPattern, IndexNames::nameToType(IndexNames::findPluginName(keyPattern)), false, // multikey @@ -261,9 +262,9 @@ std::pair<IndexEntry, std::unique_ptr<ProjectionExecAgg>> makeWildcardEntry(BSON } // A version of the above for CoreIndexInfo, used for plan cache update tests. -std::pair<CoreIndexInfo, std::unique_ptr<ProjectionExecAgg>> makeWildcardUpdate( - BSONObj keyPattern) { - auto projExec = WildcardKeyGenerator::createProjectionExec(keyPattern, {}); +std::pair<CoreIndexInfo, std::unique_ptr<projection_executor::ProjectionExecutor>> +makeWildcardUpdate(BSONObj keyPattern) { + auto projExec = WildcardKeyGenerator::createProjectionExecutor(keyPattern, {}); return {CoreIndexInfo(keyPattern, IndexNames::nameToType(IndexNames::findPluginName(keyPattern)), false, // sparse diff --git a/src/mongo/db/query/planner_ixselect_test.cpp b/src/mongo/db/query/planner_ixselect_test.cpp index e1018a87944..406104b96b0 100644 --- a/src/mongo/db/query/planner_ixselect_test.cpp +++ b/src/mongo/db/query/planner_ixselect_test.cpp @@ -1050,15 +1050,15 @@ TEST(QueryPlannerIXSelectTest, NoStringComparisonType) { } } -// Helper which constructs an IndexEntry and returns it along with an owned ProjectionExecAgg, which -// is non-null if the requested entry represents a wildcard index and null otherwise. When non-null, -// it simulates the ProjectionExecAgg that is owned by the $** IndexAccessMethod. +// Helper which constructs an IndexEntry and returns it along with an owned ProjectionExecutor, +// which is non-null if the requested entry represents a wildcard index and null otherwise. When +// non-null, it simulates the ProjectionExecutor that is owned by the $** IndexAccessMethod. auto makeIndexEntry(BSONObj keyPattern, MultikeyPaths multiKeyPaths, std::set<FieldRef> multiKeyPathSet = {}, BSONObj infoObj = BSONObj()) { auto projExec = (keyPattern.firstElement().fieldNameStringData().endsWith("$**"_sd) - ? WildcardKeyGenerator::createProjectionExec( + ? WildcardKeyGenerator::createProjectionExecutor( keyPattern, infoObj.getObjectField("wildcardProjection")) : nullptr); diff --git a/src/mongo/db/query/planner_wildcard_helpers.cpp b/src/mongo/db/query/planner_wildcard_helpers.cpp index 43f4ab69bb3..edf768c75a3 100644 --- a/src/mongo/db/query/planner_wildcard_helpers.cpp +++ b/src/mongo/db/query/planner_wildcard_helpers.cpp @@ -36,6 +36,7 @@ #include <vector> #include "mongo/bson/util/builder.h" +#include "mongo/db/exec/projection_executor_utils.h" #include "mongo/db/index/wildcard_key_generator.h" #include "mongo/db/query/index_bounds.h" #include "mongo/util/log.h" @@ -355,13 +356,12 @@ void expandWildcardIndexEntry(const IndexEntry& wildcardIndex, invariant(wildcardIndex.multikeyPaths.empty()); // Obtain the projection executor from the parent wildcard IndexEntry. - const auto* projExec = wildcardIndex.wildcardProjection; + auto projExec = wildcardIndex.wildcardProjection; invariant(projExec); - const auto projectedFields = projExec->applyProjectionToFields(fields); - - const auto& includedPaths = projExec->getExhaustivePaths(); - + const auto projectedFields = + projection_executor_utils::applyProjectionToFields(projExec, fields); + const auto& includedPaths = projection_executor_utils::extractExhaustivePaths(projExec); out->reserve(out->size() + projectedFields.size()); for (auto&& fieldName : projectedFields) { // Convert string 'fieldName' into a FieldRef, to better facilitate the subsequent checks. diff --git a/src/mongo/db/query/projection_policies.h b/src/mongo/db/query/projection_policies.h index 4d2d6a54fba..09094227c86 100644 --- a/src/mongo/db/query/projection_policies.h +++ b/src/mongo/db/query/projection_policies.h @@ -64,18 +64,24 @@ struct ProjectionPolicies { FindOnlyFeaturesPolicy::kBanFindOnlyFeatures; static ProjectionPolicies findProjectionPolicies() { - return ProjectionPolicies{ - ProjectionPolicies::kDefaultIdPolicyDefault, - ProjectionPolicies::kArrayRecursionPolicyDefault, - ProjectionPolicies::kComputedFieldsPolicyDefault, - ProjectionPolicies::FindOnlyFeaturesPolicy::kAllowFindOnlyFeatures}; + return ProjectionPolicies{kDefaultIdPolicyDefault, + kArrayRecursionPolicyDefault, + kComputedFieldsPolicyDefault, + FindOnlyFeaturesPolicy::kAllowFindOnlyFeatures}; } static ProjectionPolicies aggregateProjectionPolicies() { - return ProjectionPolicies{ProjectionPolicies::kDefaultIdPolicyDefault, - ProjectionPolicies::kArrayRecursionPolicyDefault, - ProjectionPolicies::kComputedFieldsPolicyDefault, - ProjectionPolicies::FindOnlyFeaturesPolicy::kBanFindOnlyFeatures}; + return ProjectionPolicies{kDefaultIdPolicyDefault, + kArrayRecursionPolicyDefault, + kComputedFieldsPolicyDefault, + FindOnlyFeaturesPolicy::kBanFindOnlyFeatures}; + } + + static ProjectionPolicies wildcardIndexSpecProjectionPolicies() { + return ProjectionPolicies{DefaultIdPolicy::kExcludeId, + ArrayRecursionPolicy::kDoNotRecurseNestedArrays, + ComputedFieldsPolicy::kBanComputedFields, + FindOnlyFeaturesPolicy::kBanFindOnlyFeatures}; } ProjectionPolicies( diff --git a/src/mongo/db/query/query_planner_wildcard_index_test.cpp b/src/mongo/db/query/query_planner_wildcard_index_test.cpp index 318d43728e9..4044262b10a 100644 --- a/src/mongo/db/query/query_planner_wildcard_index_test.cpp +++ b/src/mongo/db/query/query_planner_wildcard_index_test.cpp @@ -71,7 +71,7 @@ protected: const bool isMultikey = !multikeyPathSet.empty(); BSONObj infoObj = BSON("wildcardProjection" << wildcardProjection); - _projExec = WildcardKeyGenerator::createProjectionExec(keyPattern, wildcardProjection); + _projExec = WildcardKeyGenerator::createProjectionExecutor(keyPattern, wildcardProjection); params.indices.push_back({std::move(keyPattern), IndexType::INDEX_WILDCARD, @@ -87,7 +87,7 @@ protected: _projExec.get()}); } - std::unique_ptr<ProjectionExecAgg> _projExec; + std::unique_ptr<projection_executor::ProjectionExecutor> _projExec; }; // diff --git a/src/mongo/embedded/stitch_support/stitch_support.cpp b/src/mongo/embedded/stitch_support/stitch_support.cpp index 0613899cfdc..8aa464be738 100644 --- a/src/mongo/embedded/stitch_support/stitch_support.cpp +++ b/src/mongo/embedded/stitch_support/stitch_support.cpp @@ -36,6 +36,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/db/client.h" #include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" #include "mongo/db/matcher/matcher.h" #include "mongo/db/ops/parsed_update.h" #include "mongo/db/query/collation/collator_factory_interface.h" @@ -202,8 +203,7 @@ struct stitch_support_v1_projection { mongo::ServiceContext::UniqueClient client; mongo::ServiceContext::UniqueOperationContext opCtx; - std::unique_ptr<mongo::parsed_aggregation_projection::ParsedAggregationProjection> - projectionExec; + std::unique_ptr<mongo::projection_executor::ProjectionExecutor> projectionExec; bool requiresMatch = false; stitch_support_v1_matcher* matcher; |