/**
* Copyright (C) 2012 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 .
*
* 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/accumulation_statement.h"
#include "mongo/db/pipeline/accumulator.h"
#include "mongo/db/pipeline/document.h"
#include "mongo/db/pipeline/document_value_test_util.h"
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/query/collation/collator_interface_mock.h"
#include "mongo/dbtests/dbtests.h"
#include "mongo/stdx/memory.h"
namespace AccumulatorTests {
using boost::intrusive_ptr;
using std::numeric_limits;
using std::string;
/**
* Takes the name of an Accumulator as its first argument and a list of pairs of arguments and
* expected results as its second argument, and asserts that for the given Accumulator the arguments
* evaluate to the expected results.
*/
static void assertExpectedResults(
std::string accumulatorName,
const intrusive_ptr& expCtx,
std::initializer_list, Value>> operations) {
auto factory = AccumulationStatement::getFactory(accumulatorName);
for (auto&& op : operations) {
try {
// Asserts that result equals expected result when not sharded.
{
boost::intrusive_ptr accum(factory(expCtx));
for (auto&& val : op.first) {
accum->process(val, false);
}
Value result = accum->getValue(false);
ASSERT_VALUE_EQ(op.second, result);
ASSERT_EQUALS(op.second.getType(), result.getType());
}
// Asserts that result equals expected result when all input is on one shard.
{
boost::intrusive_ptr accum(factory(expCtx));
boost::intrusive_ptr shard(factory(expCtx));
for (auto&& val : op.first) {
shard->process(val, false);
}
accum->process(shard->getValue(true), true);
Value result = accum->getValue(false);
ASSERT_VALUE_EQ(op.second, result);
ASSERT_EQUALS(op.second.getType(), result.getType());
}
// Asserts that result equals expected result when each input is on a separate shard.
{
boost::intrusive_ptr accum(factory(expCtx));
for (auto&& val : op.first) {
boost::intrusive_ptr shard(factory(expCtx));
shard->process(val, false);
accum->process(shard->getValue(true), true);
}
Value result = accum->getValue(false);
ASSERT_VALUE_EQ(op.second, result);
ASSERT_EQUALS(op.second.getType(), result.getType());
}
} catch (...) {
log() << "failed with arguments: " << Value(op.first);
throw;
}
}
}
TEST(Accumulators, Avg) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults(
"$avg",
expCtx,
{
// No documents evaluated.
{{}, Value(BSONNULL)},
// One int value is converted to double.
{{Value(3)}, Value(3.0)},
// One long value is converted to double.
{{Value(-4LL)}, Value(-4.0)},
// One double value.
{{Value(22.6)}, Value(22.6)},
// Averaging two ints.
{{Value(10), Value(11)}, Value(10.5)},
// Averaging two longs.
{{Value(10LL), Value(11LL)}, Value(10.5)},
// Averaging two doubles.
{{Value(10.0), Value(11.0)}, Value(10.5)},
// The average of an int and a double is a double.
{{Value(10), Value(11.0)}, Value(10.5)},
// The average of a long and a double is a double.
{{Value(5LL), Value(1.0)}, Value(3.0)},
// The average of an int and a long is a double.
{{Value(5), Value(3LL)}, Value(4.0)},
// Averaging an int, long, and double.
{{Value(1), Value(2LL), Value(6.0)}, Value(3.0)},
// Unlike $sum, two ints do not overflow in the 'total' portion of the average.
{{Value(numeric_limits::max()), Value(numeric_limits::max())},
Value(static_cast(numeric_limits::max()))},
// Two longs do overflow in the 'total' portion of the average.
{{Value(numeric_limits::max()), Value(numeric_limits::max())},
Value(static_cast(numeric_limits::max()))},
// Averaging two decimals.
{{Value(Decimal128("-1234567890.1234567889")),
Value(Decimal128("-1234567890.1234567891"))},
Value(Decimal128("-1234567890.1234567890"))},
// Averaging two longs and a decimal results in an accurate decimal result.
{{Value(1234567890123456788LL),
Value(1234567890123456789LL),
Value(Decimal128("1234567890123456790.037037036703702"))},
Value(Decimal128("1234567890123456789.012345678901234"))},
// Averaging a double and a decimal
{{Value(1.0E22), Value(Decimal128("9999999999999999999999.9999999999"))},
Value(Decimal128("9999999999999999999999.99999999995"))},
});
}
TEST(Accumulators, First) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults(
"$first",
expCtx,
{// No documents evaluated.
{{}, Value()},
// The accumulator evaluates one document and retains its value.
{{Value(5)}, Value(5)},
// The accumulator evaluates one document with the field missing, returns missing value.
{{Value()}, Value()},
// The accumulator evaluates two documents and retains the value in the first.
{{Value(5), Value(7)}, Value(5)},
// The accumulator evaluates two documents and retains the missing value in the first.
{{Value(), Value(7)}, Value()}});
}
TEST(Accumulators, Last) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults(
"$last",
expCtx,
{// No documents evaluated.
{{}, Value()},
// The accumulator evaluates one document and retains its value.
{{Value(5)}, Value(5)},
// The accumulator evaluates one document with the field missing, returns missing value.
{{Value()}, Value()},
// The accumulator evaluates two documents and retains the value in the last.
{{Value(5), Value(7)}, Value(7)},
// The accumulator evaluates two documents and retains the missing value in the last.
{{Value(7), Value()}, Value()}});
}
TEST(Accumulators, Min) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults(
"$min",
expCtx,
{// No documents evaluated.
{{}, Value(BSONNULL)},
// The accumulator evaluates one document and retains its value.
{{Value(5)}, Value(5)},
// The accumulator evaluates one document with the field missing and returns null.
{{Value()}, Value(BSONNULL)},
// The accumulator evaluates two documents and retains the minimum value.
{{Value(5), Value(7)}, Value(5)},
// The accumulator evaluates two documents and ignores the missing value.
{{Value(7), Value()}, Value(7)}});
}
TEST(Accumulators, MinRespectsCollation) {
intrusive_ptr expCtx(new ExpressionContextForTest());
CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
expCtx->setCollator(&collator);
assertExpectedResults("$min", expCtx, {{{Value("abc"_sd), Value("cba"_sd)}, Value("cba"_sd)}});
}
TEST(Accumulators, Max) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults(
"$max",
expCtx,
{// No documents evaluated.
{{}, Value(BSONNULL)},
// The accumulator evaluates one document and retains its value.
{{Value(5)}, Value(5)},
// The accumulator evaluates one document with the field missing and returns null.
{{Value()}, Value(BSONNULL)},
// The accumulator evaluates two documents and retains the maximum value.
{{Value(5), Value(7)}, Value(7)},
// The accumulator evaluates two documents and ignores the missing value.
{{Value(7), Value()}, Value(7)}});
}
TEST(Accumulators, MaxRespectsCollation) {
intrusive_ptr expCtx(new ExpressionContextForTest());
CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
expCtx->setCollator(&collator);
assertExpectedResults("$max", expCtx, {{{Value("abc"_sd), Value("cba"_sd)}, Value("abc"_sd)}});
}
TEST(Accumulators, Sum) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults(
"$sum",
expCtx,
{// No documents evaluated.
{{}, Value(0)},
// An int.
{{Value(10)}, Value(10)},
// A long.
{{Value(10LL)}, Value(10LL)},
// A double.
{{Value(10.0)}, Value(10.0)},
// A long that cannot be expressed as an int.
{{Value(60000000000LL)}, Value(60000000000LL)},
// A non integer valued double.
{{Value(7.5)}, Value(7.5)},
// A nan double.
{{Value(numeric_limits::quiet_NaN())}, Value(numeric_limits::quiet_NaN())},
// Two ints are summed.
{{Value(4), Value(5)}, Value(9)},
// An int and a long.
{{Value(4), Value(5LL)}, Value(9LL)},
// Two longs.
{{Value(4LL), Value(5LL)}, Value(9LL)},
// An int and a double.
{{Value(4), Value(5.5)}, Value(9.5)},
// A long and a double.
{{Value(4LL), Value(5.5)}, Value(9.5)},
// Two doubles.
{{Value(2.5), Value(5.5)}, Value(8.0)},
// An int, a long, and a double.
{{Value(5), Value(99LL), Value(0.2)}, Value(104.2)},
// Two decimals.
{{Value(Decimal128("-10.100")), Value(Decimal128("20.200"))}, Value(Decimal128("10.100"))},
// Two longs and a decimal.
{{Value(10LL), Value(10LL), Value(Decimal128("10.000"))}, Value(Decimal128("30.000"))},
// A double and a decimal.
{{Value(2.5), Value(Decimal128("2.5"))}, Value(Decimal128("5.0"))},
// An int, long, double and decimal.
{{Value(10), Value(10LL), Value(10.5), Value(Decimal128("9.6"))},
Value(Decimal128("40.1"))},
// A negative value is summed.
{{Value(5), Value(-8.5)}, Value(-3.5)},
// A long and a negative int are summed.
{{Value(5LL), Value(-6)}, Value(-1LL)},
// Two ints do not overflow.
{{Value(numeric_limits::max()), Value(10)}, Value(numeric_limits::max() + 10LL)},
// Two negative ints do not overflow.
{{Value(-numeric_limits::max()), Value(-10)},
Value(-numeric_limits::max() - 10LL)},
// An int and a long do not trigger an int overflow.
{{Value(numeric_limits::max()), Value(1LL)},
Value(static_cast(numeric_limits::max()) + 1)},
// An int and a double do not trigger an int overflow.
{{Value(numeric_limits::max()), Value(1.0)},
Value(static_cast(numeric_limits::max()) + 1.0)},
// An int and a long overflow into a double.
{{Value(1), Value(numeric_limits::max())},
Value(-static_cast(numeric_limits::min()))},
// Two longs overflow into a double.
{{Value(numeric_limits::max()), Value(numeric_limits::max())},
Value(static_cast(numeric_limits::max()) * 2)},
// A long and a double do not trigger a long overflow.
{{Value(numeric_limits::max()), Value(1.0)},
Value(numeric_limits::max() + 1.0)},
// Two doubles overflow to infinity.
{{Value(numeric_limits::max()), Value(numeric_limits::max())},
Value(numeric_limits::infinity())},
// Two large integers do not overflow if a double is added later.
{{Value(numeric_limits::max()),
Value(numeric_limits::max()),
Value(1.0)},
Value(static_cast(numeric_limits::max()) +
static_cast(numeric_limits::max()))},
// An int and a NaN double.
{{Value(4), Value(numeric_limits::quiet_NaN())},
Value(numeric_limits::quiet_NaN())},
// Null values are ignored.
{{Value(5), Value(BSONNULL)}, Value(5)},
// Missing values are ignored.
{{Value(9), Value()}, Value(9)}});
}
TEST(Accumulators, AddToSetRespectsCollation) {
intrusive_ptr expCtx(new ExpressionContextForTest());
CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual);
expCtx->setCollator(&collator);
assertExpectedResults("$addToSet",
expCtx,
{{{Value("a"_sd), Value("b"_sd), Value("c"_sd)},
Value(std::vector{Value("a"_sd)})}});
}
/* ------------------------- AccumulatorMergeObjects -------------------------- */
namespace AccumulatorMergeObjects {
TEST(AccumulatorMergeObjects, MergingZeroObjectsShouldReturnEmptyDocument) {
intrusive_ptr expCtx(new ExpressionContextForTest());
assertExpectedResults("$mergeObjects", expCtx, {{{}, {Value(Document({}))}}});
}
TEST(AccumulatorMergeObjects, MergingWithSingleObjectShouldLeaveUnchanged) {
intrusive_ptr 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 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 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 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 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