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