From cbb82fd1b270f84e544243acbba2cb3fed779c28 Mon Sep 17 00:00:00 2001 From: Ian Boros Date: Mon, 17 Aug 2020 15:02:53 -0400 Subject: SERVER-50218 Change shard server and config server op observers to support $v:2 update oplog entries --- src/mongo/db/s/config_server_op_observer.cpp | 17 ++- src/mongo/db/s/shard_server_op_observer.cpp | 74 +++++----- src/mongo/db/update/SConscript | 4 +- .../db/update/update_oplog_entry_serialization.cpp | 162 +++++++++++++++++++++ .../db/update/update_oplog_entry_serialization.h | 65 ++++++++- .../update_oplog_entry_serialization_test.cpp | 159 ++++++++++++++++++++ 6 files changed, 428 insertions(+), 53 deletions(-) create mode 100644 src/mongo/db/update/update_oplog_entry_serialization.cpp create mode 100644 src/mongo/db/update/update_oplog_entry_serialization_test.cpp diff --git a/src/mongo/db/s/config_server_op_observer.cpp b/src/mongo/db/s/config_server_op_observer.cpp index e04a4f01b5a..eb4692e1a75 100644 --- a/src/mongo/db/s/config_server_op_observer.cpp +++ b/src/mongo/db/s/config_server_op_observer.cpp @@ -34,6 +34,7 @@ #include "mongo/db/s/config_server_op_observer.h" #include "mongo/db/s/config/sharding_catalog_manager.h" +#include "mongo/db/update/update_oplog_entry_serialization.h" #include "mongo/db/vector_clock_mutable.h" #include "mongo/logv2/log.h" #include "mongo/s/catalog/type_config_version.h" @@ -174,17 +175,21 @@ void ConfigServerOpObserver::onApplyOps(OperationContext* opCtx, if (updateShard["ns"].str() != ShardType::ConfigNS.ns()) { return; } + auto updateElem = updateShard["o"]; - if (updateElem.type() != Object) { + if (updateElem.type() != BSONType::Object) { return; } - auto updateObj = updateElem.Obj(); - auto setElem = updateObj["$set"]; - if (setElem.type() != Object) { + + auto updateObj = updateElem.embeddedObject(); + if (update_oplog_entry::extractUpdateType(updateObj) == + update_oplog_entry::UpdateType::kReplacement) { return; } - auto setObj = setElem.Obj(); - auto newTopologyTime = setObj[ShardType::topologyTime()].timestamp(); + + auto newTopologyTime = + update_oplog_entry::extractNewValueForField(updateObj, ShardType::topologyTime()) + .timestamp(); if (newTopologyTime == Timestamp()) { return; } diff --git a/src/mongo/db/s/shard_server_op_observer.cpp b/src/mongo/db/s/shard_server_op_observer.cpp index 70d6da97ec1..3cef6d2aebd 100644 --- a/src/mongo/db/s/shard_server_op_observer.cpp +++ b/src/mongo/db/s/shard_server_op_observer.cpp @@ -46,6 +46,7 @@ #include "mongo/db/s/sharding_initialization_mongod.h" #include "mongo/db/s/sharding_state.h" #include "mongo/db/s/type_shard_identity.h" +#include "mongo/db/update/update_oplog_entry_serialization.h" #include "mongo/logv2/log.h" #include "mongo/s/balancer_configuration.h" #include "mongo/s/catalog/type_shard_collection.h" @@ -280,7 +281,11 @@ void ShardServerOpObserver::onInserts(OperationContext* opCtx, } void ShardServerOpObserver::onUpdate(OperationContext* opCtx, const OplogUpdateEntryArgs& args) { - if (args.nss == NamespaceString::kShardConfigCollectionsNamespace) { + const auto& updateDoc = args.updateArgs.update; + // Most of these handlers do not need to run when the update is a full document replacement. + const bool isReplacementUpdate = (update_oplog_entry::extractUpdateType(updateDoc) == + update_oplog_entry::UpdateType::kReplacement); + if (args.nss == NamespaceString::kShardConfigCollectionsNamespace && !isReplacementUpdate) { // Notification of routing table changes are only needed on secondaries if (isStandaloneOrPrimary(opCtx)) { return; @@ -309,32 +314,27 @@ void ShardServerOpObserver::onUpdate(OperationContext* opCtx, const OplogUpdateE return NamespaceString(coll); }()); - // Parse the '$set' update - BSONElement setElement; - Status setStatus = - bsonExtractTypedField(args.updateArgs.update, StringData("$set"), Object, &setElement); - if (setStatus.isOK()) { - BSONObj setField = setElement.Obj(); + auto enterCriticalSectionFieldNewVal = update_oplog_entry::extractNewValueForField( + updateDoc, ShardCollectionType::kEnterCriticalSectionCounterFieldName); + auto refreshingFieldNewVal = update_oplog_entry::extractNewValueForField( + updateDoc, ShardCollectionType::kRefreshingFieldName); - // Need the WUOW to retain the lock for CollectionVersionLogOpHandler::commit() - AutoGetCollection autoColl(opCtx, updatedNss, MODE_IX); - - auto refreshingField = setField.getField(ShardCollectionType::kRefreshingFieldName); - if (refreshingField.isBoolean() && !refreshingField.boolean()) { - opCtx->recoveryUnit()->registerChange( - std::make_unique(opCtx, updatedNss)); - } + // Need the WUOW to retain the lock for CollectionVersionLogOpHandler::commit(). + AutoGetCollection autoColl(opCtx, updatedNss, MODE_IX); + if (refreshingFieldNewVal.isBoolean() && !refreshingFieldNewVal.boolean()) { + opCtx->recoveryUnit()->registerChange( + std::make_unique(opCtx, updatedNss)); + } - if (setField.hasField(ShardCollectionType::kEnterCriticalSectionCounterFieldName)) { - // Force subsequent uses of the namespace to refresh the filtering metadata so they - // can synchronize with any work happening on the primary (e.g., migration critical - // section). - CollectionShardingRuntime::get(opCtx, updatedNss)->clearFilteringMetadata(opCtx); - } + if (enterCriticalSectionFieldNewVal.ok()) { + // Force subsequent uses of the namespace to refresh the filtering metadata so they + // can synchronize with any work happening on the primary (e.g., migration critical + // section). + CollectionShardingRuntime::get(opCtx, updatedNss)->clearFilteringMetadata(opCtx); } } - if (args.nss == NamespaceString::kShardConfigDatabasesNamespace) { + if (args.nss == NamespaceString::kShardConfigDatabasesNamespace && !isReplacementUpdate) { // Notification of routing table changes are only needed on secondaries if (isStandaloneOrPrimary(opCtx)) { return; @@ -353,31 +353,25 @@ void ShardServerOpObserver::onUpdate(OperationContext* opCtx, const OplogUpdateE 40478, bsonExtractStringField(args.updateArgs.criteria, ShardDatabaseType::name.name(), &db)); - // Parse the '$set' update - BSONElement setElement; - Status setStatus = - bsonExtractTypedField(args.updateArgs.update, StringData("$set"), Object, &setElement); - if (setStatus.isOK()) { - BSONObj setField = setElement.Obj(); - - if (setField.hasField(ShardDatabaseType::enterCriticalSectionCounter.name())) { - AutoGetDb autoDb(opCtx, db, MODE_X); - auto dss = DatabaseShardingState::get(opCtx, db); - auto dssLock = DatabaseShardingState::DSSLock::lockExclusive(opCtx, dss); - dss->setDbVersion(opCtx, boost::none, dssLock); - } + auto enterCriticalSectionCounterFieldNewVal = update_oplog_entry::extractNewValueForField( + updateDoc, ShardDatabaseType::enterCriticalSectionCounter.name()); + + if (enterCriticalSectionCounterFieldNewVal.ok()) { + AutoGetDb autoDb(opCtx, db, MODE_X); + auto dss = DatabaseShardingState::get(opCtx, db); + auto dssLock = DatabaseShardingState::DSSLock::lockExclusive(opCtx, dss); + dss->setDbVersion(opCtx, boost::none, dssLock); } } - if (args.nss == NamespaceString::kRangeDeletionNamespace) { + if (args.nss == NamespaceString::kRangeDeletionNamespace && !isReplacementUpdate) { if (!isStandaloneOrPrimary(opCtx)) return; - BSONElement unsetElement; - Status status = bsonExtractTypedField( - args.updateArgs.update, StringData("$unset"), Object, &unsetElement); + const auto pendingFieldRemovedStatus = + update_oplog_entry::isFieldRemovedByUpdate(args.updateArgs.update, "pending"); - if (unsetElement.Obj().hasField("pending")) { + if (pendingFieldRemovedStatus == update_oplog_entry::FieldRemovedStatus::kFieldRemoved) { auto deletionTask = RangeDeletionTask::parse( IDLParserErrorContext("ShardServerOpObserver"), args.updateArgs.updatedDoc); diff --git a/src/mongo/db/update/SConscript b/src/mongo/db/update/SConscript index 7a9d1167d21..18782d34ebc 100644 --- a/src/mongo/db/update/SConscript +++ b/src/mongo/db/update/SConscript @@ -12,12 +12,14 @@ env.Library( 'storage_validation.cpp', 'v1_log_builder.cpp', 'v2_log_builder.cpp', + 'update_oplog_entry_serialization.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/bson/mutable/mutable_bson', '$BUILD_DIR/mongo/db/common', '$BUILD_DIR/mongo/db/matcher/expressions', + 'update_document_diff', ], ) @@ -49,7 +51,6 @@ env.Library( '$BUILD_DIR/mongo/db/update_index_data', '$BUILD_DIR/mongo/db/vector_clock_mutable', 'update_common', - 'update_document_diff', ], ) @@ -62,6 +63,7 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/db/pipeline/pipeline', + 'update_common', 'update_document_diff', 'update_nodes', ], diff --git a/src/mongo/db/update/update_oplog_entry_serialization.cpp b/src/mongo/db/update/update_oplog_entry_serialization.cpp new file mode 100644 index 00000000000..56db472570b --- /dev/null +++ b/src/mongo/db/update/update_oplog_entry_serialization.cpp @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side 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/db/update/update_oplog_entry_serialization.h" + +#include "mongo/db/update/document_diff_serialization.h" +#include "mongo/db/update/update_oplog_entry_version.h" + +namespace mongo::update_oplog_entry { +BSONObj makeDeltaOplogEntry(const doc_diff::Diff& diff) { + BSONObjBuilder builder; + builder.append("$v", static_cast(UpdateOplogEntryVersion::kDeltaV2)); + builder.append(kDiffObjectFieldName, diff); + return builder.obj(); +} + +namespace { +BSONElement extractNewValueForFieldFromV1Entry(const BSONObj& oField, StringData fieldName) { + // Check the '$set' section. + auto setElt = oField["$set"]; + if (setElt.ok()) { + // The $set field in a $v:1 entry should always be an object. + invariant(setElt.type() == BSONType::Object); + return setElt.embeddedObject()[fieldName]; + } + + // The field is either in the $unset section, or was not modified at all. + return BSONElement(); +} + +BSONElement extractNewValueForFieldFromV2Entry(const BSONObj& oField, StringData fieldName) { + auto diffField = oField[kDiffObjectFieldName]; + + // Every $v:2 oplog entry should have a 'diff' field that is an object. + invariant(diffField.type() == BSONType::Object); + doc_diff::DocumentDiffReader reader(diffField.embeddedObject()); + + boost::optional nextMod; + while ((nextMod = reader.nextUpdate()) || (nextMod = reader.nextInsert())) { + if (nextMod->fieldNameStringData() == fieldName) { + return *nextMod; + } + } + + // The field may appear in the "delete" section or not at all. + return BSONElement(); +} + +FieldRemovedStatus isFieldRemovedByV1Update(const BSONObj& oField, StringData fieldName) { + auto unsetElt = oField["$unset"]; + if (unsetElt.ok()) { + invariant(unsetElt.type() == BSONType::Object); + if (unsetElt.embeddedObject()[fieldName].ok()) { + return FieldRemovedStatus::kFieldRemoved; + } + } + return FieldRemovedStatus::kFieldNotRemoved; +} + +FieldRemovedStatus isFieldRemovedByV2Update(const BSONObj& oField, StringData fieldName) { + auto diffField = oField[kDiffObjectFieldName]; + + // Every $v:2 oplog entry should have a 'diff' field that is an object. + invariant(diffField.type() == BSONType::Object); + doc_diff::DocumentDiffReader reader(diffField.embeddedObject()); + + boost::optional nextDelete; + while ((nextDelete = reader.nextDelete())) { + if (*nextDelete == fieldName) { + return FieldRemovedStatus::kFieldRemoved; + } + } + return FieldRemovedStatus::kFieldNotRemoved; +} +} // namespace + +UpdateType extractUpdateType(const BSONObj& updateDocument) { + // Use the "$v" field to determine which type of update this is. Note $v:1 updates were allowed + // to omit the $v field so that case must be handled carefully. + auto vElt = updateDocument[kUpdateOplogEntryVersionFieldName]; + + if (!vElt.ok()) { + // We're dealing with a $v:1 entry if the first field name starts with a '$'. Otherwise + // it's a replacement update, which does not have a specific version name. + if (updateDocument.firstElementFieldNameStringData().startsWith("$")) { + return UpdateType::kV1Modifier; + } + + // The version field is missing and the first field is not prefixed by "$". This means we + // have a replacement update. + return UpdateType::kReplacement; + } else if (vElt.numberInt() == static_cast(UpdateOplogEntryVersion::kUpdateNodeV1)) { + return UpdateType::kV1Modifier; + } else if (vElt.numberInt() == static_cast(UpdateOplogEntryVersion::kDeltaV2)) { + return UpdateType::kV2Delta; + } + + // Unrecognized oplog entry version. + MONGO_UNREACHABLE; +} + +BSONElement extractNewValueForField(const BSONObj& oField, StringData fieldName) { + invariant(fieldName.find('.') == std::string::npos, "field name cannot contain dots"); + + auto type = extractUpdateType(oField); + + if (type == UpdateType::kV1Modifier) { + return extractNewValueForFieldFromV1Entry(oField, fieldName); + } else if (type == UpdateType::kV2Delta) { + return extractNewValueForFieldFromV2Entry(oField, fieldName); + } else if (type == UpdateType::kReplacement) { + return oField[fieldName]; + } + + // Clearly an unsupported format. + MONGO_UNREACHABLE; +} + +FieldRemovedStatus isFieldRemovedByUpdate(const BSONObj& oField, StringData fieldName) { + invariant(fieldName.find('.') == std::string::npos, "field name cannot contain dots"); + + auto type = extractUpdateType(oField); + + if (type == UpdateType::kV1Modifier) { + return isFieldRemovedByV1Update(oField, fieldName); + } else if (type == UpdateType::kV2Delta) { + return isFieldRemovedByV2Update(oField, fieldName); + } else if (type == UpdateType::kReplacement) { + // The field was definitely *not* removed if it's still in the post image. Otherwise, + // we cannot tell if it was removed without looking at the pre image. + return oField.hasField(fieldName) ? FieldRemovedStatus::kFieldNotRemoved + : FieldRemovedStatus::kUnknown; + } + MONGO_UNREACHABLE; +} +} // namespace mongo::update_oplog_entry diff --git a/src/mongo/db/update/update_oplog_entry_serialization.h b/src/mongo/db/update/update_oplog_entry_serialization.h index 040523b35da..8d95193f40f 100644 --- a/src/mongo/db/update/update_oplog_entry_serialization.h +++ b/src/mongo/db/update/update_oplog_entry_serialization.h @@ -41,15 +41,30 @@ static inline constexpr StringData kDiffObjectFieldName = "diff"_sd; constexpr size_t kSizeOfDeltaOplogEntryMetadata = 15; +/** + * Represents the "type" of an update oplog entry. For historical reasons these do not always + * correspond to the value in the $v field of the entry. To determine the type of an update oplog + * entry use extractUpdateType(). + * + * Pipeline updates are not represented here because they are always logged with replacement + * entries or $v:2 delta entries. + */ +enum class UpdateType { + kReplacement, + kV1Modifier, + kV2Delta, +}; + +/** + * Used for indicating whether a field was removed or not. 'kUnknown' represents the case where it + * cannot be determined whether a field was removed by just examining the update. + */ +enum class FieldRemovedStatus { kFieldRemoved, kFieldNotRemoved, kUnknown }; + /** * Given a diff, produce the contents for the 'o' field of a $v: 2 delta-style oplog entry. */ -inline BSONObj makeDeltaOplogEntry(const doc_diff::Diff& diff) { - BSONObjBuilder builder; - builder.append("$v", static_cast(UpdateOplogEntryVersion::kDeltaV2)); - builder.append(kDiffObjectFieldName, diff); - return builder.obj(); -} +BSONObj makeDeltaOplogEntry(const doc_diff::Diff& diff); /** * Produce the contents of the 'o' field of a replacement style oplog entry. @@ -57,4 +72,42 @@ inline BSONObj makeDeltaOplogEntry(const doc_diff::Diff& diff) { inline BSONObj makeReplacementOplogEntry(const BSONObj& replacement) { return replacement; } + +/** + * Given the 'o' field of an update oplog entry, determine its type. If the type cannot be + * determined, boost::none is returned. + */ +UpdateType extractUpdateType(const BSONObj& oField); + +/** + * Given the 'o' field of an update oplog entry, this function will attempt to recover the new + * value for the top-level field provided in 'fieldName'. Will return: + * + * -An EOO BSONElement if the field was deleted as part of the update or if the field's new value + * cannot be recovered from the update object. The latter case can happen when a field is not + * modified by the update at all, or when the field is an object and one of its subfields is + * modified. + * -A BSONElement with field's new value if it was added or set to a new value as part of the + * update. + * + * 'fieldName' *MUST* be a top-level field. It may not contain dots. + * + * It is a programming error to call this function with a value for 'updateObj' that is not a valid + * update. + */ +BSONElement extractNewValueForField(const BSONObj& updateObj, StringData fieldName); + +/** + * Given the 'o' field of an update oplog entry document, this function will determine whether the + * given field was deleted by the update. 'fieldName' must be a top-level field, and may not + * include any dots. + * + * If this function is passed a replacement style update it may return 'kUnknown' because it is + * impossible to determine whether a field was deleted during a replacement update without + * examining the pre-image. + * + * It is a programming error to call this function with a value for 'updateObj' that is not a valid + * update. + */ +FieldRemovedStatus isFieldRemovedByUpdate(const BSONObj& updateObj, StringData fieldName); } // namespace mongo::update_oplog_entry diff --git a/src/mongo/db/update/update_oplog_entry_serialization_test.cpp b/src/mongo/db/update/update_oplog_entry_serialization_test.cpp new file mode 100644 index 00000000000..5dfc3293cde --- /dev/null +++ b/src/mongo/db/update/update_oplog_entry_serialization_test.cpp @@ -0,0 +1,159 @@ +/** + * Copyright (C) 2020-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * 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 + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side 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. + */ + +#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest + +#include "mongo/platform/basic.h" + +#include "mongo/bson/json.h" +#include "mongo/db/update/update_oplog_entry_serialization.h" +#include "mongo/unittest/death_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo::update_oplog_entry { +namespace { + +TEST(UpdateOplogSerializationTest, ReadV1EntryWithVersionField) { + auto setField = BSON("a" << 1 << "b" << 2); + BSONObj o(BSON("$v" << 1 << "$set" << setField)); + + ASSERT_BSONELT_EQ(extractNewValueForField(o, "a"), setField["a"]); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "b"), setField["b"]); + + ASSERT(isFieldRemovedByUpdate(o, "a") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "z") == FieldRemovedStatus::kFieldNotRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadV1EntryWithoutVersionField) { + auto setField = BSON("a" << 1 << "b" << 2); + BSONObj o(BSON("$set" << setField)); + + ASSERT_BSONELT_EQ(extractNewValueForField(o, "a"), setField["a"]); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "b"), setField["b"]); + + ASSERT(isFieldRemovedByUpdate(o, "a") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "z") == FieldRemovedStatus::kFieldNotRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadV1EntryWithSetAndUnset) { + auto setField = BSON("a" << 1 << "b" << 2); + auto unsetField = BSON("c" << true << "d" << true); + BSONObj o(BSON("$set" << setField << "$unset" << unsetField)); + + ASSERT_BSONELT_EQ(extractNewValueForField(o, "a"), setField["a"]); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "b"), setField["b"]); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "c"), BSONElement()); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "d"), BSONElement()); + + ASSERT(isFieldRemovedByUpdate(o, "a") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "b") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "c") == FieldRemovedStatus::kFieldRemoved); + ASSERT(isFieldRemovedByUpdate(o, "d") == FieldRemovedStatus::kFieldRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadV1EntryWhichIncludesDottedPath) { + // While our function for getting modified fields only supports top-level fields, + // there should be no problem if the oplog entry contains modifications to + // dotted paths. + + auto setField = BSON("a.b.c" << 1 << "x" << 2); + auto unsetField = BSON("d.e.f" << true << "y" << true); + BSONObj o(BSON("$set" << setField << "$unset" << unsetField)); + + ASSERT_BSONELT_EQ(extractNewValueForField(o, "x"), setField["x"]); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "y"), BSONElement()); + + ASSERT(isFieldRemovedByUpdate(o, "x") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "y") == FieldRemovedStatus::kFieldRemoved); + ASSERT(isFieldRemovedByUpdate(o, "z") == FieldRemovedStatus::kFieldNotRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadV2Entry) { + auto v2Entry = fromjson( + "{d: {deletedField1: false, deletedField2: false}, u: {updatedField: 'foo'}, i: " + "{insertedField: 'bar'}}"); + BSONObj o(makeDeltaOplogEntry(v2Entry)); + + ASSERT_BSONELT_EQ(extractNewValueForField(o, "deletedField1"), BSONElement()); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "deletedField2"), BSONElement()); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "updatedField"), v2Entry["u"]["updatedField"]); + ASSERT_BSONELT_EQ(extractNewValueForField(o, "insertedField"), v2Entry["i"]["insertedField"]); + + ASSERT(isFieldRemovedByUpdate(o, "deletedField1") == FieldRemovedStatus::kFieldRemoved); + ASSERT(isFieldRemovedByUpdate(o, "deletedField2") == FieldRemovedStatus::kFieldRemoved); + ASSERT(isFieldRemovedByUpdate(o, "updatedField") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "insertedField") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "nonexistentField") == FieldRemovedStatus::kFieldNotRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadV1EntryWithSubfieldModified) { + auto setField = BSON("a.b" << 1 << "x" << 2); + BSONObj o(BSON("$set" << setField)); + + // We cannot recover the entire new value for field 'a' so an EOO element is returned. + ASSERT_BSONELT_EQ(extractNewValueForField(o, "a"), BSONElement()); + ASSERT(isFieldRemovedByUpdate(o, "a") == FieldRemovedStatus::kFieldNotRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadV2EntryWithSubfieldModified) { + auto v2Entry = fromjson("{sa: {i: {b: 1}}}"); + BSONObj o(makeDeltaOplogEntry(v2Entry)); + + // We cannot recover the entire new value for field 'a' so an EOO element is returned. + ASSERT_BSONELT_EQ(extractNewValueForField(o, "a"), BSONElement()); + ASSERT(isFieldRemovedByUpdate(o, "a") == FieldRemovedStatus::kFieldNotRemoved); +} + +TEST(UpdateOplogSerializationTest, ReadReplacementEntry) { + BSONObj o(BSON("foo" << 1 << "bar" << 2)); + + ASSERT_EQ(extractNewValueForField(o, "foo"), o["foo"]); + ASSERT_EQ(extractNewValueForField(o, "bar"), o["bar"]); + + ASSERT(isFieldRemovedByUpdate(o, "bar") == FieldRemovedStatus::kFieldNotRemoved); + ASSERT(isFieldRemovedByUpdate(o, "baz") == FieldRemovedStatus::kUnknown); +} + +DEATH_TEST(UpdateOplogSerializationTest, CannotExtractDottedField, "cannot contain dots") { + extractNewValueForField(BSONObj(), "a.b"); +} + +DEATH_TEST(UpdateOplogSerializationTest, CannotReadDottedField, "cannot contain dots") { + isFieldRemovedByUpdate(BSONObj(), "a.b"); +} + +DEATH_TEST(UpdateOplogSerializationTest, CannotExtractFromNonExistentVersion, "Unrecognized") { + extractNewValueForField(BSON("$v" << 10), "a"); +} + +DEATH_TEST(UpdateOplogSerializationTest, CannotReadNonExistentVersion, "Unrecognized") { + isFieldRemovedByUpdate(BSON("$v" << 10), "a"); +} +} // namespace +} // namespace mongo::update_oplog_entry -- cgit v1.2.1