diff options
-rw-r--r-- | jstests/aggregation/expressions/merge_objects.js | 160 | ||||
-rw-r--r-- | src/mongo/db/pipeline/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator.h | 34 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_add_to_set.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_avg.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_first.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_last.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_merge_objects.cpp | 93 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_min_max.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_push.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_std_dev.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_sum.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/pipeline/accumulator_test.cpp | 58 | ||||
-rw-r--r-- | src/mongo/db/pipeline/document.h | 4 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 104 | ||||
-rw-r--r-- | src/mongo/db/pipeline/value.h | 10 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.h | 8 |
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); \ |