/** * Copyright (C) 2016 MongoDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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 * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * 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 GNU Affero General 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 #include "mongo/bson/bson_depth.h" #include "mongo/bson/bsonelement.h" #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/json.h" #include "mongo/db/pipeline/aggregation_context_fixture.h" #include "mongo/db/pipeline/dependencies.h" #include "mongo/db/pipeline/document_source_mock.h" #include "mongo/db/pipeline/document_source_project.h" #include "mongo/db/pipeline/document_value_test_util.h" #include "mongo/db/pipeline/value.h" #include "mongo/unittest/unittest.h" namespace mongo { namespace { using boost::intrusive_ptr; using std::vector; // // DocumentSourceProject delegates much of its responsibilities to the ParsedAggregationProjection. // Most of the functional tests are testing ParsedAggregationProjection directly. These are meant as // simpler integration tests. // // This provides access to getExpCtx(), but we'll use a different name for this test suite. using ProjectStageTest = AggregationContextFixture; TEST_F(ProjectStageTest, InclusionProjectionShouldRemoveUnspecifiedFields) { auto project = DocumentSourceProject::create(BSON("a" << true << "c" << BSON("d" << true)), getExpCtx()); auto source = DocumentSourceMock::create("{_id: 0, a: 1, b: 1, c: {d: 1}}"); project->setSource(source.get()); // The first result exists and is as expected. auto next = project->getNext(); ASSERT_TRUE(next.isAdvanced()); ASSERT_EQUALS(1, next.getDocument().getField("a").getInt()); ASSERT(next.getDocument().getField("b").missing()); // The _id field is included by default in the root document. ASSERT_EQUALS(0, next.getDocument().getField("_id").getInt()); // The nested c.d inclusion. ASSERT_EQUALS(1, next.getDocument()["c"]["d"].getInt()); } TEST_F(ProjectStageTest, ShouldOptimizeInnerExpressions) { auto project = DocumentSourceProject::create( BSON("a" << BSON("$and" << BSON_ARRAY(BSON("$const" << true)))), getExpCtx()); project->optimize(); // The $and should have been replaced with its only argument. vector serializedArray; project->serializeToArray(serializedArray); ASSERT_BSONOBJ_EQ(serializedArray[0].getDocument().toBson(), fromjson("{$project: {_id: true, a: {$const: true}}}")); } TEST_F(ProjectStageTest, ShouldErrorOnNonObjectSpec) { BSONObj spec = BSON("$project" << "foo"); BSONElement specElement = spec.firstElement(); ASSERT_THROWS(DocumentSourceProject::createFromBson(specElement, getExpCtx()), UserException); } /** * Basic sanity check that two documents can be projected correctly with a simple inclusion * projection. */ TEST_F(ProjectStageTest, InclusionShouldBeAbleToProcessMultipleDocuments) { auto project = DocumentSourceProject::create(BSON("a" << true), getExpCtx()); auto source = DocumentSourceMock::create({"{a: 1, b: 2}", "{a: 3, b: 4}"}); project->setSource(source.get()); auto next = project->getNext(); ASSERT(next.isAdvanced()); ASSERT_EQUALS(1, next.getDocument().getField("a").getInt()); ASSERT(next.getDocument().getField("b").missing()); next = project->getNext(); ASSERT(next.isAdvanced()); ASSERT_EQUALS(3, next.getDocument().getField("a").getInt()); ASSERT(next.getDocument().getField("b").missing()); ASSERT(project->getNext().isEOF()); ASSERT(project->getNext().isEOF()); ASSERT(project->getNext().isEOF()); } /** * Basic sanity check that two documents can be projected correctly with a simple inclusion * projection. */ TEST_F(ProjectStageTest, ExclusionShouldBeAbleToProcessMultipleDocuments) { auto project = DocumentSourceProject::create(BSON("a" << false), getExpCtx()); auto source = DocumentSourceMock::create({"{a: 1, b: 2}", "{a: 3, b: 4}"}); project->setSource(source.get()); auto next = project->getNext(); ASSERT(next.isAdvanced()); ASSERT(next.getDocument().getField("a").missing()); ASSERT_EQUALS(2, next.getDocument().getField("b").getInt()); next = project->getNext(); ASSERT(next.isAdvanced()); ASSERT(next.getDocument().getField("a").missing()); ASSERT_EQUALS(4, next.getDocument().getField("b").getInt()); ASSERT(project->getNext().isEOF()); ASSERT(project->getNext().isEOF()); ASSERT(project->getNext().isEOF()); } TEST_F(ProjectStageTest, ShouldPropagatePauses) { auto project = DocumentSourceProject::create(BSON("a" << false), getExpCtx()); auto source = DocumentSourceMock::create({Document(), DocumentSource::GetNextResult::makePauseExecution(), Document(), DocumentSource::GetNextResult::makePauseExecution(), Document(), DocumentSource::GetNextResult::makePauseExecution()}); project->setSource(source.get()); ASSERT_TRUE(project->getNext().isAdvanced()); ASSERT_TRUE(project->getNext().isPaused()); ASSERT_TRUE(project->getNext().isAdvanced()); ASSERT_TRUE(project->getNext().isPaused()); ASSERT_TRUE(project->getNext().isAdvanced()); ASSERT_TRUE(project->getNext().isPaused()); ASSERT(project->getNext().isEOF()); ASSERT(project->getNext().isEOF()); ASSERT(project->getNext().isEOF()); } TEST_F(ProjectStageTest, InclusionShouldAddDependenciesOfIncludedAndComputedFields) { auto project = DocumentSourceProject::create( fromjson("{a: true, x: '$b', y: {$and: ['$c','$d']}, z: {$meta: 'textScore'}}"), getExpCtx()); DepsTracker dependencies(DepsTracker::MetadataAvailable::kTextScore); ASSERT_EQUALS(DocumentSource::EXHAUSTIVE_FIELDS, project->getDependencies(&dependencies)); ASSERT_EQUALS(5U, dependencies.fields.size()); // Implicit _id dependency. ASSERT_EQUALS(1U, dependencies.fields.count("_id")); // Inclusion dependency. ASSERT_EQUALS(1U, dependencies.fields.count("a")); // Field path expression dependency. ASSERT_EQUALS(1U, dependencies.fields.count("b")); // Nested expression dependencies. ASSERT_EQUALS(1U, dependencies.fields.count("c")); ASSERT_EQUALS(1U, dependencies.fields.count("d")); ASSERT_EQUALS(false, dependencies.needWholeDocument); ASSERT_EQUALS(true, dependencies.getNeedTextScore()); } TEST_F(ProjectStageTest, ExclusionShouldNotAddDependencies) { auto project = DocumentSourceProject::create(fromjson("{a: false, 'b.c': false}"), getExpCtx()); DepsTracker dependencies; ASSERT_EQUALS(DocumentSource::SEE_NEXT, project->getDependencies(&dependencies)); ASSERT_EQUALS(0U, dependencies.fields.size()); ASSERT_EQUALS(false, dependencies.needWholeDocument); ASSERT_EQUALS(false, dependencies.getNeedTextScore()); } TEST_F(ProjectStageTest, InclusionProjectionReportsIncludedPathsFromGetModifiedPaths) { auto project = DocumentSourceProject::create( fromjson("{a: true, 'b.c': {d: true}, e: {f: {g: true}}, h: {i: {$literal: true}}}"), getExpCtx()); auto modifiedPaths = project->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllExcept); ASSERT_EQUALS(4U, modifiedPaths.paths.size()); ASSERT_EQUALS(1U, modifiedPaths.paths.count("_id")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("a")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g")); } TEST_F(ProjectStageTest, InclusionProjectionReportsIncludedPathsButExcludesId) { auto project = DocumentSourceProject::create( fromjson("{_id: false, 'b.c': {d: true}, e: {f: {g: true}}, h: {i: {$literal: true}}}"), getExpCtx()); auto modifiedPaths = project->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kAllExcept); ASSERT_EQUALS(2U, modifiedPaths.paths.size()); ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g")); } TEST_F(ProjectStageTest, ExclusionProjectionReportsExcludedPathsAsModifiedPaths) { auto project = DocumentSourceProject::create( fromjson("{a: false, 'b.c': {d: false}, e: {f: {g: false}}}"), getExpCtx()); auto modifiedPaths = project->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kFiniteSet); ASSERT_EQUALS(3U, modifiedPaths.paths.size()); ASSERT_EQUALS(1U, modifiedPaths.paths.count("a")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g")); } TEST_F(ProjectStageTest, ExclusionProjectionReportsExcludedPathsWithIdExclusion) { auto project = DocumentSourceProject::create( fromjson("{_id: false, 'b.c': {d: false}, e: {f: {g: false}}}"), getExpCtx()); auto modifiedPaths = project->getModifiedPaths(); ASSERT(modifiedPaths.type == DocumentSource::GetModPathsReturn::Type::kFiniteSet); ASSERT_EQUALS(3U, modifiedPaths.paths.size()); ASSERT_EQUALS(1U, modifiedPaths.paths.count("_id")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("b.c.d")); ASSERT_EQUALS(1U, modifiedPaths.paths.count("e.f.g")); } TEST_F(ProjectStageTest, CanUseRemoveSystemVariableToConditionallyExcludeProjectedField) { auto project = DocumentSourceProject::create( fromjson("{a: 1, b: {$cond: [{$eq: ['$b', 4]}, '$$REMOVE', '$b']}}"), getExpCtx()); auto source = DocumentSourceMock::create({"{a: 2, b: 2}", "{a: 3, b: 4}"}); project->setSource(source.get()); auto next = project->getNext(); ASSERT(next.isAdvanced()); Document expected{{"a", 2}, {"b", 2}}; ASSERT_DOCUMENT_EQ(next.releaseDocument(), expected); next = project->getNext(); ASSERT(next.isAdvanced()); expected = Document{{"a", 3}}; ASSERT_DOCUMENT_EQ(next.releaseDocument(), expected); ASSERT(project->getNext().isEOF()); } /** * Creates BSON for a DocumentSourceProject that represents projecting a new computed field nested * 'depth' levels deep. */ BSONObj makeProjectForNestedDocument(size_t depth) { ASSERT_GTE(depth, 2U); StringBuilder builder; builder << "a"; for (size_t i = 0; i < depth - 1; ++i) { builder << ".a"; } return BSON(builder.str() << BSON("$literal" << 1)); } TEST_F(ProjectStageTest, CanAddNestedDocumentExactlyAtDepthLimit) { auto project = DocumentSourceProject::create( makeProjectForNestedDocument(BSONDepth::getMaxAllowableDepth()), getExpCtx()); auto mock = DocumentSourceMock::create(Document{{"_id", 1}}); project->setSource(mock.get()); auto next = project->getNext(); ASSERT_TRUE(next.isAdvanced()); } TEST_F(ProjectStageTest, CannotAddNestedDocumentExceedingDepthLimit) { ASSERT_THROWS_CODE( DocumentSourceProject::create( makeProjectForNestedDocument(BSONDepth::getMaxAllowableDepth() + 1), getExpCtx()), UserException, ErrorCodes::Overflow); } } // namespace } // namespace mongo