summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTess Avitabile <tess.avitabile@mongodb.com>2017-11-29 19:16:50 -0500
committerTess Avitabile <tess.avitabile@mongodb.com>2017-12-13 12:00:09 -0500
commit752daa306095fb1610bb5db13b7b106ac87ec6cb (patch)
treefcaf49589ec24b151d47d942b26e971c3d792c47
parent53adeef7dbf5170a94f3e6d25425e8524fb9d062 (diff)
downloadmongo-752daa306095fb1610bb5db13b7b106ac87ec6cb.tar.gz
SERVER-32048 Ensure updates that implicitly create an array element generate new null index keys
(cherry picked from commit 69f94d7172801725bad43dba99ca0e5400eb293c) (cherry picked from commit 80b311e0a8537a47eb723ed4b6c010b5d1006551) (cherry picked from commit 5e58d9f0b6e6b42a0c9f4817e88ee1bfb7527d81)
-rw-r--r--jstests/core/update_affects_indexes.js102
-rw-r--r--src/mongo/db/ops/modifier_add_to_set.cpp8
-rw-r--r--src/mongo/db/ops/modifier_add_to_set_test.cpp36
-rw-r--r--src/mongo/db/ops/modifier_bit.cpp8
-rw-r--r--src/mongo/db/ops/modifier_bit_test.cpp37
-rw-r--r--src/mongo/db/ops/modifier_compare.cpp7
-rw-r--r--src/mongo/db/ops/modifier_compare_test.cpp36
-rw-r--r--src/mongo/db/ops/modifier_current_date.cpp10
-rw-r--r--src/mongo/db/ops/modifier_current_date_test.cpp37
-rw-r--r--src/mongo/db/ops/modifier_inc.cpp7
-rw-r--r--src/mongo/db/ops/modifier_inc_test.cpp37
-rw-r--r--src/mongo/db/ops/modifier_interface.h6
-rw-r--r--src/mongo/db/ops/modifier_push.cpp9
-rw-r--r--src/mongo/db/ops/modifier_push_test.cpp83
-rw-r--r--src/mongo/db/ops/modifier_set.cpp6
-rw-r--r--src/mongo/db/ops/modifier_set_test.cpp37
-rw-r--r--src/mongo/db/ops/update_driver.cpp20
17 files changed, 481 insertions, 5 deletions
diff --git a/jstests/core/update_affects_indexes.js b/jstests/core/update_affects_indexes.js
new file mode 100644
index 00000000000..728ec6785a8
--- /dev/null
+++ b/jstests/core/update_affects_indexes.js
@@ -0,0 +1,102 @@
+// This is a regression test for SERVER-32048. It checks that index keys are correctly updated when
+// an update modifier implicitly creates a new array element.
+(function() {
+ "use strict";
+
+ var coll = db.update_affects_indexes;
+ coll.drop();
+ var indexKeyPattern = {
+ "a.b": 1
+ };
+ assert.commandWorked(coll.createIndex(indexKeyPattern));
+
+ // Tests that the document 'docId' has all the index keys in 'expectedKeys' and none of the
+ // index keys in 'unexpectedKeys'.
+ function assertExpectedIndexKeys(docId, expectedKeys, unexpectedKeys) {
+ for (var key of expectedKeys) {
+ var res = coll.find(docId).hint(indexKeyPattern).min(key).returnKey().toArray();
+ assert.eq(1, res.length, tojson(res));
+ assert.eq(key, res[0]);
+ }
+
+ for (var key of unexpectedKeys) {
+ var res = coll.find(docId).hint(indexKeyPattern).min(key).returnKey().toArray();
+ if (res.length > 0) {
+ assert.eq(1, res.length, tojson(res));
+ assert.neq(key, res[0]);
+ }
+ }
+ }
+
+ // $set implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 0, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 0}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 0}, {$set: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 0}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $set implicitly creates array element beyond end of array.
+ assert.writeOK(coll.insert({_id: 1, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 1}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 1}, {$set: {"a.3.c": 0}}));
+ assertExpectedIndexKeys({_id: 1}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $set implicitly creates array element in empty array (no index key changes needed).
+ assert.writeOK(coll.insert({_id: 2, a: []}));
+ assertExpectedIndexKeys({_id: 2}, [{"a.b": null}], []);
+ assert.writeOK(coll.update({_id: 2}, {$set: {"a.0.c": 0}}));
+ assertExpectedIndexKeys({_id: 2}, [{"a.b": null}], []);
+
+ // $inc implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 3, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 3}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 3}, {$inc: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 3}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $mul implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 4, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 4}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 4}, {$mul: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 4}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $addToSet implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 5, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 5}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 5}, {$addToSet: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 5}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $bit implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 6, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 6}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 6}, {$bit: {"a.1.c": {and: NumberInt(1)}}}));
+ assertExpectedIndexKeys({_id: 6}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $min implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 7, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 7}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 7}, {$min: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 7}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $max implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 8, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 8}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 8}, {$max: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 8}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $currentDate implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 9, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 9}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 9}, {$currentDate: {"a.1.c": true}}));
+ assertExpectedIndexKeys({_id: 9}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $push implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 10, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 10}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 10}, {$push: {"a.1.c": 0}}));
+ assertExpectedIndexKeys({_id: 10}, [{"a.b": 0}, {"a.b": null}], []);
+
+ // $pushAll implicitly creates array element at end of array.
+ assert.writeOK(coll.insert({_id: 11, a: [{b: 0}]}));
+ assertExpectedIndexKeys({_id: 11}, [{"a.b": 0}], [{"a.b": null}]);
+ assert.writeOK(coll.update({_id: 11}, {$pushAll: {"a.1.c": [0]}}));
+ assertExpectedIndexKeys({_id: 11}, [{"a.b": 0}, {"a.b": null}], []);
+}());
diff --git a/src/mongo/db/ops/modifier_add_to_set.cpp b/src/mongo/db/ops/modifier_add_to_set.cpp
index 383991c2b1f..44a5997f916 100644
--- a/src/mongo/db/ops/modifier_add_to_set.cpp
+++ b/src/mongo/db/ops/modifier_add_to_set.cpp
@@ -217,6 +217,8 @@ Status ModifierAddToSet::prepare(mb::Element root, StringData matchedField, Exec
// Locate the field name in 'root'.
Status status = pathsupport::findLongestPrefix(
_fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
// that the path was not viable or otherwise wrong, in which case, the mod cannot
@@ -240,6 +242,12 @@ Status ModifierAddToSet::prepare(mb::Element root, StringData matchedField, Exec
if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) {
// If no target element exists, we will simply be creating a new array.
_preparedState->addAll = true;
+
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
+
return Status::OK();
}
diff --git a/src/mongo/db/ops/modifier_add_to_set_test.cpp b/src/mongo/db/ops/modifier_add_to_set_test.cpp
index 698fb0c411f..3f1ed9057ec 100644
--- a/src/mongo/db/ops/modifier_add_to_set_test.cpp
+++ b/src/mongo/db/ops/modifier_add_to_set_test.cpp
@@ -387,4 +387,40 @@ TEST(Regressions, SERVER_12848) {
ASSERT_EQUALS(fromjson("{ $set : { 'a.1' : [ 1 ] } }"), logDoc);
}
+TEST(IndexedMod, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$addToSet: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$addToSet: {'a.0.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod mod(fromjson("{$addToSet: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
} // namespace
diff --git a/src/mongo/db/ops/modifier_bit.cpp b/src/mongo/db/ops/modifier_bit.cpp
index 8fe8f5e42d2..c557e02e139 100644
--- a/src/mongo/db/ops/modifier_bit.cpp
+++ b/src/mongo/db/ops/modifier_bit.cpp
@@ -159,6 +159,8 @@ Status ModifierBit::prepare(mutablebson::Element root,
// Locate the field name in 'root'.
Status status = pathsupport::findLongestPrefix(
_fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
@@ -184,6 +186,12 @@ Status ModifierBit::prepare(mutablebson::Element root,
// If no target element exists, the value we will write is the result of applying
// the operation to a zero-initialized integer element.
_preparedState->newValue = apply(SafeNum(static_cast<int>(0)));
+
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
+
return Status::OK();
}
diff --git a/src/mongo/db/ops/modifier_bit_test.cpp b/src/mongo/db/ops/modifier_bit_test.cpp
index ba171278b50..4acaea01259 100644
--- a/src/mongo/db/ops/modifier_bit_test.cpp
+++ b/src/mongo/db/ops/modifier_bit_test.cpp
@@ -744,4 +744,41 @@ TEST(DbUpdateTests, Bit1_4_Combined) {
ASSERT_EQUALS(BSON("$set" << BSON("x" << ((3 | 2) & 8))), logDoc);
}
+TEST(IndexedMod, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$bit: {'a.1.c': {and: NumberInt(1)}}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: NumberInt(0)}]}"));
+ Mod mod(fromjson("{$bit: {'a.0.c': {or: NumberInt(1)}}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod mod(fromjson("{$bit: {'a.1.c': {and: NumberInt(1)}}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
} // namespace
diff --git a/src/mongo/db/ops/modifier_compare.cpp b/src/mongo/db/ops/modifier_compare.cpp
index 36f800202e4..8873bec032d 100644
--- a/src/mongo/db/ops/modifier_compare.cpp
+++ b/src/mongo/db/ops/modifier_compare.cpp
@@ -107,6 +107,8 @@ Status ModifierCompare::prepare(mutablebson::Element root,
// be created during the apply.
Status status = pathsupport::findLongestPrefix(
_updatePath, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
// that the path was not viable or otherwise wrong, in which case, the mod cannot
@@ -125,6 +127,11 @@ Status ModifierCompare::prepare(mutablebson::Element root,
_preparedState->idxFound == (_updatePath.numParts() - 1));
if (!destExists) {
execInfo->noOp = false;
+
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
} else {
const int compareVal = _preparedState->elemFound.compareWithBSONElement(_val, false);
execInfo->noOp = (compareVal == 0) ||
diff --git a/src/mongo/db/ops/modifier_compare_test.cpp b/src/mongo/db/ops/modifier_compare_test.cpp
index 238c19b905a..2870849afd5 100644
--- a/src/mongo/db/ops/modifier_compare_test.cpp
+++ b/src/mongo/db/ops/modifier_compare_test.cpp
@@ -292,4 +292,40 @@ TEST(ExistingEmbeddedDoc, MaxNumber) {
ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField());
}
+TEST(IndexedMod, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$min: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$min: {'a.0.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod mod(fromjson("{$min: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
} // namespace
diff --git a/src/mongo/db/ops/modifier_current_date.cpp b/src/mongo/db/ops/modifier_current_date.cpp
index 75d0be014e3..4661d0f9670 100644
--- a/src/mongo/db/ops/modifier_current_date.cpp
+++ b/src/mongo/db/ops/modifier_current_date.cpp
@@ -159,6 +159,8 @@ Status ModifierCurrentDate::prepare(mutablebson::Element root,
// be created during the apply.
Status status = pathsupport::findLongestPrefix(
_updatePath, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
// that the path was not viable or otherwise wrong, in which case, the mod cannot
@@ -173,6 +175,14 @@ Status ModifierCurrentDate::prepare(mutablebson::Element root,
// there is any conflict among mods.
execInfo->fieldRef[0] = &_updatePath;
+ if (!_preparedState->elemFound.ok() ||
+ _preparedState->idxFound < (_updatePath.numParts() - 1)) {
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
+ }
+
return Status::OK();
}
diff --git a/src/mongo/db/ops/modifier_current_date_test.cpp b/src/mongo/db/ops/modifier_current_date_test.cpp
index 126b983adbc..b5d608b12e0 100644
--- a/src/mongo/db/ops/modifier_current_date_test.cpp
+++ b/src/mongo/db/ops/modifier_current_date_test.cpp
@@ -359,4 +359,41 @@ TEST(DottedTimestampInput, EmptyStartDoc) {
validateOplogEntry(oplogFormat, logDoc);
}
+TEST(BoolInput, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$currentDate: {'a.1.c': true}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(BoolInput, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$currentDate: {'a.0.c': true}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(BoolInput, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod mod(fromjson("{$currentDate: {'a.1.c': true}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
} // namespace
diff --git a/src/mongo/db/ops/modifier_inc.cpp b/src/mongo/db/ops/modifier_inc.cpp
index b683a1f9561..a1632ff4776 100644
--- a/src/mongo/db/ops/modifier_inc.cpp
+++ b/src/mongo/db/ops/modifier_inc.cpp
@@ -132,6 +132,8 @@ Status ModifierInc::prepare(mutablebson::Element root,
// be created during the apply.
Status status = pathsupport::findLongestPrefix(
_fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
// that the path was not viable or otherwise wrong, in which case, the mod cannot
@@ -163,6 +165,11 @@ Status ModifierInc::prepare(mutablebson::Element root,
if (_mode == MODE_MUL)
_preparedState->newValue *= SafeNum(static_cast<int>(0));
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
+
return Status::OK();
}
diff --git a/src/mongo/db/ops/modifier_inc_test.cpp b/src/mongo/db/ops/modifier_inc_test.cpp
index 52c829f7842..a86e6620243 100644
--- a/src/mongo/db/ops/modifier_inc_test.cpp
+++ b/src/mongo/db/ops/modifier_inc_test.cpp
@@ -642,4 +642,41 @@ TEST(Multiplication, ApplyMissingElementDouble) {
ASSERT_EQUALS(mongo::NumberDouble, doc.root().rightChild().getType());
}
+TEST(IndexedMod, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$inc: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$inc: {'a.0.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod mod(fromjson("{$inc: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
} // namespace
diff --git a/src/mongo/db/ops/modifier_interface.h b/src/mongo/db/ops/modifier_interface.h
index eba8b6c324d..2e114eefcb4 100644
--- a/src/mongo/db/ops/modifier_interface.h
+++ b/src/mongo/db/ops/modifier_interface.h
@@ -190,11 +190,17 @@ struct ModifierInterface::ExecInfo {
ExecInfo() : noOp(false), context(ANY_CONTEXT) {
for (int i = 0; i < MAX_NUM_FIELDS; i++) {
fieldRef[i] = NULL;
+ indexOfArrayWithNewElement[i] = boost::none;
}
}
// The fields of concern to the driver: no other op may modify the fields listed here.
FieldRef* fieldRef[MAX_NUM_FIELDS]; // not owned here
+
+ // For each modified field ref, the index of the path component representing an existing array
+ // that gained a new element.
+ boost::optional<size_t> indexOfArrayWithNewElement[MAX_NUM_FIELDS];
+
bool noOp;
UpdateContext context;
};
diff --git a/src/mongo/db/ops/modifier_push.cpp b/src/mongo/db/ops/modifier_push.cpp
index 636a9f4450a..5dc0f32c7aa 100644
--- a/src/mongo/db/ops/modifier_push.cpp
+++ b/src/mongo/db/ops/modifier_push.cpp
@@ -421,6 +421,8 @@ Status ModifierPush::prepare(mutablebson::Element root,
// be created during the apply.
Status status = pathsupport::findLongestPrefix(
_fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
// that the path was not viable or otherwise wrong, in which case, the mod cannot
@@ -448,6 +450,13 @@ Status ModifierPush::prepare(mutablebson::Element root,
// there is any conflict among mods.
execInfo->fieldRef[0] = &_fieldRef;
+ if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) {
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
+ }
+
return Status::OK();
}
diff --git a/src/mongo/db/ops/modifier_push_test.cpp b/src/mongo/db/ops/modifier_push_test.cpp
index 43a18a44ef9..e3370d964ff 100644
--- a/src/mongo/db/ops/modifier_push_test.cpp
+++ b/src/mongo/db/ops/modifier_push_test.cpp
@@ -1437,4 +1437,87 @@ TEST(ToPosition, Back) {
ASSERT_EQUALS(fromjson("{$set: {'a.1':1}}"), logDoc);
}
+TEST(IndexedMod, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$push: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareReportCreatedArrayElementPushAll) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ auto modObj = fromjson("{$pushAll: {'a.1.c': [2]}}");
+ ModifierPush mod(ModifierPush::PUSH_ALL);
+ ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(),
+ ModifierInterface::Options::normal()));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod mod(fromjson("{$push: {'a.0.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElementPushAll) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ auto modObj = fromjson("{$pushAll: {'a.0.c': [2]}}");
+ ModifierPush mod(ModifierPush::PUSH_ALL);
+ ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(),
+ ModifierInterface::Options::normal()));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod mod(fromjson("{$push: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectFieldPushAll) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ auto modObj = fromjson("{$pushAll: {'a.1.c': [2]}}");
+ ModifierPush mod(ModifierPush::PUSH_ALL);
+ ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(),
+ ModifierInterface::Options::normal()));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(mod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
} // unnamed namespace
diff --git a/src/mongo/db/ops/modifier_set.cpp b/src/mongo/db/ops/modifier_set.cpp
index c6966fae079..a4f73982fa8 100644
--- a/src/mongo/db/ops/modifier_set.cpp
+++ b/src/mongo/db/ops/modifier_set.cpp
@@ -126,6 +126,8 @@ Status ModifierSet::prepare(mutablebson::Element root,
// be created during the apply.
Status status = pathsupport::findLongestPrefix(
_fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound);
+ const auto elemFoundIsArray =
+ _preparedState->elemFound.ok() && _preparedState->elemFound.getType() == BSONType::Array;
// FindLongestPrefix may say the path does not exist at all, which is fine here, or
// that the path was not viable or otherwise wrong, in which case, the mod cannot
@@ -154,6 +156,10 @@ Status ModifierSet::prepare(mutablebson::Element root,
// If the field path is not fully present, then this mod cannot be in place, nor is it a noOp.
if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) {
+ if (elemFoundIsArray) {
+ // Report that an existing array will gain a new element as a result of this mod.
+ execInfo->indexOfArrayWithNewElement[0] = _preparedState->idxFound;
+ }
return Status::OK();
}
diff --git a/src/mongo/db/ops/modifier_set_test.cpp b/src/mongo/db/ops/modifier_set_test.cpp
index 1579b096722..b83270da957 100644
--- a/src/mongo/db/ops/modifier_set_test.cpp
+++ b/src/mongo/db/ops/modifier_set_test.cpp
@@ -429,6 +429,43 @@ TEST(IndexedMod, PrepareNonViablePath) {
ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo));
}
+TEST(IndexedMod, PrepareReportCreatedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod setMod(fromjson("{$set: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_TRUE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_EQUALS(*execInfo.indexOfArrayWithNewElement[0], 0u);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportModifiedArrayElement) {
+ Document doc(fromjson("{a: [{b: 0}]}"));
+ Mod setMod(fromjson("{$set: {'a.0.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
+TEST(IndexedMod, PrepareDoNotReportCreatedNumericObjectField) {
+ Document doc(fromjson("{a: {'0': {b: 0}}}"));
+ Mod setMod(fromjson("{$set: {'a.1.c': 2}}"));
+
+ ModifierInterface::ExecInfo execInfo;
+ ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo));
+
+ ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.c");
+ ASSERT_FALSE(execInfo.indexOfArrayWithNewElement[0]);
+ ASSERT_FALSE(execInfo.noOp);
+}
+
TEST(IndexedMod, PrepareApplyInPlace) {
Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 1}]}"));
Mod setMod(fromjson("{$set: {'a.2.b': 2}}"));
diff --git a/src/mongo/db/ops/update_driver.cpp b/src/mongo/db/ops/update_driver.cpp
index a99376174a8..94292e24874 100644
--- a/src/mongo/db/ops/update_driver.cpp
+++ b/src/mongo/db/ops/update_driver.cpp
@@ -288,11 +288,21 @@ Status UpdateDriver::update(StringData matchedField,
// is not a no-op and it is in a valid context -- then we switch back to a
// non-in-place mode.
//
- // TODO: make mightBeIndexed and fieldRef like each other.
- if (!_affectIndices && !execInfo.noOp && _indexedFields &&
- _indexedFields->mightBeIndexed(execInfo.fieldRef[i]->dottedField())) {
- _affectIndices = true;
- doc->disableInPlaceUpdates();
+ // To determine if indexes are affected: If we did not create a new element in an array,
+ // check whether the full path affects indexes. If we did create a new element in an
+ // array, check whether the array itself might affect any indexes. This is necessary
+ // because if there is an index {"a.b": 1}, and we set "a.1.c" and implicitly create an
+ // array element in "a", then we may need to add a null key to the index {"a.b": 1},
+ // even though "a.1.c" does not appear to affect the index.
+ if (!_affectIndices && !execInfo.noOp && _indexedFields) {
+ auto pathLengthForIndexCheck = execInfo.indexOfArrayWithNewElement[i]
+ ? *execInfo.indexOfArrayWithNewElement[i] + 1
+ : execInfo.fieldRef[i]->numParts();
+ if (_indexedFields->mightBeIndexed(
+ execInfo.fieldRef[i]->dottedSubstring(0, pathLengthForIndexCheck))) {
+ _affectIndices = true;
+ doc->disableInPlaceUpdates();
+ }
}
}