diff options
Diffstat (limited to 'src/mongo/db/exec/projection_executor_test.cpp')
-rw-r--r-- | src/mongo/db/exec/projection_executor_test.cpp | 682 |
1 files changed, 526 insertions, 156 deletions
diff --git a/src/mongo/db/exec/projection_executor_test.cpp b/src/mongo/db/exec/projection_executor_test.cpp index 0d97b093b1b..6d0d06a84d4 100644 --- a/src/mongo/db/exec/projection_executor_test.cpp +++ b/src/mongo/db/exec/projection_executor_test.cpp @@ -29,212 +29,582 @@ #include "mongo/platform/basic.h" -#include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/projection_executor.h" -#include "mongo/db/pipeline/aggregation_context_fixture.h" -#include "mongo/db/query/collation/collator_interface_mock.h" -#include "mongo/db/query/projection_ast_util.h" + +#include <vector> + +#include "mongo/bson/bsonmisc.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/exec/document_value/document.h" +#include "mongo/db/exec/document_value/value.h" +#include "mongo/db/exec/inclusion_projection_executor.h" +#include "mongo/db/exec/projection_executor.h" +#include "mongo/db/exec/projection_executor_builder.h" +#include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/projection_parser.h" #include "mongo/unittest/unittest.h" namespace mongo::projection_executor { namespace { -constexpr auto kOptimzeExecutor = true; +template <typename T> +BSONObj wrapInLiteral(const T& arg) { + return BSON("$literal" << arg); +} -class ProjectionExecutorTest : public AggregationContextFixture { -public: - projection_ast::Projection parseWithDefaultPolicies( - const BSONObj& projectionBson, boost::optional<BSONObj> matchExprBson = boost::none) { - return parseWithPolicies(projectionBson, matchExprBson, ProjectionPolicies{}); - } +// Helper to simplify the creation of a ProjectionExecutor with default policies. +auto makeProjectionWithDefaultPolicies(BSONObj spec) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ProjectionPolicies defaultPolicies; + auto projection = projection_ast::parse(expCtx, spec, defaultPolicies); + return buildProjectionExecutor( + expCtx, &projection, defaultPolicies, true /* optimizeExecutor */); +} - projection_ast::Projection parseWithFindFeaturesEnabled( - const BSONObj& projectionBson, boost::optional<BSONObj> matchExprBson = boost::none) { - auto policy = ProjectionPolicies::findProjectionPolicies(); - return parseWithPolicies(projectionBson, matchExprBson, policy); - } +// Helper to simplify the creation of a ProjectionExecutor which bans computed fields. +auto makeProjectionWithBannedComputedFields(BSONObj spec) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + ProjectionPolicies banComputedFields{ + ProjectionPolicies::kDefaultIdPolicyDefault, + ProjectionPolicies::kArrayRecursionPolicyDefault, + ProjectionPolicies::ComputedFieldsPolicy::kBanComputedFields}; + auto projection = projection_ast::parse(expCtx, spec, banComputedFields); + return buildProjectionExecutor( + expCtx, &projection, banComputedFields, true /* optimizeExecutor */); +} - projection_ast::Projection parseWithPolicies(const BSONObj& projectionBson, - boost::optional<BSONObj> matchExprBson, - ProjectionPolicies policies) { - StatusWith<std::unique_ptr<MatchExpression>> swMatchExpression(nullptr); - if (matchExprBson) { - swMatchExpression = MatchExpressionParser::parse(*matchExprBson, getExpCtx()); - uassertStatusOK(swMatchExpression.getStatus()); - } - - return projection_ast::parse(getExpCtx(), - projectionBson, - swMatchExpression.getValue().get(), - matchExprBson.get_value_or(BSONObj()), - policies); - } -}; +// +// Error cases. +// + +TEST(ProjectionExecutorErrors, ShouldRejectDuplicateFieldNames) { + // Include/exclude the same field twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << false)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "b" << false))), + AssertionException); + + // Mix of include/exclude and adding a field. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "a" << wrapInLiteral(0))), + AssertionException); + + // Adding the same field twice. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a" << wrapInLiteral(0))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectDuplicateIds) { + // Include/exclude _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << false)), + AssertionException); + + // Mix of including/excluding and adding _id. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id" << true)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << false << "_id" << wrapInLiteral(0))), + AssertionException); + + // Adding _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("_id" << wrapInLiteral(1) << "_id" << wrapInLiteral(0))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectFieldsWithSharedPrefix) { + // Include/exclude Fields with a shared prefix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "a.b" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << false)), + AssertionException); + + // Mix of include/exclude and adding a shared prefix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "a.b" << true)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a" << wrapInLiteral(0))), + AssertionException); + + // Adding a shared prefix twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << wrapInLiteral(1) << "a.b" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b.c.d" << wrapInLiteral(1) << "a.b.c" << wrapInLiteral(0))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectPathConflictsWithNonAlphaNumericCharacters) { + // Include/exclude non-alphanumeric fields with a shared prefix. First assert that the non- + // alphanumeric fields are accepted when no prefixes are present. + ASSERT(makeProjectionWithDefaultPolicies( + BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true << "a.b c" << true))); + ASSERT(makeProjectionWithDefaultPolicies( + BSON("a.b c" << false << "a.b?c" << false << "a.b" << false << "a.b-c" << false))); + + // Then assert that we throw when we introduce a prefixed field. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b-c" << true << "a.b" << true << "a.b?c" << true + << "a.b c" << true << "a.b.d" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b.d" << false << "a.b c" << false + << "a.b?c" << false << "a.b" + << false << "a.b-c" << false)), + AssertionException); + + // Adding the same field twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b?c" << wrapInLiteral(1) << "a.b?c" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b c" << wrapInLiteral(0) << "a.b c" << wrapInLiteral(1))), + AssertionException); + + // Mix of include/exclude and adding a shared prefix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b-c" << true << "a.b" << wrapInLiteral(1) << "a.b?c" << true + << "a.b c" << true << "a.b.d" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b.d" << false << "a.b c" << false << "a.b?c" << false << "a.b" + << wrapInLiteral(0) << "a.b-c" << false)), + AssertionException); -TEST_F(ProjectionExecutorTest, CanProjectInclusionWithIdPath) { - auto projWithId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &projWithId, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{_id: 123, a: 'abc'}")}, - executor->applyTransformation( - Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); + // Adding a shared prefix twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b-c" << wrapInLiteral(1) << "a.b" << wrapInLiteral(1) << "a.b?c" + << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b.d" + << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a.b.d" << wrapInLiteral(1) << "a.b c" << wrapInLiteral(1) << "a.b?c" + << wrapInLiteral(1) << "a.b" << wrapInLiteral(0) << "a.b-c" + << wrapInLiteral(1))), + AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldRejectMixOfIdAndSubFieldsOfId) { + // Include/exclude _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id" << true << "_id.x" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << false)), + AssertionException); - auto projWithoutId = parseWithDefaultPolicies(fromjson("{a: 1, _id: 0}")); - executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 'abc'}")}, - executor->applyTransformation( - Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); + // Mix of including/excluding and adding _id. + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1) << "_id.x" << true)), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id.x" << false << "_id" << wrapInLiteral(0))), + AssertionException); + + // Adding _id twice. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("_id" << wrapInLiteral(1) << "_id.x" << wrapInLiteral(0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("_id.b.c.d" << wrapInLiteral(1) << "_id.b.c" << wrapInLiteral(0))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectInclusionUndottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: 'abc', b: 'def'}")}, - executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); +TEST(ProjectionExecutorErrors, ShouldAllowMixOfIdInclusionAndExclusion) { + // Mixing "_id" inclusion with exclusion. + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false << "_id" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true << "a.b.c" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); } -TEST_F(ProjectionExecutorTest, CanProjectInclusionDottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 1, 'a.d': 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: 'abc', d: 'ghi'}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: 'abc', c: 'def', d: 'ghi'}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectMixOfInclusionAndExclusion) { + // Simple mix. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << false)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << true)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << false << "c" << true))), + AssertionException); + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("_id" << BSON("b" << false << "c" << true))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.b" << false << "a.c" << true)), + AssertionException); + + // Mix while also adding a field. + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << true << "b" << wrapInLiteral(1) << "c" << false)), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << false << "b" << wrapInLiteral(1) << "c" << true)), + AssertionException); + + // Mix of "_id" subfield inclusion and exclusion. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("_id.x" << true << "a.b.c" << false)), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExpression) { - auto proj = parseWithDefaultPolicies(fromjson("{c: {$add: ['$a', '$b']}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{c: 3}")}, - executor->applyTransformation(Document{fromjson("{a: 1, b: 2}")})); +TEST(ProjectionExecutorErrors, ShouldRejectMixOfExclusionAndComputedFields) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1) << "b" << false)), + AssertionException); + + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b" << false << "a.c" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS( + makeProjectionWithDefaultPolicies(BSON("a.b" << wrapInLiteral(1) << "a.c" << false)), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << false << "c" << wrapInLiteral(1)))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << false))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExclusionWithIdPath) { - auto projWithoutId = parseWithDefaultPolicies(fromjson("{a: 0, _id: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &projWithoutId, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{b: 'def', c: 'ghi'}")}, - executor->applyTransformation( - Document{fromjson("{_id: 123, a: 'abc', b: 'def', c: 'ghi'}")})); +TEST(ProjectionExecutorErrors, ShouldRejectFieldNamesStartingWithADollar) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 0)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$dollar" << 1)), AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 0)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b.$dollar" << 1)), AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 0))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("b" << BSON("$dollar" << 1))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 0)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << 1)), AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExclusionUndottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{a: 0, b: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{c: 'ghi'}")}, - executor->applyTransformation(Document{fromjson("{a: 'abc', b: 'def', c: 'ghi'}")})); +TEST(ProjectionExecutorErrors, ShouldRejectTopLevelExpressions) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$add" << BSON_ARRAY(4 << 2))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectExclusionDottedPath) { - auto proj = parseWithDefaultPolicies(fromjson("{'a.b': 0, 'a.d': 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {c: 'def'}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: 'abc', c: 'def', d: 'ghi'}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectExpressionWithMultipleFieldNames) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << 1 << "$add" << BSON_ARRAY(4 << 2)))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << BSON("c" << 1 << "$add" << BSON_ARRAY(4 << 2))))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << BSON("$add" << BSON_ARRAY(4 << 2) << "c" << 1)))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindPositional) { - auto proj = - parseWithFindFeaturesEnabled(fromjson("{'a.b.$': 1}"), fromjson("{'a.b': {$gte: 3}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [3]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectEmptyProjection) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSONObj()), AssertionException); +} - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: {b: [4]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [4, 3, 2]}}")})); +TEST(ProjectionExecutorErrors, ShouldRejectEmptyNestedObject) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSONObj())), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << false << "b" << BSONObj())), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << BSONObj())), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a.b" << BSONObj())), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << BSONObj()))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithInclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: [{b: 3}]}")}, - executor->applyTransformation(Document{fromjson("{a: [{b: 1}, {b: 2}, {b: 3}]}")})); +TEST(ProjectionExecutorErrors, ShouldErrorOnInvalidExpression) { + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies( + BSON("a" << true << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindElemMatch) { +TEST(ProjectionExecutorErrors, ShouldErrorOnInvalidFieldPath) { + // Empty field names. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << wrapInLiteral(2))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << false)), AssertionException); - const BSONObj obj = fromjson("{a: [{b: 3, c: 1}, {b: 1, c: 2}, {b: 1, c: 3}]}"); - { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: 1}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 2}]}")}, - executor->applyTransformation(Document{obj})); - } + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << true))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a" << BSON("" << false))), + AssertionException); - { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: 1, c: 3}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 1, c: 3}]}")}, - executor->applyTransformation(Document{obj})); - } + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << true))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("" << BSON("a" << false))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("a." << false)), AssertionException); + + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON(".a" << false)), AssertionException); + + // Not testing field names with null bytes, since that is invalid BSON, and won't make it to the + // $project stage without a previous error. + + // Field names starting with '$'. + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("$x" << wrapInLiteral(2))), + AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << true)), AssertionException); + ASSERT_THROWS(makeProjectionWithDefaultPolicies(BSON("c.$d" << false)), AssertionException); +} + +TEST(ProjectionExecutorErrors, ShouldNotErrorOnTwoNestedFields) { + makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << true)); + makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a" << BSON("c" << true))); +} + +// +// Determining exclusion vs. inclusion. +// + +TEST(ProjectionExecutorType, ShouldAllowDottedFieldInSubDocument) { + auto proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << true))); + ASSERT(proj->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << wrapInLiteral(1)))); + ASSERT(proj->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + proj = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b.c" << false))); + ASSERT(proj->getType() == TransformerInterface::TransformerType::kExclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldDefaultToInclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldDetectExclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldDetectInclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldTreatOnlyComputedFieldsAsAnInclusionProjection) { + auto parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = + makeProjectionWithDefaultPolicies(BSON("_id" << false << "a" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = + makeProjectionWithDefaultPolicies(BSON("_id" << false << "a.b.c" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id.x" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("_id" << BSON("x" << wrapInLiteral(1)))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithDefaultPolicies(BSON("x" << BSON("_id" << wrapInLiteral(1)))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); } -TEST_F(ProjectionExecutorTest, ElemMatchRespectsCollator) { - CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); - getExpCtx()->setCollator(&collator); +TEST(ProjectionExecutorType, ShouldAllowMixOfInclusionAndComputedFields) { + auto parsedProject = + makeProjectionWithDefaultPolicies(BSON("a" << true << "b" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = + makeProjectionWithDefaultPolicies(BSON("a.b" << true << "a.c" << wrapInLiteral(1))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {$gte: 'abc'}}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); + parsedProject = makeProjectionWithDefaultPolicies( + BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{ a: [ \"zdd\" ] }")}, - executor->applyTransformation(Document{fromjson("{a: ['zaa', 'zbb', 'zdd', 'zee']}")})); + parsedProject = makeProjectionWithDefaultPolicies(BSON("a" << BSON("b" << true << "c" + << "stringLiteral"))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); } -TEST_F(ProjectionExecutorTest, CanProjectFindElemMatchWithExclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{a: {$elemMatch: {b: {$gte: 3}}}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 3}], d: 'def'}")}, - executor->applyTransformation(Document{ - fromjson("{a: [{b: 1}, {b: 2}, {b: 3}], c: 'abc', d: 'def'}")})); +TEST(ProjectionExecutorType, ShouldRejectMixOfInclusionAndBannedComputedFields) { + ASSERT_THROWS( + makeProjectionWithBannedComputedFields(BSON("a" << true << "b" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS( + makeProjectionWithBannedComputedFields(BSON("a.b" << true << "a.c" << wrapInLiteral(1))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << true << "c" << wrapInLiteral(1)))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields(BSON("a" << BSON("b" << true << "c" + << "stringLiteral"))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceWithInclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, ShouldRejectOnlyComputedFieldsWhenComputedFieldsAreBanned) { + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << wrapInLiteral(1) << "b" << wrapInLiteral(2))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a.b" << wrapInLiteral(1) << "a.c" << wrapInLiteral(2))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), + AssertionException); + + ASSERT_THROWS(makeProjectionWithBannedComputedFields( + BSON("a" << BSON("b" << wrapInLiteral(1) << "c" << wrapInLiteral(2)))), + AssertionException); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithInclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 1}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}, c: 'abc'}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, ShouldAcceptInclusionProjectionWhenComputedFieldsAreBanned) { + auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false << "a.b.c" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << true)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << true))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kInclusionProjection); +} + +TEST(ProjectionExecutorType, ShouldAcceptExclusionProjectionWhenComputedFieldsAreBanned) { + auto parsedProject = makeProjectionWithBannedComputedFields(BSON("a" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id.x" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << BSON("x" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("x" << BSON("_id" << false))); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); + + parsedProject = makeProjectionWithBannedComputedFields(BSON("_id" << false)); + ASSERT(parsedProject->getType() == TransformerInterface::TransformerType::kExclusionProjection); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceBasicWithExclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: 3}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [1,2,3]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, ShouldCoerceNumericsToBools) { + std::vector<Value> zeros = {Value(0), Value(0LL), Value(0.0), Value(Decimal128(0))}; + for (auto&& zero : zeros) { + auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", zero}}.toBson()); + ASSERT(parsedProject->getType() == + TransformerInterface::TransformerType::kExclusionProjection); + } + + std::vector<Value> nonZeroes = { + Value(1), Value(-1), Value(3), Value(1LL), Value(1.0), Value(Decimal128(1))}; + for (auto&& nonZero : nonZeroes) { + auto parsedProject = makeProjectionWithDefaultPolicies(Document{{"a", nonZero}}.toBson()); + ASSERT(parsedProject->getType() == + TransformerInterface::TransformerType::kInclusionProjection); + } } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceSkipLimitWithExclusion) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, c: 0}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: 'abc'}")})); +TEST(ProjectionExecutorType, GetExpressionForPathGetsTopLevelExpression) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto projectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); + auto expr = Expression::parseObject(expCtx, projectObj, expCtx->variablesParseState); + ProjectionPolicies defaultPolicies; + auto node = InclusionNode(defaultPolicies); + node.addExpressionForPath(FieldPath("key"), expr); + BSONObjBuilder bob; + ASSERT_EQ(expr, node.getExpressionForPath(FieldPath("key"))); } -TEST_F(ProjectionExecutorTest, CanProjectFindSliceAndPositional) { - auto proj = parseWithFindFeaturesEnabled(fromjson("{'a.b': {$slice: [1,2]}, 'c.$': 1}"), - fromjson("{c: {$gte: 6}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ( - Document{fromjson("{a: {b: [2,3]}, c: [6]}")}, - executor->applyTransformation(Document{fromjson("{a: {b: [1,2,3,4]}, c: [5,6,7]}")})); +TEST(ProjectionExecutorType, GetExpressionForPathGetsCorrectTopLevelExpression) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto correctObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); + auto incorrectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 2) << BSON("$const" << 4))); + auto correctExpr = Expression::parseObject(expCtx, correctObj, expCtx->variablesParseState); + auto incorrectExpr = Expression::parseObject(expCtx, incorrectObj, expCtx->variablesParseState); + ProjectionPolicies defaultPolicies; + auto node = InclusionNode(defaultPolicies); + node.addExpressionForPath(FieldPath("key"), correctExpr); + node.addExpressionForPath(FieldPath("other"), incorrectExpr); + BSONObjBuilder bob; + ASSERT_EQ(correctExpr, node.getExpressionForPath(FieldPath("key"))); } -TEST_F(ProjectionExecutorTest, ExecutorOptimizesExpression) { - auto proj = parseWithDefaultPolicies(fromjson("{a: 1, b: {$add: [1, 2]}}")); - auto executor = buildProjectionExecutor(getExpCtx(), &proj, {}, kOptimzeExecutor); - ASSERT_DOCUMENT_EQ(Document{fromjson("{_id: true, a: true, b: {$const: 3}}")}, - executor->serializeTransformation(boost::none)); +TEST(ProjectionExecutorType, GetExpressionForPathGetsNonTopLevelExpression) { + const boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + auto projectObj = BSON("$add" << BSON_ARRAY(BSON("$const" << 1) << BSON("$const" << 3))); + auto expr = Expression::parseObject(expCtx, projectObj, expCtx->variablesParseState); + ProjectionPolicies defaultPolicies; + auto node = InclusionNode(defaultPolicies); + node.addExpressionForPath(FieldPath("key.second"), expr); + BSONObjBuilder bob; + ASSERT_EQ(expr, node.getExpressionForPath(FieldPath("key.second"))); } + } // namespace } // namespace mongo::projection_executor |