diff options
author | Justin Seyster <justin.seyster@mongodb.com> | 2017-08-08 22:48:05 -0400 |
---|---|---|
committer | Justin Seyster <justin.seyster@mongodb.com> | 2017-08-08 22:49:08 -0400 |
commit | bf99c6ff2fbd95590aa86818c07b9bc121557f06 (patch) | |
tree | db049bdd046b31b07cfac1a9711b2f16f22976ed /src/mongo | |
parent | c7661b14867cd058e1a67986b8e05a7020fc0a5e (diff) | |
download | mongo-bf99c6ff2fbd95590aa86818c07b9bc121557f06.tar.gz |
SERVER-28772 PushNode
A later commit will remove support for the deprecated $pushAll.
Diffstat (limited to 'src/mongo')
25 files changed, 1508 insertions, 67 deletions
diff --git a/src/mongo/db/ops/SConscript b/src/mongo/db/ops/SConscript index 3566f93bf11..6cd60f6c9e1 100644 --- a/src/mongo/db/ops/SConscript +++ b/src/mongo/db/ops/SConscript @@ -88,15 +88,6 @@ env.CppUnitTest( ) env.CppUnitTest( - target='modifier_push_sorter_test', - source='modifier_push_sorter_test.cpp', - LIBDEPS=[ - '$BUILD_DIR/mongo/db/query/collation/collator_interface_mock', - 'update', - ], -) - -env.CppUnitTest( target='modifier_pull_all_test', source='modifier_pull_all_test.cpp', LIBDEPS=[ diff --git a/src/mongo/db/ops/modifier_push.h b/src/mongo/db/ops/modifier_push.h index 7eb7c31bc64..0f472dfa8d3 100644 --- a/src/mongo/db/ops/modifier_push.h +++ b/src/mongo/db/ops/modifier_push.h @@ -35,7 +35,7 @@ #include "mongo/db/field_ref.h" #include "mongo/db/jsobj.h" #include "mongo/db/ops/modifier_interface.h" -#include "mongo/db/ops/modifier_push_sorter.h" +#include "mongo/db/update/push_sorter.h" namespace mongo { diff --git a/src/mongo/db/update/SConscript b/src/mongo/db/update/SConscript index be2ed56c9e8..2a1475ccf45 100644 --- a/src/mongo/db/update/SConscript +++ b/src/mongo/db/update/SConscript @@ -53,6 +53,16 @@ env.CppUnitTest( ], ) +env.CppUnitTest( + target='push_sorter_test', + source='push_sorter_test.cpp', + LIBDEPS=[ + '$BUILD_DIR/mongo/bson/mutable/mutable_bson', + '$BUILD_DIR/mongo/db/matcher/expressions', + '$BUILD_DIR/mongo/db/query/collation/collator_interface_mock', + ], +) + env.Library( target='update', source=[ @@ -68,6 +78,7 @@ env.Library( 'pop_node.cpp', 'pull_node.cpp', 'pullall_node.cpp', + 'push_node.cpp', 'rename_node.cpp', 'set_node.cpp', 'unset_node.cpp', @@ -96,6 +107,7 @@ env.CppUnitTest( 'pop_node_test.cpp', 'pull_node_test.cpp', 'pullall_node_test.cpp', + 'push_node_test.cpp', 'rename_node_test.cpp', 'set_node_test.cpp', 'unset_node_test.cpp', diff --git a/src/mongo/db/update/addtoset_node.cpp b/src/mongo/db/update/addtoset_node.cpp index 70d88ebf411..469669210ca 100644 --- a/src/mongo/db/update/addtoset_node.cpp +++ b/src/mongo/db/update/addtoset_node.cpp @@ -102,7 +102,10 @@ void AddToSetNode::setCollator(const CollatorInterface* collator) { deduplicate(_elements, _collator); } -bool AddToSetNode::updateExistingElement(mutablebson::Element* element) const { +PathCreatingNode::UpdateExistingElementResult AddToSetNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { uassert(ErrorCodes::BadValue, str::stream() << "Cannot apply $addToSet to non-array field. Field named '" << element->getFieldName() @@ -127,7 +130,7 @@ bool AddToSetNode::updateExistingElement(mutablebson::Element* element) const { } if (elementsToAdd.empty()) { - return false; + return UpdateExistingElementResult::kNoOp; } for (auto&& elem : elementsToAdd) { @@ -135,7 +138,7 @@ bool AddToSetNode::updateExistingElement(mutablebson::Element* element) const { invariantOK(element->pushBack(toAdd)); } - return true; + return UpdateExistingElementResult::kUpdated; } void AddToSetNode::setValueForNewElement(mutablebson::Element* element) const { diff --git a/src/mongo/db/update/addtoset_node.h b/src/mongo/db/update/addtoset_node.h index c36d9415750..c7b1ad6baf9 100644 --- a/src/mongo/db/update/addtoset_node.h +++ b/src/mongo/db/update/addtoset_node.h @@ -47,7 +47,9 @@ public: void setCollator(const CollatorInterface* collator) final; protected: - bool updateExistingElement(mutablebson::Element* element) const final; + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; void setValueForNewElement(mutablebson::Element* element) const final; private: diff --git a/src/mongo/db/update/arithmetic_node.cpp b/src/mongo/db/update/arithmetic_node.cpp index cb122bfa863..6525e714f4c 100644 --- a/src/mongo/db/update/arithmetic_node.cpp +++ b/src/mongo/db/update/arithmetic_node.cpp @@ -73,7 +73,10 @@ Status ArithmeticNode::init(BSONElement modExpr, const CollatorInterface* collat return Status::OK(); } -bool ArithmeticNode::updateExistingElement(mutablebson::Element* element) const { +PathCreatingNode::UpdateExistingElementResult ArithmeticNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { if (!element->isNumeric()) { mutablebson::Element idElem = mutablebson::findFirstChildNamed(element->getDocument().root(), "_id"); @@ -101,12 +104,12 @@ bool ArithmeticNode::updateExistingElement(mutablebson::Element* element) const // If the updated value is identical to the original value, treat this as a no-op. Caveat: // if the found element is in a deserialized state, we can't do that. if (element->getValue().ok() && valueToSet.isIdentical(originalValue)) { - return false; + return UpdateExistingElementResult::kNoOp; } else { // This can fail if 'valueToSet' is not representable as a 64-bit integer. uassertStatusOK(element->setValueSafeNum(valueToSet)); - return true; + return UpdateExistingElementResult::kUpdated; } } diff --git a/src/mongo/db/update/arithmetic_node.h b/src/mongo/db/update/arithmetic_node.h index 7004adafcfe..46e2ba19069 100644 --- a/src/mongo/db/update/arithmetic_node.h +++ b/src/mongo/db/update/arithmetic_node.h @@ -51,7 +51,9 @@ public: void setCollator(const CollatorInterface* collator) final {} protected: - bool updateExistingElement(mutablebson::Element* element) const final; + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; void setValueForNewElement(mutablebson::Element* element) const final; private: diff --git a/src/mongo/db/update/bit_node.cpp b/src/mongo/db/update/bit_node.cpp index fa8f0960c83..26a920769f2 100644 --- a/src/mongo/db/update/bit_node.cpp +++ b/src/mongo/db/update/bit_node.cpp @@ -89,7 +89,10 @@ Status BitNode::init(BSONElement modExpr, const CollatorInterface* collator) { return Status::OK(); } -bool BitNode::updateExistingElement(mutablebson::Element* element) const { +PathCreatingNode::UpdateExistingElementResult BitNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { if (!element->isIntegral()) { mutablebson::Element idElem = mutablebson::findFirstChildNamed(element->getDocument().root(), "_id"); @@ -106,9 +109,9 @@ bool BitNode::updateExistingElement(mutablebson::Element* element) const { if (!value.isIdentical(element->getValueSafeNum())) { invariantOK(element->setValueSafeNum(value)); - return true; + return UpdateExistingElementResult::kUpdated; } else { - return false; + return UpdateExistingElementResult::kNoOp; } } diff --git a/src/mongo/db/update/bit_node.h b/src/mongo/db/update/bit_node.h index 0e5f21f4e06..3edc84179a7 100644 --- a/src/mongo/db/update/bit_node.h +++ b/src/mongo/db/update/bit_node.h @@ -47,7 +47,9 @@ public: void setCollator(const CollatorInterface* collator) final {} protected: - bool updateExistingElement(mutablebson::Element* element) const final; + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; void setValueForNewElement(mutablebson::Element* element) const final; private: diff --git a/src/mongo/db/update/compare_node.cpp b/src/mongo/db/update/compare_node.cpp index c41dbe886b1..166499f40f9 100644 --- a/src/mongo/db/update/compare_node.cpp +++ b/src/mongo/db/update/compare_node.cpp @@ -46,13 +46,16 @@ void CompareNode::setCollator(const CollatorInterface* collator) { _collator = collator; } -bool CompareNode::updateExistingElement(mutablebson::Element* element) const { +PathCreatingNode::UpdateExistingElementResult CompareNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { const auto compareVal = element->compareWithBSONElement(_val, _collator, false); if ((compareVal == 0) || ((_mode == CompareMode::kMax) ? (compareVal > 0) : (compareVal < 0))) { - return false; + return UpdateExistingElementResult::kNoOp; } else { invariantOK(element->setValueBSONElement(_val)); - return true; + return UpdateExistingElementResult::kUpdated; } } diff --git a/src/mongo/db/update/compare_node.h b/src/mongo/db/update/compare_node.h index eb071652d5d..11421a08eb5 100644 --- a/src/mongo/db/update/compare_node.h +++ b/src/mongo/db/update/compare_node.h @@ -51,7 +51,9 @@ public: void setCollator(const CollatorInterface* collator) final; protected: - bool updateExistingElement(mutablebson::Element* element) const final; + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; void setValueForNewElement(mutablebson::Element* element) const final; private: diff --git a/src/mongo/db/update/current_date_node.cpp b/src/mongo/db/update/current_date_node.cpp index 0224fc9ba33..a251ad14854 100644 --- a/src/mongo/db/update/current_date_node.cpp +++ b/src/mongo/db/update/current_date_node.cpp @@ -93,9 +93,12 @@ Status CurrentDateNode::init(BSONElement modExpr, const CollatorInterface* colla return Status::OK(); } -bool CurrentDateNode::updateExistingElement(mutablebson::Element* element) const { +PathCreatingNode::UpdateExistingElementResult CurrentDateNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { setValue(element, _typeIsDate); - return true; + return UpdateExistingElementResult::kUpdated; } void CurrentDateNode::setValueForNewElement(mutablebson::Element* element) const { diff --git a/src/mongo/db/update/current_date_node.h b/src/mongo/db/update/current_date_node.h index c30fd5b1974..e35402da37b 100644 --- a/src/mongo/db/update/current_date_node.h +++ b/src/mongo/db/update/current_date_node.h @@ -47,7 +47,9 @@ public: void setCollator(const CollatorInterface* collator) final {} protected: - bool updateExistingElement(mutablebson::Element* element) const final; + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; void setValueForNewElement(mutablebson::Element* element) const final; private: diff --git a/src/mongo/db/update/modifier_table.cpp b/src/mongo/db/update/modifier_table.cpp index 72611147891..e25cbbc2d62 100644 --- a/src/mongo/db/update/modifier_table.cpp +++ b/src/mongo/db/update/modifier_table.cpp @@ -55,6 +55,7 @@ #include "mongo/db/update/pop_node.h" #include "mongo/db/update/pull_node.h" #include "mongo/db/update/pullall_node.h" +#include "mongo/db/update/push_node.h" #include "mongo/db/update/rename_node.h" #include "mongo/db/update/set_node.h" #include "mongo/db/update/unset_node.h" @@ -212,6 +213,8 @@ std::unique_ptr<UpdateLeafNode> makeUpdateLeafNode(ModifierType modType) { return stdx::make_unique<PullNode>(); case MOD_PULL_ALL: return stdx::make_unique<PullAllNode>(); + case MOD_PUSH: + return stdx::make_unique<PushNode>(); case MOD_RENAME: return stdx::make_unique<RenameNode>(); case MOD_SET: diff --git a/src/mongo/db/update/path_creating_node.cpp b/src/mongo/db/update/path_creating_node.cpp index 920d0054d8a..155f41029cd 100644 --- a/src/mongo/db/update/path_creating_node.cpp +++ b/src/mongo/db/update/path_creating_node.cpp @@ -130,7 +130,9 @@ UpdateNode::ApplyResult PathCreatingNode::apply(ApplyParams applyParams) const { } // We found an existing element at the update path. - if (!updateExistingElement(&applyParams.element)) { + auto updateResult = updateExistingElement( + &applyParams.element, applyParams.pathTaken, applyParams.logBuilder); + if (updateResult == UpdateExistingElementResult::kNoOp) { return ApplyResult::noopResult(); // Successful no-op update. } @@ -143,7 +145,11 @@ UpdateNode::ApplyResult PathCreatingNode::apply(ApplyParams applyParams) const { checkImmutablePathsNotModified( applyParams.element, applyParams.pathTaken.get(), applyParams.immutablePaths, original); - valueToLog = applyParams.element; + if (updateResult == UpdateExistingElementResult::kUpdated) { + valueToLog = applyParams.element; + } else { + // updateExistingElement() has already performed logging, so we don't set a log value. + } } else { // We did not find an element at the update path. Create one. auto newElementFieldName = @@ -218,7 +224,7 @@ UpdateNode::ApplyResult PathCreatingNode::apply(ApplyParams applyParams) const { } // Log the operation. - if (applyParams.logBuilder) { + if (applyParams.logBuilder && valueToLog.ok()) { auto logElement = applyParams.logBuilder->getDocument().makeElementWithNewFieldName(fullPath, valueToLog); invariant(logElement.ok()); diff --git a/src/mongo/db/update/path_creating_node.h b/src/mongo/db/update/path_creating_node.h index 2fe69a47064..c806a5791f4 100644 --- a/src/mongo/db/update/path_creating_node.h +++ b/src/mongo/db/update/path_creating_node.h @@ -46,12 +46,20 @@ public: ApplyResult apply(ApplyParams applyParams) const final; protected: + enum class UpdateExistingElementResult { + kNoOp, + kUpdated, + kUpdatedAndLogged, // PathCreatingNode::apply() does not need to do any logging. + }; + /** * PathCreatingNode::apply() calls the updateExistingElement() method when applying its update * to an existing path. The child's implementation of this method is responsible for either * updating the given Element or returning false to indicate that no update is necessary. */ - virtual bool updateExistingElement(mutablebson::Element* element) const = 0; + virtual UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const = 0; /** * PathCreatingNode::apply() calls the setValueForNewElement() method when it must materialize a diff --git a/src/mongo/db/update/push_node.cpp b/src/mongo/db/update/push_node.cpp new file mode 100644 index 00000000000..0d4339c20a3 --- /dev/null +++ b/src/mongo/db/update/push_node.cpp @@ -0,0 +1,317 @@ +/** + * 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 <http://www.gnu.org/licenses/>. + * + * 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 <numeric> + +#include "mongo/base/simple_string_data_comparator.h" +#include "mongo/bson/mutable/algorithm.h" +#include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/update/update_internal_node.h" + +namespace mongo { + +const StringData PushNode::kEachClauseName = "$each"_sd; +const StringData PushNode::kSliceClauseName = "$slice"; +const StringData PushNode::kSortClauseName = "$sort"; +const StringData PushNode::kPositionClauseName = "$position"; + +namespace { + +/** + * When the $sort clause in a $push modifer is an object, that object should pass the checks in + * this function. + */ +Status checkSortClause(const BSONObj& sortObject) { + if (sortObject.isEmpty()) { + return Status(ErrorCodes::BadValue, + "The $sort pattern is empty when it should be a set of fields."); + } + + for (auto&& patternElement : sortObject) { + double orderVal = patternElement.isNumber() ? patternElement.Number() : 0; + if (orderVal != -1 && orderVal != 1) { + return Status(ErrorCodes::BadValue, "The $sort element value must be either 1 or -1"); + } + + FieldRef sortField(patternElement.fieldName()); + if (sortField.numParts() == 0) { + return Status(ErrorCodes::BadValue, "The $sort field cannot be empty"); + } + + for (size_t i = 0; i < sortField.numParts(); ++i) { + if (sortField.getPart(i).size() == 0) { + return Status(ErrorCodes::BadValue, + str::stream() << "The $sort field is a dotted field " + "but has an empty part: " + << sortField.dottedField()); + } + } + } + + return Status::OK(); +} + +} // namespace + +Status PushNode::init(BSONElement modExpr, const CollatorInterface* collator) { + invariant(modExpr.ok()); + + if (modExpr.type() == BSONType::Object && modExpr[kEachClauseName]) { + std::set<StringData> validClauseNames{ + kEachClauseName, kSliceClauseName, kSortClauseName, kPositionClauseName}; + auto clausesFound = + SimpleStringDataComparator::kInstance.makeStringDataUnorderedMap<const BSONElement>(); + + for (auto&& modifier : modExpr.embeddedObject()) { + auto clauseName = modifier.fieldNameStringData(); + + auto foundClauseName = validClauseNames.find(clauseName); + if (foundClauseName == validClauseNames.end()) { + return Status(ErrorCodes::BadValue, + str::stream() << "Unrecognized clause in $push: " + << modifier.fieldNameStringData()); + } + + if (clausesFound.find(*foundClauseName) != clausesFound.end()) { + return Status(ErrorCodes::BadValue, + str::stream() << "Only one " << clauseName << " is supported."); + } + + clausesFound.insert(std::make_pair(*foundClauseName, modifier)); + } + + // Parse $each. + auto eachIt = clausesFound.find(kEachClauseName); + invariant(eachIt != clausesFound.end()); // We already checked for a $each clause. + const auto& eachClause = eachIt->second; + if (eachClause.type() != BSONType::Array) { + return Status(ErrorCodes::BadValue, + str::stream() << "The argument to $each in $push must be" + " an array but it was of type: " + << typeName(eachClause.type())); + } + + for (auto&& item : eachClause.embeddedObject()) { + _valuesToPush.push_back(item); + } + + // Parse (optional) $slice. + auto sliceIt = clausesFound.find(kSliceClauseName); + if (sliceIt != clausesFound.end()) { + auto sliceClause = sliceIt->second; + auto parsedSliceValue = MatchExpressionParser::parseIntegerElementToLong(sliceClause); + if (parsedSliceValue.isOK()) { + _slice = parsedSliceValue.getValue(); + } else { + return Status(ErrorCodes::BadValue, + str::stream() << "The value for $slice must " + "be an integer value but was given type: " + << typeName(sliceClause.type())); + } + } + + // Parse (optional) $sort. + auto sortIt = clausesFound.find(kSortClauseName); + if (sortIt != clausesFound.end()) { + auto sortClause = sortIt->second; + + if (sortClause.type() == BSONType::Object) { + auto status = checkSortClause(sortClause.embeddedObject()); + + if (status.isOK()) { + _sort = PatternElementCmp(sortClause.embeddedObject(), collator); + } else { + return status; + } + } else if (sortClause.isNumber()) { + double orderVal = sortClause.Number(); + if (orderVal == -1 || orderVal == 1) { + _sort = PatternElementCmp(BSON("" << orderVal), collator); + } else { + return Status(ErrorCodes::BadValue, + "The $sort element value must be either 1 or -1"); + } + } else { + return Status(ErrorCodes::BadValue, + "The $sort is invalid: use 1/-1 to sort the whole element, " + "or {field:1/-1} to sort embedded fields"); + } + } + + // Parse (optional) $position. + auto positionIt = clausesFound.find(kPositionClauseName); + if (positionIt != clausesFound.end()) { + auto positionClause = positionIt->second; + auto parsedPositionValue = + MatchExpressionParser::parseIntegerElementToLong(positionClause); + if (parsedPositionValue.isOK()) { + _position = parsedPositionValue.getValue(); + } else { + return Status(ErrorCodes::BadValue, + str::stream() << "The value for $position must " + "be an integer value, not of type: " + << typeName(positionClause.type())); + } + } + } else { + // No $each clause. We treat the value of $push as the element to add to the array. + _valuesToPush.push_back(modExpr); + } + + return Status::OK(); +} + +PushNode::PushResult PushNode::insertElementsWithPosition( + mutablebson::Element* array, long long position, const std::vector<BSONElement> valuesToPush) { + if (valuesToPush.empty()) { + return PushResult::kNoOp; + } + + auto& document = array->getDocument(); + auto firstElementToInsert = + document.makeElementWithNewFieldName(StringData(), valuesToPush.front()); + + // We assume that no array has more than std::numerical_limits<long long>::max() elements. + long long arraySize = static_cast<long long>(countChildren(*array)); + + // We insert the first element of 'valuesToPush' at the location requested in the 'position' + // variable. + PushResult result; + if (arraySize == 0) { + invariantOK(array->pushBack(firstElementToInsert)); + result = PushResult::kModifyArray; // See comment describing PushResult. + } else if (position > arraySize) { + invariantOK(array->pushBack(firstElementToInsert)); + result = PushResult::kAppendToEndOfArray; + } else if (position > 0) { + auto insertAfter = getNthChild(*array, position - 1); + invariantOK(insertAfter.addSiblingRight(firstElementToInsert)); + result = PushResult::kModifyArray; + } else if (position < 0 && -position < arraySize) { + auto insertAfter = getNthChild(*array, arraySize - (-position) - 1); + invariantOK(insertAfter.addSiblingRight(firstElementToInsert)); + result = PushResult::kModifyArray; + } else { + invariantOK(array->pushFront(firstElementToInsert)); + result = PushResult::kModifyArray; + } + + // We insert all the rest of the elements after the one we just inserted. + std::accumulate(std::next(valuesToPush.begin()), + valuesToPush.end(), + firstElementToInsert, + [&document](auto& insertAfter, auto& valueToInsert) { + auto nextElementToInsert = + document.makeElementWithNewFieldName(StringData(), valueToInsert); + invariantOK(insertAfter.addSiblingRight(nextElementToInsert)); + return nextElementToInsert; + }); + + return result; +} + +PushNode::PushResult PushNode::performPush(mutablebson::Element* element, + FieldRef* elementPath) const { + if (element->getType() != BSONType::Array) { + invariant(elementPath); // We can only hit this error if we are updating an existing path. + auto idElem = mutablebson::findFirstChildNamed(element->getDocument().root(), "_id"); + uasserted(ErrorCodes::BadValue, + str::stream() << "The field '" << elementPath->dottedField() << "'" + << " must be an array but is of type " + << typeName(element->getType()) + << " in document {" + << (idElem.ok() ? idElem.toString() : "no id") + << "}"); + } + + auto result = insertElementsWithPosition(element, _position, _valuesToPush); + + if (_sort) { + result = PushResult::kModifyArray; + sortChildren(*element, *_sort); + } + + while (static_cast<long long>(countChildren(*element)) > std::abs(_slice)) { + result = PushResult::kModifyArray; + if (_slice >= 0) { + invariantOK(element->popBack()); + } else { + // A negative value in '_slice' trims the array down to abs(_slice) but removes entries + // from the front of the array instead of the back. + invariantOK(element->popFront()); + } + } + + return result; +} + +PathCreatingNode::UpdateExistingElementResult PushNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { + auto originalSize = countChildren(*element); + + switch (performPush(element, elementPath.get())) { + case PushResult::kNoOp: + return UpdateExistingElementResult::kNoOp; + case PushResult::kAppendToEndOfArray: + if (logBuilder) { + // Special case: the only modification to the array is to add new elements, so we + // can log the update as a $set for each new element, rather than logging a $set + // with the entire array contents. + auto position = originalSize; + for (auto&& valueToLog : _valuesToPush) { + std::string positionString(str::stream() << position); + UpdateInternalNode::FieldRefTempAppend tempAppend(*elementPath, positionString); + uassertStatusOK(logBuilder->addToSetsWithNewFieldName( + elementPath->dottedField(), valueToLog)); + + ++position; + } + } + + // Indicate that PathCreatingNode::apply() does not need to do any more logging. + return UpdateExistingElementResult::kUpdatedAndLogged; + case PushResult::kModifyArray: + return UpdateExistingElementResult::kUpdated; + default: + MONGO_UNREACHABLE; + } +} + +void PushNode::setValueForNewElement(mutablebson::Element* element) const { + BSONObj emptyArray; + invariantOK(element->setValueArray(emptyArray)); + (void)performPush(element, nullptr); +} + +} // namespace mongo diff --git a/src/mongo/db/update/push_node.h b/src/mongo/db/update/push_node.h new file mode 100644 index 00000000000..38784dc7365 --- /dev/null +++ b/src/mongo/db/update/push_node.h @@ -0,0 +1,93 @@ +/** + * 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 <http://www.gnu.org/licenses/>. + * + * 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. + */ + +#pragma once + +#include <boost/optional.hpp> +#include <limits> +#include <vector> + +#include "mongo/db/update/path_creating_node.h" +#include "mongo/db/update/push_sorter.h" +#include "mongo/stdx/memory.h" + +namespace mongo { + +class PushNode final : public PathCreatingNode { +public: + PushNode() + : _slice(std::numeric_limits<long long>::max()), + _position(std::numeric_limits<long long>::max()) {} + Status init(BSONElement modExpr, const CollatorInterface* collator) final; + + std::unique_ptr<UpdateNode> clone() const final { + return stdx::make_unique<PushNode>(*this); + } + + void setCollator(const CollatorInterface* collator) final { + if (_sort) { + invariant(!_sort->collator); + _sort->collator = collator; + } + } + +protected: + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; + void setValueForNewElement(mutablebson::Element* element) const final; + +private: + /** + * Used to describe the result of the PerformPush operation. Note that appending to any empty + * array is always considered kModifyArray. That's because we want $push onto an empty to array + * to trigger a log entry with a $set on the entire array. + */ + enum class PushResult { + kNoOp, // The array is left exactly as it was. + kAppendToEndOfArray, // The only change to the array is items appended to the end. + kModifyArray // Any other modification of the array. + }; + + static PushResult insertElementsWithPosition(mutablebson::Element* array, + long long position, + const std::vector<BSONElement> valuesToPush); + PushResult performPush(mutablebson::Element* element, FieldRef* elementPath) const; + + static const StringData kEachClauseName; + static const StringData kSliceClauseName; + static const StringData kSortClauseName; + static const StringData kPositionClauseName; + + std::vector<BSONElement> _valuesToPush; + long long _slice; + long long _position; + boost::optional<PatternElementCmp> _sort; +}; + +} // namespace mongo diff --git a/src/mongo/db/update/push_node_test.cpp b/src/mongo/db/update/push_node_test.cpp new file mode 100644 index 00000000000..a06b26484bd --- /dev/null +++ b/src/mongo/db/update/push_node_test.cpp @@ -0,0 +1,986 @@ +/** + * 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 <http://www.gnu.org/licenses/>. + * + * 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/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::Document; +using mongo::mutablebson::Element; +using mongo::mutablebson::countChildren; + +TEST(PushNodeTest, EachClauseWithNonArrayObjectFails) { + auto update = fromjson("{$push: {x: {$each: {'0': 1}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, EachClauseWithPrimitiveFails) { + auto update = fromjson("{$push: {x: {$each: 1}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, PositionClauseWithObjectFails) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $position: {a: 1}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, PositionClauseWithNonIntegerFails) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $position: -2.1}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, PositionClauseWithIntegerDoubleSucceeds) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $position: -2.0}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_OK(status); +} + +TEST(PushNodeTest, SliceClauseWithObjectFails) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: {a: 1}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, SliceClauseWithNonIntegerFails) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: -2.1}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, SliceClauseWithIntegerDoubleSucceeds) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: 2.0}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["x"], collator)); +} + +TEST(PushNodeTest, SliceClauseWithArrayFails) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: [1, 2]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, SliceClauseWithStringFails) { + auto update = fromjson("{$push: {x: {$each: [1, 2], $slice: '-1'}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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: {}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST(PushNodeTest, PushEachWithDuplicatePositionClauseFails) { + auto update = fromjson("{$push: {x: {$each: [{a: 1}], $position: 1, $position: 2}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + auto status = node.init(update["$push"]["x"], collator); + ASSERT_NOT_OK(status); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + +TEST_F(PushNodeTest, ApplyToNonArrayFails) { + auto update = fromjson("{$push: {a: 1}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + Document doc(fromjson("{_id: 'test_object', a: 1}")); + setPathTaken("a"); + addIndexedPath("a"); + ASSERT_THROWS_CODE_AND_WHAT( + node.apply(getApplyParams(doc.root()["a"])), + UserException, + 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}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["choices.first.votes"], collator)); + + 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]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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]}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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: []}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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: []}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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: []}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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); + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], &collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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); + 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 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<testData> 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))); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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<testData> 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))); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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<testItem> 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<testData> testDataList; + for (auto sortAFirst : std::set<bool>{false, true}) { + for (auto sortOrderA : std::set<int>{-1, 1}) { + for (auto sortOrderB : std::set<int>{-1, 1}) { + for (int sliceValue = -5; sliceValue <= 5; ++sliceValue) { + std::vector<testItem> resultingItems(testItemList); + + std::sort( + resultingItems.begin(), + resultingItems.end(), + [sortAFirst, sortOrderA, sortOrderB](const auto& left, const auto& right) { + std::pair<int, int> leftPair(left.aVal, left.bVal); + std::pair<int, int> 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<size_t>(sliceValue) < resultingItems.size()) { + resultingItems = std::vector<testItem>(resultingItems.begin(), + resultingItems.begin() + sliceValue); + } else if (sliceValue < 0 && + static_cast<size_t>(-sliceValue) < resultingItems.size()) { + resultingItems = std::vector<testItem>(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))); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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}}}"); + const CollatorInterface* collator = nullptr; + PushNode node; + ASSERT_OK(node.init(update["$push"]["a"], collator)); + + 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 diff --git a/src/mongo/db/ops/modifier_push_sorter.h b/src/mongo/db/update/push_sorter.h index 6e2ef53cfcd..875d464ce00 100644 --- a/src/mongo/db/ops/modifier_push_sorter.h +++ b/src/mongo/db/update/push_sorter.h @@ -1,5 +1,5 @@ /** - * Copyright (C) 2013 10gen Inc. + * Copyright (C) 2017 10gen 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, diff --git a/src/mongo/db/ops/modifier_push_sorter_test.cpp b/src/mongo/db/update/push_sorter_test.cpp index c0c5f4dd46f..42cc0f3783d 100644 --- a/src/mongo/db/ops/modifier_push_sorter_test.cpp +++ b/src/mongo/db/update/push_sorter_test.cpp @@ -1,5 +1,5 @@ /** - * Copyright (C) 2013 10gen Inc. + * Copyright (C) 2017 10gen 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, @@ -26,7 +26,7 @@ * it in the license file. */ -#include "mongo/db/ops/modifier_push_sorter.h" +#include "mongo/db/update/push_sorter.h" #include "mongo/bson/mutable/algorithm.h" #include "mongo/bson/mutable/document.h" @@ -37,13 +37,9 @@ #include "mongo/db/query/collation/collator_interface_mock.h" #include "mongo/unittest/unittest.h" +namespace mongo { namespace { -using mongo::BSONObj; -using mongo::CollatorInterface; -using mongo::CollatorInterfaceMock; -using mongo::PatternElementCmp; -using mongo::fromjson; using mongo::mutablebson::Document; using mongo::mutablebson::Element; using mongo::mutablebson::sortChildren; @@ -201,4 +197,5 @@ TEST_F(ObjectArray, SortRespectsCollation) { ASSERT_BSONOBJ_EQ(getOrigObj(2), getSortedObj(1)); } -} // unnamed namespace +} // namespace +} // namespace mongo diff --git a/src/mongo/db/update/rename_node.cpp b/src/mongo/db/update/rename_node.cpp index bf108c54aa2..0368314ba84 100644 --- a/src/mongo/db/update/rename_node.cpp +++ b/src/mongo/db/update/rename_node.cpp @@ -63,7 +63,9 @@ public: return Status::OK(); } - bool updateExistingElement(mutablebson::Element* element) const final { + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final { // In the case of a $rename where the source and destination have the same value, (e.g., we // are applying {$rename: {a: b}} to the document {a: "foo", b: "foo"}), there's no need to // modify the destination element. However, the source and destination values must be @@ -72,9 +74,9 @@ public: auto considerFieldName = false; if (_elemToSet.compareWithElement(*element, comparator, considerFieldName) != 0) { invariantOK(element->setValueElement(_elemToSet)); - return true; + return UpdateExistingElementResult::kUpdated; } else { - return false; + return UpdateExistingElementResult::kNoOp; } } diff --git a/src/mongo/db/update/set_node.cpp b/src/mongo/db/update/set_node.cpp index 7d824c044fc..f65eb5b234b 100644 --- a/src/mongo/db/update/set_node.cpp +++ b/src/mongo/db/update/set_node.cpp @@ -42,14 +42,17 @@ Status SetNode::init(BSONElement modExpr, const CollatorInterface* collator) { return Status::OK(); } -bool SetNode::updateExistingElement(mutablebson::Element* element) const { +PathCreatingNode::UpdateExistingElementResult SetNode::updateExistingElement( + mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const { // If 'element' is deserialized, then element.getValue() will be EOO, which will never equal // _val. if (element->getValue().binaryEqualValues(_val)) { - return false; + return UpdateExistingElementResult::kNoOp; } else { invariantOK(element->setValueBSONElement(_val)); - return true; + return UpdateExistingElementResult::kUpdated; } } diff --git a/src/mongo/db/update/set_node.h b/src/mongo/db/update/set_node.h index 26c4a66d9a5..754426330ff 100644 --- a/src/mongo/db/update/set_node.h +++ b/src/mongo/db/update/set_node.h @@ -49,7 +49,9 @@ public: void setCollator(const CollatorInterface* collator) final {} protected: - bool updateExistingElement(mutablebson::Element* element) const final; + UpdateExistingElementResult updateExistingElement(mutablebson::Element* element, + std::shared_ptr<FieldRef> elementPath, + LogBuilder* logBuilder) const final; void setValueForNewElement(mutablebson::Element* element) const final; private: diff --git a/src/mongo/db/update/update_object_node_test.cpp b/src/mongo/db/update/update_object_node_test.cpp index 511a38dc461..018719b827a 100644 --- a/src/mongo/db/update/update_object_node_test.cpp +++ b/src/mongo/db/update/update_object_node_test.cpp @@ -233,6 +233,20 @@ TEST(UpdateObjectNodeTest, ValidSetOnInsertPathParsesSuccessfully) { foundIdentifiers)); } +TEST(UpdateObjectNodeTest, ValidPushParsesSuccessfully) { + auto update = fromjson("{$push: {'a.b': {$each: [0, 1], $sort: 1, $position: 0, $slice: 10}}}"); + const CollatorInterface* collator = nullptr; + std::map<StringData, std::unique_ptr<ExpressionWithPlaceholder>> arrayFilters; + std::set<std::string> foundIdentifiers; + UpdateObjectNode root; + ASSERT_OK(UpdateObjectNode::parseAndMerge(&root, + modifiertable::ModifierType::MOD_PUSH, + update["$push"]["a.b"], + collator, + arrayFilters, + foundIdentifiers)); +} + TEST(UpdateObjectNodeTest, MultiplePositionalElementsFailToParse) { auto update = fromjson("{$set: {'a.$.b.$': 5}}"); const CollatorInterface* collator = nullptr; @@ -301,24 +315,6 @@ TEST(UpdateObjectNodeTest, PositionalElementFirstPositionFailsToParse) { "Cannot have positional (i.e. '$') element in the first position in path '$'"); } -// TODO SERVER-28777: All modifier types should succeed. -TEST(UpdateObjectNodeTest, PushFailsToParse) { - auto update = fromjson("{$push: {a: 5}}"); - const CollatorInterface* collator = nullptr; - std::map<StringData, std::unique_ptr<ExpressionWithPlaceholder>> arrayFilters; - std::set<std::string> foundIdentifiers; - UpdateObjectNode root; - auto result = UpdateObjectNode::parseAndMerge(&root, - modifiertable::ModifierType::MOD_PUSH, - update["$push"]["a"], - collator, - arrayFilters, - foundIdentifiers); - ASSERT_NOT_OK(result); - ASSERT_EQ(result.getStatus().code(), ErrorCodes::FailedToParse); - ASSERT_EQ(result.getStatus().reason(), "Cannot construct modifier of type 10"); -} - TEST(UpdateObjectNodeTest, TwoModifiersOnSameFieldFailToParse) { auto update = fromjson("{$set: {a: 5}}"); const CollatorInterface* collator = nullptr; |