TEST_F(UnsetNodeTest, ApplyCannotRemoveRequiredPartOfDBRef) { auto update = fromjson("{$unset: {'a.$id': true}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.$id"], expCtx)); mutablebson::Document doc(fromjson("{a: {$ref: 'c', $id: 0}}")); setPathTaken("a.$id"); ASSERT_THROWS_CODE_AND_WHAT(node.apply(getApplyParams(doc.root()["a"]["$id"])), AssertionException, ErrorCodes::InvalidDBRef, "The DBRef $ref field must be followed by a $id field"); }
TEST_F(UnsetNodeTest, ApplyCannotRemoveRequiredPartOfDBRef) { auto update = fromjson("{$unset: {'a.$id': true}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.$id"], collator)); mutablebson::Document doc(fromjson("{a: {$ref: 'c', $id: 0}}")); setPathTaken("a.$id"); ASSERT_THROWS_CODE_AND_WHAT(node.apply(getApplyParams(doc.root()["a"]["$id"])), AssertionException, ErrorCodes::InvalidDBRef, "The DBRef $ref field must be followed by a $id field"); }
TEST_F(UnsetNodeTest, ApplyCannotRemovePrefixOfImmutablePath) { auto update = fromjson("{$unset: {a: true}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a"], collator)); mutablebson::Document doc(fromjson("{a: {b: 1}}")); setPathTaken("a"); addImmutablePath("a.b"); ASSERT_THROWS_CODE_AND_WHAT(node.apply(getApplyParams(doc.root()["a"])), AssertionException, ErrorCodes::ImmutableField, "Unsetting the path 'a' would modify the immutable field 'a.b'"); }
TEST_F(UnsetNodeTest, ApplyCannotRemoveSuffixOfImmutablePath) { auto update = fromjson("{$unset: {'a.b.c': true}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.b.c"], expCtx)); mutablebson::Document doc(fromjson("{a: {b: {c: 1}}}")); setPathTaken("a.b.c"); addImmutablePath("a.b"); ASSERT_THROWS_CODE_AND_WHAT( node.apply(getApplyParams(doc.root()["a"]["b"]["c"])), AssertionException, ErrorCodes::ImmutableField, "Performing an update on the path 'a.b.c' would modify the immutable field 'a.b'"); }
TEST_F(UnsetNodeTest, ApplyNoIndexDataNoLogBuilder) { auto update = fromjson("{$unset: {a: 1}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a"], collator)); mutablebson::Document doc(fromjson("{a: 5}")); setPathTaken("a"); setLogBuilderToNull(); auto result = node.apply(getApplyParams(doc.root()["a"])); ASSERT_FALSE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); }
TEST_F(UnsetNodeTest, ApplyNoIndexDataNoLogBuilder) { auto update = fromjson("{$unset: {a: 1}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a"], expCtx)); mutablebson::Document doc(fromjson("{a: 5}")); setPathTaken("a"); setLogBuilderToNull(); auto result = node.apply(getApplyParams(doc.root()["a"])); ASSERT_FALSE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(getModifiedPaths(), "{a}"); }
TEST_F(UnsetNodeTest, ApplyFieldWithDot) { auto update = fromjson("{$unset: {'a.b': 1}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.b"], collator)); mutablebson::Document doc(fromjson("{'a.b':4, a: {b: 2}}")); setPathTaken("a.b"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]["b"])); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{'a.b':4, a: {}}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$unset: {'a.b': true}}"), getLogDoc()); }
TEST_F(UnsetNodeTest, UnsetNoOpEmptyDoc) { auto update = fromjson("{$unset: {a: 1}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a"], collator)); mutablebson::Document doc(fromjson("{}")); setPathToCreate("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root())); ASSERT_TRUE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{}"), doc); ASSERT_TRUE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{}"), getLogDoc()); }
TEST_F(UnsetNodeTest, ApplyFieldWithDot) { auto update = fromjson("{$unset: {'a.b': 1}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.b"], expCtx)); mutablebson::Document doc(fromjson("{'a.b':4, a: {b: 2}}")); setPathTaken("a.b"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]["b"])); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{'a.b':4, a: {}}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$unset: {'a.b': true}}"), getLogDoc()); ASSERT_EQUALS(getModifiedPaths(), "{a.b}"); }
TEST_F(UnsetNodeTest, UnsetNoOpEmptyDoc) { auto update = fromjson("{$unset: {a: 1}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a"], expCtx)); mutablebson::Document doc(fromjson("{}")); setPathToCreate("a"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root())); ASSERT_TRUE(result.noop); ASSERT_FALSE(result.indexesAffected); ASSERT_EQUALS(fromjson("{}"), doc); ASSERT_TRUE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{}"), getLogDoc()); ASSERT_EQUALS(getModifiedPaths(), "{a}"); }
TEST_F(UnsetNodeTest, UnsetPositional) { auto update = fromjson("{$unset: {'a.$': 1}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.$"], collator)); mutablebson::Document doc(fromjson("{a: [0, 1, 2]}")); setPathTaken("a.1"); setMatchedField("1"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"]["1"])); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, null, 2]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$unset: {'a.1': true}}"), getLogDoc()); }
TEST_F(UnsetNodeTest, UnsetPositional) { auto update = fromjson("{$unset: {'a.$': 1}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.$"], expCtx)); mutablebson::Document doc(fromjson("{a: [0, 1, 2]}")); setPathTaken("a.1"); setMatchedField("1"); addIndexedPath("a"); auto result = node.apply(getApplyParams(doc.root()["a"][1])); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); ASSERT_EQUALS(fromjson("{a: [0, null, 2]}"), doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$unset: {'a.1': true}}"), getLogDoc()); ASSERT_EQUALS(getModifiedPaths(), "{a.1}"); }
TEST_F(UnsetNodeTest, ApplyCanRemoveRequiredPartOfDBRefIfValidateForStorageIsFalse) { auto update = fromjson("{$unset: {'a.$id': true}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.$id"], collator)); mutablebson::Document doc(fromjson("{a: {$ref: 'c', $id: 0}}")); setPathTaken("a.$id"); addIndexedPath("a"); setValidateForStorage(false); auto result = node.apply(getApplyParams(doc.root()["a"]["$id"])); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); auto updated = BSON("a" << BSON("$ref" << "c")); ASSERT_EQUALS(updated, doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$unset: {'a.$id': true}}"), getLogDoc()); }
TEST_F(UnsetNodeTest, ApplyCanRemoveRequiredPartOfDBRefIfValidateForStorageIsFalse) { auto update = fromjson("{$unset: {'a.$id': true}}"); boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); UnsetNode node; ASSERT_OK(node.init(update["$unset"]["a.$id"], expCtx)); mutablebson::Document doc(fromjson("{a: {$ref: 'c', $id: 0}}")); setPathTaken("a.$id"); addIndexedPath("a"); setValidateForStorage(false); auto result = node.apply(getApplyParams(doc.root()["a"]["$id"])); ASSERT_FALSE(result.noop); ASSERT_TRUE(result.indexesAffected); auto updated = BSON("a" << BSON("$ref" << "c")); ASSERT_EQUALS(updated, doc); ASSERT_FALSE(doc.isInPlaceModeEnabled()); ASSERT_EQUALS(fromjson("{$unset: {'a.$id': true}}"), getLogDoc()); ASSERT_EQUALS(getModifiedPaths(), "{a.$id}"); }
UpdateNode::ApplyResult RenameNode::apply(ApplyParams applyParams) const { // It would make sense to store fromFieldRef and toFieldRef as members during // RenameNode::init(), but FieldRef is not copyable. auto fromFieldRef = std::make_shared<FieldRef>(_val.fieldName()); FieldRef toFieldRef(_val.valueStringData()); mutablebson::Document& document = applyParams.element.getDocument(); size_t fromIdxFound; mutablebson::Element fromElement(document.end()); auto status = pathsupport::findLongestPrefix(*fromFieldRef, document.root(), &fromIdxFound, &fromElement); if (!status.isOK() || !fromElement.ok() || fromIdxFound != (fromFieldRef->numParts() - 1)) { // We could safely remove this restriction (thereby treating a rename with a non-viable // source path as a no-op), but most updates fail on an attempt to update a non-viable path, // so we throw an error for consistency. if (status == ErrorCodes::PathNotViable) { uassertStatusOK(status); MONGO_UNREACHABLE; // The previous uassertStatusOK should always throw. } // The element we want to rename does not exist. When that happens, we treat the operation // as a no-op. The attempted from/to paths are still considered modified. if (applyParams.modifiedPaths) { applyParams.modifiedPaths->keepShortest(*fromFieldRef); applyParams.modifiedPaths->keepShortest(toFieldRef); } return ApplyResult::noopResult(); } // Renaming through an array is prohibited. Check that our source path does not contain an // array. (The element being renamed may be an array, however.) for (auto currentElement = fromElement.parent(); currentElement != document.root(); currentElement = currentElement.parent()) { invariant(currentElement.ok()); if (BSONType::Array == currentElement.getType()) { auto idElem = mutablebson::findFirstChildNamed(document.root(), "_id"); uasserted(ErrorCodes::BadValue, str::stream() << "The source field cannot be an array element, '" << fromFieldRef->dottedField() << "' in doc with " << (idElem.ok() ? idElem.toString() : "no id") << " has an array field called '" << currentElement.getFieldName() << "'"); } } // Check that our destination path does not contain an array. (If the rename will overwrite an // existing element, that element may be an array. Iff pathToCreate is empty, "element" // represents an element that we are going to overwrite.) for (auto currentElement = applyParams.pathToCreate->empty() ? applyParams.element.parent() : applyParams.element; currentElement != document.root(); currentElement = currentElement.parent()) { invariant(currentElement.ok()); if (BSONType::Array == currentElement.getType()) { auto idElem = mutablebson::findFirstChildNamed(document.root(), "_id"); uasserted(ErrorCodes::BadValue, str::stream() << "The destination field cannot be an array element, '" << toFieldRef.dottedField() << "' in doc with " << (idElem.ok() ? idElem.toString() : "no id") << " has an array field called '" << currentElement.getFieldName() << "'"); } } // Once we've determined that the rename is valid and found the source element, the actual work // gets broken out into a $set operation and an $unset operation. Note that, generally, we // should call the init() method of a ModifierNode before calling its apply() method, but the // init() methods of SetElementNode and UnsetNode don't do anything, so we can skip them. SetElementNode setElement(fromElement); auto setElementApplyResult = setElement.apply(applyParams); ApplyParams unsetParams(applyParams); unsetParams.element = fromElement; unsetParams.pathToCreate = std::make_shared<FieldRef>(); unsetParams.pathTaken = fromFieldRef; UnsetNode unsetElement; auto unsetElementApplyResult = unsetElement.apply(unsetParams); // Determine the final result based on the results of the $set and $unset. ApplyResult applyResult; applyResult.indexesAffected = setElementApplyResult.indexesAffected || unsetElementApplyResult.indexesAffected; // The $unset would only be a no-op if the source element did not exist, in which case we would // have exited early with a no-op result. invariant(!unsetElementApplyResult.noop); return applyResult; }
using namespace mongo; using mongo::mutablebson::Document; using mongo::mutablebson::Element; using mongo::mutablebson::countChildren; DEATH_TEST(UnsetNodeTest, InitFailsForEmptyElement, "Invariant failure modExpr.ok()") { auto update = fromjson("{$unset: {}}"); const CollatorInterface* collator = nullptr; UnsetNode node; node.init(update["$unset"].embeddedObject().firstElement(), collator).transitional_ignore(); } DEATH_TEST(UnsetNodeTest, ApplyToRootFails, "Invariant failure parent.ok()") { auto update = fromjson("{$unset: {}}"); const CollatorInterface* collator = nullptr; UnsetNode node; ASSERT_OK(node.init(update["$unset"], collator)); Document doc(fromjson("{a: 5}")); FieldRef pathToCreate(""); FieldRef pathTaken(""); StringData matchedField; auto fromReplication = false; auto validateForStorage = true; FieldRefSet immutablePaths; const UpdateIndexData* indexData = nullptr; LogBuilder* logBuilder = nullptr; auto indexesAffected = false; auto noop = false; node.apply(doc.root(), &pathToCreate,