diff options
author | Tess Avitabile <tess.avitabile@mongodb.com> | 2017-04-21 16:13:21 -0400 |
---|---|---|
committer | Tess Avitabile <tess.avitabile@mongodb.com> | 2017-05-01 10:00:22 -0400 |
commit | 314809d7cdda82d6c2741dbe2daf119270e5ad02 (patch) | |
tree | 6fe3ede8bac16e227d1046611e4bcb827fd99e39 | |
parent | 5273c2bad7df58c44afdd677609b8656f399a906 (diff) | |
download | mongo-314809d7cdda82d6c2741dbe2daf119270e5ad02.tar.gz |
SERVER-28759 Implement SetNode::apply()
-rw-r--r-- | src/mongo/db/update/SConscript | 3 | ||||
-rw-r--r-- | src/mongo/db/update/path_support.cpp | 14 | ||||
-rw-r--r-- | src/mongo/db/update/path_support_test.cpp | 24 | ||||
-rw-r--r-- | src/mongo/db/update/set_node.cpp | 69 | ||||
-rw-r--r-- | src/mongo/db/update/set_node.h | 10 | ||||
-rw-r--r-- | src/mongo/db/update/set_node_test.cpp | 1688 | ||||
-rw-r--r-- | src/mongo/db/update/update_leaf_node.h | 30 |
7 files changed, 1836 insertions, 2 deletions
diff --git a/src/mongo/db/update/SConscript b/src/mongo/db/update/SConscript index 2bf1d3b756b..82d60443cd9 100644 --- a/src/mongo/db/update/SConscript +++ b/src/mongo/db/update/SConscript @@ -61,6 +61,7 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/db/ops/update', + '$BUILD_DIR/mongo/db/update_index_data', 'update_common', ], ) @@ -77,6 +78,7 @@ env.CppUnitTest( target='set_node_test', source='set_node_test.cpp', LIBDEPS=[ + '$BUILD_DIR/mongo/bson/mutable/mutable_bson_test_utils', 'update', ], ) @@ -99,7 +101,6 @@ env.Library( '$BUILD_DIR/mongo/db/common', '$BUILD_DIR/mongo/db/ops/update', '$BUILD_DIR/mongo/db/query/query_planner', - '$BUILD_DIR/mongo/db/update_index_data', 'update', ], ) diff --git a/src/mongo/db/update/path_support.cpp b/src/mongo/db/update/path_support.cpp index 5e336bb8202..993d4195bc4 100644 --- a/src/mongo/db/update/path_support.cpp +++ b/src/mongo/db/update/path_support.cpp @@ -162,6 +162,14 @@ Status createPathAt(const FieldRef& prefix, mutablebson::Element newElem) { Status status = Status::OK(); + if (elemFound.getType() != BSONType::Object && elemFound.getType() != BSONType::Array) { + return Status(ErrorCodes::PathNotViable, + str::stream() << "Cannot create field '" << prefix.getPart(idxFound) + << "' in element {" + << elemFound.toString() + << "}"); + } + // Sanity check that 'idxField' is an actual part. const size_t size = prefix.numParts(); if (idxFound >= size) { @@ -177,7 +185,11 @@ Status createPathAt(const FieldRef& prefix, if (elemFound.getType() == mongo::Array) { size_t newIdx = 0; if (!isNumeric(prefix.getPart(idxFound), &newIdx)) { - return Status(ErrorCodes::InvalidPath, "Array require numeric fields"); + return Status(ErrorCodes::PathNotViable, + str::stream() << "Cannot create field '" << prefix.getPart(idxFound) + << "' in element {" + << elemFound.toString() + << "}"); } status = maybePadTo(&elemFound, newIdx); diff --git a/src/mongo/db/update/path_support_test.cpp b/src/mongo/db/update/path_support_test.cpp index 5091a2b8cba..5ea820bbf7b 100644 --- a/src/mongo/db/update/path_support_test.cpp +++ b/src/mongo/db/update/path_support_test.cpp @@ -198,6 +198,18 @@ TEST_F(SimpleDoc, NotCommonPrefix) { ASSERT_EQUALS(root().rightChild().leftSibling().getFieldName(), "a"); } +TEST_F(SimpleDoc, CreatePathAtFailsIfElemFoundIsNonObjectNonArray) { + setField("b"); + + auto elemFound = root()["a"]; + auto newElem = doc().makeElementInt("b", 1); + ASSERT_TRUE(newElem.ok()); + auto result = createPathAt(field(), 0, elemFound, newElem); + ASSERT_NOT_OK(result); + ASSERT_EQ(result.code(), ErrorCodes::PathNotViable); + ASSERT_EQ(result.reason(), "Cannot create field 'b' in element {a: 1}"); +} + class NestedDoc : public mongo::unittest::Test { public: NestedDoc() : _doc() {} @@ -520,6 +532,18 @@ TEST_F(ArrayDoc, NonNumericPathInArray) { ASSERT_EQUALS(elemFound.compareWithElement(root()["b"], nullptr), 0); } +TEST_F(ArrayDoc, CreatePathAtFailsIfElemFoundIsArrayAndIdxFoundFieldIsNonNumeric) { + setField("b"); + + auto elemFound = root()["a"]; + auto newElem = doc().makeElementInt("b", 1); + ASSERT_TRUE(newElem.ok()); + auto result = createPathAt(field(), 0, elemFound, newElem); + ASSERT_NOT_OK(result); + ASSERT_EQ(result.code(), ErrorCodes::PathNotViable); + ASSERT_EQ(result.reason(), "Cannot create field 'b' in element {a: []}"); +} + // // Tests of equality extraction from MatchExpressions // NONGOAL: Testing query/match expression parsing and optimization diff --git a/src/mongo/db/update/set_node.cpp b/src/mongo/db/update/set_node.cpp index 26c6be59ee3..3cc44424600 100644 --- a/src/mongo/db/update/set_node.cpp +++ b/src/mongo/db/update/set_node.cpp @@ -30,6 +30,8 @@ #include "mongo/db/update/set_node.h" +#include "mongo/db/update/path_support.h" + namespace mongo { Status SetNode::init(BSONElement modExpr, const CollatorInterface* collator) { @@ -40,4 +42,71 @@ Status SetNode::init(BSONElement modExpr, const CollatorInterface* collator) { return Status::OK(); } +Status SetNode::apply(mutablebson::Element element, + FieldRef* pathToCreate, + FieldRef* pathTaken, + StringData matchedField, + bool fromReplication, + const UpdateIndexData* indexData, + LogBuilder* logBuilder, + bool* indexesAffected, + bool* noop) { + *indexesAffected = false; + *noop = false; + + // Set the element with the new value. + if (pathToCreate->empty()) { + // If 'element' is deserialized, then element.getValue() will be EOO, which will never equal + // _val. + if (element.getValue().binaryEqualValues(_val)) { + *noop = true; + return Status::OK(); + } + element.setValueBSONElement(_val); + } else { + auto elemToSet = element.getDocument().makeElementWithNewFieldName( + pathToCreate->getPart(pathToCreate->numParts() - 1), _val); + invariant(elemToSet.ok()); + auto status = pathsupport::createPathAt(*pathToCreate, 0, element, elemToSet); + if (!status.isOK()) { + // $sets on non-viable paths are ignored when the update came from replication. We do + // not error because idempotency requires that any other update modifiers must still be + // applied. For example, consider applying the following updates twice to an initially + // empty document: + // {$set: {c: 0}} + // {$set: {'a.b': 0, c: 1}} + // {$set: {a: 0}} + // Setting 'a.b' will fail the second time, but we must still set 'c'. + if (status.code() == ErrorCodes::PathNotViable && fromReplication) { + *noop = true; + return Status::OK(); + } + return status; + } + } + + // Create full field path of set element. + StringBuilder builder; + builder << pathTaken->dottedField(); + if (!pathTaken->empty() && !pathToCreate->empty()) { + builder << "."; + } + builder << pathToCreate->dottedField(); + auto fullPath = builder.str(); + + // Determine if indexes are affected. + if (indexData && indexData->mightBeIndexed(fullPath)) { + *indexesAffected = true; + } + + // Log the set. + if (logBuilder) { + auto logElement = logBuilder->getDocument().makeElementWithNewFieldName(fullPath, _val); + invariant(logElement.ok()); + return logBuilder->addToSets(logElement); + } + + return Status::OK(); +} + } // namespace mongo diff --git a/src/mongo/db/update/set_node.h b/src/mongo/db/update/set_node.h index 8ad64010a50..1a0d32a72b0 100644 --- a/src/mongo/db/update/set_node.h +++ b/src/mongo/db/update/set_node.h @@ -41,6 +41,16 @@ public: void setCollator(const CollatorInterface* collator) final {} + Status apply(mutablebson::Element element, + FieldRef* pathToCreate, + FieldRef* pathTaken, + StringData matchedField, + bool fromReplication, + const UpdateIndexData* indexData, + LogBuilder* logBuilder, + bool* indexesAffected, + bool* noop) final; + private: BSONElement _val; }; diff --git a/src/mongo/db/update/set_node_test.cpp b/src/mongo/db/update/set_node_test.cpp index c600af34158..e2fe5fe14fa 100644 --- a/src/mongo/db/update/set_node_test.cpp +++ b/src/mongo/db/update/set_node_test.cpp @@ -30,6 +30,8 @@ #include "mongo/db/update/set_node.h" +#include "mongo/bson/mutable/algorithm.h" +#include "mongo/bson/mutable/mutable_bson_test_utils.h" #include "mongo/db/json.h" #include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" @@ -37,6 +39,9 @@ namespace { using namespace mongo; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; +using mongo::mutablebson::countChildren; DEATH_TEST(SetNodeTest, InitFailsForEmptyElement, "Invariant failure modExpr.ok()") { auto update = fromjson("{$set: {}}"); @@ -52,4 +57,1687 @@ TEST(SetNodeTest, InitSucceedsForNonemptyElement) { ASSERT_OK(node.init(update["$set"]["a"], collator)); } +TEST(SetNodeTest, ApplyNoOp) { + auto update = fromjson("{$set: {a: 5}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 5}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 5}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{}"), logDoc); +} + +TEST(SetNodeTest, ApplyEmptyPathToCreate) { + auto update = fromjson("{$set: {a: 6}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 5}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 6}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{$set: {a: 6}}"), logDoc); +} + +TEST(SetNodeTest, ApplyCreatePath) { + auto update = fromjson("{$set: {'a.b.c': 6}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b.c"], collator)); + + Document doc(fromjson("{a: {d: 5}}")); + FieldRef pathToCreate("b.c"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {d: 5, b: {c: 6}}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{$set: {'a.b.c': 6}}"), logDoc); +} + +TEST(SetNodeTest, ApplyCreatePathFromRoot) { + auto update = fromjson("{$set: {'a.b': 6}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{c: 5}")); + FieldRef pathToCreate("a.b"); + FieldRef pathTaken(""); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{c: 5, a: {b: 6}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{$set: {'a.b': 6}}"), logDoc); +} + +TEST(SetNodeTest, ApplyPositional) { + auto update = fromjson("{$set: {'a.$': 6}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.$"], collator)); + + Document doc(fromjson("{a: [0, 1, 2]}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.1"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [0, 1, 2]}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{$set: {'a.1': 6}}"), logDoc); +} + +TEST(SetNodeTest, ApplyNonViablePathToCreate) { + auto update = fromjson("{$set: {'a.b': 5}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: 5}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + auto result = node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop); + ASSERT_NOT_OK(result); + ASSERT_EQ(result.code(), ErrorCodes::PathNotViable); + ASSERT_EQ(result.reason(), "Cannot create field 'b' in element {a: 5}"); +} + +TEST(SetNodeTest, ApplyNonViablePathToCreateFromReplicationIsNoOp) { + auto update = fromjson("{$set: {'a.b': 5}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: 5}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 5}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{}"), logDoc); +} + +TEST(SetNodeTest, ApplyNoIndexDataNoLogBuilder) { + auto update = fromjson("{$set: {a: 6}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 5}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 6}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyDoesNotAffectIndexes) { + auto update = fromjson("{$set: {a: 6}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 5}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 6}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, TypeChangeIsNotANoop) { + auto update = fromjson("{$set: {a: NumberLong(2)}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: NumberInt(2)}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: NumberLong(2)}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, IdentityOpOnDeserializedIsNotANoOp) { + // Apply an op that would be a no-op. + auto update = fromjson("{$set: {a: {b : NumberInt(2)}}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: { b: NumberInt(0)}}")); + // Apply a mutation to the document that will make it non-serialized. + doc.root()["a"]["b"].setValueInt(2); + + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b : NumberInt(2)}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyEmptyDocument) { + auto update = fromjson("{$set: {a: 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{}")); + FieldRef pathToCreate("a"); + FieldRef pathTaken(""); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyInPlace) { + auto update = fromjson("{$set: {a: 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 1}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyOverridePath) { + auto update = fromjson("{$set: {a: 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: {b: 1}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyChangeType) { + auto update = fromjson("{$set: {a: 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 'str'}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNewPath) { + auto update = fromjson("{$set: {a: 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{b: 1}")); + FieldRef pathToCreate("a"); + FieldRef pathTaken(""); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{b: 1, a: 2}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyLog) { + auto update = fromjson("{$set: {a: 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a"], collator)); + + Document doc(fromjson("{a: 1}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: 2}}"), logDoc); +} + +TEST(SetNodeTest, ApplyNoOpDottedPath) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: {b: 2}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b : 2}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, TypeChangeOnDottedPathIsNotANoOp) { + auto update = fromjson("{$set: {'a.b': NumberInt(2)}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: {b: NumberLong(2)}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b : NumberLong(2)}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyPathNotViable) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a:1}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_NOT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + logBuilder, + &indexesAffected, + &noop)); +} + +TEST(SetNodeTest, ApplyPathNotViableArrray) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a:[{b:1}]}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_NOT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + logBuilder, + &indexesAffected, + &noop)); +} + +TEST(SetNodeTest, ApplyInPlaceDottedPath) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: {b: 1}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyChangeTypeDottedPath) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: {b: 'str'}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyChangePath) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: {b: {c: 1}}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyExtendPath) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{a: {c: 1}}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {c: 1, b: 2}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNewDottedPath) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{c: 1}")); + FieldRef pathToCreate("a.b"); + FieldRef pathTaken(""); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{c: 1, a: {b: 2}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyEmptyDoc) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{}")); + FieldRef pathToCreate("a.b"); + FieldRef pathTaken(""); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyFieldWithDot) { + auto update = fromjson("{$set: {'a.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b"], collator)); + + Document doc(fromjson("{'a.b':4}")); + FieldRef pathToCreate("a.b"); + FieldRef pathTaken(""); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root(), + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{'a.b':4, a: {b: 2}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNoOpArrayIndex) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.2.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["2"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, TypeChangeInArrayIsNotANoOp) { + auto update = fromjson("{$set: {'a.2.b': NumberInt(2)}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 2.0}]}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.2.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["2"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: NumberInt(2)}]}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNonViablePath) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: 0}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_NOT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + logBuilder, + &indexesAffected, + &noop)); +} + +TEST(SetNodeTest, ApplyInPlaceArrayIndex) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 1}]}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.2.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["2"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNormalArray) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: [{b: 0},{b: 1}]}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyPaddingArray) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: [{b: 0}]}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: 0},null,{b: 2}]}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNumericObject) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: {b: 0}}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {b: 0, '2': {b: 2}}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyNumericField) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: {'2': {b: 1}}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.2.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["2"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyExtendNumericField) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: {'2': {c: 1}}}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a.2"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["2"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {'2': {c: 1, b: 2}}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyEmptyObject) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: {}}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyEmptyArray) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: []}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.2.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [null, null, {b: 2}]}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplyLogDottedPath) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: [{b:0}, {b:1}]}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_EQUALS(fromjson("{a: [{b:0}, {b:1}, {b:2}]}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); +} + +TEST(SetNodeTest, LogEmptyArray) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: []}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_EQUALS(fromjson("{a: [null, null, {b:2}]}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); +} + +TEST(SetNodeTest, LogEmptyObject) { + auto update = fromjson("{$set: {'a.2.b': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.2.b"], collator)); + + Document doc(fromjson("{a: {}}")); + FieldRef pathToCreate("2.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); +} + +TEST(SetNodeTest, ApplyNoOpComplex) { + auto update = fromjson("{$set: {'a.1.b': {c: 1, d: 1}}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.1.b"], collator)); + + Document doc(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, d: 1}}]}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.1.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.1.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["1"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, d: 1}}]}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ApplySameStructure) { + auto update = fromjson("{$set: {'a.1.b': {c: 1, d: 1}}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.1.b"], collator)); + + Document doc(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, xxx: 1}}]}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("a.1.b"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("a.1.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["1"]["b"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, d: 1}}]}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, NonViablePathWithoutRepl) { + auto update = fromjson("{$set: {'a.1.b': 1}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.1.b"], collator)); + + Document doc(fromjson("{a: 1}")); + FieldRef pathToCreate("1.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = false; + const UpdateIndexData* indexData = nullptr; + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_NOT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + indexData, + logBuilder, + &indexesAffected, + &noop)); +} + +TEST(SetNodeTest, SingleFieldFromReplication) { + auto update = fromjson("{$set: {'a.1.b': 1}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.1.b"], collator)); + + Document doc(fromjson("{_id:1, a: 1}")); + FieldRef pathToCreate("1.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a.1.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{_id:1, a: 1}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, SingleFieldNoIdFromReplication) { + auto update = fromjson("{$set: {'a.1.b': 1}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.1.b"], collator)); + + Document doc(fromjson("{a: 1}")); + FieldRef pathToCreate("1.b"); + FieldRef pathTaken("a"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a.1.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: 1}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, NestedFieldFromReplication) { + auto update = fromjson("{$set: {'a.a.1.b': 1}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.a.1.b"], collator)); + + Document doc(fromjson("{_id:1, a: {a: 1}}")); + FieldRef pathToCreate("1.b"); + FieldRef pathTaken("a.a"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a.a.1.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{_id:1, a: {a: 1}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, DoubleNestedFieldFromReplication) { + auto update = fromjson("{$set: {'a.b.c.d': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.b.c.d"], collator)); + + Document doc(fromjson("{_id:1, a: {b: {c: 1}}}")); + FieldRef pathToCreate("d"); + FieldRef pathTaken("a.b.c"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a.b.c.d"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["b"]["c"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{_id:1, a: {b: {c: 1}}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, NestedFieldNoIdFromReplication) { + auto update = fromjson("{$set: {'a.a.1.b': 1}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.a.1.b"], collator)); + + Document doc(fromjson("{a: {a: 1}}")); + FieldRef pathToCreate("1.b"); + FieldRef pathTaken("a.a"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a.a.1.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{a: {a: 1}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, ReplayArrayFieldNotAppendedIntermediateFromReplication) { + auto update = fromjson("{$set: {'a.0.b': [0,2]}}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["a.0.b"], collator)); + + Document doc(fromjson("{_id: 0, a: [1, {b: [1]}]}")); + FieldRef pathToCreate("b"); + FieldRef pathTaken("a.0"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("a.0.b"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["a"]["0"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_TRUE(noop); + ASSERT_FALSE(indexesAffected); + ASSERT_EQUALS(fromjson("{_id: 0, a: [1, {b: [1]}]}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); +} + +TEST(SetNodeTest, Set6) { + auto update = fromjson("{$set: {'r.a': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["r.a"], collator)); + + Document doc(fromjson("{_id: 1, r: {a:1, b:2}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("r.a"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("r.a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["r"]["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{_id: 1, r: {a:2, b:2}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'r.a': 2}}"), logDoc); +} + +TEST(SetNodeTest, Set6FromRepl) { + auto update = fromjson("{$set: { 'r.a': 2}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["r.a"], collator)); + + Document doc(fromjson("{_id: 1, r: {a:1, b:2}}")); + FieldRef pathToCreate(""); + FieldRef pathTaken("r.a"); + StringData matchedField; + auto fromReplication = true; + UpdateIndexData indexData; + indexData.addPath("r.a"); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["r"]["a"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + &logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{_id: 1, r: {a:2, b:2} }"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'r.a': 2}}"), logDoc); +} + +TEST(SetNodeTest, ApplySetModToEphemeralDocument) { + // The following mod when applied to a document constructed node by node exposed a + // latent debug only defect in mutable BSON, so this is more a test of mutable than + // $set. + auto update = fromjson("{ $set: { x: { a: 100, b: 2 }}}"); + const CollatorInterface* collator = nullptr; + SetNode node; + ASSERT_OK(node.init(update["$set"]["x"], collator)); + + Document doc; + Element x = doc.makeElementObject("x"); + doc.root().pushBack(x); + Element a = doc.makeElementInt("a", 100); + x.pushBack(a); + + FieldRef pathToCreate(""); + FieldRef pathTaken("x"); + StringData matchedField; + auto fromReplication = false; + UpdateIndexData indexData; + indexData.addPath("x"); + LogBuilder* logBuilder = nullptr; + auto indexesAffected = false; + auto noop = false; + ASSERT_OK(node.apply(doc.root()["x"], + &pathToCreate, + &pathTaken, + matchedField, + fromReplication, + &indexData, + logBuilder, + &indexesAffected, + &noop)); + ASSERT_FALSE(noop); + ASSERT_TRUE(indexesAffected); + ASSERT_EQUALS(fromjson("{ x : { a : 100, b : 2 } }"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); +} + } // namespace diff --git a/src/mongo/db/update/update_leaf_node.h b/src/mongo/db/update/update_leaf_node.h index 2b378036c7d..8f51dbf4a14 100644 --- a/src/mongo/db/update/update_leaf_node.h +++ b/src/mongo/db/update/update_leaf_node.h @@ -29,7 +29,11 @@ #pragma once #include "mongo/bson/bsonelement.h" +#include "mongo/bson/mutable/element.h" +#include "mongo/db/field_ref.h" +#include "mongo/db/update/log_builder.h" #include "mongo/db/update/update_node.h" +#include "mongo/db/update_index_data.h" namespace mongo { @@ -53,6 +57,32 @@ public: * multiple documents. */ virtual Status init(BSONElement modExpr, const CollatorInterface* collator) = 0; + + /** + * Applies the update node to 'element', creating the fields in 'pathToCreate' if required by + * the leaves (i.e. the leaves are not all $unset). 'pathTaken' is the path through the root + * document to 'element', ending with the field name of 'element'. 'pathToCreate' is the path + * taken through the UpdateNode tree beyond where the path existed in the document. For example, + * if the update is {$set: {'a.b.c': 5}}, and the document is {a: {}}, then at the leaf node, + * pathTaken="a" and pathToCreate="b.c" If there was a positional ($) element in the update + * expression, 'matchedField' is the index of the array element that caused the query to match + * the document. 'fromReplication' is provided because some modifiers may ignore certain errors + * when the update is from replication. Uses the index information in 'indexData' to determine + * whether indexes are affected. If a LogBuilder is provided, logs the update. Outputs whether + * the operation was a no-op. Returns a non-OK status if the update node cannot be applied to + * the document. + * + * TODO SERVER-28761: Move this virtual method into UpdateNode. + */ + virtual Status apply(mutablebson::Element element, + FieldRef* pathToCreate, + FieldRef* pathTaken, + StringData matchedField, + bool fromReplication, + const UpdateIndexData* indexData, + LogBuilder* logBuilder, + bool* indexesAffected, + bool* noop) = 0; }; } // namespace mongo |