/**
* Copyright (C) 2017 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 "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::Element;
using mongo::mutablebson::countChildren;
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"])),
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"]));
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());
}
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()));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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()));
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());
}
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()));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
ASSERT_TRUE(result.noop);
ASSERT_FALSE(result.indexesAffected);
ASSERT_EQUALS(fromjson("{a: []}"), doc);
ASSERT_TRUE(doc.isInPlaceModeEnabled());
ASSERT_EQUALS(fromjson("{}"), getLogDoc());
}
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()));
ASSERT_FALSE(result.noop);
ASSERT_TRUE(result.indexesAffected);
ASSERT_EQUALS(fromjson("{a: []}"), doc);
ASSERT_FALSE(doc.isInPlaceModeEnabled());
ASSERT_EQUALS(fromjson("{$set: {a: []}}"), getLogDoc());
}
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"]));
ASSERT_TRUE(result.noop);
ASSERT_FALSE(result.indexesAffected);
ASSERT_EQUALS(fromjson("{a: [0]}"), doc);
ASSERT_TRUE(doc.isInPlaceModeEnabled());
ASSERT_EQUALS(fromjson("{}"), getLogDoc());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
ASSERT_FALSE(result.noop);
ASSERT_EQUALS(fromjson("{a: ['dd', 'fc', 'gb', 'ha']}"), doc);
ASSERT_FALSE(doc.isInPlaceModeEnabled());
// Now with a collator.
CollatorInterfaceMock mockCollator(CollatorInterfaceMock::MockType::kReverseString);
node.setCollator(&mockCollator);
mutablebson::Document doc2(fromjson("{a: ['dd', 'fc', 'gb']}"));
result = node.apply(getApplyParams(doc2.root()["a"]));
ASSERT_FALSE(result.noop);
ASSERT_EQUALS(fromjson("{a: ['ha', 'gb', 'fc', 'dd']}"), doc2);
ASSERT_FALSE(doc2.isInPlaceModeEnabled());
}
// 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,
UpdateNode::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 {
int sliceValue;
BSONObj resultingDoc;
};
// We repeat the same test for several different values of $slice.
std::vector testDataList{{-2, fromjson("{a: [1]}")},
{-1, fromjson("{a: [1]}")},
{0, fromjson("{a: []}")},
{1, fromjson("{a: [1]}")},
{2, 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: []}"));
setPathTaken("a");
setLogBuilderToNull();
auto result = node.apply(getApplyParams(doc.root()["a"]));
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]}"));
setPathTaken("a");
setLogBuilderToNull();
auto result = node.apply(getApplyParams(doc.root()["a"]));
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}]}"));
setPathTaken("a");
setLogBuilderToNull();
auto result = node.apply(getApplyParams(doc.root()["a"]));
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
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"]));
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());
}
} // namespace
} // namespace mongo