summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/aggregation/bugs/server11675.js4
-rw-r--r--jstests/aggregation/bugs/server6177.js8
-rw-r--r--jstests/aggregation/bugs/server6238.js4
-rw-r--r--jstests/aggregation/bugs/server6290.js21
-rw-r--r--jstests/aggregation/use_query_project_and_sort.js2
-rw-r--r--jstests/core/wildcard_index_validindex.js4
-rw-r--r--src/mongo/db/SConscript14
-rw-r--r--src/mongo/db/catalog/index_spec_validate_test.cpp8
-rw-r--r--src/mongo/db/exec/projection.cpp3
-rw-r--r--src/mongo/db/exec/projection_exec_agg.cpp6
-rw-r--r--src/mongo/db/exec/projection_executor.cpp14
-rw-r--r--src/mongo/db/exec/projection_executor.h5
-rw-r--r--src/mongo/db/exec/projection_executor_test.cpp45
-rw-r--r--src/mongo/db/pipeline/SConscript5
-rw-r--r--src/mongo/db/pipeline/document_source_count_test.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_lookup_test.cpp9
-rw-r--r--src/mongo/db/pipeline/document_source_project.cpp19
-rw-r--r--src/mongo/db/pipeline/parsed_add_fields.cpp91
-rw-r--r--src/mongo/db/pipeline/parsed_add_fields.h90
-rw-r--r--src/mongo/db/pipeline/parsed_aggregation_projection.cpp344
-rw-r--r--src/mongo/db/pipeline/parsed_aggregation_projection.h109
-rw-r--r--src/mongo/db/pipeline/parsed_aggregation_projection_test.cpp32
-rw-r--r--src/mongo/db/pipeline/parsed_exclusion_projection.cpp116
-rw-r--r--src/mongo/db/pipeline/parsed_exclusion_projection.h22
-rw-r--r--src/mongo/db/pipeline/parsed_exclusion_projection_test.cpp208
-rw-r--r--src/mongo/db/pipeline/parsed_find_projection_test.cpp64
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection.cpp219
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection.h68
-rw-r--r--src/mongo/db/pipeline/parsed_inclusion_projection_test.cpp422
-rw-r--r--src/mongo/db/pipeline/pipeline_test.cpp24
-rw-r--r--src/mongo/db/query/projection_ast_test.cpp7
-rw-r--r--src/mongo/db/query/projection_parser.cpp76
-rw-r--r--src/mongo/db/query/projection_test.cpp32
-rw-r--r--src/mongo/embedded/stitch_support/stitch_support.cpp4
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;