summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/aggregation/expressions/merge_objects.js160
-rw-r--r--src/mongo/db/pipeline/SConscript1
-rw-r--r--src/mongo/db/pipeline/accumulator.h34
-rw-r--r--src/mongo/db/pipeline/accumulator_add_to_set.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_avg.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_first.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_last.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_merge_objects.cpp93
-rw-r--r--src/mongo/db/pipeline/accumulator_min_max.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_push.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_std_dev.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_sum.cpp2
-rw-r--r--src/mongo/db/pipeline/accumulator_test.cpp58
-rw-r--r--src/mongo/db/pipeline/document.h4
-rw-r--r--src/mongo/db/pipeline/expression_test.cpp104
-rw-r--r--src/mongo/db/pipeline/value.h10
-rw-r--r--src/mongo/unittest/unittest.h8
17 files changed, 458 insertions, 30 deletions
diff --git a/jstests/aggregation/expressions/merge_objects.js b/jstests/aggregation/expressions/merge_objects.js
new file mode 100644
index 00000000000..599e182ff5a
--- /dev/null
+++ b/jstests/aggregation/expressions/merge_objects.js
@@ -0,0 +1,160 @@
+// Tests for the $mergeObjects aggregation expression.
+(function() {
+ "use strict";
+
+ // For assertErrorCode().
+ load("jstests/aggregation/extras/utils.js");
+
+ let coll = db.merge_object_expr;
+ coll.drop();
+
+ // Test merging two objects together.
+ assert.writeOK(coll.insert({_id: 0, subObject: {b: 1, c: 1}}));
+ let result = coll.aggregate([
+ {$match: {_id: 0}},
+ {$project: {mergedDocument: {$mergeObjects: ["$subObject", {d: 1}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 0, mergedDocument: {b: 1, c: 1, d: 1}}]);
+
+ // Test merging the root document with a new field.
+ assert.writeOK(coll.insert({_id: 1, a: 0, b: 1}));
+ result =
+ coll.aggregate([
+ {$match: {_id: 1}},
+ {$project: {mergedDocument: {$mergeObjects: ["$$ROOT", {newField: "newValue"}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 1, mergedDocument: {_id: 1, a: 0, b: 1, newField: "newValue"}}]);
+
+ // Test replacing a field in the root.
+ assert.writeOK(coll.insert({_id: 2, a: 0, b: 1}));
+ result = coll.aggregate([
+ {$match: {_id: 2}},
+ {$project: {mergedDocument: {$mergeObjects: ["$$ROOT", {a: "newValue"}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 2, mergedDocument: {_id: 2, a: "newValue", b: 1}}]);
+
+ // Test overriding a document with root.
+ assert.writeOK(coll.insert({_id: 3, a: 0, b: 1}));
+ result =
+ coll.aggregate([
+ {$match: {_id: 3}},
+ {$project: {mergedDocument: {$mergeObjects: [{a: "defaultValue"}, "$$ROOT"]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 3, mergedDocument: {a: 0, _id: 3, b: 1}}]);
+
+ // Test replacing root with merged document.
+ assert.writeOK(coll.insert({_id: 4, a: 0, subObject: {b: 1, c: 2}}));
+ result = coll.aggregate([
+ {$match: {_id: 4}},
+ {$replaceRoot: {newRoot: {$mergeObjects: ["$$ROOT", "$subObject"]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 4, a: 0, subObject: {b: 1, c: 2}, b: 1, c: 2}]);
+
+ // Test merging with an embedded object.
+ assert.writeOK(coll.insert({_id: 5, subObject: {b: 1, c: 1}}));
+ result = coll.aggregate([
+ {$match: {_id: 5}},
+ {
+ $project: {
+ mergedDocument:
+ {$mergeObjects: ["$subObject", {subObject1: {d: 1}}, {e: 1}]}
+ }
+ }
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 5, mergedDocument: {b: 1, c: 1, subObject1: {d: 1}, e: 1}}]);
+
+ // Test for errors on non-document types.
+ assert.writeOK(coll.insert({_id: 6, a: "string"}));
+ assertErrorCode(coll,
+ [
+ {$match: {_id: 6}},
+ {$project: {mergedDocument: {$mergeObjects: ["$a", {a: "newString"}]}}}
+ ],
+ 40400);
+
+ assert.writeOK(coll.insert({_id: 7, a: {b: 1}, c: 1}));
+ assertErrorCode(
+ coll,
+ [{$match: {_id: 7}}, {$project: {mergedDocument: {$mergeObjects: ["$a", "$c"]}}}],
+ 40400);
+
+ // Test outputs with null values.
+ assert.writeOK(coll.insert({_id: 8, a: {b: 1}}));
+ result = coll.aggregate([
+ {$match: {_id: 8}},
+ {$project: {mergedDocument: {$mergeObjects: ["$a", {b: null}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 8, mergedDocument: {b: null}}]);
+
+ // Test output with undefined values.
+ assert.writeOK(coll.insert({_id: 9, a: {b: 1}}));
+ result = coll.aggregate([
+ {$match: {_id: 9}},
+ {$project: {mergedDocument: {$mergeObjects: ["$a", {b: undefined}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 9, mergedDocument: {b: undefined}}]);
+
+ // Test output with missing values.
+ assert.writeOK(coll.insert({_id: 10, a: {b: 1}}));
+ result =
+ coll.aggregate([
+ {$match: {_id: 10}},
+ {$project: {mergedDocument: {$mergeObjects: ["$a", {b: "$nonExistentField"}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 10, mergedDocument: {b: 1}}]);
+
+ assert.writeOK(coll.insert({_id: 11, a: {b: 1}}));
+ result = coll.aggregate([
+ {$match: {_id: 11}},
+ {$project: {mergedDocument: {$mergeObjects: ["$a", {b: ""}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 11, mergedDocument: {b: ""}}]);
+
+ // Test outputs with empty values.
+ assert.writeOK(coll.insert({_id: 12, b: 1, c: 1}));
+ result =
+ coll.aggregate([{$match: {_id: 12}}, {$project: {mergedDocument: {$mergeObjects: [{}]}}}])
+ .toArray();
+ assert.eq(result, [{_id: 12, mergedDocument: {}}]);
+
+ result = coll.aggregate(
+ [{$match: {_id: 12}}, {$project: {mergedDocument: {$mergeObjects: [{}, {}]}}}])
+ .toArray();
+ assert.eq(result, [{_id: 12, mergedDocument: {}}]);
+
+ // Test merge within a $group stage.
+ assert.writeOK(coll.insert({_id: 13, group: 1, obj: {}}));
+ assert.writeOK(coll.insert({_id: 14, group: 1, obj: {a: 2, b: 2}}));
+ assert.writeOK(coll.insert({_id: 15, group: 1, obj: {a: 1, c: 3}}));
+ assert.writeOK(coll.insert({_id: 16, group: 2, obj: {a: 1, b: 1}}));
+ result = coll.aggregate([
+ {$match: {_id: {$in: [13, 14, 15, 16]}}},
+ {$sort: {_id: 1}},
+ {$group: {_id: "$group", mergedDocument: {$mergeObjects: "$obj"}}},
+ {$sort: {_id: 1}},
+ ])
+ .toArray();
+ assert.eq(
+ result,
+ [{_id: 1, mergedDocument: {a: 1, b: 2, c: 3}}, {_id: 2, mergedDocument: {a: 1, b: 1}}]);
+
+ // Test merge with $$REMOVE operator.
+ assert.writeOK(coll.insert({_id: 17, a: {b: 2}}));
+ result = coll.aggregate([
+ {$match: {_id: 17}},
+ {$project: {mergedDocument: {$mergeObjects: ["$a", {b: "$$REMOVE"}]}}}
+ ])
+ .toArray();
+ assert.eq(result, [{_id: 17, mergedDocument: {b: 2}}]);
+
+}());
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript
index 82c6fb925d2..0f9a473454c 100644
--- a/src/mongo/db/pipeline/SConscript
+++ b/src/mongo/db/pipeline/SConscript
@@ -190,6 +190,7 @@ env.Library(
'accumulator_push.cpp',
'accumulator_std_dev.cpp',
'accumulator_sum.cpp',
+ 'accumulator_merge_objects.cpp'
],
LIBDEPS=[
'document_value',
diff --git a/src/mongo/db/pipeline/accumulator.h b/src/mongo/db/pipeline/accumulator.h
index 28645e90a9b..a7a97620607 100644
--- a/src/mongo/db/pipeline/accumulator.h
+++ b/src/mongo/db/pipeline/accumulator.h
@@ -63,7 +63,7 @@ public:
/** Marks the end of the evaluate() phase and return accumulated result.
* toBeMerged should be true when the outputs will be merged by process().
*/
- virtual Value getValue(bool toBeMerged) const = 0;
+ virtual Value getValue(bool toBeMerged) = 0;
/// The name of the op as used in a serialization of the pipeline.
virtual const char* getOpName() const = 0;
@@ -106,7 +106,7 @@ public:
explicit AccumulatorAddToSet(const boost::intrusive_ptr<ExpressionContext>& expCtx);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -131,7 +131,7 @@ public:
explicit AccumulatorFirst(const boost::intrusive_ptr<ExpressionContext>& expCtx);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -149,7 +149,7 @@ public:
explicit AccumulatorLast(const boost::intrusive_ptr<ExpressionContext>& expCtx);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -166,7 +166,7 @@ public:
explicit AccumulatorSum(const boost::intrusive_ptr<ExpressionContext>& expCtx);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -198,7 +198,7 @@ public:
AccumulatorMinMax(const boost::intrusive_ptr<ExpressionContext>& expCtx, Sense sense);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -237,7 +237,7 @@ public:
explicit AccumulatorPush(const boost::intrusive_ptr<ExpressionContext>& expCtx);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -254,7 +254,7 @@ public:
explicit AccumulatorAvg(const boost::intrusive_ptr<ExpressionContext>& expCtx);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -280,7 +280,7 @@ public:
AccumulatorStdDev(const boost::intrusive_ptr<ExpressionContext>& expCtx, bool isSamp);
void processInternal(const Value& input, bool merging) final;
- Value getValue(bool toBeMerged) const final;
+ Value getValue(bool toBeMerged) final;
const char* getOpName() const final;
void reset() final;
@@ -306,4 +306,20 @@ public:
static boost::intrusive_ptr<Accumulator> create(
const boost::intrusive_ptr<ExpressionContext>& expCtx);
};
+
+class AccumulatorMergeObjects : public Accumulator {
+public:
+ AccumulatorMergeObjects(const boost::intrusive_ptr<ExpressionContext>& expCtx);
+
+ void processInternal(const Value& input, bool merging) final;
+ Value getValue(bool toBeMerged) final;
+ const char* getOpName() const final;
+ void reset() final;
+
+ static boost::intrusive_ptr<Accumulator> create(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx);
+
+private:
+ MutableDocument _output;
+};
}
diff --git a/src/mongo/db/pipeline/accumulator_add_to_set.cpp b/src/mongo/db/pipeline/accumulator_add_to_set.cpp
index 00845b32fc8..4f6048529a1 100644
--- a/src/mongo/db/pipeline/accumulator_add_to_set.cpp
+++ b/src/mongo/db/pipeline/accumulator_add_to_set.cpp
@@ -70,7 +70,7 @@ void AccumulatorAddToSet::processInternal(const Value& input, bool merging) {
}
}
-Value AccumulatorAddToSet::getValue(bool toBeMerged) const {
+Value AccumulatorAddToSet::getValue(bool toBeMerged) {
return Value(vector<Value>(_set.begin(), _set.end()));
}
diff --git a/src/mongo/db/pipeline/accumulator_avg.cpp b/src/mongo/db/pipeline/accumulator_avg.cpp
index da0c08fce9c..6f0e8e04813 100644
--- a/src/mongo/db/pipeline/accumulator_avg.cpp
+++ b/src/mongo/db/pipeline/accumulator_avg.cpp
@@ -101,7 +101,7 @@ Decimal128 AccumulatorAvg::_getDecimalTotal() const {
return _decimalTotal.add(_nonDecimalTotal.getDecimal());
}
-Value AccumulatorAvg::getValue(bool toBeMerged) const {
+Value AccumulatorAvg::getValue(bool toBeMerged) {
if (toBeMerged) {
if (_isDecimal)
return Value(Document{{subTotalName, _getDecimalTotal()}, {countName, _count}});
diff --git a/src/mongo/db/pipeline/accumulator_first.cpp b/src/mongo/db/pipeline/accumulator_first.cpp
index 3092bd0e9da..60c97f88124 100644
--- a/src/mongo/db/pipeline/accumulator_first.cpp
+++ b/src/mongo/db/pipeline/accumulator_first.cpp
@@ -53,7 +53,7 @@ void AccumulatorFirst::processInternal(const Value& input, bool merging) {
}
}
-Value AccumulatorFirst::getValue(bool toBeMerged) const {
+Value AccumulatorFirst::getValue(bool toBeMerged) {
return _first;
}
diff --git a/src/mongo/db/pipeline/accumulator_last.cpp b/src/mongo/db/pipeline/accumulator_last.cpp
index 5f465b1bf4e..c66fe5801dc 100644
--- a/src/mongo/db/pipeline/accumulator_last.cpp
+++ b/src/mongo/db/pipeline/accumulator_last.cpp
@@ -49,7 +49,7 @@ void AccumulatorLast::processInternal(const Value& input, bool merging) {
_memUsageBytes = sizeof(*this) + _last.getApproximateSize() - sizeof(Value);
}
-Value AccumulatorLast::getValue(bool toBeMerged) const {
+Value AccumulatorLast::getValue(bool toBeMerged) {
return _last;
}
diff --git a/src/mongo/db/pipeline/accumulator_merge_objects.cpp b/src/mongo/db/pipeline/accumulator_merge_objects.cpp
new file mode 100644
index 00000000000..1584d8ba2c4
--- /dev/null
+++ b/src/mongo/db/pipeline/accumulator_merge_objects.cpp
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2017 10gen Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * 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 GNU Affero General 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/platform/basic.h"
+
+#include "mongo/db/pipeline/accumulator.h"
+
+#include "mongo/db/pipeline/accumulation_statement.h"
+#include "mongo/db/pipeline/expression.h"
+#include "mongo/db/pipeline/value.h"
+
+namespace mongo {
+
+using boost::intrusive_ptr;
+
+/* ------------------------- AccumulatorMergeObjects ----------------------------- */
+
+REGISTER_ACCUMULATOR(mergeObjects, AccumulatorMergeObjects::create);
+REGISTER_EXPRESSION(mergeObjects, ExpressionFromAccumulator<AccumulatorMergeObjects>::parse);
+
+const char* AccumulatorMergeObjects::getOpName() const {
+ return "$mergeObjects";
+}
+
+intrusive_ptr<Accumulator> AccumulatorMergeObjects::create(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx) {
+ return new AccumulatorMergeObjects(expCtx);
+}
+
+AccumulatorMergeObjects::AccumulatorMergeObjects(
+ const boost::intrusive_ptr<ExpressionContext>& expCtx)
+ : Accumulator(expCtx) {
+ _memUsageBytes = sizeof(*this);
+}
+
+void AccumulatorMergeObjects::reset() {
+ _memUsageBytes = sizeof(*this);
+ _output.reset();
+}
+
+void AccumulatorMergeObjects::processInternal(const Value& input, bool merging) {
+ if (input.nullish()) {
+ return;
+ }
+
+ uassert(40400,
+ str::stream() << "$mergeObjects requires object inputs, but input " << input.toString()
+ << " is of type "
+ << typeName(input.getType()),
+ (input.getType() == BSONType::Object));
+
+ FieldIterator iter = input.getDocument().fieldIterator();
+ while (iter.more()) {
+ Document::FieldPair pair = iter.next();
+ // Ignore missing values only, null and undefined are still considered.
+ if (pair.second.missing())
+ continue;
+
+ _output.setField(pair.first, pair.second);
+ }
+ _memUsageBytes = sizeof(*this) + _output.getApproximateSize();
+}
+
+Value AccumulatorMergeObjects::getValue(bool toBeMerged) {
+ return _output.freezeToValue();
+}
+
+} // namespace mongo
diff --git a/src/mongo/db/pipeline/accumulator_min_max.cpp b/src/mongo/db/pipeline/accumulator_min_max.cpp
index f5c4b254b88..ee086d2a2c2 100644
--- a/src/mongo/db/pipeline/accumulator_min_max.cpp
+++ b/src/mongo/db/pipeline/accumulator_min_max.cpp
@@ -61,7 +61,7 @@ void AccumulatorMinMax::processInternal(const Value& input, bool merging) {
}
}
-Value AccumulatorMinMax::getValue(bool toBeMerged) const {
+Value AccumulatorMinMax::getValue(bool toBeMerged) {
if (_val.missing()) {
return Value(BSONNULL);
}
diff --git a/src/mongo/db/pipeline/accumulator_push.cpp b/src/mongo/db/pipeline/accumulator_push.cpp
index 9239d8b8a18..64b140a2c7c 100644
--- a/src/mongo/db/pipeline/accumulator_push.cpp
+++ b/src/mongo/db/pipeline/accumulator_push.cpp
@@ -67,7 +67,7 @@ void AccumulatorPush::processInternal(const Value& input, bool merging) {
}
}
-Value AccumulatorPush::getValue(bool toBeMerged) const {
+Value AccumulatorPush::getValue(bool toBeMerged) {
return Value(vpValue);
}
diff --git a/src/mongo/db/pipeline/accumulator_std_dev.cpp b/src/mongo/db/pipeline/accumulator_std_dev.cpp
index f45678dce27..159f95f16df 100644
--- a/src/mongo/db/pipeline/accumulator_std_dev.cpp
+++ b/src/mongo/db/pipeline/accumulator_std_dev.cpp
@@ -83,7 +83,7 @@ void AccumulatorStdDev::processInternal(const Value& input, bool merging) {
}
}
-Value AccumulatorStdDev::getValue(bool toBeMerged) const {
+Value AccumulatorStdDev::getValue(bool toBeMerged) {
if (!toBeMerged) {
const long long adjustedCount = (_isSamp ? _count - 1 : _count);
if (adjustedCount <= 0)
diff --git a/src/mongo/db/pipeline/accumulator_sum.cpp b/src/mongo/db/pipeline/accumulator_sum.cpp
index bbbf596d4a8..524284e55ed 100644
--- a/src/mongo/db/pipeline/accumulator_sum.cpp
+++ b/src/mongo/db/pipeline/accumulator_sum.cpp
@@ -89,7 +89,7 @@ intrusive_ptr<Accumulator> AccumulatorSum::create(
return new AccumulatorSum(expCtx);
}
-Value AccumulatorSum::getValue(bool toBeMerged) const {
+Value AccumulatorSum::getValue(bool toBeMerged) {
switch (totalType) {
case NumberInt:
if (nonDecimalTotal.fitsLong())
diff --git a/src/mongo/db/pipeline/accumulator_test.cpp b/src/mongo/db/pipeline/accumulator_test.cpp
index 3fcb1d70ad7..4ed1a6b486b 100644
--- a/src/mongo/db/pipeline/accumulator_test.cpp
+++ b/src/mongo/db/pipeline/accumulator_test.cpp
@@ -344,4 +344,62 @@ TEST(Accumulators, AddToSetRespectsCollation) {
Value(std::vector<Value>{Value("a"_sd)})}});
}
+/* ------------------------- AccumulatorMergeObjects -------------------------- */
+
+namespace AccumulatorMergeObjects {
+
+TEST(AccumulatorMergeObjects, MergingZeroObjectsShouldReturnEmptyDocument) {
+ intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest());
+
+ assertExpectedResults("$mergeObjects", expCtx, {{{}, {Value(Document({}))}}});
+}
+
+TEST(AccumulatorMergeObjects, MergingWithSingleObjectShouldLeaveUnchanged) {
+ intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest());
+
+ assertExpectedResults("$mergeObjects", expCtx, {{{}, {Value(Document({}))}}});
+
+ auto doc = Value(Document({{"a", 1}, {"b", 1}}));
+ assertExpectedResults("$mergeObjects", expCtx, {{{doc}, doc}});
+}
+
+TEST(AccumulatorMergeObjects, MergingDisjointObjectsShouldIncludeAllFields) {
+ intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest());
+ auto first = Value(Document({{"a", 1}, {"b", 1}}));
+ auto second = Value(Document({{"c", 1}}));
+ assertExpectedResults("$mergeObjects",
+ expCtx,
+ {{{first, second}, Value(Document({{"a", 1}, {"b", 1}, {"c", 1}}))}});
+}
+
+TEST(AccumulatorMergeObjects, MergingIntersectingObjectsShouldOverrideInOrderReceived) {
+ intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest());
+ auto first = Value(Document({{"a", "oldValue"_sd}, {"b", 0}, {"c", 1}}));
+ auto second = Value(Document({{"a", "newValue"_sd}}));
+ assertExpectedResults(
+ "$mergeObjects",
+ expCtx,
+ {{{first, second}, Value(Document({{"a", "newValue"_sd}, {"b", 0}, {"c", 1}}))}});
+}
+
+TEST(AccumulatorMergeObjects, MergingIntersectingEmbeddedObjectsShouldOverrideInOrderReceived) {
+ intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest());
+ auto firstSubDoc = Document({{"a", 1}, {"b", 2}, {"c", 3}});
+ auto secondSubDoc = Document({{"a", 2}, {"b", 1}});
+ auto first = Value(Document({{"d", 1}, {"subDoc", firstSubDoc}}));
+ auto second = Value(Document({{"subDoc", secondSubDoc}}));
+ auto expected = Value(Document({{"d", 1}, {"subDoc", secondSubDoc}}));
+ assertExpectedResults("$mergeObjects", expCtx, {{{first, second}, expected}});
+}
+
+TEST(AccumulatorMergeObjects, MergingWithEmptyDocumentShouldIgnore) {
+ intrusive_ptr<ExpressionContext> expCtx(new ExpressionContextForTest());
+ auto first = Value(Document({{"a", 0}, {"b", 1}, {"c", 1}}));
+ auto second = Value(Document({}));
+ auto expected = Value(Document({{"a", 0}, {"b", 1}, {"c", 1}}));
+ assertExpectedResults("$mergeObjects", expCtx, {{{first, second}, expected}});
+}
+
+} // namespace AccumulatorMergeObjects
+
} // namespace AccumulatorTests
diff --git a/src/mongo/db/pipeline/document.h b/src/mongo/db/pipeline/document.h
index 416dd24a4aa..e004f7ab982 100644
--- a/src/mongo/db/pipeline/document.h
+++ b/src/mongo/db/pipeline/document.h
@@ -523,6 +523,10 @@ public:
return Document(storagePtr());
}
+ size_t getApproximateSize() {
+ return peek().getApproximateSize();
+ }
+
private:
friend class MutableValue; // for access to next constructor
explicit MutableDocument(MutableValue mv) : _storageHolder(NULL), _storage(mv.getDocPtr()) {}
diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp
index f08617396bb..4e35f76931d 100644
--- a/src/mongo/db/pipeline/expression_test.cpp
+++ b/src/mongo/db/pipeline/expression_test.cpp
@@ -54,24 +54,35 @@ using std::vector;
using std::list;
/**
+ * Creates an expression given by 'expressionName' and evaluates it using
+ * 'operands' as inputs, returning the result.
+ */
+static Value evaluateExpression(const string& expressionName,
+ const vector<ImplicitValue>& operands) {
+ intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ VariablesIdGenerator idGenerator;
+ VariablesParseState vps(&idGenerator);
+ const BSONObj obj = BSON(expressionName << ImplicitValue::convertToValue(operands));
+ auto expression = Expression::parseExpression(expCtx, obj, vps);
+ Value result = expression->evaluate(Document());
+ return result;
+}
+
+/**
* Takes the name of an expression as its first argument and a list of pairs of arguments and
* expected results as its second argument, and asserts that for the given expression the arguments
* evaluate to the expected results.
*/
-static void assertExpectedResults(string expression,
- initializer_list<pair<vector<Value>, Value>> operations) {
+static void assertExpectedResults(
+ const string& expression,
+ initializer_list<pair<vector<ImplicitValue>, ImplicitValue>> operations) {
for (auto&& op : operations) {
try {
- intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
- VariablesIdGenerator idGenerator;
- VariablesParseState vps(&idGenerator);
- const BSONObj obj = BSON(expression << Value(op.first));
- auto expression = Expression::parseExpression(expCtx, obj, vps);
- Value result = expression->evaluate(Document());
+ Value result = evaluateExpression(expression, op.first);
ASSERT_VALUE_EQ(op.second, result);
ASSERT_EQUALS(op.second.getType(), result.getType());
} catch (...) {
- log() << "failed with arguments: " << Value(op.first);
+ log() << "failed with arguments: " << ImplicitValue::convertToValue(op.first);
throw;
}
}
@@ -3890,6 +3901,81 @@ TEST(BuiltinRemoveVariableTest, RemoveSerializesCorrectlyAfterOptimization) {
} // namespace BuiltinRemoveVariable
+/* ------------------------- ExpressionMergeObjects -------------------------- */
+
+namespace ExpressionMergeObjects {
+
+TEST(ExpressionMergeObjects, MergingWithSingleObjectShouldLeaveUnchanged) {
+ assertExpectedResults("$mergeObjects", {{{}, {Document({})}}});
+
+ auto doc = Document({{"a", 1}, {"b", 1}});
+ assertExpectedResults("$mergeObjects", {{{doc}, doc}});
+}
+
+TEST(ExpressionMergeObjects, MergingDisjointObjectsShouldIncludeAllFields) {
+ auto first = Document({{"a", 1}, {"b", 1}});
+ auto second = Document({{"c", 1}});
+ assertExpectedResults("$mergeObjects",
+ {{{first, second}, Document({{"a", 1}, {"b", 1}, {"c", 1}})}});
+}
+
+TEST(ExpressionMergeObjects, MergingIntersectingObjectsShouldOverrideInOrderReceived) {
+ auto first = Document({{"a", "oldValue"_sd}, {"b", 0}, {"c", 1}});
+ auto second = Document({{"a", "newValue"_sd}});
+ assertExpectedResults(
+ "$mergeObjects", {{{first, second}, Document({{"a", "newValue"_sd}, {"b", 0}, {"c", 1}})}});
+}
+
+TEST(ExpressionMergeObjects, MergingIntersectingEmbeddedObjectsShouldOverrideInOrderReceived) {
+ auto firstSubDoc = Document({{"a", 1}, {"b", 2}, {"c", 3}});
+ auto secondSubDoc = Document({{"a", 2}, {"b", 1}});
+ auto first = Document({{"d", 1}, {"subDoc", firstSubDoc}});
+ auto second = Document({{"subDoc", secondSubDoc}});
+ auto expected = Document({{"d", 1}, {"subDoc", secondSubDoc}});
+ assertExpectedResults("$mergeObjects", {{{first, second}, expected}});
+}
+
+TEST(ExpressionMergeObjects, MergingWithEmptyDocumentShouldIgnore) {
+ auto first = Document({{"a", 0}, {"b", 1}, {"c", 1}});
+ auto second = Document({});
+ auto expected = Document({{"a", 0}, {"b", 1}, {"c", 1}});
+ assertExpectedResults("$mergeObjects", {{{first, second}, expected}});
+}
+
+TEST(ExpressionMergeObjects, MergingSingleArgumentArrayShouldUnwindAndMerge) {
+ std::vector<Document> first = {Document({{"a", 1}}), Document({{"a", 2}})};
+ auto expected = Document({{"a", 2}});
+ assertExpectedResults("$mergeObjects", {{{first}, expected}});
+}
+
+TEST(ExpressionMergeObjects, MergingArrayWithDocumentShouldThrowException) {
+ std::vector<Document> first = {Document({{"a", 1}}), Document({{"a", 2}})};
+ auto second = Document({{"b", 2}});
+ ASSERT_THROWS_CODE(evaluateExpression("$mergeObjects", {first, second}), UserException, 40400);
+}
+
+TEST(ExpressionMergeObjects, MergingArrayContainingInvalidTypesShouldThrowException) {
+ std::vector<Value> first = {Value(Document({{"validType", 1}})), Value("invalidType"_sd)};
+ ASSERT_THROWS_CODE(evaluateExpression("$mergeObjects", {first}), UserException, 40400);
+}
+
+TEST(ExpressionMergeObjects, MergingNonObjectsShouldThrowException) {
+ ASSERT_THROWS_CODE(
+ evaluateExpression("$mergeObjects", {"invalidArg"_sd}), UserException, 40400);
+
+ ASSERT_THROWS_CODE(
+ evaluateExpression("$mergeObjects", {"invalidArg"_sd, Document({{"validArg", 1}})}),
+ UserException,
+ 40400);
+
+ ASSERT_THROWS_CODE(evaluateExpression("$mergeObjects", {1, Document({{"validArg", 1}})}),
+ UserException,
+ 40400);
+}
+
+} // namespace ExpressionMergeObjects
+
+
namespace ToLower {
class ExpectedResultBase {
diff --git a/src/mongo/db/pipeline/value.h b/src/mongo/db/pipeline/value.h
index 57aa2c6a789..4749194ae29 100644
--- a/src/mongo/db/pipeline/value.h
+++ b/src/mongo/db/pipeline/value.h
@@ -358,6 +358,16 @@ class ImplicitValue : public Value {
public:
template <typename T>
ImplicitValue(T arg) : Value(std::move(arg)) {}
+
+ /**
+ * Converts a vector of Implicit values to a single Value object.
+ */
+ static Value convertToValue(const std::vector<ImplicitValue>& vec) {
+ std::vector<Value> values;
+ for_each(
+ vec.begin(), vec.end(), ([&](const ImplicitValue& val) { values.push_back(val); }));
+ return Value(values);
+ }
};
}
diff --git a/src/mongo/unittest/unittest.h b/src/mongo/unittest/unittest.h
index 6bb7bffdff8..adef50b2369 100644
--- a/src/mongo/unittest/unittest.h
+++ b/src/mongo/unittest/unittest.h
@@ -135,9 +135,9 @@
* Behaves like ASSERT_THROWS, above, but also fails if calling getCode() on the thrown exception
* does not return an error code equal to EXPECTED_CODE.
*/
-#define ASSERT_THROWS_CODE(STATEMENT, EXCEPTION_TYPE, EXPECTED_CODE) \
- ASSERT_THROWS_PRED(STATEMENT, EXCEPTION_TYPE, ([](const EXCEPTION_TYPE& ex) { \
- return (EXPECTED_CODE) == ex.getCode(); \
+#define ASSERT_THROWS_CODE(STATEMENT, EXCEPTION_TYPE, EXPECTED_CODE) \
+ ASSERT_THROWS_PRED(STATEMENT, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \
+ return (EXPECTED_CODE) == ex.getCode(); \
}))
/**
@@ -146,7 +146,7 @@
* does not return a string equal to EXPECTED_WHAT.
*/
#define ASSERT_THROWS_CODE_AND_WHAT(STATEMENT, EXCEPTION_TYPE, EXPECTED_CODE, EXPECTED_WHAT) \
- ASSERT_THROWS_PRED(STATEMENT, EXCEPTION_TYPE, ([](const EXCEPTION_TYPE& ex) { \
+ ASSERT_THROWS_PRED(STATEMENT, EXCEPTION_TYPE, ([&](const EXCEPTION_TYPE& ex) { \
return (EXPECTED_CODE) == ex.getCode() && \
::mongo::StringData(ex.what()) == \
::mongo::StringData(EXPECTED_WHAT); \