/** * 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 * . * * 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/update/push_node.h" #include "mongo/bson/mutable/algorithm.h" #include "mongo/bson/mutable/mutable_bson_test_utils.h" #include "mongo/db/json.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/collation/collator_interface_mock.h" #include "mongo/db/update/update_node_test_fixture.h" #include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" namespace mongo { namespace { using PushNodeTest = UpdateNodeTest; using mongo::mutablebson::countChildren; using mongo::mutablebson::Element; TEST(PushNodeTest, EachClauseWithNonArrayObjectFails) { auto update = fromjson("{$push: {x: {$each: {'0': 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, EachClauseWithPrimitiveFails) { auto update = fromjson("{$push: {x: {$each: 1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PositionClauseWithObjectFails) { auto update = fromjson("{$push: {x: {$each: [1, 2], $position: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PositionClauseWithNonIntegerFails) { auto update = fromjson("{$push: {x: {$each: [1, 2], $position: -2.1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PositionClauseWithIntegerDoubleSucceeds) { auto update = fromjson("{$push: {x: {$each: [1, 2], $position: -2.0}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_OK(status); } TEST(PushNodeTest, SliceClauseWithObjectFails) { auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SliceClauseWithNonIntegerFails) { auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: -2.1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SliceClauseWithIntegerDoubleSucceeds) { auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: 2.0}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["x"], expCtx)); } TEST(PushNodeTest, SliceClauseWithArrayFails) { auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: [1, 2]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SliceClauseWithStringFails) { auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: '-1'}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithArrayFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: [{a: 1}]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithInvalidSortPatternFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {a: 100}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithEmptyPathFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {'': 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithEmptyFieldNamesFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {'.': 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithEmptyFieldSuffixFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {'a.': 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithEmptyFieldPrefixFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {'.b': 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithEmptyFieldInfixFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {'a..b': 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, SortClauseWithEmptyObjectFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PushEachWithInvalidClauseFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1}, {a: 2}], $xxx: -1, $sort: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PushEachWithDuplicateSortClauseFails) { auto update = fromjson( "{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $sort: {a: 1}, $sort: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PushEachWithDuplicateSliceClauseFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1},{a: 2}], $slice: -2.0, $slice: -2, $sort: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PushEachWithDuplicateEachClauseFails) { auto update = fromjson("{$push: {x: {$each:[{a: 1}], $each:[{a: 2}], $slice: -3, $sort: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST(PushNodeTest, PushEachWithDuplicatePositionClauseFails) { auto update = fromjson("{$push: {x: {$each: [{a: 1}], $position: 1, $position: 2}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; auto status = node.init(update["$push"]["x"], expCtx); ASSERT_NOT_OK(status); ASSERT_EQUALS(ErrorCodes::BadValue, status); } TEST_F(PushNodeTest, ApplyToNonArrayFails) { auto update = fromjson("{$push: {a: 1}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{_id: 'test_object', a: 1}")); setPathTaken("a"); addIndexedPath("a"); ASSERT_THROWS_CODE_AND_WHAT( node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()), AssertionException, ErrorCodes::BadValue, "The field 'a' must be an array but is of type int in document {_id: \"test_object\"}"); } TEST_F(PushNodeTest, ApplyToEmptyArray) { auto update = fromjson("{$push: {a: 1}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToEmptyDocument) { auto update = fromjson("{$push: {a: 1}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{}")); setPathToCreate("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToArrayWithOneElement) { auto update = fromjson("{$push: {a: 1}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, 1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {'a.1': 1}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToDottedPathElement) { auto update = fromjson("{$push: {'choices.first.votes': 1}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["choices.first.votes"], expCtx)); mutablebson::Document doc( fromjson("{_id : 1 , " " question : 'a', " " choices: {first: { choice: 'b'}, " " second: { choice: 'c'}}" "}")); setPathToCreate("votes"); setPathTaken("choices.first"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["choices"]["first"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{_id: 1, " " question: 'a', " " choices: {first: {choice: 'b', votes: [1]}, " " second: {choice: 'c'}}" "}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {'choices.first.votes': [1]}}"), getLogDoc()); ASSERT_EQUALS("{choices.first.votes}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplySimpleEachToEmptyArray) { auto update = fromjson("{$push: {a: {$each: [1]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplySimpleEachToEmptyDocument) { auto update = fromjson("{$push: {a: {$each: [1]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{}")); setPathToCreate("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyMultipleEachToEmptyDocument) { auto update = fromjson("{$push: {a: {$each: [1, 2]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{}")); setPathToCreate("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1, 2]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1, 2]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplySimpleEachToArrayWithOneElement) { auto update = fromjson("{$push: {a: {$each: [1]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, 1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {'a.1': 1}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyMultipleEachToArrayWithOneElement) { auto update = fromjson("{$push: {a: {$each: [1, 2]}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, 1, 2]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {'a.1': 1, 'a.2': 2}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyEmptyEachToEmptyArray) { auto update = fromjson("{$push: {a: {$each: []}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_TRUE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: []}"), doc); ASSERT_TRUE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyEmptyEachToEmptyDocument) { auto update = fromjson("{$push: {a: {$each: []}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{}")); setPathToCreate("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: []}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: []}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyEmptyEachToArrayWithOneElement) { auto update = fromjson("{$push: {a: {$each: []}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_TRUE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0]}"), doc); ASSERT_TRUE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToArrayWithSlice) { auto update = fromjson("{$push: {a: {$each: [2, -1], $slice: 1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [3]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [3]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [3]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyWithNumericSort) { auto update = fromjson("{$push: {a: {$each: [2, -1], $sort: 1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [3]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [-1, 2, 3]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [-1, 2, 3]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyWithReverseNumericSort) { auto update = fromjson("{$push: {a: {$each: [4, -1], $sort: -1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [3]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [4, 3, -1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [4, 3, -1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyWithMixedSort) { auto update = fromjson("{$push: {a: {$each: [4, -1], $sort: 1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [3, 't', {b: 1}, {a: 1}]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [-1, 3, 4, 't', {a: 1}, {b: 1}]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [-1, 3, 4, 't', {a: 1}, {b: 1}]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyWithReverseMixedSort) { auto update = fromjson("{$push: {a: {$each: [4, -1], $sort: -1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [3, 't', {b: 1}, {a: 1}]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [{b: 1}, {a: 1}, 't', 4, 3, -1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [{b: 1}, {a: 1}, 't', 4, 3, -1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyWithEmbeddedFieldSort) { auto update = fromjson("{$push: {a: {$each: [4, -1], $sort: {a: 1}}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [3, 't', {b: 1}, {a: 1}]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [3, 't', {b: 1}, 4, -1, {a: 1}]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [3, 't', {b: 1}, 4, -1, {a: 1}]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplySortWithCollator) { auto update = fromjson("{$push: {a: {$each: ['ha'], $sort: 1}}}"); CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); expCtx->setCollator(&collator); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: ['dd', 'fc', 'gb']}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: ['ha', 'gb', 'fc', 'dd']}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: ['ha', 'gb', 'fc', 'dd']}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplySortAfterSetCollator) { auto update = fromjson("{$push: {a: {$each: ['ha'], $sort: 1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: ['dd', 'fc', 'gb']}")); setPathTaken("a"); setLogBuilderToNull(); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_EQUALS(fromjson("{a: ['dd', 'fc', 'gb', 'ha']}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS("{a}", getModifiedPaths()); // Now with a collator. resetApplyParams(); setPathTaken("a"); setLogBuilderToNull(); CollatorInterfaceMock mockCollator(CollatorInterfaceMock::MockType::kReverseString); node.setCollator(&mockCollator); mutablebson::Document doc2(fromjson("{a: ['dd', 'fc', 'gb']}")); result = node.apply(getApplyParams(doc2.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_EQUALS(fromjson("{a: ['ha', 'gb', 'fc', 'dd']}"), doc2); ASSERT_FALSE(doc2.isInPlaceModeEnabled()); ASSERT_EQUALS("{a}", getModifiedPaths()); } // Some of the below tests apply multiple different update modifiers. This special check function // prints out the modifier when it observes a failure, which will help with diagnosis. void checkDocumentAndResult(BSONObj updateModifier, BSONObj expectedDocument, const mutablebson::Document& actualDocument, UpdateExecutor::ApplyResult applyResult) { if (expectedDocument == actualDocument && !applyResult.noop && !applyResult.indexesAffected) { // Check succeeded. } else { FAIL(str::stream() << "apply() failure for " << updateModifier << ". Expected " << expectedDocument << " (noop = false, indexesAffected = false) but got " << actualDocument.toString() << " (noop = " << (applyResult.noop ? "true" : "false") << ", indexesAffected = " << (applyResult.indexesAffected ? "true" : "false") << ")."); } } TEST_F(PushNodeTest, ApplyToEmptyArrayWithSliceValues) { struct testData { long long sliceValue; BSONObj resultingDoc; }; // We repeat the same test for several different values of $slice. std::vector testDataList{{LLONG_MIN, fromjson("{a: [1]}")}, {-2, fromjson("{a: [1]}")}, {-1, fromjson("{a: [1]}")}, {0, fromjson("{a: []}")}, {1, fromjson("{a: [1]}")}, {2, fromjson("{a: [1]}")}, {LLONG_MAX, fromjson("{a: [1]}")}}; for (const auto& data : testDataList) { auto update = BSON( "$push" << BSON("a" << BSON("$each" << BSON_ARRAY(1) << "$slice" << data.sliceValue))); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); resetApplyParams(); setPathTaken("a"); setLogBuilderToNull(); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); checkDocumentAndResult(update, data.resultingDoc, doc, result); } } TEST_F(PushNodeTest, ApplyToPopulatedArrayWithSliceValues) { struct testData { int sliceValue; BSONObj resultingDoc; }; // We repeat the same test with for several different values of $slice. std::vector testDataList{{-4, fromjson("{a: [2, 3, 1]}")}, {-3, fromjson("{a: [2, 3, 1]}")}, {-2, fromjson("{a: [3, 1]}")}, {-1, fromjson("{a: [1]}")}, {0, fromjson("{a: []}")}, {1, fromjson("{a: [2]}")}, {2, fromjson("{a: [2, 3]}")}, {3, fromjson("{a: [2, 3, 1]}")}, {4, fromjson("{a: [2, 3, 1]}")}}; for (const auto& data : testDataList) { auto update = BSON( "$push" << BSON("a" << BSON("$each" << BSON_ARRAY(1) << "$slice" << data.sliceValue))); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [2, 3]}")); resetApplyParams(); setPathTaken("a"); setLogBuilderToNull(); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); checkDocumentAndResult(update, data.resultingDoc, doc, result); } } // In this test, we apply // {$push: {a: {$each: [{a: 2, b: 1}, {a: 1, b: 1}], $slice: N, $sort: {a: I, b: J}}}} AND // {$push: {a: {$each: [{a: 2, b: 1}, {a: 1, b: 1}], $slice: N, $sort: {b: J, a: I}}}} // for every N in [-5, 5], I in {-1, 1}, and J in {-1, 1}. // Each application is to the test document {a: [{a: 2, b: 3}, {a: 3, b: 1}]}. TEST_F(PushNodeTest, ApplyToPopulatedArrayWithSortAndSliceValues) { struct testItem { int aVal; int bVal; }; std::vector testItemList{{2, 3}, {3, 1}, {2, 1}, {1, 1}}; struct testData { int sliceValue; BSONObj sortOrder; BSONObj resultingDoc; }; // These nested loops compute all of the modifiers we want to test, as well as what the result // of their applications should be. std::vector testDataList; for (auto sortAFirst : std::set{false, true}) { for (auto sortOrderA : std::set{-1, 1}) { for (auto sortOrderB : std::set{-1, 1}) { for (int sliceValue = -5; sliceValue <= 5; ++sliceValue) { std::vector resultingItems(testItemList); std::sort( resultingItems.begin(), resultingItems.end(), [sortAFirst, sortOrderA, sortOrderB](const auto& left, const auto& right) { std::pair leftPair(left.aVal, left.bVal); std::pair rightPair(right.aVal, right.bVal); if (sortOrderA == -1) { std::swap(leftPair.first, rightPair.first); } if (sortOrderB == -1) { std::swap(leftPair.second, rightPair.second); } if (!sortAFirst) { std::swap(leftPair.first, leftPair.second); std::swap(rightPair.first, rightPair.second); } return leftPair < rightPair; }); if (sliceValue >= 0 && static_cast(sliceValue) < resultingItems.size()) { resultingItems = std::vector(resultingItems.begin(), resultingItems.begin() + sliceValue); } else if (sliceValue < 0 && static_cast(-sliceValue) < resultingItems.size()) { resultingItems = std::vector(resultingItems.end() - (-sliceValue), resultingItems.end()); } testData newData; newData.sliceValue = sliceValue; if (sortAFirst) { newData.sortOrder = BSON("a" << sortOrderA << "b" << sortOrderB); } else { newData.sortOrder = BSON("b" << sortOrderB << "a" << sortOrderA); } BSONArrayBuilder arrBuilder; for (auto item : resultingItems) { arrBuilder.append(BSON("a" << item.aVal << "b" << item.bVal)); } newData.resultingDoc = BSON("a" << arrBuilder.arr()); testDataList.push_back(newData); } } } } // Once we've generated testDataList, we actually apply and verify all the modifiers. for (const auto& data : testDataList) { auto update = BSON("$push" << BSON("a" << BSON("$each" << BSON_ARRAY(BSON("a" << 2 << "b" << 1) << BSON("a" << 1 << "b" << 1)) << "$slice" << data.sliceValue << "$sort" << data.sortOrder))); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [{a: 2, b: 3}, {a: 3, b: 1}]}")); resetApplyParams(); setPathTaken("a"); setLogBuilderToNull(); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); checkDocumentAndResult(update, data.resultingDoc, doc, result); } } TEST_F(PushNodeTest, ApplyToEmptyArrayWithPositionZero) { auto update = fromjson("{$push: {a: {$each: [1], $position: 0}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToEmptyArrayWithPositionOne) { auto update = fromjson("{$push: {a: {$each: [1], $position: 1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToEmptyArrayWithLargePosition) { auto update = fromjson("{$push: {a: {$each: [1], $position: 1000}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToSingletonArrayWithPositionZero) { auto update = fromjson("{$push: {a: {$each: [1], $position: 0}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1, 0]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1, 0]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToSingletonArrayWithLargePosition) { auto update = fromjson("{$push: {a: {$each: [1], $position: 1000}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, 1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {'a.1': 1}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToEmptyArrayWithNegativePosition) { auto update = fromjson("{$push: {a: {$each: [1], $position: -1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: []}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToSingletonArrayWithNegativePosition) { auto update = fromjson("{$push: {a: {$each: [1], $position: -1}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [1, 0]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [1, 0]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToPopulatedArrayWithNegativePosition) { auto update = fromjson("{$push: {a: {$each: [5], $position: -2}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0, 1, 2, 3, 4]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, 1, 2, 5, 3, 4]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [0, 1, 2, 5, 3, 4]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyToPopulatedArrayWithOutOfBoundsNegativePosition) { auto update = fromjson("{$push: {a: {$each: [5], $position: -1000}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0, 1, 2, 3, 4]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [5, 0, 1, 2, 3, 4]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [5, 0, 1, 2, 3, 4]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, ApplyMultipleElementsPushWithNegativePosition) { auto update = fromjson("{$push: {a: {$each: [5, 6, 7], $position: -2}}}"); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0, 1, 2, 3, 4]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, 1, 2, 5, 6, 7, 3, 4]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [0, 1, 2, 5, 6, 7, 3, 4]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } TEST_F(PushNodeTest, PushWithMinIntAsPosition) { auto update = BSON("$push" << BSON("a" << BSON("$each" << BSON_ARRAY(5) << "$position" << std::numeric_limits::min()))); boost::intrusive_ptr expCtx(new ExpressionContextForTest()); PushNode node; ASSERT_OK(node.init(update["$push"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: [0, 1, 2, 3, 4]}")); setPathTaken("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]), getUpdateNodeApplyParams()); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [5, 0, 1, 2, 3, 4]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$set: {a: [5, 0, 1, 2, 3, 4]}}"), getLogDoc()); ASSERT_EQUALS("{a}", getModifiedPaths()); } } // namespace } // namespace mongo