1 
2 /**
3  *    Copyright (C) 2018-present MongoDB, Inc.
4  *
5  *    This program is free software: you can redistribute it and/or modify
6  *    it under the terms of the Server Side Public License, version 1,
7  *    as published by MongoDB, Inc.
8  *
9  *    This program is distributed in the hope that it will be useful,
10  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *    Server Side Public License for more details.
13  *
14  *    You should have received a copy of the Server Side Public License
15  *    along with this program. If not, see
16  *    <http://www.mongodb.com/licensing/server-side-public-license>.
17  *
18  *    As a special exception, the copyright holders give permission to link the
19  *    code of portions of this program with the OpenSSL library under certain
20  *    conditions as described in each individual source file and distribute
21  *    linked combinations including the program with the OpenSSL library. You
22  *    must comply with the Server Side Public License in all respects for
23  *    all of the code used other than as permitted herein. If you modify file(s)
24  *    with this exception, you may extend this exception to your version of the
25  *    file(s), but you are not obligated to do so. If you do not wish to do so,
26  *    delete this exception statement from your version. If you delete this
27  *    exception statement from all source files in the program, then also delete
28  *    it in the license file.
29  */
30 
31 #include "mongo/platform/basic.h"
32 
33 #include <vector>
34 
35 #include "mongo/bson/bson_depth.h"
36 #include "mongo/bson/bsonelement.h"
37 #include "mongo/bson/bsonmisc.h"
38 #include "mongo/bson/bsonobj.h"
39 #include "mongo/bson/json.h"
40 #include "mongo/db/pipeline/aggregation_context_fixture.h"
41 #include "mongo/db/pipeline/dependencies.h"
42 #include "mongo/db/pipeline/document_source_mock.h"
43 #include "mongo/db/pipeline/document_source_project.h"
44 #include "mongo/db/pipeline/document_value_test_util.h"
45 #include "mongo/db/pipeline/value.h"
46 #include "mongo/unittest/unittest.h"
47 
48 namespace mongo {
49 namespace {
50 using boost::intrusive_ptr;
51 using std::vector;
52 
53 //
54 // DocumentSourceProject delegates much of its responsibilities to the ParsedAggregationProjection.
55 // Most of the functional tests are testing ParsedAggregationProjection directly. These are meant as
56 // simpler integration tests.
57 //
58 
59 // This provides access to getExpCtx(), but we'll use a different name for this test suite.
60 using ProjectStageTest = AggregationContextFixture;
61 
TEST_F(ProjectStageTest,InclusionProjectionShouldRemoveUnspecifiedFields)62 TEST_F(ProjectStageTest, InclusionProjectionShouldRemoveUnspecifiedFields) {
63     auto project =
64         DocumentSourceProject::create(BSON("a" << true << "c" << BSON("d" << true)), getExpCtx());
65     auto source = DocumentSourceMock::create("{_id: 0, a: 1, b: 1, c: {d: 1}}");
66     project->setSource(source.get());
67     // The first result exists and is as expected.
68     auto next = project->getNext();
69     ASSERT_TRUE(next.isAdvanced());
70     ASSERT_EQUALS(1, next.getDocument().getField("a").getInt());
71     ASSERT(next.getDocument().getField("b").missing());
72     // The _id field is included by default in the root document.
73     ASSERT_EQUALS(0, next.getDocument().getField("_id").getInt());
74     // The nested c.d inclusion.
75     ASSERT_EQUALS(1, next.getDocument()["c"]["d"].getInt());
76 }
77 
TEST_F(ProjectStageTest,ShouldOptimizeInnerExpressions)78 TEST_F(ProjectStageTest, ShouldOptimizeInnerExpressions) {
79     auto project = DocumentSourceProject::create(
80         BSON("a" << BSON("$and" << BSON_ARRAY(BSON("$const" << true)))), getExpCtx());
81     project->optimize();
82     // The $and should have been replaced with its only argument.
83     vector<Value> serializedArray;
84     project->serializeToArray(serializedArray);
85     ASSERT_BSONOBJ_EQ(serializedArray[0].getDocument().toBson(),
86                       fromjson("{$project: {_id: true, a: {$const: true}}}"));
87 }
88 
TEST_F(ProjectStageTest,ShouldErrorOnNonObjectSpec)89 TEST_F(ProjectStageTest, ShouldErrorOnNonObjectSpec) {
90     BSONObj spec = BSON("$project"
91                         << "foo");
92     BSONElement specElement = spec.firstElement();
93     ASSERT_THROWS(DocumentSourceProject::createFromBson(specElement, getExpCtx()),
94                   AssertionException);
95 }
96 
97 /**
98  * Basic sanity check that two documents can be projected correctly with a simple inclusion
99  * projection.
100  */
TEST_F(ProjectStageTest,InclusionShouldBeAbleToProcessMultipleDocuments)101 TEST_F(ProjectStageTest, InclusionShouldBeAbleToProcessMultipleDocuments) {
102     auto project = DocumentSourceProject::create(BSON("a" << true), getExpCtx());
103     auto source = DocumentSourceMock::create({"{a: 1, b: 2}", "{a: 3, b: 4}"});
104     project->setSource(source.get());
105     auto next = project->getNext();
106     ASSERT(next.isAdvanced());
107     ASSERT_EQUALS(1, next.getDocument().getField("a").getInt());
108     ASSERT(next.getDocument().getField("b").missing());
109 
110     next = project->getNext();
111     ASSERT(next.isAdvanced());
112     ASSERT_EQUALS(3, next.getDocument().getField("a").getInt());
113     ASSERT(next.getDocument().getField("b").missing());
114 
115     ASSERT(project->getNext().isEOF());
116     ASSERT(project->getNext().isEOF());
117     ASSERT(project->getNext().isEOF());
118 }
119 
120 /**
121  * Basic sanity check that two documents can be projected correctly with a simple inclusion
122  * projection.
123  */
TEST_F(ProjectStageTest,ExclusionShouldBeAbleToProcessMultipleDocuments)124 TEST_F(ProjectStageTest, ExclusionShouldBeAbleToProcessMultipleDocuments) {
125     auto project = DocumentSourceProject::create(BSON("a" << false), getExpCtx());
126     auto source = DocumentSourceMock::create({"{a: 1, b: 2}", "{a: 3, b: 4}"});
127     project->setSource(source.get());
128     auto next = project->getNext();
129     ASSERT(next.isAdvanced());
130     ASSERT(next.getDocument().getField("a").missing());
131     ASSERT_EQUALS(2, next.getDocument().getField("b").getInt());
132 
133     next = project->getNext();
134     ASSERT(next.isAdvanced());
135     ASSERT(next.getDocument().getField("a").missing());
136     ASSERT_EQUALS(4, next.getDocument().getField("b").getInt());
137 
138     ASSERT(project->getNext().isEOF());
139     ASSERT(project->getNext().isEOF());
140     ASSERT(project->getNext().isEOF());
141 }
142 
TEST_F(ProjectStageTest,ShouldPropagatePauses)143 TEST_F(ProjectStageTest, ShouldPropagatePauses) {
144     auto project = DocumentSourceProject::create(BSON("a" << false), getExpCtx());
145     auto source = DocumentSourceMock::create({Document(),
146                                               DocumentSource::GetNextResult::makePauseExecution(),
147                                               Document(),
148                                               DocumentSource::GetNextResult::makePauseExecution(),
149                                               Document(),
150                                               DocumentSource::GetNextResult::makePauseExecution()});
151     project->setSource(source.get());
152 
153     ASSERT_TRUE(project->getNext().isAdvanced());
154     ASSERT_TRUE(project->getNext().isPaused());
155     ASSERT_TRUE(project->getNext().isAdvanced());
156     ASSERT_TRUE(project->getNext().isPaused());
157     ASSERT_TRUE(project->getNext().isAdvanced());
158     ASSERT_TRUE(project->getNext().isPaused());
159 
160     ASSERT(project->getNext().isEOF());
161     ASSERT(project->getNext().isEOF());
162     ASSERT(project->getNext().isEOF());
163 }
164 
TEST_F(ProjectStageTest,InclusionShouldAddDependenciesOfIncludedAndComputedFields)165 TEST_F(ProjectStageTest, InclusionShouldAddDependenciesOfIncludedAndComputedFields) {
166     auto project = DocumentSourceProject::create(
167         fromjson("{a: true, x: '$b', y: {$and: ['$c','$d']}, z: {$meta: 'textScore'}}"),
168         getExpCtx());
169     DepsTracker dependencies(DepsTracker::MetadataAvailable::kTextScore);
170     ASSERT_EQUALS(DocumentSource::EXHAUSTIVE_FIELDS, project->getDependencies(&dependencies));
171     ASSERT_EQUALS(5U, dependencies.fields.size());
172 
173     // Implicit _id dependency.
174     ASSERT_EQUALS(1U, dependencies.fields.count("_id"));
175 
176     // Inclusion dependency.
177     ASSERT_EQUALS(1U, dependencies.fields.count("a"));
178 
179     // Field path expression dependency.
180     ASSERT_EQUALS(1U, dependencies.fields.count("b"));
181 
182     // Nested expression dependencies.
183     ASSERT_EQUALS(1U, dependencies.fields.count("c"));
184     ASSERT_EQUALS(1U, dependencies.fields.count("d"));
185     ASSERT_EQUALS(false, dependencies.needWholeDocument);
186     ASSERT_EQUALS(true, dependencies.getNeedTextScore());
187 }
188 
TEST_F(ProjectStageTest,ExclusionShouldNotAddDependencies)189 TEST_F(ProjectStageTest, ExclusionShouldNotAddDependencies) {
190     auto project = DocumentSourceProject::create(fromjson("{a: false, 'b.c': false}"), getExpCtx());
191 
192     DepsTracker dependencies;
193     ASSERT_EQUALS(DocumentSource::SEE_NEXT, project->getDependencies(&dependencies));
194 
195     ASSERT_EQUALS(0U, dependencies.fields.size());
196     ASSERT_EQUALS(false, dependencies.needWholeDocument);
197     ASSERT_EQUALS(false, dependencies.getNeedTextScore());
198 }
199 
TEST_F(ProjectStageTest,InclusionProjectionReportsIncludedPathsFromGetModifiedPaths)200 TEST_F(ProjectStageTest, InclusionProjectionReportsIncludedPathsFromGetModifiedPaths) {
201     auto project = DocumentSourceProject::create(
202         fromjson("{a: true, 'b.c': {d: true}, e: {f: {g: true}}, h: {i: {$literal: true}}}"),
203         getExpCtx());
204 
205     auto modifiedPaths = project->getModifiedPaths();
206     ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllExcept);
207     ASSERT_EQUALS(4U, modifiedPaths.paths.size());
208     ASSERT_EQUALS(1U, modifiedPaths.paths.count("_id"));
209     ASSERT_EQUALS(1U, modifiedPaths.paths.count("a"));
210     ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d"));
211     ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g"));
212 }
213 
TEST_F(ProjectStageTest,InclusionProjectionReportsIncludedPathsButExcludesId)214 TEST_F(ProjectStageTest, InclusionProjectionReportsIncludedPathsButExcludesId) {
215     auto project = DocumentSourceProject::create(
216         fromjson("{_id: false, 'b.c': {d: true}, e: {f: {g: true}}, h: {i: {$literal: true}}}"),
217         getExpCtx());
218 
219     auto modifiedPaths = project->getModifiedPaths();
220     ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllExcept);
221     ASSERT_EQUALS(2U, modifiedPaths.paths.size());
222     ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d"));
223     ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g"));
224 }
225 
TEST_F(ProjectStageTest,ExclusionProjectionReportsExcludedPathsAsModifiedPaths)226 TEST_F(ProjectStageTest, ExclusionProjectionReportsExcludedPathsAsModifiedPaths) {
227     auto project = DocumentSourceProject::create(
228         fromjson("{a: false, 'b.c': {d: false}, e: {f: {g: false}}}"), getExpCtx());
229 
230     auto modifiedPaths = project->getModifiedPaths();
231     ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kFiniteSet);
232     ASSERT_EQUALS(3U, modifiedPaths.paths.size());
233     ASSERT_EQUALS(1U, modifiedPaths.paths.count("a"));
234     ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d"));
235     ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g"));
236 }
237 
TEST_F(ProjectStageTest,ExclusionProjectionReportsExcludedPathsWithIdExclusion)238 TEST_F(ProjectStageTest, ExclusionProjectionReportsExcludedPathsWithIdExclusion) {
239     auto project = DocumentSourceProject::create(
240         fromjson("{_id: false, 'b.c': {d: false}, e: {f: {g: false}}}"), getExpCtx());
241 
242     auto modifiedPaths = project->getModifiedPaths();
243     ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kFiniteSet);
244     ASSERT_EQUALS(3U, modifiedPaths.paths.size());
245     ASSERT_EQUALS(1U, modifiedPaths.paths.count("_id"));
246     ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d"));
247     ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g"));
248 }
249 
TEST_F(ProjectStageTest,CanUseRemoveSystemVariableToConditionallyExcludeProjectedField)250 TEST_F(ProjectStageTest, CanUseRemoveSystemVariableToConditionallyExcludeProjectedField) {
251     auto project = DocumentSourceProject::create(
252         fromjson("{a: 1, b: {$cond: [{$eq: ['$b', 4]}, '$$REMOVE', '$b']}}"), getExpCtx());
253     auto source = DocumentSourceMock::create({"{a: 2, b: 2}", "{a: 3, b: 4}"});
254     project->setSource(source.get());
255     auto next = project->getNext();
256     ASSERT(next.isAdvanced());
257     Document expected{{"a", 2}, {"b", 2}};
258     ASSERT_DOCUMENT_EQ(next.releaseDocument(), expected);
259 
260     next = project->getNext();
261     ASSERT(next.isAdvanced());
262     expected = Document{{"a", 3}};
263     ASSERT_DOCUMENT_EQ(next.releaseDocument(), expected);
264 
265     ASSERT(project->getNext().isEOF());
266 }
267 
268 /**
269  * Creates BSON for a DocumentSourceProject that represents projecting a new computed field nested
270  * 'depth' levels deep.
271  */
makeProjectForNestedDocument(size_t depth)272 BSONObj makeProjectForNestedDocument(size_t depth) {
273     ASSERT_GTE(depth, 2U);
274     StringBuilder builder;
275     builder << "a";
276     for (size_t i = 0; i < depth - 1; ++i) {
277         builder << ".a";
278     }
279     return BSON(builder.str() << BSON("$literal" << 1));
280 }
281 
TEST_F(ProjectStageTest,CanAddNestedDocumentExactlyAtDepthLimit)282 TEST_F(ProjectStageTest, CanAddNestedDocumentExactlyAtDepthLimit) {
283     auto project = DocumentSourceProject::create(
284         makeProjectForNestedDocument(BSONDepth::getMaxAllowableDepth()), getExpCtx());
285     auto mock = DocumentSourceMock::create(Document{{"_id", 1}});
286     project->setSource(mock.get());
287 
288     auto next = project->getNext();
289     ASSERT_TRUE(next.isAdvanced());
290 }
291 
TEST_F(ProjectStageTest,CannotAddNestedDocumentExceedingDepthLimit)292 TEST_F(ProjectStageTest, CannotAddNestedDocumentExceedingDepthLimit) {
293     ASSERT_THROWS_CODE(
294         DocumentSourceProject::create(
295             makeProjectForNestedDocument(BSONDepth::getMaxAllowableDepth() + 1), getExpCtx()),
296         AssertionException,
297         ErrorCodes::Overflow);
298 }
299 }  // namespace
300 }  // namespace mongo
301