// pipelinetests.cpp : Unit tests for some classes within src/mongo/db/pipeline. /** * 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/pch.h" #include "mongo/db/interrupt_status.h" #include "mongo/db/interrupt_status_mongod.h" #include "mongo/db/pipeline/document.h" #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/field_path.h" #include "mongo/db/pipeline/pipeline.h" #include "mongo/dbtests/dbtests.h" namespace PipelineTests { namespace FieldPath { using mongo::FieldPath; /** FieldPath constructed from empty string. */ class Empty { public: void run() { ASSERT_THROWS( FieldPath path( "" ), UserException ); } }; /** FieldPath constructed from empty vector. */ class EmptyVector { public: void run() { vector vec; ASSERT_THROWS( FieldPath path( vec ), MsgAssertionException ); } }; /** FieldPath constructed from a simple string (without dots). */ class Simple { public: void run() { FieldPath path( "foo" ); ASSERT_EQUALS( 1U, path.getPathLength() ); ASSERT_EQUALS( "foo", path.getFieldName( 0 ) ); ASSERT_EQUALS( "foo", path.getPath( false ) ); ASSERT_EQUALS( "$foo", path.getPath( true ) ); } }; /** FieldPath constructed from a single element vector. */ class SimpleVector { public: void run() { vector vec( 1, "foo" ); FieldPath path( vec ); ASSERT_EQUALS( 1U, path.getPathLength() ); ASSERT_EQUALS( "foo", path.getFieldName( 0 ) ); ASSERT_EQUALS( "foo", path.getPath( false ) ); } }; /** FieldPath consisting of a '$' character. */ class DollarSign { public: void run() { ASSERT_THROWS( FieldPath path( "$" ), UserException ); } }; /** FieldPath with a '$' prefix. */ class DollarSignPrefix { public: void run() { ASSERT_THROWS( FieldPath path( "$a" ), UserException ); } }; /** FieldPath constructed from a string with one dot. */ class Dotted { public: void run() { FieldPath path( "foo.bar" ); ASSERT_EQUALS( 2U, path.getPathLength() ); ASSERT_EQUALS( "foo", path.getFieldName( 0 ) ); ASSERT_EQUALS( "bar", path.getFieldName( 1 ) ); ASSERT_EQUALS( "foo.bar", path.getPath( false ) ); ASSERT_EQUALS( "$foo.bar", path.getPath( true ) ); } }; /** FieldPath constructed from a single element vector containing a dot. */ class VectorWithDot { public: void run() { vector vec( 1, "fo.o" ); ASSERT_THROWS( FieldPath path( vec ), UserException ); } }; /** FieldPath constructed from a two element vector. */ class TwoFieldVector { public: void run() { vector vec; vec.push_back( "foo" ); vec.push_back( "bar" ); FieldPath path( vec ); ASSERT_EQUALS( 2U, path.getPathLength() ); ASSERT_EQUALS( "foo.bar", path.getPath( false ) ); } }; /** FieldPath with a '$' prefix in the second field. */ class DollarSignPrefixSecondField { public: void run() { ASSERT_THROWS( FieldPath path( "a.$b" ), UserException ); } }; /** FieldPath constructed from a string with two dots. */ class TwoDotted { public: void run() { FieldPath path( "foo.bar.baz" ); ASSERT_EQUALS( 3U, path.getPathLength() ); ASSERT_EQUALS( "foo", path.getFieldName( 0 ) ); ASSERT_EQUALS( "bar", path.getFieldName( 1 ) ); ASSERT_EQUALS( "baz", path.getFieldName( 2 ) ); ASSERT_EQUALS( "foo.bar.baz", path.getPath( false ) ); } }; /** FieldPath constructed from a string ending in a dot. */ class TerminalDot { public: void run() { ASSERT_THROWS( FieldPath path( "foo." ), UserException ); } }; /** FieldPath constructed from a string beginning with a dot. */ class PrefixDot { public: void run() { ASSERT_THROWS( FieldPath path( ".foo" ), UserException ); } }; /** FieldPath constructed from a string with adjacent dots. */ class AdjacentDots { public: void run() { ASSERT_THROWS( FieldPath path( "foo..bar" ), UserException ); } }; /** FieldPath constructed from a string with one letter between two dots. */ class LetterBetweenDots { public: void run() { FieldPath path( "foo.a.bar" ); ASSERT_EQUALS( 3U, path.getPathLength() ); ASSERT_EQUALS( "foo.a.bar", path.getPath( false ) ); } }; /** FieldPath containing a null character. */ class NullCharacter { public: void run() { ASSERT_THROWS( FieldPath path( string( "foo.b\0r", 7 ) ), UserException ); } }; /** FieldPath constructed with a vector containing a null character. */ class VectorNullCharacter { public: void run() { vector vec; vec.push_back( "foo" ); vec.push_back( string( "b\0r", 3 ) ); ASSERT_THROWS( FieldPath path( vec ), UserException ); } }; /** Tail of a FieldPath. */ class Tail { public: void run() { FieldPath path = FieldPath( "foo.bar" ).tail(); ASSERT_EQUALS( 1U, path.getPathLength() ); ASSERT_EQUALS( "bar", path.getPath( false ) ); } }; /** Tail of a FieldPath with three fields. */ class TailThreeFields { public: void run() { FieldPath path = FieldPath( "foo.bar.baz" ).tail(); ASSERT_EQUALS( 2U, path.getPathLength() ); ASSERT_EQUALS( "bar.baz", path.getPath( false ) ); } }; } // namespace FieldPath namespace Optimizations { using namespace mongo; namespace Sharded { class Base { public: // These all return json arrays of pipeline operators virtual string inputPipeJson() = 0; virtual string shardPipeJson() = 0; virtual string mergePipeJson() = 0; BSONObj pipelineFromJsonArray(const string& array) { return fromjson("{pipeline: " + array + "}"); } virtual void run() { const BSONObj inputBson = pipelineFromJsonArray(inputPipeJson()); const BSONObj shardPipeExpected = pipelineFromJsonArray(shardPipeJson()); const BSONObj mergePipeExpected = pipelineFromJsonArray(mergePipeJson()); intrusive_ptr ctx = new ExpressionContext(InterruptStatusMongod::status, NamespaceString("a.collection")); string errmsg; intrusive_ptr mergePipe = Pipeline::parseCommand(errmsg, inputBson, ctx); ASSERT_EQUALS(errmsg, ""); ASSERT(mergePipe != NULL); intrusive_ptr shardPipe = mergePipe->splitForSharded(); ASSERT(shardPipe != NULL); ASSERT_EQUALS(shardPipe->serialize()["pipeline"], Value(shardPipeExpected["pipeline"])); ASSERT_EQUALS(mergePipe->serialize()["pipeline"], Value(mergePipeExpected["pipeline"])); } virtual ~Base() {}; }; // General test to make sure all optimizations support empty pipelines class Empty : public Base { string inputPipeJson() { return "[]"; } string shardPipeJson() { return "[]"; } string mergePipeJson() { return "[]"; } }; namespace moveFinalUnwindFromShardsToMerger { class OneUnwind : public Base { string inputPipeJson() { return "[{$unwind: '$a'}]}"; } string shardPipeJson() { return "[]}"; } string mergePipeJson() { return "[{$unwind: '$a'}]}"; } }; class TwoUnwind : public Base { string inputPipeJson() { return "[{$unwind: '$a'}, {$unwind: '$b'}]}"; } string shardPipeJson() { return "[]}"; } string mergePipeJson() { return "[{$unwind: '$a'}, {$unwind: '$b'}]}"; } }; class UnwindNotFinal : public Base { string inputPipeJson() { return "[{$unwind: '$a'}, {$match: {a:1}}]}"; } string shardPipeJson() { return "[{$unwind: '$a'}, {$match: {a:1}}]}"; } string mergePipeJson() { return "[]}"; } }; class UnwindWithOther : public Base { string inputPipeJson() { return "[{$match: {a:1}}, {$unwind: '$a'}]}"; } string shardPipeJson() { return "[{$match: {a:1}}]}"; } string mergePipeJson() { return "[{$unwind: '$a'}]}"; } }; } // namespace moveFinalUnwindFromShardsToMerger namespace limitFieldsSentFromShardsToMerger { // These tests use $limit to split the pipelines between shards and merger as it is // always a split point and neutral in terms of needed fields. class NeedWholeDoc : public Base { string inputPipeJson() { return "[{$limit:1}]"; } string shardPipeJson() { return "[{$limit:1}]"; } string mergePipeJson() { return "[{$limit:1}]"; } }; class JustNeedsId : public Base { string inputPipeJson() { return "[{$limit:1}, {$group: {_id: '$_id'}}]"; } string shardPipeJson() { return "[{$limit:1}, {$project: {_id:true}}]"; } string mergePipeJson() { return "[{$limit:1}, {$group: {_id: '$_id'}}]"; } }; class JustNeedsNonId : public Base { string inputPipeJson() { return "[{$limit:1}, {$group: {_id: '$a.b'}}]"; } string shardPipeJson() { return "[{$limit:1}, {$project: {_id: false, a: {b: true}}}]"; } string mergePipeJson() { return "[{$limit:1}, {$group: {_id: '$a.b'}}]"; } }; class NothingNeeded : public Base { string inputPipeJson() { return "[{$limit:1}" ",{$group: {_id: {$const: null}, count: {$sum: {$const: 1}}}}" "]"; } string shardPipeJson() { return "[{$limit:1}" ",{$project: {_id: true}}" "]"; } string mergePipeJson() { return "[{$limit:1}" ",{$group: {_id: {$const: null}, count: {$sum: {$const: 1}}}}" "]"; } }; class JustNeedsMetadata : public Base { // Currently this optimization doesn't handle metadata and the shards assume it // needs to be propagated implicitly. Therefore the $project produced should be // the same as in NothingNeeded. string inputPipeJson() { return "[{$limit:1}, {$project: {_id: false, a: {$meta: 'textScore'}}}]"; } string shardPipeJson() { return "[{$limit:1}, {$project: {_id: true}}]"; } string mergePipeJson() { return "[{$limit:1}, {$project: {_id: false, a: {$meta: 'textScore'}}}]"; } }; class ShardAlreadyExhaustive : public Base { // No new project should be added. This test reflects current behavior where the // 'a' field is still sent because it is explicitly asked for, even though it // isn't actually needed. If this changes in the future, this test will need to // change. string inputPipeJson() { return "[{$project: {_id:true, a:true}}" ",{$limit:1}" ",{$group: {_id: '$_id'}}" "]"; } string shardPipeJson() { return "[{$project: {_id:true, a:true}}" ",{$limit:1}" "]"; } string mergePipeJson() { return "[{$limit:1}" ",{$group: {_id: '$_id'}}" "]"; } }; } // namespace limitFieldsSentFromShardsToMerger } // namespace Sharded } // namespace Optimizations class All : public Suite { public: All() : Suite( "pipeline" ) { } void setupTests() { add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); add(); } } myall; } // namespace PipelineTests