/** * Copyright (C) 2019-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 * . * * 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 "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/exec/projection_executor_utils.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" namespace mongo::projection_executor_utils { namespace positional_projection_tests { /** * Applies a find()-style positional projection at the given 'path' using 'matchSpec' to create * a 'MatchExpression' to match an element on the first array in the 'path'. If no value for * 'postImage' is provided, then the post-image used will be the value passed for the 'preImage'. */ auto applyPositional(const BSONObj& matchSpec, const std::string& path, const Document& preImage, boost::optional postImage = boost::none) { boost::intrusive_ptr expCtx(new ExpressionContextForTest()); auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchSpec, expCtx)); return projection_executor_utils::applyFindPositionalProjection( preImage, postImage.value_or(preImage), *matchExpr, path); } TEST(PositionalProjection, CorrectlyProjectsSimplePath) { ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1, foo: [6]}")}, applyPositional(fromjson("{bar: 1, foo: {$gte: 5}}"), "foo", Document{fromjson("{bar: 1, foo: [1,2,6,10]}")})); } TEST(PositionalProjection, CorrectlyProjectsDottedPath) { ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1, x: {y: [6]}}")}, applyPositional(fromjson("{a: 1, 'x.y': {$gte: 5}}"), "x.y", Document{fromjson("{a: 1, x: {y: [1,2,6,10]}}")})); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: 1, x: {y: {z: [6]}}}")}, applyPositional(fromjson("{a: 1, 'x.y.z': {$gte: 5}}"), "x.y.z", Document{fromjson("{a: 1, x: {y: {z: [1,2,6,10]}}}")})); } TEST(PositionalProjection, ProjectsValueUnmodifiedIfFieldIsNotArray) { auto doc = Document{fromjson("{foo: 3}")}; ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc)); } TEST(PositionalProjection, FailsToProjectPositionalPathComponentsForNestedArrays) { ASSERT_THROWS_CODE(applyPositional(fromjson("{'x.0.y': 42}"), "x.0.y", Document{fromjson("{x: [{y: [11, 42]}]}")}), AssertionException, 51247); } TEST(PositionalProjection, CorrectlyProjectsNestedArrays) { ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [1,2]}]}")}, applyPositional(fromjson("{'a.b': 1}"), "a", Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3,4]}]}")}, applyPositional(fromjson("{'a.b': 3}"), "a", Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: [3,4]}]}")}, applyPositional(fromjson("{'a.b': 3}"), "a.b", Document{fromjson("{a: [{b: [1,2]}, {b: [3,4]}]}")})); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [['d','e','f']]}")}, applyPositional(fromjson("{a: {$gt: ['a','b','c']}}"), "a", Document{fromjson("{a: [['a','b','c'],['d','e','f']]}")})); } TEST(PositionalProjection, FailsToProjectWithMultipleConditionsOnArray) { ASSERT_THROWS_CODE(applyPositional(fromjson("{$or: [{'x.y': 1}, {'x.y': 2}]}"), "x", Document{fromjson("{x: [{y: [1,2]}]}")}), AssertionException, 51246); } TEST(PositionalProjection, CanMergeWithExistingFieldsInOutputDocument) { auto doc = Document{fromjson("{foo: {bar: [1,2,6,10]}}")}; ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: {bar: [6]}}")}, applyPositional(fromjson("{'foo.bar': {$gte: 5}}"), "foo.bar", doc)); doc = Document{fromjson("{bar: 1, foo: {bar: [1,2,6,10]}}")}; ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1, foo: {bar: [6]}}")}, applyPositional(fromjson("{bar: 1, 'foo.bar': {$gte: 5}}"), "foo.bar", doc)); doc = Document{fromjson("{bar: 1, foo: 3}")}; ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc)); } TEST(PositionalProjection, AppliesMatchExpressionToPreImageAndStoresResultInPostImage) { auto preImage = Document{fromjson("{foo: 1, bar: [1,2,6,10]}")}; auto postImage = Document{fromjson("{bar: [1,2,6,10]}")}; ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: [6]}")}, applyPositional(fromjson("{foo: 1, bar: 6}"), "bar", preImage, postImage)); } } // namespace positional_projection_tests namespace elem_match_projection_tests { auto applyElemMatch(const BSONObj& match, const std::string& path, const Document& input) { boost::intrusive_ptr expCtx(new ExpressionContextForTest()); auto matchObj = BSON(path << BSON("$elemMatch" << match)); auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchObj, expCtx)); return projection_executor_utils::applyFindElemMatchProjection(input, *matchExpr, path); } TEST(ElemMatchProjection, CorrectlyProjectsNonObjectElement) { ASSERT_VALUE_EQ( Document{fromjson("{foo: [4]}")}["foo"], applyElemMatch(fromjson("{$in: [4]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")})); ASSERT_VALUE_EQ( Document{fromjson("{foo: [4]}")}["foo"], applyElemMatch(fromjson("{$nin: [1,2,3]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")})); } TEST(ElemMatchProjection, CorrectlyProjectsObjectElement) { ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); } TEST(ElemMatchProjection, CorrectlyProjectsArrayElement) { ASSERT_VALUE_EQ(Document{fromjson("{foo: [[3,4]]}")}["foo"], applyElemMatch(fromjson("{$gt: [1,2]}"), "foo", Document{fromjson("{foo: [[1,2], [3,4]]}")})); } TEST(ElemMatchProjection, ProjectsAsEmptyDocumentIfInputIsEmpty) { ASSERT_VALUE_EQ({}, applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", {})); } TEST(ElemMatchProjection, RemovesFieldFromOutputDocumentIfUnableToMatchArrayElement) { ASSERT_VALUE_EQ({}, applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", Document{fromjson("{foo: [{bar: 1, z: 1}, " "{bar: 2, z: 2}]}")})); ASSERT_VALUE_EQ( {}, applyElemMatch(fromjson("{bar: {$gte: 20}}"), "foo", Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); } TEST(ElemMatchProjection, CorrectlyProjectsWithMultipleCriteriaInMatchExpression) { ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 2, z: 2}]}")}["foo"], applyElemMatch(fromjson("{bar: {$gt: 1, $lt: 6}}"), "foo", Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); } TEST(ElemMatchProjection, CanMergeWithExistingFieldsInInputDocument) { ASSERT_VALUE_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); ASSERT_VALUE_EQ( Document{fromjson("{foo: [{bar: 6, z: 6}]}")}["foo"], applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, " "{bar: 6, z: 6}, {bar: 10, z: 10}]}")})); } TEST(ElemMatchProjection, RertursEmptyValuefItContainsNumericSubfield) { ASSERT_VALUE_EQ( {}, applyElemMatch(fromjson("{$gt: 2}"), "foo", Document{BSON("foo" << BSON(0 << 3))})); ASSERT_VALUE_EQ({}, applyElemMatch(fromjson("{$gt: 2}"), "foo", Document{BSON("bar" << 1 << "foo" << BSON(0 << 3))})); } } // namespace elem_match_projection_tests namespace slice_projection_tests { DEATH_TEST_REGEX(SliceProjection, ShouldFailIfNegativeLimitSpecifiedWithPositiveSkip, "Invariant failure.*limit >= 0") { auto doc = Document{fromjson("{a: [1,2,3,4]}")}; projection_executor_utils::applyFindSliceProjection(doc, "a", 1, -1); } DEATH_TEST_REGEX(SliceProjection, ShouldFailIfNegativeLimitSpecifiedWithNegativeSkip, "Invariant failure.*limit >= 0") { auto doc = Document{fromjson("{a: [1,2,3,4]}")}; projection_executor_utils::applyFindSliceProjection(doc, "a", -1, -1); } TEST(SliceProjection, CorrectlyProjectsSimplePath) { auto doc = Document{fromjson("{a: [1,2,3,4]}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [1,2,3]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 3)); ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [2,3,4]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, -3)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", -3, 1)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [2,3,4]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", -3, 4)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [4]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", 3, 1)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", -5, 5)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: []}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", 5, 2)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", -5, 2)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, projection_executor_utils::applyFindSliceProjection( doc, "a", boost::none, std::numeric_limits::max())); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [1,2,3,4]}")}, projection_executor_utils::applyFindSliceProjection( doc, "a", boost::none, std::numeric_limits::min())); ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [1,2,3,4]}")}, projection_executor_utils::applyFindSliceProjection( doc, "a", std::numeric_limits::min(), std::numeric_limits::max())); ASSERT_DOCUMENT_EQ( Document{fromjson("{a: []}")}, projection_executor_utils::applyFindSliceProjection( doc, "a", std::numeric_limits::max(), std::numeric_limits::max())); doc = Document{fromjson("{a: [{b: 1, c: 1}, {b: 2, c: 2}, {b: 3, c: 3}], d: 2}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [{b: 1, c: 1}], d: 2}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 1)); ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [{b: 3, c: 3}], d: 2}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, -1)); ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: 2, c: 2}], d: 2}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", 1, 1)); doc = Document{fromjson("{a: 1}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: 1}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 2)); doc = Document{fromjson("{a: {b: 1}}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: 1}}")}, projection_executor_utils::applyFindSliceProjection(doc, "a", boost::none, 2)); } TEST(SliceProjection, CorrectlyProjectsDottedPath) { auto doc = Document{fromjson("{a: {b: [1,2,3], c: 1}}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: [1,2], c: 1}}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); doc = Document{fromjson("{a: {b: [1,2,3], c: 1}, d: 1}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: [1,2], c: 1}, d: 1}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); doc = Document{fromjson("{a: {b: [[1,2], [3,4], [5,6]]}}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: [[1,2], [3,4]]}}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); doc = Document{fromjson("{a: [{b: {c: [1,2,3,4]}}, {b: {c: [5,6,7,8]}}], d: 1}")}; ASSERT_DOCUMENT_EQ(Document{fromjson("{a: [{b: {c: [4]}}, {b: {c: [8]}}], d: 1}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b.c", -1, 2)); doc = Document{fromjson("{a: {b: 1}}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: 1}}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); doc = Document{fromjson("{a: {b: {c: 1}}}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: {b: {c: 1}}}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, 2)); doc = Document{fromjson("{a: [{b: [1,2,3], c: 1}]}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [{b: [3], c: 1}]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, -1)); doc = Document{fromjson("{a: [{b: [1,2,3], c: 4}, {b: [5,6,7], c: 8}]}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [{b: [3], c: 4}, {b: [7], c: 8}]}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b", boost::none, -1)); doc = Document{fromjson("{a: [{b: [{x:1, c: [1, 2]}, {y: 1, c: [3, 4]}]}], z: 1}")}; ASSERT_DOCUMENT_EQ( Document{fromjson("{a: [{b: [{x:1, c: [1]}, {y: 1, c: [3]}]}], z: 1}")}, projection_executor_utils::applyFindSliceProjection(doc, "a.b.c", boost::none, 1)); } } // namespace slice_projection_tests } // namespace mongo::projection_executor_utils