summaryrefslogtreecommitdiff
path: root/src/mongo/db/pipeline/document_source_bucket_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/pipeline/document_source_bucket_test.cpp')
-rw-r--r--src/mongo/db/pipeline/document_source_bucket_test.cpp286
1 files changed, 286 insertions, 0 deletions
diff --git a/src/mongo/db/pipeline/document_source_bucket_test.cpp b/src/mongo/db/pipeline/document_source_bucket_test.cpp
new file mode 100644
index 00000000000..4f6f3c7f9c3
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_bucket_test.cpp
@@ -0,0 +1,286 @@
+/**
+ * Copyright (C) 2016 MongoDB 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 <boost/intrusive_ptr.hpp>
+#include <vector>
+
+#include "mongo/bson/bsonobj.h"
+#include "mongo/bson/json.h"
+#include "mongo/db/pipeline/aggregation_context_fixture.h"
+#include "mongo/db/pipeline/document.h"
+#include "mongo/db/pipeline/document_source.h"
+#include "mongo/db/pipeline/document_value_test_util.h"
+#include "mongo/db/pipeline/value.h"
+#include "mongo/db/pipeline/value_comparator.h"
+#include "mongo/unittest/unittest.h"
+
+namespace mongo {
+namespace {
+
+using std::vector;
+using boost::intrusive_ptr;
+
+class BucketReturnsGroupAndSort : public AggregationContextFixture {
+public:
+ void testCreateFromBsonResult(BSONObj bucketSpec, Value expectedGroupExplain) {
+ vector<intrusive_ptr<DocumentSource>> result =
+ DocumentSourceBucket::createFromBson(bucketSpec.firstElement(), getExpCtx());
+
+ ASSERT_EQUALS(result.size(), 2UL);
+
+ const auto* groupStage = dynamic_cast<DocumentSourceGroup*>(result[0].get());
+ ASSERT(groupStage);
+
+ const auto* sortStage = dynamic_cast<DocumentSourceSort*>(result[1].get());
+ ASSERT(sortStage);
+
+ // Serialize the DocumentSourceGroup and DocumentSourceSort from $bucket so that we can
+ // check the explain output to make sure $group and $sort have the correct fields.
+ const bool explain = true;
+ vector<Value> explainedStages;
+ groupStage->serializeToArray(explainedStages, explain);
+ sortStage->serializeToArray(explainedStages, explain);
+ ASSERT_EQUALS(explainedStages.size(), 2UL);
+
+ auto groupExplain = explainedStages[0];
+ ASSERT_VALUE_EQ(groupExplain["$group"], expectedGroupExplain);
+
+ auto sortExplain = explainedStages[1];
+
+ auto expectedSortExplain = Value{Document{{"sortKey", Document{{"_id", 1}}}}};
+ ASSERT_VALUE_EQ(sortExplain["$sort"], expectedSortExplain);
+ }
+};
+
+TEST_F(BucketReturnsGroupAndSort, BucketUsesDefaultOutputWhenNoOutputSpecified) {
+ const auto spec =
+ fromjson("{$bucket : {groupBy :'$x', boundaries : [ 0, 2 ], default : 'other'}}");
+ auto expectedGroupExplain =
+ Value(fromjson("{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const : "
+ "0}]}, {$lt : ['$x', {$const : 2}]}]}, then : {$const : 0}}], default : "
+ "{$const : 'other'}}}, count : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWhenOutputSpecified) {
+ const auto spec = fromjson(
+ "{$bucket : {groupBy : '$x', boundaries : [0, 2], output : { number : {$sum : 1}}}}");
+ auto expectedGroupExplain = Value(fromjson(
+ "{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const : 0}]}, {$lt : "
+ "['$x', {$const : 2}]}]}, then : {$const : 0}}]}}, number : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWhenNoDefaultSpecified) {
+ const auto spec = fromjson("{$bucket : { groupBy : '$x', boundaries : [0, 2]}}");
+ auto expectedGroupExplain = Value(fromjson(
+ "{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const : 0}]}, {$lt : "
+ "['$x', {$const : 2}]}]}, then : {$const : 0}}]}}, count : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWhenBoundariesAreSameCanonicalType) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [0, 1.5]}}");
+ auto expectedGroupExplain = Value(fromjson(
+ "{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const : 0}]}, {$lt : "
+ "['$x', {$const : 1.5}]}]}, then : {$const : 0}}]}},count : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWhenBoundariesAreConstantExpressions) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [0, {$add : [4, 5]}]}}");
+ auto expectedGroupExplain = Value(fromjson(
+ "{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const : 0}]}, {$lt : "
+ "['$x', {$const : 9}]}]}, then : {$const : 0}}]}}, count : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWhenDefaultIsConstantExpression) {
+ const auto spec =
+ fromjson("{$bucket : {groupBy : '$x', boundaries : [0, 1], default: {$add : [4, 5]}}}");
+ auto expectedGroupExplain =
+ Value(fromjson("{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const :"
+ "0}]}, {$lt : ['$x', {$const : 1}]}]}, then : {$const : 0}}], default : "
+ "{$const : 9}}}, count : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+TEST_F(BucketReturnsGroupAndSort, BucketSucceedsWithMultipleBoundaryValues) {
+ auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [0, 1, 2]}}");
+ auto expectedGroupExplain =
+ Value(fromjson("{_id : {$switch : {branches : [{case : {$and : [{$gte : ['$x', {$const : "
+ "0}]}, {$lt : ['$x', {$const : 1}]}]}, then : {$const : 0}}, {case : {$and "
+ ": [{$gte : ['$x', {$const : 1}]}, {$lt : ['$x', {$const : 2}]}]}, then : "
+ "{$const : 1}}]}}, count : {$sum : {$const : 1}}}"));
+
+ testCreateFromBsonResult(spec, expectedGroupExplain);
+}
+
+class InvalidBucketSpec : public AggregationContextFixture {
+public:
+ vector<intrusive_ptr<DocumentSource>> createBucket(BSONObj bucketSpec) {
+ auto sources = DocumentSourceBucket::createFromBson(bucketSpec.firstElement(), getExpCtx());
+ for (auto&& source : sources) {
+ source->injectExpressionContext(getExpCtx());
+ }
+ return sources;
+ }
+};
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonObject) {
+ auto spec = fromjson("{$bucket : 1}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40201);
+
+ spec = fromjson("{$bucket : 'test'}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40201);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithUnknownField) {
+ const auto spec =
+ fromjson("{$bucket : {groupBy : '$x', boundaries : [0, 1, 2], unknown : 'field'}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40197);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNoGroupBy) {
+ const auto spec = fromjson("{$bucket : {boundaries : [0, 1, 2]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40198);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNoBoundaries) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x'}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40198);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonExpressionGroupBy) {
+ auto spec = fromjson("{$bucket : {groupBy : {test : 'obj'}, boundaries : [0, 1, 2]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40202);
+
+ spec = fromjson("{$bucket : {groupBy : 'test', boundaries : [0, 1, 2]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40202);
+
+ spec = fromjson("{$bucket : {groupBy : 1, boundaries : [0, 1, 2]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40202);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonArrayBoundaries) {
+ auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : 'test'}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40200);
+
+ spec = fromjson("{$bucket : {groupBy : '$x', boundaries : 1}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40200);
+
+ spec = fromjson("{$bucket : {groupBy : '$x', boundaries : {test : 'obj'}}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40200);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNotEnoughBoundaries) {
+ auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [0]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40192);
+
+ spec = fromjson("{$bucket : {groupBy : '$x', boundaries : []}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40192);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonConstantValueBoundaries) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : ['$x', '$y', '$z']}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40191);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithMixedTypesBoundaries) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [0, 'test']}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40193);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonUniqueBoundaries) {
+ auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [1, 1, 2, 3]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40194);
+
+ spec = fromjson("{$bucket : {groupBy : '$x', boundaries : ['a', 'b', 'b', 'c']}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40194);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonSortedBoundaries) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [4, 5, 3, 6]}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40194);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWithNonConstantExpressionDefault) {
+ const auto spec =
+ fromjson("{$bucket : {groupBy : '$x', boundaries : [0, 1, 2], default : '$x'}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40195);
+}
+
+TEST_F(InvalidBucketSpec, BucketFailsWhenDefaultIsInBoundariesRange) {
+ auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [1, 2, 4], default : 3}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40199);
+
+ spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [1, 2, 4], default : 1}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40199);
+}
+
+TEST_F(InvalidBucketSpec, GroupFailsForBucketWithInvalidOutputField) {
+ auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [1, 2, 3], output : 'test'}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40196);
+
+ spec = fromjson(
+ "{$bucket : {groupBy : '$x', boundaries : [1, 2, 3], output : {number : 'test'}}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40234);
+
+ spec = fromjson(
+ "{$bucket : {groupBy : '$x', boundaries : [1, 2, 3], output : {'test.test' : {$sum : "
+ "1}}}}");
+ ASSERT_THROWS_CODE(createBucket(spec), UserException, 40235);
+}
+
+TEST_F(InvalidBucketSpec, SwitchFailsForBucketWhenNoDefaultSpecified) {
+ const auto spec = fromjson("{$bucket : {groupBy : '$x', boundaries : [1, 2, 3]}}");
+ vector<intrusive_ptr<DocumentSource>> bucketStages = createBucket(spec);
+
+ ASSERT_EQUALS(bucketStages.size(), 2UL);
+
+ auto* groupStage = dynamic_cast<DocumentSourceGroup*>(bucketStages[0].get());
+ ASSERT(groupStage);
+
+ const auto* sortStage = dynamic_cast<DocumentSourceSort*>(bucketStages[1].get());
+ ASSERT(sortStage);
+
+ auto doc = Document{{"x", 4}};
+ auto source = DocumentSourceMock::create(doc);
+ groupStage->setSource(source.get());
+ ASSERT_THROWS_CODE(groupStage->getNext(), UserException, 40066);
+}
+} // namespace
+} // namespace mongo