summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Boros <ian.boros@mongodb.com>2020-08-17 15:02:53 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-09-09 03:06:57 +0000
commitcbb82fd1b270f84e544243acbba2cb3fed779c28 (patch)
treeafe0fc762f61d49a67551e8b5a1e990965f9e5de
parent58fd67fc2232a4ca591ff66443fd22213d4b5cac (diff)
downloadmongo-cbb82fd1b270f84e544243acbba2cb3fed779c28.tar.gz
SERVER-50218 Change shard server and config server op observers to support $v:2 update oplog entries
-rw-r--r--src/mongo/db/s/config_server_op_observer.cpp17
-rw-r--r--src/mongo/db/s/shard_server_op_observer.cpp74
-rw-r--r--src/mongo/db/update/SConscript4
-rw-r--r--src/mongo/db/update/update_oplog_entry_serialization.cpp162
-rw-r--r--src/mongo/db/update/update_oplog_entry_serialization.h65
-rw-r--r--src/mongo/db/update/update_oplog_entry_serialization_test.cpp159
6 files changed, 428 insertions, 53 deletions
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<CollectionVersionLogOpHandler>(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<CollectionVersionLogOpHandler>(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
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * 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<int>(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<BSONElement> 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<StringData> 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<int>(UpdateOplogEntryVersion::kUpdateNodeV1)) {
+ return UpdateType::kV1Modifier;
+ } else if (vElt.numberInt() == static_cast<int>(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
@@ -42,14 +42,29 @@ 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<int>(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
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * 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