summaryrefslogtreecommitdiff
path: root/src/mongo
diff options
context:
space:
mode:
authorJustin Seyster <justin.seyster@mongodb.com>2017-08-08 22:48:05 -0400
committerJustin Seyster <justin.seyster@mongodb.com>2017-08-08 22:49:08 -0400
commitbf99c6ff2fbd95590aa86818c07b9bc121557f06 (patch)
treedb049bdd046b31b07cfac1a9711b2f16f22976ed /src/mongo
parentc7661b14867cd058e1a67986b8e05a7020fc0a5e (diff)
downloadmongo-bf99c6ff2fbd95590aa86818c07b9bc121557f06.tar.gz
SERVER-28772 PushNode
A later commit will remove support for the deprecated $pushAll.
Diffstat (limited to 'src/mongo')
-rw-r--r--src/mongo/db/ops/SConscript9
-rw-r--r--src/mongo/db/ops/modifier_push.h2
-rw-r--r--src/mongo/db/update/SConscript12
-rw-r--r--src/mongo/db/update/addtoset_node.cpp9
-rw-r--r--src/mongo/db/update/addtoset_node.h4
-rw-r--r--src/mongo/db/update/arithmetic_node.cpp9
-rw-r--r--src/mongo/db/update/arithmetic_node.h4
-rw-r--r--src/mongo/db/update/bit_node.cpp9
-rw-r--r--src/mongo/db/update/bit_node.h4
-rw-r--r--src/mongo/db/update/compare_node.cpp9
-rw-r--r--src/mongo/db/update/compare_node.h4
-rw-r--r--src/mongo/db/update/current_date_node.cpp7
-rw-r--r--src/mongo/db/update/current_date_node.h4
-rw-r--r--src/mongo/db/update/modifier_table.cpp3
-rw-r--r--src/mongo/db/update/path_creating_node.cpp12
-rw-r--r--src/mongo/db/update/path_creating_node.h10
-rw-r--r--src/mongo/db/update/push_node.cpp317
-rw-r--r--src/mongo/db/update/push_node.h93
-rw-r--r--src/mongo/db/update/push_node_test.cpp986
-rw-r--r--src/mongo/db/update/push_sorter.h (renamed from src/mongo/db/ops/modifier_push_sorter.h)2
-rw-r--r--src/mongo/db/update/push_sorter_test.cpp (renamed from src/mongo/db/ops/modifier_push_sorter_test.cpp)13
-rw-r--r--src/mongo/db/update/rename_node.cpp8
-rw-r--r--src/mongo/db/update/set_node.cpp9
-rw-r--r--src/mongo/db/update/set_node.h4
-rw-r--r--src/mongo/db/update/update_object_node_test.cpp32
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;