diff options
34 files changed, 772 insertions, 1329 deletions
diff --git a/jstests/aggregation/bugs/server11675.js b/jstests/aggregation/bugs/server11675.js index b9214934163..72f9471bc8a 100644 --- a/jstests/aggregation/bugs/server11675.js +++ b/jstests/aggregation/bugs/server11675.js @@ -217,8 +217,6 @@ assert.eq(res[0].scoreAgain, res[0].score); assertErrorCode(coll, [{$sort: {text: 1}}, {$match: {$text: {$search: 'apple banana'}}}], 17313); // wrong $stage, but correct position -assertErrorCode(coll, - [{$project: {searchValue: {$text: {$search: 'apple banana'}}}}], - ErrorCodes.InvalidPipelineOperator); +assertErrorCode(coll, [{$project: {searchValue: {$text: {$search: 'apple banana'}}}}], 31325); assertErrorCode(coll, [{$sort: {$text: {$search: 'apple banana'}}}], 17312); })(); diff --git a/jstests/aggregation/bugs/server6177.js b/jstests/aggregation/bugs/server6177.js index 952cb008b67..caf54edea1e 100644 --- a/jstests/aggregation/bugs/server6177.js +++ b/jstests/aggregation/bugs/server6177.js @@ -8,7 +8,7 @@ c.drop(); c.save({}); -assertErrorCode(c, {$project: {'x': {$add: [1]}, 'x.b': 1}}, 40176); -assertErrorCode(c, {$project: {'x.b': 1, 'x': {$add: [1]}}}, 40176); -assertErrorCode(c, {$project: {'x': {'b': 1}, 'x.b': 1}}, 40176); -assertErrorCode(c, {$project: {'x.b': 1, 'x': {'b': 1}}}, 40176); +assertErrorCode(c, {$project: {'x': {$add: [1]}, 'x.b': 1}}, 31249); +assertErrorCode(c, {$project: {'x.b': 1, 'x': {$add: [1]}}}, 31250); +assertErrorCode(c, {$project: {'x': {'b': 1}, 'x.b': 1}}, 31250); +assertErrorCode(c, {$project: {'x.b': 1, 'x': {'b': 1}}}, 31250); diff --git a/jstests/aggregation/bugs/server6238.js b/jstests/aggregation/bugs/server6238.js index 41ed074a1a0..ddc29ec33d8 100644 --- a/jstests/aggregation/bugs/server6238.js +++ b/jstests/aggregation/bugs/server6238.js @@ -8,8 +8,8 @@ c.insert({a: 1}); // assert that we get the proper error in both $project and $group assertErrorCode(c, {$project: {$a: "$a"}}, 16410); -assertErrorCode(c, {$project: {a: {$b: "$a"}}}, ErrorCodes.InvalidPipelineOperator); -assertErrorCode(c, {$project: {a: {"$b": "$a"}}}, ErrorCodes.InvalidPipelineOperator); +assertErrorCode(c, {$project: {a: {$b: "$a"}}}, 31325); +assertErrorCode(c, {$project: {a: {"$b": "$a"}}}, 31325); assertErrorCode(c, {$project: {'a.$b': "$a"}}, 16410); assertErrorCode(c, {$group: {_id: "$_id", $a: {$sum: 1}}}, 40236); assertErrorCode(c, {$group: {_id: {$a: "$a"}}}, ErrorCodes.InvalidPipelineOperator); diff --git a/jstests/aggregation/bugs/server6290.js b/jstests/aggregation/bugs/server6290.js index 756c2f571e8..7105cfd220e 100644 --- a/jstests/aggregation/bugs/server6290.js +++ b/jstests/aggregation/bugs/server6290.js @@ -8,19 +8,20 @@ t.drop(); t.save({}); -var error = ErrorCodes.InvalidPipelineOperator; - // $isoDate is an invalid operator. -assertErrorCode(t, {$project: {a: {$isoDate: [{year: 1}]}}}, error); +assertErrorCode(t, {$project: {a: {$isoDate: [{year: 1}]}}}, 31325); // $date is an invalid operator. -assertErrorCode(t, {$project: {a: {$date: [{year: 1}]}}}, error); +assertErrorCode(t, {$project: {a: {$date: [{year: 1}]}}}, 31325); // Alternative operands. -assertErrorCode(t, {$project: {a: {$isoDate: []}}}, error); -assertErrorCode(t, {$project: {a: {$date: []}}}, error); -assertErrorCode(t, {$project: {a: {$isoDate: 'foo'}}}, error); -assertErrorCode(t, {$project: {a: {$date: 'foo'}}}, error); +assertErrorCode(t, {$project: {a: {$isoDate: []}}}, 31325); +assertErrorCode(t, {$project: {a: {$date: []}}}, 31325); +assertErrorCode(t, {$project: {a: {$isoDate: 'foo'}}}, 31325); +assertErrorCode(t, {$project: {a: {$date: 'foo'}}}, 31325); // Test with $group. -assertErrorCode(t, {$group: {_id: 0, a: {$first: {$isoDate: [{year: 1}]}}}}, error); -assertErrorCode(t, {$group: {_id: 0, a: {$first: {$date: [{year: 1}]}}}}, error); +assertErrorCode(t, + {$group: {_id: 0, a: {$first: {$isoDate: [{year: 1}]}}}}, + ErrorCodes.InvalidPipelineOperator); +assertErrorCode( + t, {$group: {_id: 0, a: {$first: {$date: [{year: 1}]}}}}, ErrorCodes.InvalidPipelineOperator); diff --git a/jstests/aggregation/use_query_project_and_sort.js b/jstests/aggregation/use_query_project_and_sort.js index de16c125183..20c54e92619 100644 --- a/jstests/aggregation/use_query_project_and_sort.js +++ b/jstests/aggregation/use_query_project_and_sort.js @@ -21,7 +21,7 @@ assert.commandWorked(bulk.execute()); function assertQueryCoversProjectionAndSort(pipeline) { const explainOutput = coll.explain().aggregate(pipeline); - assert(isQueryPlan(explainOutput)); + assert(isQueryPlan(explainOutput), explainOutput); assert(!planHasStage(db, explainOutput, "FETCH"), "Expected pipeline " + tojsononeline(pipeline) + " *not* to include a FETCH stage in the explain output: " + tojson(explainOutput)); diff --git a/jstests/core/wildcard_index_validindex.js b/jstests/core/wildcard_index_validindex.js index f647bbcc969..224ce19fe11 100644 --- a/jstests/core/wildcard_index_validindex.js +++ b/jstests/core/wildcard_index_validindex.js @@ -106,11 +106,11 @@ assert.commandFailedWithCode(coll.createIndex({"$**": "hello"}), ErrorCodes.Cann // Cannot create an wildcard index with mixed inclusion exclusion. assert.commandFailedWithCode( - createIndexHelper({"$**": 1}, {name: kIndexName, wildcardProjection: {a: 1, b: 0}}), 40178); + createIndexHelper({"$**": 1}, {name: kIndexName, wildcardProjection: {a: 1, b: 0}}), 31254); // Cannot create an wildcard index with computed fields. assert.commandFailedWithCode( createIndexHelper({"$**": 1}, {name: kIndexName, wildcardProjection: {a: 1, b: "string"}}), - ErrorCodes.FailedToParse); + 51271); // Cannot create an wildcard index with an empty projection. assert.commandFailedWithCode( createIndexHelper({"$**": 1}, {name: kIndexName, wildcardProjection: {}}), diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript index 8c051a3916a..d32fc244d1b 100644 --- a/src/mongo/db/SConscript +++ b/src/mongo/db/SConscript @@ -970,22 +970,25 @@ env.Library( ) env.Library( - target='projection_exec_agg', + target='projection_executor', source=[ - 'exec/projection_exec_agg.cpp', + 'exec/find_projection_executor.cpp', + 'exec/projection_executor.cpp', ], LIBDEPS=[ + 'matcher/expressions', 'pipeline/parsed_aggregation_projection', ], ) env.Library( - target='projection_executor', + target='projection_exec_agg', source=[ - 'exec/find_projection_executor.cpp', + 'exec/projection_exec_agg.cpp' ], LIBDEPS=[ - 'matcher/expressions' + 'projection_executor', + 'query/projection_ast', ], ) @@ -1018,7 +1021,6 @@ env.Library( 'exec/pipeline_proxy.cpp', 'exec/plan_stage.cpp', 'exec/projection.cpp', - 'exec/projection_executor.cpp', 'exec/queued_data_stage.cpp', 'exec/record_store_fast_count.cpp', 'exec/requires_all_indices_stage.cpp', diff --git a/src/mongo/db/catalog/index_spec_validate_test.cpp b/src/mongo/db/catalog/index_spec_validate_test.cpp index 72343c238f7..a24f0a5fce8 100644 --- a/src/mongo/db/catalog/index_spec_validate_test.cpp +++ b/src/mongo/db/catalog/index_spec_validate_test.cpp @@ -598,7 +598,7 @@ TEST(IndexSpecWildcard, FailsWithInclusionExcludingIdSubfield) { << "wildcardProjection" << BSON("_id.field" << 0 << "a" << 1 << "b" << 1)), serverGlobalParams.featureCompatibility); - ASSERT_EQ(result.getStatus().code(), 40179); + ASSERT_EQ(result.getStatus().code(), 31253); } TEST(IndexSpecWildcard, FailsWithExclusionIncludingIdSubfield) { @@ -608,7 +608,7 @@ TEST(IndexSpecWildcard, FailsWithExclusionIncludingIdSubfield) { << "wildcardProjection" << BSON("_id.field" << 1 << "a" << 0 << "b" << 0)), serverGlobalParams.featureCompatibility); - ASSERT_EQ(result.getStatus().code(), 40178); + ASSERT_EQ(result.getStatus().code(), 31254); } TEST(IndexSpecWildcard, FailsWithMixedProjection) { @@ -618,7 +618,7 @@ TEST(IndexSpecWildcard, FailsWithMixedProjection) { << "indexName" << "wildcardProjection" << BSON("a" << 1 << "b" << 0)), serverGlobalParams.featureCompatibility); - ASSERT_EQ(result.getStatus().code(), 40178); + ASSERT_EQ(result.getStatus().code(), 31254); } TEST(IndexSpecWildcard, FailsWithComputedFieldsInProjection) { @@ -629,7 +629,7 @@ TEST(IndexSpecWildcard, FailsWithComputedFieldsInProjection) { << BSON("a" << 1 << "b" << "string")), serverGlobalParams.featureCompatibility); - ASSERT_EQ(result.getStatus().code(), ErrorCodes::FailedToParse); + ASSERT_EQ(result.getStatus().code(), 51271); } TEST(IndexSpecWildcard, FailsWhenProjectionPluginNotWildcard) { diff --git a/src/mongo/db/exec/projection.cpp b/src/mongo/db/exec/projection.cpp index 514b90a78a4..13e48a44c4c 100644 --- a/src/mongo/db/exec/projection.cpp +++ b/src/mongo/db/exec/projection.cpp @@ -174,7 +174,8 @@ ProjectionStageDefault::ProjectionStageDefault(boost::intrusive_ptr<ExpressionCo : ProjectionStage{expCtx, projObj, ws, std::move(child), "PROJECTION_DEFAULT"}, _wantRecordId{projection->metadataDeps()[DocumentMetadataFields::kRecordId]}, _projectType{projection->type()}, - _executor{projection_executor::buildProjectionExecutor(expCtx, projection, {})} {} + _executor{projection_executor::buildProjectionExecutor( + expCtx, projection, {}, true /* optimizeExecutor */)} {} Status ProjectionStageDefault::transform(WorkingSetMember* member) const { Document input; diff --git a/src/mongo/db/exec/projection_exec_agg.cpp b/src/mongo/db/exec/projection_exec_agg.cpp index 67433e12078..94eafdafb7f 100644 --- a/src/mongo/db/exec/projection_exec_agg.cpp +++ b/src/mongo/db/exec/projection_exec_agg.cpp @@ -32,8 +32,10 @@ #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 { @@ -78,7 +80,9 @@ public: ProjectionPolicies projectionPolicies{idPolicy, recursionPolicy, computedFieldsPolicy}; // Construct a ParsedAggregationProjection for the given projection spec and policies. - _projection = ParsedAggregationProjection::create(expCtx, projSpec, projectionPolicies); + 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) { diff --git a/src/mongo/db/exec/projection_executor.cpp b/src/mongo/db/exec/projection_executor.cpp index 19a7fd7eb55..cecc5a468d4 100644 --- a/src/mongo/db/exec/projection_executor.cpp +++ b/src/mongo/db/exec/projection_executor.cpp @@ -246,13 +246,16 @@ private: template <typename Executor> auto buildProjectionExecutor(boost::intrusive_ptr<ExpressionContext> expCtx, const projection_ast::ProjectionPathASTNode* root, - const ProjectionPolicies policies) { + const ProjectionPolicies policies, + bool optimizeExecutor) { ProjectionExecutorVisitorContext<Executor> context{ {std::make_unique<Executor>(expCtx, policies), expCtx}}; ProjectionExecutorVisitor<Executor> executorVisitor{&context}; projection_ast::PathTrackingWalker walker{&context, {&executorVisitor}, {}}; projection_ast_walker::walk(&walker, root); - context.data().executor->optimize(); + if (optimizeExecutor) { + context.data().executor->optimize(); + } return std::move(context.data().executor); } } // namespace @@ -260,16 +263,17 @@ auto buildProjectionExecutor(boost::intrusive_ptr<ExpressionContext> expCtx, std::unique_ptr<ParsedAggregationProjection> buildProjectionExecutor( boost::intrusive_ptr<ExpressionContext> expCtx, const projection_ast::Projection* projection, - const ProjectionPolicies policies) { + const ProjectionPolicies policies, + bool optimizeExecutor) { invariant(projection); switch (projection->type()) { case kInclusion: return buildProjectionExecutor<ParsedInclusionProjection>( - expCtx, projection->root(), policies); + expCtx, projection->root(), policies, optimizeExecutor); case kExclusion: return buildProjectionExecutor<ParsedExclusionProjection>( - expCtx, projection->root(), policies); + expCtx, projection->root(), policies, optimizeExecutor); default: MONGO_UNREACHABLE; } diff --git a/src/mongo/db/exec/projection_executor.h b/src/mongo/db/exec/projection_executor.h index e3b7b2b24dc..3045e4ca710 100644 --- a/src/mongo/db/exec/projection_executor.h +++ b/src/mongo/db/exec/projection_executor.h @@ -37,9 +37,12 @@ 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<parsed_aggregation_projection::ParsedAggregationProjection> buildProjectionExecutor( boost::intrusive_ptr<ExpressionContext> expCtx, const projection_ast::Projection* projection, - ProjectionPolicies policies); + ProjectionPolicies policies, + bool optimizeExecutor); } // 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 bd5e77c56d7..0d97b093b1b 100644 --- a/src/mongo/db/exec/projection_executor_test.cpp +++ b/src/mongo/db/exec/projection_executor_test.cpp @@ -38,6 +38,9 @@ #include "mongo/unittest/unittest.h" namespace mongo::projection_executor { +namespace { +constexpr auto kOptimzeExecutor = true; + class ProjectionExecutorTest : public AggregationContextFixture { public: projection_ast::Projection parseWithDefaultPolicies( @@ -70,13 +73,13 @@ public: TEST_F(ProjectionExecutorTest, CanProjectInclusionWithIdPath) { auto projWithId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &projWithId, {}); + 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, {}); + executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 'abc'}")}, executor->applyTransformation( Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); @@ -84,7 +87,7 @@ TEST_F(ProjectionExecutorTest, CanProjectInclusionWithIdPath) { TEST_F(ProjectionExecutorTest, CanProjectInclusionUndottedPath) { auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}")})); @@ -92,7 +95,7 @@ TEST_F(ProjectionExecutorTest, CanProjectInclusionUndottedPath) { TEST_F(ProjectionExecutorTest, CanProjectInclusionDottedPath) { auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 1, 'a.d': 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}}")})); @@ -100,14 +103,14 @@ TEST_F(ProjectionExecutorTest, CanProjectInclusionDottedPath) { TEST_F(ProjectionExecutorTest, CanProjectExpression) { auto proj = parseWithDefaultPolicies(fromjson("{c: {$add: ['$a', '$b']}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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, {}); + 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'}")})); @@ -115,7 +118,7 @@ TEST_F(ProjectionExecutorTest, CanProjectExclusionWithIdPath) { TEST_F(ProjectionExecutorTest, CanProjectExclusionUndottedPath) { auto proj = parseWithDefaultPolicies(fromjson("{a: 0, b: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); ASSERT_DOCUMENT_EQ( Document{fromjson("{c: 'ghi'}")}, executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); @@ -123,7 +126,7 @@ TEST_F(ProjectionExecutorTest, CanProjectExclusionUndottedPath) { TEST_F(ProjectionExecutorTest, CanProjectExclusionDottedPath) { auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 0, 'a.d': 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}}")})); @@ -132,7 +135,7 @@ TEST_F(ProjectionExecutorTest, CanProjectExclusionDottedPath) { TEST_F(ProjectionExecutorTest, CanProjectFindPositional) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b.$': 1}"), fromjson("{'a.b': {$gte: 3}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [3]}}")}, executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}}")})); @@ -142,7 +145,7 @@ TEST_F(ProjectionExecutorTest, CanProjectFindPositional) { TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithInclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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}]}")})); @@ -153,14 +156,14 @@ 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, {}); + 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, {}); + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 3}]}")}, executor->applyTransformation(Document{obj})); } @@ -171,8 +174,7 @@ TEST_F(ProjectionExecutorTest, ElemMatchRespectsCollator) { getExpCtx()->setCollator(&collator); auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {$gte: 'abc'}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); - + auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); ASSERT_DOCUMENT_EQ( Document{fromjson("{ a: [ \"zdd\" ] }")}, @@ -181,7 +183,7 @@ TEST_F(ProjectionExecutorTest, ElemMatchRespectsCollator) { TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithExclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}")})); @@ -189,7 +191,7 @@ TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithExclusion) { TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithInclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}")})); @@ -197,7 +199,7 @@ TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithInclusion) { TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithInclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}")})); @@ -205,7 +207,7 @@ TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithInclusion) { TEST_F(ProjectionExecutorTest, CanProjectFindSliceBasicWithExclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: 3}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}")})); @@ -213,7 +215,7 @@ TEST_F(ProjectionExecutorTest, CanProjectFindSliceBasicWithExclusion) { TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithExclusion) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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'}")})); @@ -222,7 +224,7 @@ TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithExclusion) { TEST_F(ProjectionExecutorTest, CanProjectFindSliceAndPositional) { auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, 'c.$': 1}"), fromjson("{c: {$gte: 6}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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]}")})); @@ -230,8 +232,9 @@ TEST_F(ProjectionExecutorTest, CanProjectFindSliceAndPositional) { TEST_F(ProjectionExecutorTest, ExecutorOptimizesExpression) { auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: {$add: [1, 2]}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}); + 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/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 29cec941a02..ee2fb4ac34e 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -375,17 +375,13 @@ pipelineEnv.Library( env.Library( target='parsed_aggregation_projection', source=[ - 'parsed_aggregation_projection.cpp', 'parsed_aggregation_projection_node.cpp', - 'parsed_exclusion_projection.cpp', - 'parsed_inclusion_projection.cpp', 'parsed_add_fields.cpp', ], LIBDEPS=[ 'expression', 'field_path', '$BUILD_DIR/mongo/db/matcher/expressions', - '$BUILD_DIR/mongo/db/query/projection_ast', ] ) @@ -501,6 +497,7 @@ 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', diff --git a/src/mongo/db/pipeline/document_source_count_test.cpp b/src/mongo/db/pipeline/document_source_count_test.cpp index 69b98cc37e9..9ffd8a6d120 100644 --- a/src/mongo/db/pipeline/document_source_count_test.cpp +++ b/src/mongo/db/pipeline/document_source_count_test.cpp @@ -78,7 +78,7 @@ public: auto groupExplain = explainedStages[0]; ASSERT_VALUE_EQ(groupExplain["$group"], expectedGroupExplain); - Value expectedProjectExplain = Value{Document{{"_id", false}, {countName, true}}}; + Value expectedProjectExplain = Value{Document{{countName, true}, {"_id", false}}}; auto projectExplain = explainedStages[1]; ASSERT_VALUE_EQ(projectExplain["$project"], expectedProjectExplain); } diff --git a/src/mongo/db/pipeline/document_source_lookup_test.cpp b/src/mongo/db/pipeline/document_source_lookup_test.cpp index 704349d800c..7a40528f931 100644 --- a/src/mongo/db/pipeline/document_source_lookup_test.cpp +++ b/src/mongo/db/pipeline/document_source_lookup_test.cpp @@ -813,8 +813,9 @@ TEST_F(DocumentSourceLookUpTest, // Verify that the $project is identified as non-correlated and the cache is placed after it. auto docSource = DocumentSourceLookUp::createFromBson( fromjson("{$lookup: {let: {var1: '$_id'}, pipeline: [{$match: {x: 1}}, {$sort: {x: 1}}, " - "{$project: {_id: false, projectedField: {$let: {vars: {var1: 'abc'}, in: " - "'$$var1'}}}}, {$addFields: {varField: {$sum: ['$x', '$$var1']}}}], from: 'coll', " + "{$project: {projectedField: {$let: {vars: {var1: 'abc'}, in: " + "'$$var1'}}, _id: false}}, {$addFields: {varField: {$sum: ['$x', '$$var1']}}}], " + "from: 'coll', " "as: 'as'}}") .firstElement(), expCtx); @@ -830,8 +831,8 @@ TEST_F(DocumentSourceLookUpTest, auto expectedPipe = fromjson( str::stream() << "[{mock: {}}, {$match: {x: {$eq: 1}}}, {$sort: {sortKey: {x: 1}}}, " - "{$project: {_id: false, " - "projectedField: {$let: {vars: {var1: {$const: 'abc'}}, in: '$$var1'}}}}," + "{$project: {projectedField: {$let: {vars: {var1: {$const: 'abc'}}, " + "in: '$$var1'}}, _id: false}}," << sequentialCacheStageObj() << ", {$addFields: {varField: {$sum: ['$x', {$const: 5}]}}}]"); diff --git a/src/mongo/db/pipeline/document_source_project.cpp b/src/mongo/db/pipeline/document_source_project.cpp index f37b1a1ced5..a626eb2b41f 100644 --- a/src/mongo/db/pipeline/document_source_project.cpp +++ b/src/mongo/db/pipeline/document_source_project.cpp @@ -34,8 +34,10 @@ #include <boost/optional.hpp> #include <boost/smart_ptr/intrusive_ptr.hpp> +#include "mongo/db/exec/projection_executor.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 { @@ -71,11 +73,18 @@ intrusive_ptr<DocumentSource> DocumentSourceProject::create( // is caught here so we can add the name that was actually specified by the user, be it // $project or an alias. try { - return ParsedAggregationProjection::create( - expCtx, - projectSpec, - {ProjectionPolicies::DefaultIdPolicy::kIncludeId, - ProjectionPolicies::ArrayRecursionPolicy::kRecurseNestedArrays}); + auto policies = ProjectionPolicies::aggregateProjectionPolicies(); + auto projection = projection_ast::parse(expCtx, projectSpec, policies); + // We won't optimize the executor on creation, and will do it as part of the + // pipeline optimization process when requested via the 'optimize()' method on + // 'DocumentSourceSingleDocumentTransformation'. + // + // Note that this is also important for $lookup inner pipelines to not being + // optimized too early, as it may lead to incorrect positioning of the caching + // stage due to missing dependencies on certain variables, as they could have been + // optimized away. + return projection_executor::buildProjectionExecutor( + expCtx, &projection, policies, false /* optimizeExecutor */); } catch (DBException& ex) { ex.addContext("Invalid " + specifiedName.toString()); throw; diff --git a/src/mongo/db/pipeline/parsed_add_fields.cpp b/src/mongo/db/pipeline/parsed_add_fields.cpp index d756c444fa7..a4b9677da84 100644 --- a/src/mongo/db/pipeline/parsed_add_fields.cpp +++ b/src/mongo/db/pipeline/parsed_add_fields.cpp @@ -33,12 +33,101 @@ #include <algorithm> +#include "mongo/db/matcher/expression_algo.h" #include "mongo/db/pipeline/parsed_aggregation_projection.h" namespace mongo { - namespace parsed_aggregation_projection { +using TransformerType = TransformerInterface::TransformerType; + +using expression::isPathPrefixOf; + +// +// ProjectionSpecValidator +// + +void ProjectionSpecValidator::uassertValid(const BSONObj& spec) { + ProjectionSpecValidator(spec).validate(); +} + +void ProjectionSpecValidator::ensurePathDoesNotConflictOrThrow(const std::string& path) { + auto result = _seenPaths.emplace(path); + auto pos = result.first; + + // Check whether the path was a duplicate of an existing path. + auto conflictingPath = boost::make_optional(!result.second, *pos); + + // Check whether the preceding path prefixes this path. + if (!conflictingPath && pos != _seenPaths.begin()) { + conflictingPath = + boost::make_optional(isPathPrefixOf(*std::prev(pos), path), *std::prev(pos)); + } + + // Check whether this path prefixes the subsequent path. + if (!conflictingPath && std::next(pos) != _seenPaths.end()) { + conflictingPath = + boost::make_optional(isPathPrefixOf(path, *std::next(pos)), *std::next(pos)); + } + + uassert(40176, + str::stream() << "specification contains two conflicting paths. " + "Cannot specify both '" + << path << "' and '" << *conflictingPath << "': " << _rawObj.toString(), + !conflictingPath); +} + +void ProjectionSpecValidator::validate() { + if (_rawObj.isEmpty()) { + uasserted(40177, "specification must have at least one field"); + } + for (auto&& elem : _rawObj) { + parseElement(elem, FieldPath(elem.fieldName())); + } +} + +void ProjectionSpecValidator::parseElement(const BSONElement& elem, const FieldPath& pathToElem) { + if (elem.type() == BSONType::Object) { + parseNestedObject(elem.Obj(), pathToElem); + } else { + ensurePathDoesNotConflictOrThrow(pathToElem.fullPath()); + } +} + +void ProjectionSpecValidator::parseNestedObject(const BSONObj& thisLevelSpec, + const FieldPath& prefix) { + if (thisLevelSpec.isEmpty()) { + uasserted( + 40180, + str::stream() << "an empty object is not a valid value. Found empty object at path " + << prefix.fullPath()); + } + for (auto&& elem : thisLevelSpec) { + auto fieldName = elem.fieldNameStringData(); + if (fieldName[0] == '$') { + // This object is an expression specification like {$add: [...]}. It will be parsed + // into an Expression later, but for now, just track that the prefix has been + // specified and skip it. + if (thisLevelSpec.nFields() != 1) { + uasserted(40181, + str::stream() << "an expression specification must contain exactly " + "one field, the name of the expression. Found " + << thisLevelSpec.nFields() << " fields in " + << thisLevelSpec.toString() << ", while parsing object " + << _rawObj.toString()); + } + ensurePathDoesNotConflictOrThrow(prefix.fullPath()); + continue; + } + if (fieldName.find('.') != std::string::npos) { + uasserted(40183, + str::stream() << "cannot use dotted field name '" << fieldName + << "' in a sub object: " << _rawObj.toString()); + } + parseElement(elem, FieldPath::getFullyQualifiedPath(prefix.fullPath(), fieldName)); + } +} + std::unique_ptr<ParsedAddFields> ParsedAddFields::create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const BSONObj& spec) { // Verify that we don't have conflicting field paths, etc. diff --git a/src/mongo/db/pipeline/parsed_add_fields.h b/src/mongo/db/pipeline/parsed_add_fields.h index 0960bfc1cf5..5ea10e55c4b 100644 --- a/src/mongo/db/pipeline/parsed_add_fields.h +++ b/src/mongo/db/pipeline/parsed_add_fields.h @@ -37,8 +37,94 @@ #include "mongo/db/pipeline/parsed_inclusion_projection.h" namespace mongo { - namespace parsed_aggregation_projection { +/** + * 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 @@ -82,7 +168,7 @@ public: /** * Parses the addFields specification given by 'spec', populating internal data structures. */ - void parse(const BSONObj& spec) final; + void parse(const BSONObj& spec); Document serializeTransformation( boost::optional<ExplainOptions::Verbosity> explain) const final { diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection.cpp deleted file mode 100644 index 561af013548..00000000000 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.cpp +++ /dev/null @@ -1,344 +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. - */ - -#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery - -#include "mongo/platform/basic.h" - -#include "mongo/db/pipeline/parsed_aggregation_projection.h" - -#include <boost/optional.hpp> -#include <string> - -#include "mongo/bson/bsonelement.h" -#include "mongo/bson/bsonobj.h" -#include "mongo/db/matcher/expression_algo.h" -#include "mongo/db/pipeline/field_path.h" -#include "mongo/db/pipeline/parsed_exclusion_projection.h" -#include "mongo/db/pipeline/parsed_inclusion_projection.h" -#include "mongo/db/query/projection_parser.h" -#include "mongo/stdx/unordered_set.h" -#include "mongo/util/assert_util.h" -#include "mongo/util/log.h" -#include "mongo/util/str.h" - -namespace mongo { -namespace parsed_aggregation_projection { - -using TransformerType = TransformerInterface::TransformerType; - -using expression::isPathPrefixOf; - -// -// ProjectionSpecValidator -// - -void ProjectionSpecValidator::uassertValid(const BSONObj& spec) { - ProjectionSpecValidator(spec).validate(); -} - -void ProjectionSpecValidator::ensurePathDoesNotConflictOrThrow(const std::string& path) { - auto result = _seenPaths.emplace(path); - auto pos = result.first; - - // Check whether the path was a duplicate of an existing path. - auto conflictingPath = boost::make_optional(!result.second, *pos); - - // Check whether the preceding path prefixes this path. - if (!conflictingPath && pos != _seenPaths.begin()) { - conflictingPath = - boost::make_optional(isPathPrefixOf(*std::prev(pos), path), *std::prev(pos)); - } - - // Check whether this path prefixes the subsequent path. - if (!conflictingPath && std::next(pos) != _seenPaths.end()) { - conflictingPath = - boost::make_optional(isPathPrefixOf(path, *std::next(pos)), *std::next(pos)); - } - - uassert(40176, - str::stream() << "specification contains two conflicting paths. " - "Cannot specify both '" - << path << "' and '" << *conflictingPath << "': " << _rawObj.toString(), - !conflictingPath); -} - -void ProjectionSpecValidator::validate() { - if (_rawObj.isEmpty()) { - uasserted(40177, "specification must have at least one field"); - } - for (auto&& elem : _rawObj) { - parseElement(elem, FieldPath(elem.fieldName())); - } -} - -void ProjectionSpecValidator::parseElement(const BSONElement& elem, const FieldPath& pathToElem) { - if (elem.type() == BSONType::Object) { - parseNestedObject(elem.Obj(), pathToElem); - } else { - ensurePathDoesNotConflictOrThrow(pathToElem.fullPath()); - } -} - -void ProjectionSpecValidator::parseNestedObject(const BSONObj& thisLevelSpec, - const FieldPath& prefix) { - if (thisLevelSpec.isEmpty()) { - uasserted( - 40180, - str::stream() << "an empty object is not a valid value. Found empty object at path " - << prefix.fullPath()); - } - for (auto&& elem : thisLevelSpec) { - auto fieldName = elem.fieldNameStringData(); - if (fieldName[0] == '$') { - // This object is an expression specification like {$add: [...]}. It will be parsed - // into an Expression later, but for now, just track that the prefix has been - // specified and skip it. - if (thisLevelSpec.nFields() != 1) { - uasserted(40181, - str::stream() << "an expression specification must contain exactly " - "one field, the name of the expression. Found " - << thisLevelSpec.nFields() << " fields in " - << thisLevelSpec.toString() << ", while parsing object " - << _rawObj.toString()); - } - ensurePathDoesNotConflictOrThrow(prefix.fullPath()); - continue; - } - if (fieldName.find('.') != std::string::npos) { - uasserted(40183, - str::stream() << "cannot use dotted field name '" << fieldName - << "' in a sub object: " << _rawObj.toString()); - } - parseElement(elem, FieldPath::getFullyQualifiedPath(prefix.fullPath(), fieldName)); - } -} - -namespace { - -using ComputedFieldsPolicy = ProjectionPolicies::ComputedFieldsPolicy; - -std::string makeBannedComputedFieldsErrorMessage(BSONObj projSpec) { - return str::stream() << "Bad projection specification, cannot use computed fields when parsing " - "a spec in kBanComputedFields mode: " - << projSpec.toString(); -} - -/** - * This class is responsible for determining what type of $project stage it specifies. - */ -class ProjectTypeParser { -public: - /** - * Parses 'spec' to determine whether it is an inclusion or exclusion projection. 'Computed' - * fields (ones which are defined by an expression or a literal) are treated as inclusion - * projections for in this context of the $project stage. - */ - static TransformerType parse(const BSONObj& spec, ProjectionPolicies policies) { - ProjectTypeParser parser(spec, policies); - parser.parse(); - invariant(parser._parsedType); - return *(parser._parsedType); - } - -private: - ProjectTypeParser(const BSONObj& spec, ProjectionPolicies policies) - : _rawObj(spec), _policies(policies) {} - - /** - * Traverses '_rawObj' to determine the type of projection, populating '_parsedType' in the - * process. - */ - void parse() { - size_t nFields = 0; - for (auto&& elem : _rawObj) { - parseElement(elem, FieldPath(elem.fieldName())); - nFields++; - } - - // Check for the case where we only exclude '_id'. - if (nFields == 1) { - BSONElement elem = _rawObj.firstElement(); - if (elem.fieldNameStringData() == "_id" && (elem.isBoolean() || elem.isNumber()) && - !elem.trueValue()) { - _parsedType = TransformerType::kExclusionProjection; - } - } - - // Default to inclusion if nothing (except maybe '_id') is explicitly included or excluded. - if (!_parsedType) { - _parsedType = TransformerInterface::TransformerType::kInclusionProjection; - } - } - - /** - * Parses a single BSONElement. 'pathToElem' should include the field name of 'elem'. - * - * Delegates to parseSubObject() if 'elem' is an object. Otherwise updates '_parsedType' if - * appropriate. - * - * Throws a AssertionException if this element represents a mix of projection types. If we are - * parsing in ComputedFieldsPolicy::kBanComputedFields mode, an inclusion projection - * which contains computed fields will also be rejected. - */ - void parseElement(const BSONElement& elem, const FieldPath& pathToElem) { - if (elem.type() == BSONType::Object) { - return parseNestedObject(elem.Obj(), pathToElem); - } - - // If this element is not a boolean or numeric value, then it is a literal value. These are - // illegal if we are in kBanComputedFields parse mode. - uassert(ErrorCodes::FailedToParse, - makeBannedComputedFieldsErrorMessage(_rawObj), - elem.isBoolean() || elem.isNumber() || - _policies.computedFieldsPolicy != ComputedFieldsPolicy::kBanComputedFields); - - if (pathToElem.fullPath() == "_id") { - // If the _id field is a computed value, then this must be an inclusion projection. If - // it is numeric or boolean, then this does not determine the projection type, due to - // the fact that inclusions may explicitly exclude _id and exclusions may include _id. - if (!elem.isBoolean() && !elem.isNumber()) { - uassert(ErrorCodes::FailedToParse, - str::stream() << "Bad projection specification, '_id' may not be a " - "computed field in an exclusion projection: " - << _rawObj.toString(), - !_parsedType || - _parsedType == - TransformerInterface::TransformerType::kInclusionProjection); - _parsedType = TransformerInterface::TransformerType::kInclusionProjection; - } - } else if ((elem.isBoolean() || elem.isNumber()) && !elem.trueValue()) { - // If this is an excluded field other than '_id', ensure that the projection type has - // not already been set to kInclusionProjection. - uassert( - 40178, - str::stream() << "Bad projection specification, cannot exclude fields " - "other than '_id' in an inclusion projection: " - << _rawObj.toString(), - !_parsedType || - (*_parsedType == TransformerInterface::TransformerType::kExclusionProjection)); - _parsedType = TransformerInterface::TransformerType::kExclusionProjection; - } else { - // A boolean true, a truthy numeric value, or any expression can only be used with an - // inclusion projection. Note that literal values like "string" or null are also treated - // as expressions. - uassert( - 40179, - str::stream() << "Bad projection specification, cannot include fields or " - "add computed fields during an exclusion projection: " - << _rawObj.toString(), - !_parsedType || - (*_parsedType == TransformerInterface::TransformerType::kInclusionProjection)); - _parsedType = TransformerInterface::TransformerType::kInclusionProjection; - } - } - - /** - * Traverses 'thisLevelSpec', parsing each element in turn. - * - * Throws a AssertionException if 'thisLevelSpec' represents an invalid mix of projections. If - * we are parsing in ComputedFieldsPolicy::kBanComputedFields mode, an inclusion - * projection which contains computed fields will also be rejected. - */ - void parseNestedObject(const BSONObj& thisLevelSpec, const FieldPath& prefix) { - - for (auto&& elem : thisLevelSpec) { - auto fieldName = elem.fieldNameStringData(); - if (fieldName[0] == '$') { - // This object is an expression specification like {$add: [...]}. It will be parsed - // into an Expression later, but for now, just track that the prefix has been - // specified, validate that computed projections are legal, and skip it. - uassert(ErrorCodes::FailedToParse, - makeBannedComputedFieldsErrorMessage(_rawObj), - _policies.computedFieldsPolicy != ComputedFieldsPolicy::kBanComputedFields); - uassert(40182, - str::stream() << "Bad projection specification, cannot include fields or " - "add computed fields during an exclusion projection: " - << _rawObj.toString(), - !_parsedType || - _parsedType == - TransformerInterface::TransformerType::kInclusionProjection); - _parsedType = TransformerInterface::TransformerType::kInclusionProjection; - continue; - } - parseElement(elem, FieldPath::getFullyQualifiedPath(prefix.fullPath(), fieldName)); - } - } - - // The original object. Used to generate more helpful error messages. - const BSONObj& _rawObj; - - // This will be populated during parse(). - boost::optional<TransformerType> _parsedType; - - // Policies associated with the projection which determine its runtime behaviour. - ProjectionPolicies _policies; -}; - -} // namespace - -std::unique_ptr<ParsedAggregationProjection> ParsedAggregationProjection::create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const BSONObj& spec, - ProjectionPolicies policies) { - // Checks that the specification was valid, and throws if it is not. - ProjectionSpecValidator::uassertValid(spec); - // Check for any conflicting specifications, and determine the type of the projection. - auto projectionType = ProjectTypeParser::parse(spec, policies); - // kComputed is a projection type reserved for $addFields, and should never be detected by the - // ProjectTypeParser. - invariant(projectionType != TransformerType::kComputedProjection); - - // We can't use make_unique() here, since the branches have different types. - std::unique_ptr<ParsedAggregationProjection> parsedProject( - projectionType == TransformerType::kInclusionProjection - ? static_cast<ParsedAggregationProjection*>( - new ParsedInclusionProjection(expCtx, policies)) - : static_cast<ParsedAggregationProjection*>( - new ParsedExclusionProjection(expCtx, policies))); - - // Actually parse the specification. - parsedProject->parse(spec); - - // If we got here, it means that parsing using ParsedAggProjection::parse() succeeded. - // Since the query system is currently between two implementations of projection, we want to - // make sure that the old version and new version agree on which projections are valid. Check - // that the new parser also finds this projection valid. - try { - projection_ast::parse(expCtx, spec, policies); - } catch (const DBException& e) { - error() << "old parser found projection " << spec << " valid but new parser returned " - << e.toStatus(); - MONGO_UNREACHABLE; - } - - return parsedProject; -} -} // namespace parsed_aggregation_projection -} // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection.h b/src/mongo/db/pipeline/parsed_aggregation_projection.h index e7ac7d2ab95..5c7e2a24b44 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection.h +++ b/src/mongo/db/pipeline/parsed_aggregation_projection.h @@ -47,95 +47,6 @@ class Document; class ExpressionContext; namespace parsed_aggregation_projection { - -/** - * 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 both - * $project specifications and $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 ParsedAggregationProjection is responsible for parsing and executing a $project. It * represents either an inclusion or exclusion projection. This is the common interface between the @@ -150,26 +61,6 @@ public: static constexpr StringData kProjectionPostImageVarName{"INTERNAL_PROJ_POST_IMAGE"_sd}; /** - * Main entry point for a ParsedAggregationProjection. - * - * Throws a AssertionException if 'spec' is an invalid projection specification. - */ - static std::unique_ptr<ParsedAggregationProjection> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const BSONObj& spec, - ProjectionPolicies policies); - - virtual ~ParsedAggregationProjection() = default; - - /** - * Parse the user-specified BSON object 'spec'. By the time this is called, 'spec' has already - * been verified to not have any conflicting path specifications, and not to mix and match - * inclusions and exclusions. 'variablesParseState' is used by any contained expressions to - * track which variables are defined so that they can later be referenced at execution time. - */ - virtual void parse(const BSONObj& spec) = 0; - - /** * Optimize any expressions contained within this projection. */ virtual void optimize() { diff --git a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp index 3f12abac9b5..337d9f74307 100644 --- a/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp @@ -38,8 +38,10 @@ #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 { @@ -52,20 +54,24 @@ BSONObj wrapInLiteral(const T& arg) { } // Helper to simplify the creation of a ParsedAggregationProjection with default policies. -std::unique_ptr<ParsedAggregationProjection> makeProjectionWithDefaultPolicies(BSONObj spec) { +auto makeProjectionWithDefaultPolicies(BSONObj spec) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ProjectionPolicies defaultPolicies; - return ParsedAggregationProjection::create(expCtx, spec, 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. -std::unique_ptr<ParsedAggregationProjection> makeProjectionWithBannedComputedFields(BSONObj spec) { +auto makeProjectionWithBannedComputedFields(BSONObj spec) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); ProjectionPolicies banComputedFields{ ProjectionPolicies::kDefaultIdPolicyDefault, ProjectionPolicies::kArrayRecursionPolicyDefault, ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields}; - return ParsedAggregationProjection::create(expCtx, spec, banComputedFields); + auto projection = projection_ast::parse(expCtx, spec, banComputedFields); + return projection_executor::buildProjectionExecutor( + expCtx, &projection, banComputedFields, true /* optimizeExecutor */); } // @@ -274,13 +280,6 @@ TEST(ParsedAggregationProjectionErrors, ShouldRejectMixOfExclusionAndComputedFie AssertionException); } -TEST(ParsedAggregationProjectionErrors, ShouldRejectDottedFieldInSubDocument) { - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << true))), - AssertionException); - ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << wrapInLiteral(1)))), - AssertionException); -} - TEST(ParsedAggregationProjectionErrors, ShouldRejectFieldNamesStartingWithADollar) { ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 0)), AssertionException); ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 1)), AssertionException); @@ -383,6 +382,17 @@ TEST(ParsedAggregationProjectionErrors, ShouldNotErrorOnTwoNestedFields) { // 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); diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection.cpp deleted file mode 100644 index de31a6cb7e4..00000000000 --- a/src/mongo/db/pipeline/parsed_exclusion_projection.cpp +++ /dev/null @@ -1,116 +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_exclusion_projection.h" - -#include <memory> - -#include "mongo/db/exec/document_value/document.h" -#include "mongo/db/exec/document_value/value.h" -#include "mongo/db/pipeline/field_path.h" - -namespace mongo { - -namespace parsed_aggregation_projection { - -Document ParsedExclusionProjection::serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const { - return _root->serialize(explain); -} - -Document ParsedExclusionProjection::applyProjection(const Document& inputDoc) const { - return _root->applyToDocument(inputDoc); -} - -void ParsedExclusionProjection::parse(const BSONObj& spec, ExclusionNode* node, size_t depth) { - bool idSpecified = false; - - for (auto elem : spec) { - const auto fieldName = elem.fieldNameStringData(); - - // Track whether the projection spec specifies a desired behavior for the _id field. - idSpecified = idSpecified || fieldName == "_id"_sd || fieldName.startsWith("_id."_sd); - - switch (elem.type()) { - case BSONType::Bool: - case BSONType::NumberInt: - case BSONType::NumberLong: - case BSONType::NumberDouble: - case BSONType::NumberDecimal: { - // We have already verified this is an exclusion projection. _id is the only field - // which is permitted to be explicitly included here. - invariant(!elem.trueValue() || elem.fieldNameStringData() == "_id"_sd); - if (!elem.trueValue()) { - node->addProjectionForPath(FieldPath(fieldName)); - } - break; - } - case BSONType::Object: { - // This object represents a nested projection specification, like the sub-object in - // {a: {b: 0, c: 0}} or {"a.b": {c: 0}}. - ExclusionNode* child; - - if (elem.fieldNameStringData().find('.') == std::string::npos) { - child = node->addOrGetChild(fieldName.toString()); - } else { - // A dotted field is not allowed in a sub-object, and should have been detected - // in ParsedAggregationProjection's parsing before we get here. - invariant(depth == 0); - - // We need to keep adding children to our tree until we create a child that - // represents this dotted path. - child = node; - auto fullPath = FieldPath(fieldName); - while (fullPath.getPathLength() > 1) { - child = child->addOrGetChild(fullPath.getFieldName(0).toString()); - fullPath = fullPath.tail(); - } - // It is illegal to construct an empty FieldPath, so the above loop ends one - // iteration too soon. Add the last path here. - child = child->addOrGetChild(fullPath.fullPath()); - } - - parse(elem.Obj(), child, depth + 1); - break; - } - default: { MONGO_UNREACHABLE; } - } - } - - // If _id was not specified, then doing nothing will cause it to be included. If the default _id - // policy is kExcludeId, we add a new entry for _id to the ExclusionNode tree here. - if (!idSpecified && _policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kExcludeId) { - _root->addProjectionForPath({FieldPath("_id")}); - } -} - -} // namespace parsed_aggregation_projection -} // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection.h b/src/mongo/db/pipeline/parsed_exclusion_projection.h index b25e97329d7..65592682f33 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection.h +++ b/src/mongo/db/pipeline/parsed_exclusion_projection.h @@ -104,19 +104,16 @@ public: } Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final; - - /** - * Parses the projection specification given by 'spec', populating internal data structures. - */ - void parse(const BSONObj& spec) final { - parse(spec, _root.get(), 0); + boost::optional<ExplainOptions::Verbosity> explain) const final { + return _root->serialize(explain); } /** * Exclude the fields specified. */ - Document applyProjection(const Document& inputDoc) const final; + Document applyProjection(const Document& inputDoc) const final { + return _root->applyToDocument(inputDoc); + } DepsTracker::State addDependencies(DepsTracker* deps) const final { if (_rootReplacementExpression) { @@ -138,15 +135,6 @@ public: } private: - /** - * Helper for parse() above. - * - * Traverses 'spec' and parses each field. Adds any excluded fields at this level to 'node', - * and recurses on any sub-objects. - */ - void parse(const BSONObj& spec, ExclusionNode* node, size_t depth); - - // The ExclusionNode tree does most of the execution work once constructed. std::unique_ptr<ExclusionNode> _root; }; diff --git a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp index 485691472a5..72a9276d7c1 100644 --- a/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp @@ -41,8 +41,10 @@ #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" +#include "mongo/db/exec/projection_executor.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" @@ -52,64 +54,44 @@ namespace { using std::vector; -// Helper to simplify the creation of a ParsedExclusionProjection with default policies. -ParsedExclusionProjection makeExclusionProjectionWithDefaultPolicies() { +auto createProjectionExecutor(const BSONObj& spec, const ProjectionPolicies& policies) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ProjectionPolicies defaultPolicies; - return {expCtx, defaultPolicies}; + auto projection = projection_ast::parse(expCtx, spec, policies); + auto executor = projection_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. +auto makeExclusionProjectionWithDefaultPolicies(const BSONObj& spec) { + return createProjectionExecutor(spec, {}); } // Helper to simplify the creation of a ParsedExclusionProjection which excludes _id by default. -ParsedExclusionProjection makeExclusionProjectionWithDefaultIdExclusion() { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); +auto makeExclusionProjectionWithDefaultIdExclusion(const BSONObj& spec) { ProjectionPolicies defaultExcludeId{ProjectionPolicies::DefaultIdPolicy::kExcludeId, ProjectionPolicies::kArrayRecursionPolicyDefault, ProjectionPolicies::kComputedFieldsPolicyDefault}; - return {expCtx, defaultExcludeId}; + return createProjectionExecutor(spec, defaultExcludeId); } // Helper to simplify the creation of a ParsedExclusionProjection which does not recurse arrays. -ParsedExclusionProjection makeExclusionProjectionWithNoArrayRecursion() { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); +auto makeExclusionProjectionWithNoArrayRecursion(const BSONObj& spec) { ProjectionPolicies noArrayRecursion{ ProjectionPolicies::kDefaultIdPolicyDefault, ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays, ProjectionPolicies::kComputedFieldsPolicyDefault}; - - return {expCtx, noArrayRecursion}; -} - -// -// Errors. -// - -DEATH_TEST(ExclusionProjectionExecutionTest, - ShouldFailWhenGivenIncludedNonIdField, - "Invariant failure !elem.trueValue() || elem.fieldNameStringData() == \"_id\"_sd") { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << true)); -} - -DEATH_TEST(ExclusionProjectionExecutionTest, - ShouldFailWhenGivenIncludedIdSubfield, - "Invariant failure !elem.trueValue() || elem.fieldNameStringData() == \"_id\"_sd") { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id.id1" << true)); -} - -TEST(ExclusionProjectionExecutionTest, ShouldAllowExplicitIdInclusionInExclusionSpec) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id" << true << "a" << false)); + return createProjectionExecutor(spec, noArrayRecursion); } TEST(ExclusionProjectionExecutionTest, ShouldSerializeToEquivalentProjection) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse( + auto exclusion = makeExclusionProjectionWithDefaultPolicies( fromjson("{a: 0, b: {c: NumberLong(0), d: 0.0}, 'x.y': false, _id: NumberInt(0)}")); // Converts numbers to bools, converts dotted paths to nested documents. Note order of excluded // fields is subject to change. - auto serialization = exclusion.serializeTransformation(boost::none); + auto serialization = exclusion->serializeTransformation(boost::none); ASSERT_EQ(serialization.size(), 4UL); ASSERT_VALUE_EQ(serialization["a"], Value(false)); ASSERT_VALUE_EQ(serialization["_id"], Value(false)); @@ -133,11 +115,11 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotAddAnyDependencies) { // need to include the "a" in the dependencies of this projection, since it will just be ignored // later. If there are no later stages, then we will finish the dependency computation // cycle without full knowledge of which fields are needed, and thus include all the fields. - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id" << false << "a" << false << "b.c" << false << "x.y.z" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies( + BSON("_id" << false << "a" << false << "b.c" << false << "x.y.z" << false)); DepsTracker deps; - exclusion.addDependencies(&deps); + exclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 0UL); ASSERT_FALSE(deps.needWholeDocument); @@ -145,10 +127,10 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotAddAnyDependencies) { } TEST(ExclusionProjectionExecutionTest, ShouldReportExcludedFieldsAsModified) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id" << false << "a" << false << "b.c" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies( + BSON("_id" << false << "a" << false << "b.c" << false)); - auto modifiedPaths = exclusion.getModifiedPaths(); + auto modifiedPaths = exclusion->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kFiniteSet); ASSERT_EQ(modifiedPaths.paths.count("_id"), 1UL); ASSERT_EQ(modifiedPaths.paths.count("a"), 1UL); @@ -158,10 +140,10 @@ TEST(ExclusionProjectionExecutionTest, ShouldReportExcludedFieldsAsModified) { TEST(ExclusionProjectionExecutionTest, ShouldReportExcludedFieldsAsModifiedWhenSpecifiedAsNestedObj) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << BSON("b" << false << "c" << BSON("d" << false)))); + auto exclusion = makeExclusionProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << false << "c" << BSON("d" << false)))); - auto modifiedPaths = exclusion.getModifiedPaths(); + auto modifiedPaths = exclusion->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kFiniteSet); ASSERT_EQ(modifiedPaths.paths.count("a.b"), 1UL); ASSERT_EQ(modifiedPaths.paths.count("a.c.d"), 1UL); @@ -173,69 +155,65 @@ TEST(ExclusionProjectionExecutionTest, // TEST(ExclusionProjectionExecutionTest, ShouldExcludeTopLevelField) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a" << false)); // More than one field in document. - auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}}); + auto result = exclusion->applyTransformation(Document{{"a", 1}, {"b", 2}}); auto expectedResult = Document{{"b", 2}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is the only field in the document. - result = exclusion.applyProjection(Document{{"a", 1}}); + result = exclusion->applyTransformation(Document{{"a", 1}}); expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is not present in the document. - result = exclusion.applyProjection(Document{{"c", 1}}); + result = exclusion->applyTransformation(Document{{"c", 1}}); expectedResult = Document{{"c", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // There are no fields in the document. - result = exclusion.applyProjection(Document{}); + result = exclusion->applyTransformation(Document{}); expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldCoerceNumericsToBools) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << Value(0) << "b" << Value(0LL) << "c" << Value(0.0) << "d" - << Value(Decimal128(0)))); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON( + "a" << Value(0) << "b" << Value(0LL) << "c" << Value(0.0) << "d" << Value(Decimal128(0)))); auto result = - exclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}, {"c", 3}}); + exclusion->applyTransformation(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}, {"c", 3}}); auto expectedResult = Document{{"_id", "ID"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldPreserveOrderOfExistingFields) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("second" << false)); - auto result = exclusion.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}}); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("second" << false)); + auto result = + exclusion->applyTransformation(Document{{"first", 0}, {"second", 1}, {"third", 2}}); auto expectedResult = Document{{"first", 0}, {"third", 2}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldImplicitlyIncludeId) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << false)); - auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a" << false)); + auto result = exclusion->applyTransformation(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); auto expectedResult = Document{{"b", 2}, {"_id", "ID"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdIfExplicitlyExcluded) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << false << "_id" << false)); - auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); + auto exclusion = + makeExclusionProjectionWithDefaultPolicies(BSON("a" << false << "_id" << false)); + auto result = exclusion->applyTransformation(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); auto expectedResult = Document{{"b", 2}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdAndKeepAllOtherFields) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id" << false)); - auto result = exclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("_id" << false)); + auto result = exclusion->applyTransformation(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); auto expectedResult = Document{{"a", 1}, {"b", 2}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } @@ -245,57 +223,54 @@ TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdAndKeepAllOtherFields) { // TEST(ExclusionProjectionExecutionTest, ShouldExcludeSubFieldsOfId) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id.x" << false << "_id" << BSON("y" << false))); - auto result = exclusion.applyProjection( + auto exclusion = makeExclusionProjectionWithDefaultPolicies( + BSON("_id.x" << false << "_id" << BSON("y" << false))); + auto result = exclusion->applyTransformation( Document{{"_id", Document{{"x", 1}, {"y", 2}, {"z", 3}}}, {"a", 1}}); auto expectedResult = Document{{"_id", Document{{"z", 3}}}, {"a", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldExcludeSimpleDottedFieldFromSubDoc) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a.b" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a.b" << false)); // More than one field in sub document. - auto result = exclusion.applyProjection(Document{{"a", Document{{"b", 1}, {"c", 2}}}}); + auto result = exclusion->applyTransformation(Document{{"a", Document{{"b", 1}, {"c", 2}}}}); auto expectedResult = Document{{"a", Document{{"c", 2}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is the only field in the sub document. - result = exclusion.applyProjection(Document{{"a", Document{{"b", 1}}}}); + result = exclusion->applyTransformation(Document{{"a", Document{{"b", 1}}}}); expectedResult = Document{{"a", Document{}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is not present in the sub document. - result = exclusion.applyProjection(Document{{"a", Document{{"c", 1}}}}); + result = exclusion->applyTransformation(Document{{"a", Document{{"c", 1}}}}); expectedResult = Document{{"a", Document{{"c", 1}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // There are no fields in sub document. - result = exclusion.applyProjection(Document{{"a", Document{}}}); + result = exclusion->applyTransformation(Document{{"a", Document{}}}); expectedResult = Document{{"a", Document{}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldNotCreateSubDocIfDottedExcludedFieldDoesNotExist) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("sub.target" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("sub.target" << false)); // Should not add the path if it doesn't exist. - auto result = exclusion.applyProjection(Document{}); + auto result = exclusion->applyTransformation(Document{}); auto expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Should not replace non-documents with documents. - result = exclusion.applyProjection(Document{{"sub", "notADocument"_sd}}); + result = exclusion->applyTransformation(Document{{"sub", "notADocument"_sd}}); expectedResult = Document{{"sub", "notADocument"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldApplyDottedExclusionToEachElementInArray) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a.b" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a.b" << false)); std::vector<Value> nestedValues = { Value(1), @@ -311,32 +286,30 @@ TEST(ExclusionProjectionExecutionTest, ShouldApplyDottedExclusionToEachElementIn Value(Document{{"c", 2}}), Value(vector<Value>{}), Value(vector<Value>{Value(1), Value(Document{{"c", 1}})})}; - auto result = exclusion.applyProjection(Document{{"a", nestedValues}}); + auto result = exclusion->applyTransformation(Document{{"a", nestedValues}}); auto expectedResult = Document{{"a", expectedNestedValues}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); // Exclude all of "a.b", "a.c", "a.d", and "a.e". - exclusion.parse( + auto exclusion = makeExclusionProjectionWithDefaultPolicies( BSON("a.b" << false << "a.c" << false << "a" << BSON("d" << false << "e" << false))); - auto result = exclusion.applyProjection( + auto result = exclusion->applyTransformation( Document{{"a", Document{{"b", 1}, {"c", 2}, {"d", 3}, {"e", 4}, {"f", 5}}}}); auto expectedResult = Document{{"a", Document{{"f", 5}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a" << false)); MutableDocument inputDocBuilder(Document{{"_id", "ID"_sd}, {"a", 1}}); inputDocBuilder.metadata().setRandVal(1.0); inputDocBuilder.metadata().setTextScore(10.0); Document inputDoc = inputDocBuilder.freeze(); - auto result = exclusion.applyProjection(inputDoc); + auto result = exclusion->applyTransformation(inputDoc); MutableDocument expectedDoc(Document{{"_id", "ID"_sd}}); expectedDoc.copyMetaDataFrom(inputDoc); @@ -348,50 +321,48 @@ TEST(ExclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) // TEST(ExclusionProjectionExecutionTest, ShouldIncludeIdByDefault) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a" << false)); - auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = exclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{{"_id", 2}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldExcludeIdWithExplicitPolicy) { - auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); - exclusion.parse(BSON("a" << false)); + auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(BSON("a" << false)); - auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = exclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitExcludeIdSpec) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id" << false << "a" << false)); + auto exclusion = + makeExclusionProjectionWithDefaultPolicies(BSON("_id" << false << "a" << false)); - auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = exclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIncludeIdSpec) { - auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); - exclusion.parse(BSON("_id" << true << "a" << false)); + auto exclusion = + makeExclusionProjectionWithDefaultIdExclusion(BSON("_id" << true << "a" << false)); - auto result = exclusion.applyProjection(Document{{"_id", 2}, {"a", 3}, {"b", 4}}); + auto result = exclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}, {"b", 4}}); auto expectedResult = Document{{"_id", 2}, {"b", 4}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefaultIncludePolicy) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("_id.id1" << false << "a" << false)); + auto exclusion = + makeExclusionProjectionWithDefaultPolicies(BSON("_id.id1" << false << "a" << false)); - auto result = exclusion.applyProjection( + auto result = exclusion->applyTransformation( Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}}); auto expectedResult = Document{{"_id", Document{{"id2", 2}}}, {"b", 4}}; @@ -399,10 +370,10 @@ TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefau } TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefaultExcludePolicy) { - auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); - exclusion.parse(BSON("_id.id1" << false << "a" << false)); + auto exclusion = + makeExclusionProjectionWithDefaultIdExclusion(BSON("_id.id1" << false << "a" << false)); - auto result = exclusion.applyProjection( + auto result = exclusion->applyTransformation( Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}}); auto expectedResult = Document{{"_id", Document{{"id2", 2}}}, {"b", 4}}; @@ -410,11 +381,10 @@ TEST(ExclusionProjectionExecutionTest, ShouldAllowExclusionOfIdSubfieldWithDefau } TEST(ExclusionProjectionExecutionTest, ShouldAllowLimitedDollarPrefixedFields) { - auto exclusion = makeExclusionProjectionWithDefaultIdExclusion(); - exclusion.parse( + auto exclusion = makeExclusionProjectionWithDefaultIdExclusion( BSON("$id" << false << "$db" << false << "$ref" << false << "$sortKey" << false)); - auto result = exclusion.applyProjection( + auto result = exclusion->applyTransformation( Document{{"$id", 5}, {"$db", 3}, {"$ref", 4}, {"$sortKey", 5}, {"someField", 6}}); auto expectedResult = Document{{"someField", 6}}; @@ -426,11 +396,10 @@ TEST(ExclusionProjectionExecutionTest, ShouldAllowLimitedDollarPrefixedFields) { // TEST(ExclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) { - auto exclusion = makeExclusionProjectionWithDefaultPolicies(); - exclusion.parse(BSON("a.b" << false)); + auto exclusion = makeExclusionProjectionWithDefaultPolicies(BSON("a.b" << false)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{c: 5}], {d: 6}]} - auto result = exclusion.applyProjection( + auto result = exclusion->applyTransformation( Document{{"a", vector<Value>{Value(1), Value(Document{{"b", 2}, {"c", 3}}), @@ -447,12 +416,11 @@ TEST(ExclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) { } TEST(ExclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecursePolicy) { - auto exclusion = makeExclusionProjectionWithNoArrayRecursion(); - exclusion.parse(BSON("a.b" << false)); + auto exclusion = makeExclusionProjectionWithNoArrayRecursion(BSON("a.b" << false)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [1, {c: 3}, [{b: 4, c: 5}], {d: // 6}]} - auto result = exclusion.applyProjection( + auto result = exclusion->applyTransformation( Document{{"a", vector<Value>{Value(1), Value(Document{{"b", 2}, {"c", 3}}), @@ -470,8 +438,7 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecurseP } TEST(ExclusionProjectionExecutionTest, ShouldNotRetainNestedArraysIfNoRecursionNeeded) { - auto exclusion = makeExclusionProjectionWithNoArrayRecursion(); - exclusion.parse(BSON("a" << false)); + auto exclusion = makeExclusionProjectionWithNoArrayRecursion(BSON("a" << false)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {} const auto inputDoc = @@ -481,12 +448,11 @@ TEST(ExclusionProjectionExecutionTest, ShouldNotRetainNestedArraysIfNoRecursionN Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}), Value(Document{{"d", 6}})}}}; - auto result = exclusion.applyProjection(inputDoc); + auto result = exclusion->applyTransformation(inputDoc); const auto expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); } - } // 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 index 66fd72ff24e..27e2ef8c3f8 100644 --- a/src/mongo/db/pipeline/parsed_find_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_find_projection_test.cpp @@ -30,22 +30,32 @@ #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 proj = ParsedAggregationProjection::create(getExpCtx(), projSpec, {}); + auto executor = createProjectionExecutor(getExpCtx(), projSpec, {}); auto matchExpr = CopyableMatchExpression{matchSpec, getExpCtx(), std::make_unique<ExtensionsCallbackNoop>(), @@ -57,8 +67,8 @@ protected: getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), path, std::move(matchExpr)); - proj->setRootReplacementExpression(expr); - return proj->applyTransformation(input); + executor->setRootReplacementExpression(expr); + return executor->applyTransformation(input); } }; @@ -69,7 +79,7 @@ protected: boost::optional<int> skip, int limit, const Document& input) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), projSpec, {}); + auto executor = createProjectionExecutor(getExpCtx(), projSpec, {}); auto expr = make_intrusive<ExpressionInternalFindSlice>( getExpCtx(), ExpressionFieldPath::parse( @@ -77,8 +87,8 @@ protected: path, skip, limit); - proj->setRootReplacementExpression(expr); - return proj->applyTransformation(input); + executor->setRootReplacementExpression(expr); + return executor->applyTransformation(input); } }; @@ -105,7 +115,7 @@ TEST_F(PositionalProjectionExecutionTest, AppliesProjectionToPreImage) { } TEST_F(PositionalProjectionExecutionTest, ShouldAddInclusionFieldsAndWholeDocumentToDependencies) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); auto matchSpec = fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"); auto matchExpr = CopyableMatchExpression{matchSpec, getExpCtx(), @@ -118,10 +128,10 @@ TEST_F(PositionalProjectionExecutionTest, ShouldAddInclusionFieldsAndWholeDocume getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), "foo.bar", std::move(matchExpr)); - proj->setRootReplacementExpression(expr); + executor->setRootReplacementExpression(expr); DepsTracker deps; - proj->addDependencies(&deps); + executor->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 2UL); ASSERT_EQ(deps.fields.count("bar"), 1UL); @@ -130,7 +140,7 @@ TEST_F(PositionalProjectionExecutionTest, ShouldAddInclusionFieldsAndWholeDocume } TEST_F(PositionalProjectionExecutionTest, ShouldConsiderAllPathsAsModified) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); auto matchSpec = fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"); auto matchExpr = CopyableMatchExpression{matchSpec, getExpCtx(), @@ -143,9 +153,9 @@ TEST_F(PositionalProjectionExecutionTest, ShouldConsiderAllPathsAsModified) { getExpCtx(), "$$" + kProjectionPostImageVarName, getExpCtx()->variablesParseState), "foo.bar", std::move(matchExpr)); - proj->setRootReplacementExpression(expr); + executor->setRootReplacementExpression(expr); - auto modifiedPaths = proj->getModifiedPaths(); + auto modifiedPaths = executor->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); } @@ -173,7 +183,7 @@ TEST_F(SliceProjectionExecutionTest, AppliesProjectionToPostImage) { } TEST_F(SliceProjectionExecutionTest, CanApplySliceAndPositionalProjectionsTogether) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{foo: 1, bar: 1}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{foo: 1, bar: 1}"), {}); auto matchSpec = fromjson("{foo: {$gte: 3}}"); auto matchExpr = CopyableMatchExpression{matchSpec, getExpCtx(), @@ -188,11 +198,11 @@ TEST_F(SliceProjectionExecutionTest, CanApplySliceAndPositionalProjectionsTogeth std::move(matchExpr)); auto sliceExpr = make_intrusive<ExpressionInternalFindSlice>(getExpCtx(), positionalExpr, "bar", 1, 1); - proj->setRootReplacementExpression(sliceExpr); + executor->setRootReplacementExpression(sliceExpr); ASSERT_DOCUMENT_EQ( Document{fromjson("{foo: [3], bar: [6]}")}, - proj->applyTransformation(Document{fromjson("{foo: [1,2,3,4], bar: [5,6,7,8]}")})); + executor->applyTransformation(Document{fromjson("{foo: [1,2,3,4], bar: [5,6,7,8]}")})); } TEST_F(SliceProjectionExecutionTest, CanApplySliceWithExclusionProjection) { @@ -204,7 +214,7 @@ TEST_F(SliceProjectionExecutionTest, CanApplySliceWithExclusionProjection) { TEST_F(SliceProjectionExecutionTest, ShouldAddFieldsAndWholeDocumentToDependenciesWithInclusionProjection) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1, _id: 0}"), {}); auto expr = make_intrusive<ExpressionInternalFindSlice>( getExpCtx(), ExpressionFieldPath::parse( @@ -212,10 +222,10 @@ TEST_F(SliceProjectionExecutionTest, "foo.bar", 1, 1); - proj->setRootReplacementExpression(expr); + executor->setRootReplacementExpression(expr); DepsTracker deps; - proj->addDependencies(&deps); + executor->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 1UL); ASSERT_EQ(deps.fields.count("bar"), 1UL); @@ -223,7 +233,7 @@ TEST_F(SliceProjectionExecutionTest, } TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithInclusionProjection) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{bar: 1}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 1}"), {}); auto expr = make_intrusive<ExpressionInternalFindSlice>( getExpCtx(), ExpressionFieldPath::parse( @@ -231,14 +241,14 @@ TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithInclusi "foo.bar", 1, 1); - proj->setRootReplacementExpression(expr); + executor->setRootReplacementExpression(expr); - auto modifiedPaths = proj->getModifiedPaths(); + auto modifiedPaths = executor->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); } TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithExclusionProjection) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{bar: 0}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 0}"), {}); auto expr = make_intrusive<ExpressionInternalFindSlice>( getExpCtx(), ExpressionFieldPath::parse( @@ -246,14 +256,14 @@ TEST_F(SliceProjectionExecutionTest, ShouldConsiderAllPathsAsModifiedWithExclusi "foo.bar", 1, 1); - proj->setRootReplacementExpression(expr); + executor->setRootReplacementExpression(expr); - auto modifiedPaths = proj->getModifiedPaths(); + auto modifiedPaths = executor->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllPaths); } TEST_F(SliceProjectionExecutionTest, ShouldAddWholeDocumentToDependenciesWithExclusionProjection) { - auto proj = ParsedAggregationProjection::create(getExpCtx(), fromjson("{bar: 0}"), {}); + auto executor = createProjectionExecutor(getExpCtx(), fromjson("{bar: 0}"), {}); auto expr = make_intrusive<ExpressionInternalFindSlice>( getExpCtx(), ExpressionFieldPath::parse( @@ -261,10 +271,10 @@ TEST_F(SliceProjectionExecutionTest, ShouldAddWholeDocumentToDependenciesWithExc "foo.bar", 1, 1); - proj->setRootReplacementExpression(expr); + executor->setRootReplacementExpression(expr); DepsTracker deps; - proj->addDependencies(&deps); + executor->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 0UL); ASSERT(deps.needWholeDocument); diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection.cpp deleted file mode 100644 index bcc3f28fd59..00000000000 --- a/src/mongo/db/pipeline/parsed_inclusion_projection.cpp +++ /dev/null @@ -1,219 +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_inclusion_projection.h" - -#include <algorithm> - -namespace mongo { - -namespace parsed_aggregation_projection { - -// -// InclusionNode -// - -InclusionNode::InclusionNode(ProjectionPolicies policies, std::string pathToNode) - : ProjectionNode(policies, std::move(pathToNode)) {} - -InclusionNode* InclusionNode::addOrGetChild(const std::string& field) { - return static_cast<InclusionNode*>(ProjectionNode::addOrGetChild(field)); -} - -void InclusionNode::reportDependencies(DepsTracker* deps) const { - for (auto&& includedField : _projectedFields) { - deps->fields.insert(FieldPath::getFullyQualifiedPath(_pathToNode, includedField)); - } - - if (!_pathToNode.empty() && !_expressions.empty()) { - // The shape of any computed fields in the output will change depending on if the field is - // an array or not, so in addition to any dependencies of the expression itself, we need to - // add this field to our dependencies. - deps->fields.insert(_pathToNode); - } - - for (auto&& expressionPair : _expressions) { - expressionPair.second->addDependencies(deps); - } - for (auto&& childPair : _children) { - childPair.second->reportDependencies(deps); - } -} - -// -// ParsedInclusionProjection -// - -void ParsedInclusionProjection::parse(const BSONObj& spec) { - // It is illegal to specify an inclusion with no output fields. - bool atLeastOneFieldInOutput = false; - - // Tracks whether or not we should apply the default _id projection policy. - bool idSpecified = false; - - for (auto elem : spec) { - auto fieldName = elem.fieldNameStringData(); - idSpecified = idSpecified || fieldName == "_id"_sd || fieldName.startsWith("_id."_sd); - if (fieldName == "_id") { - const bool idIsExcluded = (!elem.trueValue() && (elem.isNumber() || elem.isBoolean())); - if (idIsExcluded) { - // Ignoring "_id" here will cause it to be excluded from result documents. - _idExcluded = true; - continue; - } - - // At least part of "_id" is included or a computed field. Fall through to below to - // parse what exactly "_id" was specified as. - } - - atLeastOneFieldInOutput = true; - switch (elem.type()) { - case BSONType::Bool: - case BSONType::NumberInt: - case BSONType::NumberLong: - case BSONType::NumberDouble: - case BSONType::NumberDecimal: { - // This is an inclusion specification. - invariant(elem.trueValue()); - _root->addProjectionForPath(FieldPath(elem.fieldName())); - break; - } - case BSONType::Object: { - // This is either an expression, or a nested specification. - if (parseObjectAsExpression(fieldName, elem.Obj(), _expCtx->variablesParseState)) { - // It was an expression. - break; - } - - // The field name might be a dotted path. If so, we need to keep adding children - // to our tree until we create a child that represents that path. - auto remainingPath = FieldPath(elem.fieldName()); - auto* child = _root.get(); - while (remainingPath.getPathLength() > 1) { - child = child->addOrGetChild(remainingPath.getFieldName(0).toString()); - remainingPath = remainingPath.tail(); - } - // It is illegal to construct an empty FieldPath, so the above loop ends one - // iteration too soon. Add the last path here. - child = child->addOrGetChild(remainingPath.fullPath()); - - parseSubObject(elem.Obj(), _expCtx->variablesParseState, child); - break; - } - default: { - // This is a literal value. - _root->addExpressionForPath( - FieldPath(elem.fieldName()), - Expression::parseOperand(_expCtx, elem, _expCtx->variablesParseState)); - } - } - } - - if (!idSpecified) { - // _id wasn't specified, so apply the default _id projection policy here. - if (_policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kExcludeId) { - _idExcluded = true; - } else { - atLeastOneFieldInOutput = true; - _root->addProjectionForPath(FieldPath("_id")); - } - } - - uassert(16403, - str::stream() << "$project requires at least one output field: " << spec.toString(), - atLeastOneFieldInOutput); -} - -Document ParsedInclusionProjection::applyProjection(const Document& inputDoc) const { - // All expressions will be evaluated in the context of the input document, before any - // transformations have been applied. - return _root->applyToDocument(inputDoc); -} - -bool ParsedInclusionProjection::parseObjectAsExpression( - StringData pathToObject, - const BSONObj& objSpec, - const VariablesParseState& variablesParseState) { - if (objSpec.firstElementFieldName()[0] == '$') { - // This is an expression like {$add: [...]}. We have already verified that it has only one - // field. - invariant(objSpec.nFields() == 1); - _root->addExpressionForPath( - pathToObject, Expression::parseExpression(_expCtx, objSpec, variablesParseState)); - return true; - } - return false; -} - -void ParsedInclusionProjection::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 disallowed in - // ParsedAggregationProjection's parsing. - invariant(elem.fieldNameStringData().find('.') == std::string::npos); - - switch (elem.type()) { - case BSONType::Bool: - case BSONType::NumberInt: - case BSONType::NumberLong: - case BSONType::NumberDouble: - case BSONType::NumberDecimal: { - // This is an inclusion specification. - invariant(elem.trueValue()); - node->addProjectionForPath(FieldPath(elem.fieldName())); - break; - } - case BSONType::Object: { - // This is either an expression, or a nested specification. - auto fieldName = elem.fieldNameStringData().toString(); - if (parseObjectAsExpression( - FieldPath::getFullyQualifiedPath(node->getPath(), fieldName), - elem.Obj(), - variablesParseState)) { - break; - } - auto* child = node->addOrGetChild(fieldName); - parseSubObject(elem.Obj(), variablesParseState, child); - break; - } - default: { - // This is a literal value. - node->addExpressionForPath( - FieldPath(elem.fieldName()), - Expression::parseOperand(_expCtx, elem, variablesParseState)); - } - } - } -} -} // namespace parsed_aggregation_projection -} // namespace mongo diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection.h b/src/mongo/db/pipeline/parsed_inclusion_projection.h index bab0905426d..7bc69dd8cad 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection.h +++ b/src/mongo/db/pipeline/parsed_inclusion_projection.h @@ -53,11 +53,33 @@ namespace parsed_aggregation_projection { */ class InclusionNode final : public ProjectionNode { public: - InclusionNode(ProjectionPolicies policies, std::string pathToNode = ""); + InclusionNode(ProjectionPolicies policies, std::string pathToNode = "") + : ProjectionNode(policies, std::move(pathToNode)) {} - InclusionNode* addOrGetChild(const std::string& field); + InclusionNode* addOrGetChild(const std::string& field) { + return static_cast<InclusionNode*>(ProjectionNode::addOrGetChild(field)); + } + + void reportDependencies(DepsTracker* deps) const final { + for (auto&& includedField : _projectedFields) { + deps->fields.insert(FieldPath::getFullyQualifiedPath(_pathToNode, includedField)); + } - void reportDependencies(DepsTracker* deps) const final; + if (!_pathToNode.empty() && !_expressions.empty()) { + // The shape of any computed fields in the output will change depending on if the field + // is an array or not, so in addition to any dependencies of the expression itself, we + // need to add this field to our dependencies. + deps->fields.insert(_pathToNode); + } + + for (auto&& expressionPair : _expressions) { + expressionPair.second->addDependencies(deps); + } + + for (auto&& childPair : _children) { + childPair.second->reportDependencies(deps); + } + } protected: // For inclusions, we can apply an optimization here by simply appending to the output document @@ -111,20 +133,17 @@ public: } /** - * Parses the projection specification given by 'spec', populating internal data structures. - */ - void parse(const BSONObj& spec) final; - - /** * Serialize the projection. */ Document serializeTransformation( boost::optional<ExplainOptions::Verbosity> explain) const final { MutableDocument output; - if (_idExcluded) { - output.addField("_id", Value(false)); - } + _root->serialize(explain, &output); + if (output.peek()["_id"].missing()) { + output.addField("_id", Value{false}); + } + return output.freeze(); } @@ -172,32 +191,11 @@ public: * Arrays will be traversed, with any dotted/nested exclusions or computed fields applied to * each element in the array. */ - Document applyProjection(const Document& inputDoc) const final; + Document applyProjection(const Document& inputDoc) const final { + return _root->applyToDocument(inputDoc); + } private: - /** - * Attempts to parse 'objSpec' as an expression like {$add: [...]}. Adds a computed field to - * '_root' and returns true if it was successfully parsed as an expression. Returns false if it - * was not an expression specification. - * - * Throws an error if it was determined to be an expression specification, but failed to parse - * as a valid expression. - */ - bool parseObjectAsExpression(StringData pathToObject, - const BSONObj& objSpec, - const VariablesParseState& variablesParseState); - - /** - * Traverses 'subObj' and parses each field. Adds any included or computed fields at this level - * to 'node'. - */ - void parseSubObject(const BSONObj& subObj, - const VariablesParseState& variablesParseState, - InclusionNode* node); - - // Not strictly necessary to track here, but makes serialization easier. - bool _idExcluded = false; - // The InclusionNode tree does most of the execution work once constructed. std::unique_ptr<InclusionNode> _root; }; diff --git a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp b/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp index c5fdf34d4df..c7dcf9f080b 100644 --- a/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp +++ b/src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp @@ -39,8 +39,10 @@ #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" +#include "mongo/db/exec/projection_executor.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" @@ -55,64 +57,43 @@ BSONObj wrapInLiteral(const T& arg) { return BSON("$literal" << arg); } -// Helper to simplify the creation of a ParsedInclusionProjection with default policies. -ParsedInclusionProjection makeInclusionProjectionWithDefaultPolicies() { +auto createProjectionExecutor(const BSONObj& spec, const ProjectionPolicies& policies) { const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); - ProjectionPolicies defaultPolicies; - return {expCtx, defaultPolicies}; + auto projection = projection_ast::parse(expCtx, spec, policies); + auto executor = projection_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. +auto makeInclusionProjectionWithDefaultPolicies(BSONObj spec) { + return createProjectionExecutor(spec, {}); } // Helper to simplify the creation of a ParsedInclusionProjection which excludes _id by default. -ParsedInclusionProjection makeInclusionProjectionWithDefaultIdExclusion() { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); +auto makeInclusionProjectionWithDefaultIdExclusion(BSONObj spec) { ProjectionPolicies defaultExcludeId{ProjectionPolicies::DefaultIdPolicy::kExcludeId, ProjectionPolicies::kArrayRecursionPolicyDefault, ProjectionPolicies::kComputedFieldsPolicyDefault}; - return {expCtx, defaultExcludeId}; + return createProjectionExecutor(spec, defaultExcludeId); } // Helper to simplify the creation of a ParsedInclusionProjection which does not recurse arrays. -ParsedInclusionProjection makeInclusionProjectionWithNoArrayRecursion() { - const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); +auto makeInclusionProjectionWithNoArrayRecursion(BSONObj spec) { ProjectionPolicies noArrayRecursion{ ProjectionPolicies::kDefaultIdPolicyDefault, ProjectionPolicies::ArrayRecursionPolicy::kDoNotRecurseNestedArrays, ProjectionPolicies::kComputedFieldsPolicyDefault}; - return {expCtx, noArrayRecursion}; -} - -DEATH_TEST(InclusionProjectionExecutionTest, - ShouldFailWhenGivenExcludedNonIdField, - "Invariant failure elem.trueValue()") { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << false)); -} - -DEATH_TEST(InclusionProjectionExecutionTest, - ShouldFailWhenGivenIncludedIdSubfield, - "Invariant failure elem.trueValue()") { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id.id1" << false)); -} - -TEST(InclusionProjectionExecutionTest, ShouldThrowWhenParsingInvalidExpression) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - ASSERT_THROWS(inclusion.parse(BSON("a" << BSON("$gt" << BSON("bad" - << "arguments")))), - AssertionException); -} - -TEST(InclusionProjectionExecutionTest, ShouldRejectProjectionWithNoOutputFields) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - ASSERT_THROWS(inclusion.parse(BSON("_id" << false)), AssertionException); + return createProjectionExecutor(spec, noArrayRecursion); } TEST(InclusionProjectionExecutionTest, ShouldAddIncludedFieldsToDependencies) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id" << false << "a" << true << "x.y" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("_id" << false << "a" << true << "x.y" << true)); DepsTracker deps; - inclusion.addDependencies(&deps); + inclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 2UL); ASSERT_EQ(deps.fields.count("_id"), 0UL); @@ -121,11 +102,10 @@ TEST(InclusionProjectionExecutionTest, ShouldAddIncludedFieldsToDependencies) { } TEST(InclusionProjectionExecutionTest, ShouldAddIdToDependenciesIfNotSpecified) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << true)); DepsTracker deps; - inclusion.addDependencies(&deps); + inclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 2UL); ASSERT_EQ(deps.fields.count("_id"), 1UL); @@ -133,14 +113,13 @@ TEST(InclusionProjectionExecutionTest, ShouldAddIdToDependenciesIfNotSpecified) } TEST(InclusionProjectionExecutionTest, ShouldAddDependenciesOfComputedFields) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" - << "$a" - << "x" - << "$z")); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" + << "$a" + << "x" + << "$z")); DepsTracker deps; - inclusion.addDependencies(&deps); + inclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 3UL); ASSERT_EQ(deps.fields.count("_id"), 1UL); @@ -149,12 +128,11 @@ TEST(InclusionProjectionExecutionTest, ShouldAddDependenciesOfComputedFields) { } TEST(InclusionProjectionExecutionTest, ShouldAddPathToDependenciesForNestedComputedFields) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("x.y" - << "$z")); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("x.y" + << "$z")); DepsTracker deps; - inclusion.addDependencies(&deps); + inclusion->addDependencies(&deps); ASSERT_EQ(deps.fields.size(), 3UL); // Implicit "_id". @@ -166,84 +144,91 @@ TEST(InclusionProjectionExecutionTest, ShouldAddPathToDependenciesForNestedCompu } TEST(InclusionProjectionExecutionTest, ShouldSerializeToEquivalentProjection) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(fromjson("{a: {$add: ['$a', 2]}, b: {d: 3}, 'x.y': {$literal: 4}}")); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + fromjson("{a: {$add: ['$a', 2]}, b: {d: 3}, 'x.y': {$literal: 4}}")); // Adds implicit "_id" inclusion, converts numbers to bools, serializes expressions. auto expectedSerialization = Document(fromjson( "{_id: true, a: {$add: [\"$a\", {$const: 2}]}, b: {d: true}, x: {y: {$const: 4}}}")); // Should be the same if we're serializing for explain or for internal use. - ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion.serializeTransformation(boost::none)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion->serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } TEST(InclusionProjectionExecutionTest, ShouldSerializeExplicitExclusionOfId) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id" << false << "a" << true)); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("_id" << false << "a" << true)); // Adds implicit "_id" inclusion, converts numbers to bools, serializes expressions. - auto expectedSerialization = Document{{"_id", false}, {"a", true}}; + auto expectedSerialization = Document{{"a", true}, {"_id", false}}; // Should be the same if we're serializing for explain or for internal use. - ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion->serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } - TEST(InclusionProjectionExecutionTest, ShouldOptimizeTopLevelExpressions) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2)))); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2)))); - inclusion.optimize(); + inclusion->optimize(); auto expectedSerialization = Document{{"_id", true}, {"a", Document{{"$const", 3}}}}; // Should be the same if we're serializing for explain or for internal use. - ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion->serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } TEST(InclusionProjectionExecutionTest, ShouldOptimizeNestedExpressions) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b" << BSON("$add" << BSON_ARRAY(1 << 2)))); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a.b" << BSON("$add" << BSON_ARRAY(1 << 2)))); - inclusion.optimize(); + inclusion->optimize(); auto expectedSerialization = Document{{"_id", true}, {"a", Document{{"b", Document{{"$const", 3}}}}}}; // Should be the same if we're serializing for explain or for internal use. - ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion.serializeTransformation(boost::none)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); + ASSERT_DOCUMENT_EQ(expectedSerialization, inclusion->serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); - ASSERT_DOCUMENT_EQ(expectedSerialization, - inclusion.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ( + expectedSerialization, + inclusion->serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); } TEST(InclusionProjectionExecutionTest, ShouldReportThatAllExceptIncludedFieldsAreModified) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << wrapInLiteral("computedVal") << "b.c" - << wrapInLiteral("computedVal") << "d" << true << "e.f" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a" << wrapInLiteral("computedVal") << "b.c" << wrapInLiteral("computedVal") << "d" + << true << "e.f" << true)); - auto modifiedPaths = inclusion.getModifiedPaths(); + auto modifiedPaths = inclusion->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllExcept); // Included paths are not modified. ASSERT_EQ(modifiedPaths.paths.count("_id"), 1UL); @@ -257,11 +242,11 @@ TEST(InclusionProjectionExecutionTest, ShouldReportThatAllExceptIncludedFieldsAr TEST(InclusionProjectionExecutionTest, ShouldReportThatAllExceptIncludedFieldsAreModifiedWithIdExclusion) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id" << false << "a" << wrapInLiteral("computedVal") << "b.c" - << wrapInLiteral("computedVal") << "d" << true << "e.f" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("_id" << false << "a" << wrapInLiteral("computedVal") << "b.c" + << wrapInLiteral("computedVal") << "d" << true << "e.f" << true)); - auto modifiedPaths = inclusion.getModifiedPaths(); + auto modifiedPaths = inclusion->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllExcept); // Included paths are not modified. ASSERT_EQ(modifiedPaths.paths.count("d"), 1UL); @@ -280,110 +265,108 @@ TEST(InclusionProjectionExecutionTest, // TEST(InclusionProjectionExecutionTest, ShouldIncludeTopLevelField) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << true)); // More than one field in document. - auto result = inclusion.applyProjection(Document{{"a", 1}, {"b", 2}}); + auto result = inclusion->applyTransformation(Document{{"a", 1}, {"b", 2}}); auto expectedResult = Document{{"a", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is the only field in the document. - result = inclusion.applyProjection(Document{{"a", 1}}); + result = inclusion->applyTransformation(Document{{"a", 1}}); expectedResult = Document{{"a", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is not present in the document. - result = inclusion.applyProjection(Document{{"c", 1}}); + result = inclusion->applyTransformation(Document{{"c", 1}}); expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); // There are no fields in the document. - result = inclusion.applyProjection(Document{}); + result = inclusion->applyTransformation(Document{}); expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldAddComputedTopLevelField) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("newField" << wrapInLiteral("computedVal"))); - auto result = inclusion.applyProjection(Document{}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("newField" << wrapInLiteral("computedVal"))); + auto result = inclusion->applyTransformation(Document{}); auto expectedResult = Document{{"newField", "computedVal"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Computed field should replace existing field. - result = inclusion.applyProjection(Document{{"newField", "preExisting"_sd}}); + result = inclusion->applyTransformation(Document{{"newField", "preExisting"_sd}}); expectedResult = Document{{"newField", "computedVal"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldApplyBothInclusionsAndComputedFields) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true << "newField" << wrapInLiteral("computedVal"))); - auto result = inclusion.applyProjection(Document{{"a", 1}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a" << true << "newField" << wrapInLiteral("computedVal"))); + auto result = inclusion->applyTransformation(Document{{"a", 1}}); auto expectedResult = Document{{"a", 1}, {"newField", "computedVal"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldIncludeFieldsInOrderOfInputDoc) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("first" << true << "second" << true << "third" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("first" << true << "second" << true << "third" << true)); auto inputDoc = Document{{"second", 1}, {"first", 0}, {"third", 2}}; - auto result = inclusion.applyProjection(inputDoc); + auto result = inclusion->applyTransformation(inputDoc); ASSERT_DOCUMENT_EQ(result, inputDoc); } TEST(InclusionProjectionExecutionTest, ShouldApplyComputedFieldsInOrderSpecified) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("firstComputed" << wrapInLiteral("FIRST") << "secondComputed" - << wrapInLiteral("SECOND"))); - auto result = inclusion.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON( + "firstComputed" << wrapInLiteral("FIRST") << "secondComputed" << wrapInLiteral("SECOND"))); + auto result = + inclusion->applyTransformation(Document{{"first", 0}, {"second", 1}, {"third", 2}}); auto expectedResult = Document{{"firstComputed", "FIRST"_sd}, {"secondComputed", "SECOND"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldImplicitlyIncludeId) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); - auto result = inclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << true)); + auto result = inclusion->applyTransformation(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}}); auto expectedResult = Document{{"_id", "ID"_sd}, {"a", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Should leave the "_id" in the same place as in the original document. - result = inclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); + result = inclusion->applyTransformation(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); expectedResult = Document{{"a", 1}, {"_id", "ID"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldImplicitlyIncludeIdWithComputedFields) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("newField" << wrapInLiteral("computedVal"))); - auto result = inclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("newField" << wrapInLiteral("computedVal"))); + auto result = inclusion->applyTransformation(Document{{"_id", "ID"_sd}, {"a", 1}}); auto expectedResult = Document{{"_id", "ID"_sd}, {"newField", "computedVal"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldIncludeIdIfExplicitlyIncluded) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true << "_id" << true << "b" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a" << true << "_id" << true << "b" << true)); auto result = - inclusion.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}, {"c", 3}}); + inclusion->applyTransformation(Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}, {"c", 3}}); auto expectedResult = Document{{"_id", "ID"_sd}, {"a", 1}, {"b", 2}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldExcludeIdIfExplicitlyExcluded) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true << "_id" << false)); - auto result = inclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("a" << true << "_id" << false)); + auto result = inclusion->applyTransformation(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); auto expectedResult = Document{{"a", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldReplaceIdWithComputedId) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id" << wrapInLiteral("newId"))); - auto result = inclusion.applyProjection(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral("newId"))); + auto result = inclusion->applyTransformation(Document{{"a", 1}, {"b", 2}, {"_id", "ID"_sd}}); auto expectedResult = Document{{"_id", "newId"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } @@ -393,48 +376,45 @@ TEST(InclusionProjectionExecutionTest, ShouldReplaceIdWithComputedId) { // TEST(InclusionProjectionExecutionTest, ShouldIncludeSimpleDottedFieldFromSubDoc) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a.b" << true)); // More than one field in sub document. - auto result = inclusion.applyProjection(Document{{"a", Document{{"b", 1}, {"c", 2}}}}); + auto result = inclusion->applyTransformation(Document{{"a", Document{{"b", 1}, {"c", 2}}}}); auto expectedResult = Document{{"a", Document{{"b", 1}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is the only field in the sub document. - result = inclusion.applyProjection(Document{{"a", Document{{"b", 1}}}}); + result = inclusion->applyTransformation(Document{{"a", Document{{"b", 1}}}}); expectedResult = Document{{"a", Document{{"b", 1}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is not present in the sub document. - result = inclusion.applyProjection(Document{{"a", Document{{"c", 1}}}}); + result = inclusion->applyTransformation(Document{{"a", Document{{"c", 1}}}}); expectedResult = Document{{"a", Document{}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // There are no fields in sub document. - result = inclusion.applyProjection(Document{{"a", Document{}}}); + result = inclusion->applyTransformation(Document{{"a", Document{}}}); expectedResult = Document{{"a", Document{}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldNotCreateSubDocIfDottedIncludedFieldDoesNotExist) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("sub.target" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("sub.target" << true)); // Should not add the path if it doesn't exist. - auto result = inclusion.applyProjection(Document{}); + auto result = inclusion->applyTransformation(Document{}); auto expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Should not replace the first part of the path if that part exists. - result = inclusion.applyProjection(Document{{"sub", "notADocument"_sd}}); + result = inclusion->applyTransformation(Document{{"sub", "notADocument"_sd}}); expectedResult = Document{}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldApplyDottedInclusionToEachElementInArray) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a.b" << true)); vector<Value> nestedValues = {Value(1), Value(Document{}), @@ -451,63 +431,64 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyDottedInclusionToEachElementIn Value(Document{{"b", 1}}), Value(vector<Value>{}), Value(vector<Value>{Value(), Value(Document{})})}; - auto result = inclusion.applyProjection(Document{{"a", nestedValues}}); + auto result = inclusion->applyTransformation(Document{{"a", nestedValues}}); auto expectedResult = Document{{"a", expectedNestedValues}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToSubDocument) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("sub.target" << wrapInLiteral("computedVal"))); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("sub.target" << wrapInLiteral("computedVal"))); // Other fields exist in sub document, one of which is the specified field. - auto result = inclusion.applyProjection(Document{{"sub", Document{{"target", 1}, {"c", 2}}}}); + auto result = + inclusion->applyTransformation(Document{{"sub", Document{{"target", 1}, {"c", 2}}}}); auto expectedResult = Document{{"sub", Document{{"target", "computedVal"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Specified field is not present in the sub document. - result = inclusion.applyProjection(Document{{"sub", Document{{"c", 1}}}}); + result = inclusion->applyTransformation(Document{{"sub", Document{{"c", 1}}}}); expectedResult = Document{{"sub", Document{{"target", "computedVal"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // There are no fields in sub document. - result = inclusion.applyProjection(Document{{"sub", Document{}}}); + result = inclusion->applyTransformation(Document{{"sub", Document{}}}); expectedResult = Document{{"sub", Document{{"target", "computedVal"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldCreateSubDocIfDottedComputedFieldDoesntExist) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("sub.target" << wrapInLiteral("computedVal"))); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("sub.target" << wrapInLiteral("computedVal"))); // Should add the path if it doesn't exist. - auto result = inclusion.applyProjection(Document{}); + auto result = inclusion->applyTransformation(Document{}); auto expectedResult = Document{{"sub", Document{{"target", "computedVal"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Should replace non-documents with documents. - result = inclusion.applyProjection(Document{{"sub", "notADocument"_sd}}); + result = inclusion->applyTransformation(Document{{"sub", "notADocument"_sd}}); ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldCreateNestedSubDocumentsAllTheWayToComputedField) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b.c.d" << wrapInLiteral("computedVal"))); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("a.b.c.d" << wrapInLiteral("computedVal"))); // Should add the path if it doesn't exist. - auto result = inclusion.applyProjection(Document{}); + auto result = inclusion->applyTransformation(Document{}); auto expectedResult = Document{{"a", Document{{"b", Document{{"c", Document{{"d", "computedVal"_sd}}}}}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); // Should replace non-documents with documents. - result = inclusion.applyProjection(Document{{"a", Document{{"b", "other"_sd}}}}); + result = inclusion->applyTransformation(Document{{"a", Document{{"b", "other"_sd}}}}); ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToEachElementInArray) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED"))); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("a.b" << wrapInLiteral("COMPUTED"))); vector<Value> nestedValues = {Value(1), Value(Document{}), @@ -523,14 +504,14 @@ TEST(InclusionProjectionExecutionTest, ShouldAddComputedDottedFieldToEachElement Value(vector<Value>{}), Value(vector<Value>{Value(Document{{"b", "COMPUTED"_sd}}), Value(Document{{"b", "COMPUTED"_sd}})})}; - auto result = inclusion.applyProjection(Document{{"a", nestedValues}}); + auto result = inclusion->applyTransformation(Document{{"a", nestedValues}}); auto expectedResult = Document{{"a", expectedNestedValues}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldApplyInclusionsAndAdditionsToEachElementInArray) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.inc" << true << "a.comp" << wrapInLiteral("COMPUTED"))); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a.inc" << true << "a.comp" << wrapInLiteral("COMPUTED"))); vector<Value> nestedValues = {Value(1), Value(Document{}), @@ -550,28 +531,28 @@ TEST(InclusionProjectionExecutionTest, ShouldApplyInclusionsAndAdditionsToEachEl Value(vector<Value>{}), Value(vector<Value>{Value(Document{{"comp", "COMPUTED"_sd}}), Value(Document{{"inc", 1}, {"comp", "COMPUTED"_sd}})})}; - auto result = inclusion.applyProjection(Document{{"a", nestedValues}}); + auto result = inclusion->applyTransformation(Document{{"a", nestedValues}}); auto expectedResult = Document{{"a", expectedNestedValues}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldAddOrIncludeSubFieldsOfId) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id.X" << true << "_id.Z" << wrapInLiteral("NEW"))); - auto result = inclusion.applyProjection(Document{{"_id", Document{{"X", 1}, {"Y", 2}}}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("_id.X" << true << "_id.Z" << wrapInLiteral("NEW"))); + auto result = inclusion->applyTransformation(Document{{"_id", Document{{"X", 1}, {"Y", 2}}}}); auto expectedResult = Document{{"_id", Document{{"X", 1}, {"Z", "NEW"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); // 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". - inclusion.parse(BSON("a.b" << true << "a.c" << true << "a.W" << wrapInLiteral("W") << "a.X" - << wrapInLiteral("X") << "a" - << BSON("d" << true << "e" << true << "Y" << wrapInLiteral("Y") - << "Z" << wrapInLiteral("Z")))); - auto result = inclusion.applyProjection(Document{ + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a.b" << true << "a.c" << true << "a.W" << wrapInLiteral("W") << "a.X" + << wrapInLiteral("X") << "a" + << BSON("d" << true << "e" << true << "Y" << wrapInLiteral("Y") << "Z" + << wrapInLiteral("Z")))); + auto result = inclusion->applyTransformation(Document{ {"a", Document{{"b", "b"_sd}, {"c", "c"_sd}, {"d", "d"_sd}, {"e", "e"_sd}, {"f", "f"_sd}}}}); auto expectedResult = Document{{"a", @@ -587,40 +568,40 @@ TEST(InclusionProjectionExecutionTest, ShouldAllowMixedNestedAndDottedFields) { } TEST(InclusionProjectionExecutionTest, ShouldApplyNestedComputedFieldsInOrderSpecified) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << wrapInLiteral("FIRST") << "b.c" << wrapInLiteral("SECOND"))); - auto result = inclusion.applyProjection(Document{}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("a" << wrapInLiteral("FIRST") << "b.c" << wrapInLiteral("SECOND"))); + auto result = inclusion->applyTransformation(Document{}); auto expectedResult = Document{{"a", "FIRST"_sd}, {"b", Document{{"c", "SECOND"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldApplyComputedFieldsAfterAllInclusions) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("b.c" << wrapInLiteral("NEW") << "a" << true)); - auto result = inclusion.applyProjection(Document{{"a", 1}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("b.c" << wrapInLiteral("NEW") << "a" << true)); + auto result = inclusion->applyTransformation(Document{{"a", 1}}); auto expectedResult = Document{{"a", 1}, {"b", Document{{"c", "NEW"_sd}}}}; ASSERT_DOCUMENT_EQ(result, expectedResult); - result = inclusion.applyProjection(Document{{"a", 1}, {"b", 4}}); + result = inclusion->applyTransformation(Document{{"a", 1}, {"b", 4}}); ASSERT_DOCUMENT_EQ(result, expectedResult); // In this case, the field 'b' shows up first and has a nested inclusion or computed field. Even // though it is a computed field, it will appear first in the output document. This is // inconsistent, but the expected behavior, and a consequence of applying the projection // recursively to each sub-document. - result = inclusion.applyProjection(Document{{"b", 4}, {"a", 1}}); + result = inclusion->applyTransformation(Document{{"b", 4}, {"a", 1}}); expectedResult = Document{{"b", Document{{"c", "NEW"_sd}}}, {"a", 1}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ComputedFieldReplacingExistingShouldAppearAfterInclusions) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("b" << wrapInLiteral("NEW") << "a" << true)); - auto result = inclusion.applyProjection(Document{{"b", 1}, {"a", 1}}); + auto inclusion = makeInclusionProjectionWithDefaultPolicies( + BSON("b" << wrapInLiteral("NEW") << "a" << true)); + auto result = inclusion->applyTransformation(Document{{"b", 1}, {"a", 1}}); auto expectedResult = Document{{"a", 1}, {"b", "NEW"_sd}}; ASSERT_DOCUMENT_EQ(result, expectedResult); - result = inclusion.applyProjection(Document{{"a", 1}, {"b", 4}}); + result = inclusion->applyTransformation(Document{{"a", 1}, {"b", 4}}); ASSERT_DOCUMENT_EQ(result, expectedResult); } @@ -629,15 +610,14 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldReplacingExistingShouldAppea // TEST(InclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << true)); MutableDocument inputDocBuilder(Document{{"a", 1}}); inputDocBuilder.metadata().setRandVal(1.0); inputDocBuilder.metadata().setTextScore(10.0); Document inputDoc = inputDocBuilder.freeze(); - auto result = inclusion.applyProjection(inputDoc); + auto result = inclusion->applyTransformation(inputDoc); MutableDocument expectedDoc(inputDoc); expectedDoc.copyMetaDataFrom(inputDoc); @@ -649,60 +629,57 @@ TEST(InclusionProjectionExecutionTest, ShouldAlwaysKeepMetadataFromOriginalDoc) // TEST(InclusionProjectionExecutionTest, ShouldIncludeIdByDefault) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << true)); - auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = inclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{{"_id", 2}, {"a", 3}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldIncludeIdWithIncludePolicy) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a" << true)); - auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = inclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{{"_id", 2}, {"a", 3}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldExcludeIdWithExcludePolicy) { - auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(BSON("a" << true)); - auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = inclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{{"a", 3}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldOverrideIncludePolicyWithExplicitExcludeIdSpec) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id" << false << "a" << true)); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("_id" << false << "a" << true)); - auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = inclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{{"a", 3}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldOverrideExcludePolicyWithExplicitIncludeIdSpec) { - auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(); - inclusion.parse(BSON("_id" << true << "a" << true)); + auto inclusion = + makeInclusionProjectionWithDefaultIdExclusion(BSON("_id" << true << "a" << true)); - auto result = inclusion.applyProjection(Document{{"_id", 2}, {"a", 3}}); + auto result = inclusion->applyTransformation(Document{{"_id", 2}, {"a", 3}}); auto expectedResult = Document{{"_id", 2}, {"a", 3}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefaultIncludePolicy) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("_id.id1" << true << "a" << true)); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("_id.id1" << true << "a" << true)); - auto result = inclusion.applyProjection( + auto result = inclusion->applyTransformation( Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}}); auto expectedResult = Document{{"_id", Document{{"id1", 1}}}, {"a", 3}}; @@ -710,10 +687,10 @@ TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefau } TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefaultExcludePolicy) { - auto inclusion = makeInclusionProjectionWithDefaultIdExclusion(); - inclusion.parse(BSON("_id.id1" << true << "a" << true)); + auto inclusion = + makeInclusionProjectionWithDefaultIdExclusion(BSON("_id.id1" << true << "a" << true)); - auto result = inclusion.applyProjection( + auto result = inclusion->applyTransformation( Document{{"_id", Document{{"id1", 1}, {"id2", 2}}}, {"a", 3}, {"b", 4}}); auto expectedResult = Document{{"_id", Document{{"id1", 1}}}, {"a", 3}}; @@ -725,11 +702,10 @@ TEST(InclusionProjectionExecutionTest, ShouldAllowInclusionOfIdSubfieldWithDefau // TEST(InclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b" << true)); + auto inclusion = makeInclusionProjectionWithDefaultPolicies(BSON("a.b" << true)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, [{b: 4}], {}]} - auto result = inclusion.applyProjection( + auto result = inclusion->applyTransformation( Document{{"a", vector<Value>{Value(1), Value(Document{{"b", 2}, {"c", 3}}), @@ -746,11 +722,10 @@ TEST(InclusionProjectionExecutionTest, ShouldRecurseNestedArraysByDefault) { } TEST(InclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecursePolicy) { - auto inclusion = makeInclusionProjectionWithNoArrayRecursion(); - inclusion.parse(BSON("a.b" << true)); + auto inclusion = makeInclusionProjectionWithNoArrayRecursion(BSON("a.b" << true)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => {a: [{b: 2}, {}]} - auto result = inclusion.applyProjection( + auto result = inclusion->applyTransformation( Document{{"a", vector<Value>{Value(1), Value(Document{{"b", 2}, {"c", 3}}), @@ -764,8 +739,7 @@ TEST(InclusionProjectionExecutionTest, ShouldNotRecurseNestedArraysForNoRecurseP } TEST(InclusionProjectionExecutionTest, ShouldRetainNestedArraysIfNoRecursionNeeded) { - auto inclusion = makeInclusionProjectionWithNoArrayRecursion(); - inclusion.parse(BSON("a" << true)); + auto inclusion = makeInclusionProjectionWithNoArrayRecursion(BSON("a" << true)); // {a: [1, {b: 2, c: 3}, [{b: 4, c: 5}], {d: 6}]} => [output doc identical to input] const auto inputDoc = @@ -775,15 +749,15 @@ TEST(InclusionProjectionExecutionTest, ShouldRetainNestedArraysIfNoRecursionNeed Value(vector<Value>{Value(Document{{"b", 4}, {"c", 5}})}), Value(Document{{"d", 6}})}}}; - auto result = inclusion.applyProjection(inputDoc); + auto result = inclusion->applyTransformation(inputDoc); const auto& expectedResult = inputDoc; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ComputedFieldIsAddedToNestedArrayElementsForRecursePolicy) { - auto inclusion = makeInclusionProjectionWithDefaultPolicies(); - inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED"))); + auto inclusion = + makeInclusionProjectionWithDefaultPolicies(BSON("a.b" << wrapInLiteral("COMPUTED"))); vector<Value> nestedValues = {Value(1), Value(Document{}), @@ -799,14 +773,14 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldIsAddedToNestedArrayElements Value(vector<Value>{}), Value(vector<Value>{Value(Document{{"b", "COMPUTED"_sd}}), Value(Document{{"b", "COMPUTED"_sd}})})}; - auto result = inclusion.applyProjection(Document{{"a", nestedValues}}); + auto result = inclusion->applyTransformation(Document{{"a", nestedValues}}); auto expectedResult = Document{{"a", expectedNestedValues}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } TEST(InclusionProjectionExecutionTest, ComputedFieldShouldReplaceNestedArrayForNoRecursePolicy) { - auto inclusion = makeInclusionProjectionWithNoArrayRecursion(); - inclusion.parse(BSON("a.b" << wrapInLiteral("COMPUTED"))); + auto inclusion = + makeInclusionProjectionWithNoArrayRecursion(BSON("a.b" << wrapInLiteral("COMPUTED"))); // For kRecurseNestedArrays, the computed field (1) replaces any scalar values in the array with // a subdocument containing the new field, and (2) is added to each element of the array and all @@ -826,7 +800,7 @@ TEST(InclusionProjectionExecutionTest, ComputedFieldShouldReplaceNestedArrayForN Value(Document{{"b", "COMPUTED"_sd}}), Value(Document{{"b", "COMPUTED"_sd}})}; - auto result = inclusion.applyProjection(Document{{"a", nestedValues}}); + auto result = inclusion->applyTransformation(Document{{"a", nestedValues}}); auto expectedResult = Document{{"a", expectedNestedValues}}; ASSERT_DOCUMENT_EQ(result, expectedResult); } diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp index 3da457141dd..0b4a7d9725d 100644 --- a/src/mongo/db/pipeline/pipeline_test.cpp +++ b/src/mongo/db/pipeline/pipeline_test.cpp @@ -1067,15 +1067,15 @@ TEST(PipelineOptimizationTest, PartiallyDependentMatchWithRenameShouldSplitAcros TEST(PipelineOptimizationTest, NorCanSplitAcrossProjectWithRename) { string inputPipe = - "[{$project: {_id: false, x: true, y: '$z'}}," + "[{$project: {x: true, y: '$z', _id: false}}," "{$match: {$nor: [{w: {$eq: 1}}, {y: {$eq: 1}}]}}]"; string outputPipe = R"([{$match: {z : {$not: {$eq: 1}}}}, - {$project: {_id: false, x: true, y: "$z"}}, + {$project: {x: true, y: "$z", _id: false}}, {$match: {w: {$not: {$eq: 1}}}}])"; string serializedPipe = R"( [{$match: {$nor: [ {z : {$eq: 1}}]}}, - {$project: {_id: false, x: true, y: "$z"}}, + {$project: {x: true, y: "$z", _id: false}}, {$match: {$nor: [ {w: {$eq: 1}}]}}] )"; assertPipelineOptimizesAndSerializesTo(inputPipe, outputPipe, serializedPipe); @@ -1083,19 +1083,19 @@ TEST(PipelineOptimizationTest, NorCanSplitAcrossProjectWithRename) { TEST(PipelineOptimizationTest, MatchCanMoveAcrossSeveralRenames) { string inputPipe = - "[{$project: {_id: false, c: '$d'}}," + "[{$project: {c: '$d', _id: false}}," "{$addFields: {b: '$c'}}," "{$project: {a: '$b', z: 1}}," "{$match: {a: 1, z: 2}}]"; string outputPipe = "[{$match: {d: {$eq: 1}}}," - "{$project: {_id: false, c: '$d'}}," + "{$project: {c: '$d', _id: false}}," "{$match: {z: {$eq: 2}}}," "{$addFields: {b: '$c'}}," "{$project: {_id: true, z: true, a: '$b'}}]"; string serializedPipe = R"( [{$match: {d : {$eq: 1}}}, - {$project: {_id: false, c: "$d"}}, + {$project: {c: "$d", _id: false}}, {$match: {z : {$eq: 2}}}, {$addFields: {b: "$c"}}, {$project: {_id: true, z: true, a: "$b"}}])"; @@ -1104,7 +1104,7 @@ TEST(PipelineOptimizationTest, MatchCanMoveAcrossSeveralRenames) { TEST(PipelineOptimizationTest, RenameShouldNotBeAppliedToDependentMatch) { string pipeline = - "[{$project: {_id: false, x: {$add: ['$foo', '$bar']}, y: '$z'}}," + "[{$project: {x: {$add: ['$foo', '$bar']}, y: '$z', _id: false}}," "{$match: {$or: [{x: {$eq: 1}}, {y: {$eq: 1}}]}}]"; assertPipelineOptimizesTo(pipeline, pipeline); } @@ -1115,8 +1115,8 @@ TEST(PipelineOptimizationTest, MatchCannotMoveAcrossAddFieldsRenameOfDottedPath) } TEST(PipelineOptimizationTest, MatchCannotMoveAcrossProjectRenameOfDottedPath) { - string inputPipe = "[{$project: {_id: false, a: '$$CURRENT.b.c'}}, {$match: {a: {$eq: 1}}}]"; - string outputPipe = "[{$project: {_id: false, a: '$b.c'}}, {$match: {a: {$eq: 1}}}]"; + string inputPipe = "[{$project: {a: '$$CURRENT.b.c', _id: false}}, {$match: {a: {$eq: 1}}}]"; + string outputPipe = "[{$project: {a: '$b.c', _id: false}}, {$match: {a: {$eq: 1}}}]"; assertPipelineOptimizesTo(inputPipe, outputPipe); } @@ -2065,7 +2065,7 @@ class MatchWithSkipGroupAndLimit : public Base { return "[{$match: {x: 4}}, {$skip: 10}, {$group: {_id: '$y'}}, {$limit: 5}]"; } string shardPipeJson() { - return "[{$match: {x: {$eq: 4}}}, {$project: {_id: false, y: true}}]"; + return "[{$match: {x: {$eq: 4}}}, {$project: {y: true, _id: false}}]"; } string mergePipeJson() { return "[{$skip: 10}, {$group: {_id: '$y'}}, {$limit: 5}]"; @@ -2123,7 +2123,7 @@ class JustNeedsNonId : public Base { return "[{$limit:1}, {$group: {_id: '$a.b'}}]"; } string shardPipeJson() { - return "[{$limit:1}, {$project: {_id: false, a: {b: true}}}]"; + return "[{$limit:1}, {$project: {a: {b: true}, _id: false}}]"; } string mergePipeJson() { return "[{$limit:1}, {$group: {_id: '$a.b'}}]"; @@ -2239,7 +2239,7 @@ class ShardedSortGroupProjLimDoesNotBecomeTopKSortProjGroup : public Base { } string shardPipeJson() { return "[{$sort: {sortKey: {a: 1}}}" - ",{$project : {_id: false, a: true}}" + ",{$project : {a: true, _id: false}}" "]"; } string mergePipeJson() { diff --git a/src/mongo/db/query/projection_ast_test.cpp b/src/mongo/db/query/projection_ast_test.cpp index f898f458ca7..dd26389ba2e 100644 --- a/src/mongo/db/query/projection_ast_test.cpp +++ b/src/mongo/db/query/projection_ast_test.cpp @@ -95,10 +95,13 @@ void assertCanClone(Projection proj) { ASSERT_BSONOBJ_EQ(cloneBSON, cloneBSON2); } -TEST_F(ProjectionASTTest, TestParsingTypeEmptyProjectionIsExclusion) { - Projection proj = parseWithDefaultPolicies(fromjson("{}")); +TEST_F(ProjectionASTTest, TestParsingTypeEmptyProjectionIsExclusionInFind) { + Projection proj = parseWithFindFeaturesEnabled(fromjson("{}")); ASSERT(proj.type() == ProjectType::kExclusion); } +TEST_F(ProjectionASTTest, TestParsingTypeEmptyProjectionFailsInAggregation) { + ASSERT_THROWS_CODE(parseWithDefaultPolicies(fromjson("{}")), AssertionException, 51272); +} TEST_F(ProjectionASTTest, TestParsingTypeInclusion) { Projection proj = parseWithDefaultPolicies(fromjson("{a: 1, b: 1}")); diff --git a/src/mongo/db/query/projection_parser.cpp b/src/mongo/db/query/projection_parser.cpp index 9015d5bc778..decea04b326 100644 --- a/src/mongo/db/query/projection_parser.cpp +++ b/src/mongo/db/query/projection_parser.cpp @@ -35,6 +35,16 @@ namespace mongo { namespace projection_ast { namespace { +/** + * Uassert that the given policy permits using computed fields in a projection. + */ +void verifyComputedFieldsAllowed(const ProjectionPolicies& policies) { + uassert(51271, + "Bad projection specification, cannot use computed fields when parsing " + "a spec in kBanComputedFields mode", + policies.computedFieldsPolicy != + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields); +} /** * In some arcane situations, when a projection is empty, only contains top-level _id projections @@ -117,6 +127,30 @@ bool isInclusionOrExclusionType(BSONType type) { } } +/** + * Given the 'root' of the AST and the field 'path', returns the last inner 'ProjectionPathASTNode' + * in the AST on that 'path'. For example, if the AST represents a projection {'a.b.c': 1} and the + * 'path' is 'a.b', the returned node will be 'b'. If the node doesn't exist in the tree, or if the + * last node is a leaf node, the function returns 'nullptr'. For example, given the same projection + * specification and the 'path' of 'a.b.c.d', the function will return 'nullptr'. + */ +ProjectionPathASTNode* findLastInnerNodeOnPath(ProjectionPathASTNode* root, + const FieldPath& path, + size_t componentIndex) { + invariant(root); + invariant(path.getPathLength() > componentIndex); + + auto child = exact_pointer_cast<ProjectionPathASTNode*>( + root->getChild(path.getFieldName(componentIndex))); + if (path.getPathLength() == componentIndex + 1) { + return child; + } else if (!child) { + return nullptr; + } + + return findLastInnerNodeOnPath(child, path, componentIndex + 1); +} + void addNodeAtPathHelper(ProjectionPathASTNode* root, const FieldPath& path, size_t componentIndex, @@ -292,6 +326,8 @@ bool attemptToParseGenericExpression(ParseContext* parseCtx, } // It must be an expression. + verifyComputedFieldsAllowed(parseCtx->policies); + const bool isMeta = subObj.firstElementFieldNameStringData() == "$meta"; uassert(31252, "Cannot use expression other than $meta in exclusion projection", @@ -320,9 +356,10 @@ bool parseSubObjectAsExpression(ParseContext* parseCtx, const FieldPath& path, const BSONObj& subObj, ProjectionPathASTNode* parent) { - if (parseCtx->policies.findOnlyFeaturesAllowed()) { if (subObj.firstElementFieldNameStringData() == "$slice") { + verifyComputedFieldsAllowed(parseCtx->policies); + Status findSliceStatus = Status::OK(); try { attemptToParseFindSlice(parseCtx, path, subObj, parent); @@ -344,6 +381,8 @@ bool parseSubObjectAsExpression(ParseContext* parseCtx, return true; } else if (subObj.firstElementFieldNameStringData() == "$elemMatch") { + verifyComputedFieldsAllowed(parseCtx->policies); + // Validate $elemMatch arguments and dependencies. uassert(31274, str::stream() << "elemMatch: Invalid argument, object required, but got " @@ -403,6 +442,8 @@ void parseInclusion(ParseContext* ctx, ctx->idIncludedEntirely = true; } } else { + verifyComputedFieldsAllowed(ctx->policies); + uassert(31276, "Cannot specify more than one positional projection per query.", !ctx->hasPositional); @@ -489,6 +530,8 @@ void parseExclusion(ParseContext* ctx, BSONElement elem, ProjectionPathASTNode* * Treats the given element as a literal value (e.g. {a: "foo"}) and updates the tree as necessary. */ void parseLiteral(ParseContext* ctx, BSONElement elem, ProjectionPathASTNode* parent) { + verifyComputedFieldsAllowed(ctx->policies); + auto expr = Expression::parseOperand(ctx->expCtx, elem, ctx->expCtx->variablesParseState); FieldPath pathFromParent(elem.fieldNameStringData()); @@ -516,6 +559,11 @@ void parseSubObject(ParseContext* ctx, boost::optional<FieldPath> fullPathToParent, const BSONObj& obj, ProjectionPathASTNode* parent) { + uassert( + 51270, + str::stream() << "An empty sub-projection is not a valid value. Found empty object at path", + !obj.isEmpty()); + FieldPath path(objFieldName); if (obj.nFields() == 1 && obj.firstElementFieldNameStringData().startsWith("$")) { @@ -537,12 +585,11 @@ void parseSubObject(ParseContext* ctx, } } - // It's not an expression. Create a node to represent the new layer in the tree. - ProjectionPathASTNode* newParent = nullptr; - { + ProjectionPathASTNode* newParent = findLastInnerNodeOnPath(parent, path, 0); + if (!newParent) { auto ownedChild = std::make_unique<ProjectionPathASTNode>(); newParent = ownedChild.get(); - parent->addChild(objFieldName, std::move(ownedChild)); + addNodeAtPath(parent, path, std::move(ownedChild)); } const FieldPath fullPathToNewParent = fullPathToParent ? fullPathToParent->concat(path) : path; @@ -597,6 +644,11 @@ Projection parse(boost::intrusive_ptr<ExpressionContext> expCtx, const MatchExpression* const query, const BSONObj& queryObj, ProjectionPolicies policies) { + if (!policies.findOnlyFeaturesAllowed()) { + // In agg-style syntax it is illegal to have an empty projection specification. + uassert(51272, "projection specification must have at least one field", !obj.isEmpty()); + } + ProjectionPathASTNode root; ParseContext ctx{expCtx, query, queryObj, obj, policies}; @@ -619,10 +671,16 @@ Projection parse(boost::intrusive_ptr<ExpressionContext> expCtx, } invariant(ctx.type); - if (!ctx.idSpecified && policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kIncludeId && - *ctx.type == ProjectType::kInclusion) { - // Add a node to the root indicating that _id is included. - addNodeAtPath(&root, "_id", std::make_unique<BooleanConstantASTNode>(true)); + if (!ctx.idSpecified) { + if (policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kIncludeId && + *ctx.type == ProjectType::kInclusion) { + // Add a node to the root indicating that _id is included. + addNodeAtPath(&root, "_id", std::make_unique<BooleanConstantASTNode>(true)); + } else if (policies.idPolicy == ProjectionPolicies::DefaultIdPolicy::kExcludeId && + *ctx.type == ProjectType::kExclusion) { + // Add a node to the root indicating that _id is not included. + addNodeAtPath(&root, "_id", std::make_unique<BooleanConstantASTNode>(false)); + } } if (*ctx.type == ProjectType::kExclusion && ctx.idSpecified && ctx.idIncludedEntirely) { diff --git a/src/mongo/db/query/projection_test.cpp b/src/mongo/db/query/projection_test.cpp index e8ba1fa646e..22564bf49ae 100644 --- a/src/mongo/db/query/projection_test.cpp +++ b/src/mongo/db/query/projection_test.cpp @@ -96,7 +96,11 @@ void assertInvalidProjection(const char* queryStr, const char* projStr) { } TEST(QueryProjectionTest, MakeEmptyProjection) { - Projection proj(createProjection("{}", "{}")); + ASSERT_THROWS_CODE(createProjection("{}", "{}"), AssertionException, 51272); +} + +TEST(QueryProjectionTest, MakeEmptyFindProjection) { + Projection proj(createFindProjection("{}", "{}")); ASSERT_TRUE(proj.requiresDocument()); } @@ -220,8 +224,30 @@ TEST(QueryProjectionTest, InvalidPositionalProjectionDefaultPathMatchExpression) DBException); } -TEST(QueryProjectionTest, ProjectionDefaults) { - auto proj = createProjection("{}", "{}"); +TEST(QueryProjectionTest, InclusionProjectionDefaults) { + auto proj = createProjection("{}", "{_id: 1}"); + + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kSortKey]); + ASSERT_FALSE(proj.requiresDocument()); + ASSERT_FALSE(proj.requiresMatchDetails()); + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kGeoNearDist]); + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kGeoNearPoint]); + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kTextScore]); +} + +TEST(QueryProjectionTest, ExclusionProjectionDefaults) { + auto proj = createProjection("{}", "{_id: 0}"); + + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kSortKey]); + ASSERT_TRUE(proj.requiresDocument()); + ASSERT_FALSE(proj.requiresMatchDetails()); + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kGeoNearDist]); + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kGeoNearPoint]); + ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kTextScore]); +} + +TEST(QueryProjectionTest, FindProjectionDefaults) { + auto proj = createFindProjection("{}", "{}"); ASSERT_FALSE(proj.metadataDeps()[DocumentMetadataFields::kSortKey]); ASSERT_TRUE(proj.requiresDocument()); diff --git a/src/mongo/embedded/stitch_support/stitch_support.cpp b/src/mongo/embedded/stitch_support/stitch_support.cpp index 40257b99f7e..0613899cfdc 100644 --- a/src/mongo/embedded/stitch_support/stitch_support.cpp +++ b/src/mongo/embedded/stitch_support/stitch_support.cpp @@ -196,8 +196,8 @@ struct stitch_support_v1_projection { !proj.metadataDeps().any()); this->requiresMatch = proj.requiresMatchDetails(); - this->projectionExec = - mongo::projection_executor::buildProjectionExecutor(expCtx, &proj, policies); + this->projectionExec = mongo::projection_executor::buildProjectionExecutor( + expCtx, &proj, policies, true /* optimizeExecutor */); } mongo::ServiceContext::UniqueClient client; |