diff options
Diffstat (limited to 'src/mongo/db/exec/add_fields_projection_executor_test.cpp')
-rw-r--r-- | src/mongo/db/exec/add_fields_projection_executor_test.cpp | 569 |
1 files changed, 569 insertions, 0 deletions
diff --git a/src/mongo/db/exec/add_fields_projection_executor_test.cpp b/src/mongo/db/exec/add_fields_projection_executor_test.cpp new file mode 100644 index 00000000000..047bbbbc42e --- /dev/null +++ b/src/mongo/db/exec/add_fields_projection_executor_test.cpp @@ -0,0 +1,569 @@ +/** + * 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 <vector> + +#include "mongo/bson/bsonmisc.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/exec/add_fields_projection_executor.h" +#include "mongo/db/exec/document_value/document.h" +#include "mongo/db/exec/document_value/document_value_test_util.h" +#include "mongo/db/exec/document_value/value.h" +#include "mongo/db/pipeline/dependencies.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo::projection_executor { +namespace { +using std::vector; + +// These AddFieldsProjectionExecutor spec tests are a subset of the ProjectionExecutor creation +// tests. AddFieldsProjectionExecutor should behave the same way, but does not use the same +// creation, so we include an abbreviation of the same tests here. + +// Verify that AddFieldsProjectionExecutor rejects specifications with conflicting field paths. +TEST(AddFieldsProjectionExecutorSpec, ThrowsOnCreationWithConflictingFieldPaths) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + // These specs contain the same exact path. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << 1 << "a" << 2)), + AssertionException); + ASSERT_THROWS( + AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSON("b" << 1 << "b" << 2))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("_id" << 3 << "_id" << true)), + AssertionException); + + // These specs contain overlapping paths. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << 1 << "a.b" << 2)), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a.b.c" << 1 << "a" << 2)), + AssertionException); + ASSERT_THROWS( + AddFieldsProjectionExecutor::create(expCtx, BSON("_id" << true << "_id.x" << true)), + AssertionException); +} + +// Verify that AddFieldsProjectionExecutor rejects specifications that contain invalid field paths. +TEST(AddFieldsProjectionExecutorSpec, ThrowsOnCreationWithInvalidFieldPath) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + // Dotted subfields are not allowed. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSON("b.c" << true))), + AssertionException); + + // The user cannot start a field with $. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("$dollar" << 0)), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("c.$d" << true)), + AssertionException); + + // Empty field names should throw an error. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("" << 2)), AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSON("" << true))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("" << BSON("a" << true))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a." << true)), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON(".a" << true)), + AssertionException); +} + +// Verify that AddFieldsProjectionExecutor rejects specifications that contain empty objects or +// invalid expressions. +TEST(AddFieldsProjectionExecutorSpec, ThrowsOnCreationWithInvalidObjectsOrExpressions) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + // Invalid expressions should be rejected. + ASSERT_THROWS(AddFieldsProjectionExecutor::create( + expCtx, BSON("a" << BSON("$add" << BSON_ARRAY(4 << 2) << "b" << 1))), + AssertionException); + ASSERT_THROWS( + AddFieldsProjectionExecutor::create(expCtx, + BSON("a" << BSON("$gt" << BSON("bad" + << "arguments")))), + AssertionException); + ASSERT_THROWS(AddFieldsProjectionExecutor::create( + expCtx, BSON("a" << false << "b" << BSON("$unknown" << BSON_ARRAY(4 << 2)))), + AssertionException); + + // Empty specifications are not allowed. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSONObj()), AssertionException); + + // Empty nested objects are not allowed. + ASSERT_THROWS(AddFieldsProjectionExecutor::create(expCtx, BSON("a" << BSONObj())), + AssertionException); +} + +TEST(AddFieldsProjectionExecutor, DoesNotErrorOnTwoNestedFields) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor::create(expCtx, BSON("a.b" << true << "a.c" << true)); + AddFieldsProjectionExecutor::create(expCtx, BSON("a.b" << true << "a" << BSON("c" << true))); +} + +// Verify that replaced fields are not included as dependencies. +TEST(AddFieldsProjectionExecutorDeps, RemovesReplaceFieldsFromDependencies) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a" << true)); + + DepsTracker deps; + addition.addDependencies(&deps); + + ASSERT_EQ(deps.fields.size(), 0UL); + ASSERT_EQ(deps.fields.count("_id"), 0UL); // Not explicitly included. + ASSERT_EQ(deps.fields.count("a"), 0UL); // Set to true. +} + +// Verify that adding nested fields keeps the top-level field as a dependency. +TEST(AddFieldsProjectionExecutorDeps, IncludesTopLevelFieldInDependenciesWhenAddingNestedFields) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("x.y" << true)); + + DepsTracker deps; + addition.addDependencies(&deps); + + ASSERT_EQ(deps.fields.size(), 1UL); + ASSERT_EQ(deps.fields.count("_id"), 0UL); // Not explicitly included. + ASSERT_EQ(deps.fields.count("x.y"), 0UL); // Set to true. + ASSERT_EQ(deps.fields.count("x"), 1UL); // Top-level of nested field included. +} + +// Verify that fields that an expression depends on are added to the dependencies. +TEST(AddFieldsProjectionExecutorDeps, AddsDependenciesForComputedFields) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("x.y" + << "$z" + << "a" + << "$b")); + + DepsTracker deps; + addition.addDependencies(&deps); + + ASSERT_EQ(deps.fields.size(), 3UL); + ASSERT_EQ(deps.fields.count("_id"), 0UL); // Not explicitly included. + ASSERT_EQ(deps.fields.count("z"), 1UL); // Needed by the ExpressionFieldPath for x.y. + ASSERT_EQ(deps.fields.count("x"), 1UL); // Preserves top-level field, for structure. + ASSERT_EQ(deps.fields.count("a"), 0UL); // Replaced, so omitted. + ASSERT_EQ(deps.fields.count("b"), 1UL); // Needed by the ExpressionFieldPath for a. +} + +// Verify that the serialization produces the correct output: converting numbers and literals to +// their corresponding $const form. +TEST(AddFieldsProjectionExecutorSerialize, SerializesToCorrectForm) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(fromjson("{a: {$add: ['$a', 2]}, b: {d: 3}, 'x.y': {$literal: 4}}")); + + auto expectedSerialization = Document( + fromjson("{a: {$add: [\"$a\", {$const: 2}]}, b: {d: {$const: 3}}, x: {y: {$const: 4}}}")); + + // Should be the same if we're serializing for explain or for internal use. + ASSERT_DOCUMENT_EQ(expectedSerialization, addition.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); +} + +// Verify that serialize treats the _id field as any other field: including when explicity included. +TEST(AddFieldsProjectionExecutorSerialize, AddsIdToSerializeWhenExplicitlyIncluded) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("_id" << false)); + + // Adds explicit "_id" setting field, serializes expressions. + auto expectedSerialization = Document(fromjson("{_id: {$const: false}}")); + + // Should be the same if we're serializing for explain or for internal use. + ASSERT_DOCUMENT_EQ(expectedSerialization, addition.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); +} + +// Verify that serialize treats the _id field as any other field: excluded when not explicitly +// listed in the specification. We add this check because it is different behavior from $project, +// yet they derive from the same parent class. If the parent class were to change, this test would +// fail. +TEST(AddFieldsProjectionExecutorSerialize, OmitsIdFromSerializeWhenNotIncluded) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a" << true)); + + // Does not implicitly include "_id" field. + auto expectedSerialization = Document(fromjson("{a: {$const: true}}")); + + // Should be the same if we're serializing for explain or for internal use. + ASSERT_DOCUMENT_EQ(expectedSerialization, addition.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); +} + +// Verify that the $addFields stage optimizes expressions into simpler forms when possible. +TEST(AddFieldsProjectionExecutorOptimize, OptimizesTopLevelExpressions) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a" << BSON("$add" << BSON_ARRAY(1 << 2)))); + addition.optimize(); + auto expectedSerialization = Document{{"a", Document{{"$const", 3}}}}; + + // Should be the same if we're serializing for explain or for internal use. + ASSERT_DOCUMENT_EQ(expectedSerialization, addition.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); +} + +// Verify that the $addFields stage optimizes expressions even when they are nested. +TEST(AddFieldsProjectionExecutorOptimize, ShouldOptimizeNestedExpressions) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a.b" << BSON("$add" << BSON_ARRAY(1 << 2)))); + addition.optimize(); + auto expectedSerialization = Document{{"a", Document{{"b", Document{{"$const", 3}}}}}}; + + // Should be the same if we're serializing for explain or for internal use. + ASSERT_DOCUMENT_EQ(expectedSerialization, addition.serializeTransformation(boost::none)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kQueryPlanner)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecStats)); + ASSERT_DOCUMENT_EQ(expectedSerialization, + addition.serializeTransformation(ExplainOptions::Verbosity::kExecAllPlans)); +} + +// +// Top-level only. +// + +// Verify that a new field is added to the end of the document. +TEST(AddFieldsProjectionExecutorExecutionTest, AddsNewFieldToEndOfDocument) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("c" << 3)); + + // There are no fields in the document. + auto result = addition.applyProjection(Document{}); + auto expectedResult = Document{{"c", 3}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + // There are fields in the document but none of them are the added field. + result = addition.applyProjection(Document{{"a", 1}, {"b", 2}}); + expectedResult = Document{{"a", 1}, {"b", 2}, {"c", 3}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that an existing field is replaced and stays in the same order in the document. +TEST(AddFieldsProjectionExecutorExecutionTest, ReplacesFieldThatAlreadyExistsInDocument) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("c" << 3)); + + // Specified field is the only field in the document, and is replaced. + auto result = addition.applyProjection(Document{{"c", 1}}); + auto expectedResult = Document{{"c", 3}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + // Specified field is one of the fields in the document, and is replaced in its existing order. + result = addition.applyProjection(Document{{"c", 1}, {"b", 2}}); + expectedResult = Document{{"c", 3}, {"b", 2}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that replacing multiple fields preserves the original field order in the document. +TEST(AddFieldsProjectionExecutorExecutionTest, + ReplacesMultipleFieldsWhilePreservingInputFieldOrder) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("second" + << "SECOND" + << "first" + << "FIRST")); + auto result = addition.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}}); + auto expectedResult = Document{{"first", "FIRST"_sd}, {"second", "SECOND"_sd}, {"third", 2}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that adding multiple fields adds the fields in the order specified. +TEST(AddFieldsProjectionExecutorExecutionTest, AddsNewFieldsAfterExistingFieldsInOrderSpecified) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("firstComputed" + << "FIRST" + << "secondComputed" + << "SECOND")); + auto result = addition.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}}); + auto expectedResult = Document{{"first", 0}, + {"second", 1}, + {"third", 2}, + {"firstComputed", "FIRST"_sd}, + {"secondComputed", "SECOND"_sd}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that both adding and replacing fields at the same time follows the same rules as doing +// each independently. +TEST(AddFieldsProjectionExecutorExecutionTest, + ReplacesAndAddsNewFieldsWithSameOrderingRulesAsSeparately) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("firstComputed" + << "FIRST" + << "second" + << "SECOND")); + auto result = addition.applyProjection(Document{{"first", 0}, {"second", 1}, {"third", 2}}); + auto expectedResult = Document{ + {"first", 0}, {"second", "SECOND"_sd}, {"third", 2}, {"firstComputed", "FIRST"_sd}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that _id is included just like a regular field, in whatever order it appears in the +// input document, when adding new fields. +TEST(AddFieldsProjectionExecutorExecutionTest, IdFieldIsKeptInOrderItAppearsInInputDocument) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("newField" + << "computedVal")); + auto result = addition.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}}); + auto expectedResult = Document{{"_id", "ID"_sd}, {"a", 1}, {"newField", "computedVal"_sd}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + result = addition.applyProjection(Document{{"a", 1}, {"_id", "ID"_sd}}); + expectedResult = Document{{"a", 1}, {"_id", "ID"_sd}, {"newField", "computedVal"_sd}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that replacing or adding _id works just like any other field. +TEST(AddFieldsProjectionExecutorExecutionTest, ShouldReplaceIdWithComputedId) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("_id" + << "newId")); + auto result = addition.applyProjection(Document{{"_id", "ID"_sd}, {"a", 1}}); + auto expectedResult = Document{{"_id", "newId"_sd}, {"a", 1}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + result = addition.applyProjection(Document{{"a", 1}, {"_id", "ID"_sd}}); + expectedResult = Document{{"a", 1}, {"_id", "newId"_sd}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + result = addition.applyProjection(Document{{"a", 1}}); + expectedResult = Document{{"a", 1}, {"_id", "newId"_sd}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// +// Adding nested fields. +// + +// Verify that adding a dotted field keeps the other fields in the subdocument. +TEST(AddFieldsProjectionExecutorExecutionTest, + KeepsExistingSubFieldsWhenAddingSimpleDottedFieldToSubDoc) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a.b" << true)); + + // More than one field in sub document. + auto result = addition.applyProjection(Document{{"a", Document{{"b", 1}, {"c", 2}}}}); + auto expectedResult = Document{{"a", Document{{"b", true}, {"c", 2}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + // Specified field is the only field in the sub document. + result = addition.applyProjection(Document{{"a", Document{{"b", 1}}}}); + expectedResult = Document{{"a", Document{{"b", true}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + // Specified field is not present in the sub document. + result = addition.applyProjection(Document{{"a", Document{{"c", 1}}}}); + expectedResult = Document{{"a", Document{{"c", 1}, {"b", true}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + // There are no fields in sub document. + result = addition.applyProjection(Document{{"a", Document{}}}); + expectedResult = Document{{"a", Document{{"b", true}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that creating a dotted field creates the subdocument structure necessary. +TEST(AddFieldsProjectionExecutorExecutionTest, CreatesSubDocIfDottedAddedFieldDoesNotExist) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("sub.target" << true)); + + // Should add the path if it doesn't exist. + auto result = addition.applyProjection(Document{}); + auto expectedResult = Document{{"sub", Document{{"target", true}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); + + // Should replace the second part of the path if that part already exists. + result = addition.applyProjection(Document{{"sub", "notADocument"_sd}}); + expectedResult = Document{{"sub", Document{{"target", true}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that adding a dotted value to an array field sets the field in every element of the array. +// SERVER-25200: make this agree with $set. +TEST(AddFieldsProjectionExecutorExecutionTest, AppliesDottedAdditionToEachElementInArray) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a.b" << true)); + + vector<Value> nestedValues = {Value(1), + Value(Document{}), + Value(Document{{"b", 1}}), + Value(Document{{"b", 1}, {"c", 2}}), + Value(vector<Value>{}), + Value(vector<Value>{Value(1), Value(Document{{"c", 1}})})}; + + // Adds the field "b" to every object in the array. Recurses on non-empty nested arrays. + vector<Value> expectedNestedValues = { + Value(Document{{"b", true}}), + Value(Document{{"b", true}}), + Value(Document{{"b", true}}), + Value(Document{{"b", true}, {"c", 2}}), + Value(vector<Value>{}), + Value(vector<Value>{Value(Document{{"b", true}}), Value(Document{{"c", 1}, {"b", true}})})}; + auto result = addition.applyProjection(Document{{"a", nestedValues}}); + auto expectedResult = Document{{"a", expectedNestedValues}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that creation of the subdocument structure works for many layers of nesting. +TEST(AddFieldsProjectionExecutorExecutionTest, CreatesNestedSubDocumentsAllTheWayToAddedField) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a.b.c.d" + << "computedVal")); + + // Should add the path if it doesn't exist. + auto result = addition.applyProjection(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 = addition.applyProjection(Document{{"a", Document{{"b", "other"_sd}}}}); + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that _id is not special: we can add subfields to it as well. +TEST(AddFieldsProjectionExecutorExecutionTest, AddsSubFieldsOfId) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("_id.X" << true << "_id.Z" + << "NEW")); + auto result = addition.applyProjection(Document{{"_id", Document{{"X", 1}, {"Y", 2}}}}); + auto expectedResult = Document{{"_id", Document{{"X", true}, {"Y", 2}, {"Z", "NEW"_sd}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that both ways of specifying nested fields -- both dotted notation and nesting -- +// can be used together in the same specification. +TEST(AddFieldsProjectionExecutorExecutionTest, ShouldAllowMixedNestedAndDottedFields) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + // Include all of "a.b", "a.c", "a.d", and "a.e". + // Add new computed fields "a.W", "a.X", "a.Y", and "a.Z". + addition.parse(BSON("a.b" << true << "a.c" << true << "a.W" + << "W" + << "a.X" + << "X" + << "a" + << BSON("d" << true << "e" << true << "Y" + << "Y" + << "Z" + << "Z"))); + auto result = addition.applyProjection(Document{ + {"a", + Document{{"b", "b"_sd}, {"c", "c"_sd}, {"d", "d"_sd}, {"e", "e"_sd}, {"f", "f"_sd}}}}); + auto expectedResult = Document{{"a", + Document{{"b", true}, + {"c", true}, + {"d", true}, + {"e", true}, + {"f", "f"_sd}, + {"W", "W"_sd}, + {"X", "X"_sd}, + {"Y", "Y"_sd}, + {"Z", "Z"_sd}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// Verify that adding nested fields preserves the addition order in the spec. +TEST(AddFieldsProjectionExecutorExecutionTest, AddsNestedAddedFieldsInOrderSpecified) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("b.d" + << "FIRST" + << "b.c" + << "SECOND")); + auto result = addition.applyProjection(Document{}); + auto expectedResult = Document{{"b", Document{{"d", "FIRST"_sd}, {"c", "SECOND"_sd}}}}; + ASSERT_DOCUMENT_EQ(result, expectedResult); +} + +// +// Misc/Metadata. +// + +// Verify that the metadata is kept from the original input document. +TEST(AddFieldsProjectionExecutorExecutionTest, AlwaysKeepsMetadataFromOriginalDoc) { + boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); + AddFieldsProjectionExecutor addition(expCtx); + addition.parse(BSON("a" << true)); + + MutableDocument inputDocBuilder(Document{{"a", 1}}); + inputDocBuilder.metadata().setRandVal(1.0); + inputDocBuilder.metadata().setTextScore(10.0); + Document inputDoc = inputDocBuilder.freeze(); + + auto result = addition.applyProjection(inputDoc); + + MutableDocument expectedDoc(Document{{"a", true}}); + expectedDoc.copyMetaDataFrom(inputDoc); + ASSERT_DOCUMENT_EQ(result, expectedDoc.freeze()); +} +} // namespace +} // namespace mongo::projection_executor |