/** * Copyright (C) 2013 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. */ /** * This file contains tests for mongo/db/query/planner_ixselect.cpp */ #include "mongo/db/query/planner_ixselect.h" #include #include "mongo/db/json.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/query/index_tag.h" #include "mongo/unittest/unittest.h" #include "mongo/util/text.h" using namespace mongo; namespace { using std::unique_ptr; using std::string; using std::vector; /** * Utility function to create MatchExpression */ MatchExpression* parseMatchExpression(const BSONObj& obj) { StatusWithMatchExpression status = MatchExpressionParser::parse(obj); ASSERT_TRUE(status.isOK()); MatchExpression* expr(status.getValue()); return expr; } /** * Utility function to join elements in iterator range with comma */ template string toString(Iter begin, Iter end) { mongoutils::str::stream ss; ss << "["; for (Iter i = begin; i != end; i++) { if (i != begin) { ss << " "; } ss << *i; } ss << "]"; return ss; } /** * Test function for getFields() * Parses query string to obtain MatchExpression which is passed together with prefix * to QueryPlannerIXSelect::getFields() * Results are compared with expected fields (parsed from expectedFieldsStr) */ void testGetFields(const char* query, const char* prefix, const char* expectedFieldsStr) { BSONObj obj = fromjson(query); unique_ptr expr(parseMatchExpression(obj)); unordered_set fields; QueryPlannerIXSelect::getFields(expr.get(), prefix, &fields); // Verify results // First, check that results contain a superset of expected fields. vector expectedFields = StringSplitter::split(expectedFieldsStr, ","); for (vector::const_iterator i = expectedFields.begin(); i != expectedFields.end(); i++) { if (fields.find(*i) == fields.end()) { mongoutils::str::stream ss; ss << "getFields(query=" << query << ", prefix=" << prefix << "): unable to find " << *i << " in result: " << toString(fields.begin(), fields.end()); FAIL(ss); } } // Next, confirm that results do not contain any unexpected fields. if (fields.size() != expectedFields.size()) { mongoutils::str::stream ss; ss << "getFields(query=" << query << ", prefix=" << prefix << "): unexpected fields in result. expected: " << toString(expectedFields.begin(), expectedFields.end()) << ". actual: " << toString(fields.begin(), fields.end()); FAIL(ss); } } /** * Basic test cases for getFields() * Includes logical operators */ TEST(QueryPlannerIXSelectTest, GetFieldsBasic) { // Arguments to test function: query, prefix, comma-delimited list of expected fields testGetFields("{}", "", ""); testGetFields("{a: 1}", "", "a"); testGetFields("{a: 1}", "c.", "c.a"); testGetFields("{a: 1, b: 1}", "", "a,b"); testGetFields("{a: {$in: [1]}}", "", "a"); testGetFields("{$or: [{a: 1}, {b: 1}]}", "", "a,b"); } /** * Array test cases for getFields */ TEST(QueryPlannerIXSelectTest, GetFieldsArray) { testGetFields("{a: {$elemMatch: {b: 1}}}", "", "a.b"); testGetFields("{a: {$all: [{$elemMatch: {b: 1}}]}}", "", "a.b"); } /** * Negation test cases for getFields() * $ne, $nin, $nor */ TEST(QueryPlannerIXSelectTest, GetFieldsNegation) { testGetFields("{a: {$ne: 1}}", "", "a"); testGetFields("{a: {$nin: [1]}}", "", "a"); testGetFields("{$nor: [{a: 1}, {b: 1}]}", "", ""); testGetFields("{$and: [{a: 1}, {a: {$ne: 2}}]}", "", "a"); } /** * Array negation test cases for getFields */ TEST(QueryPlannerIXSelectTest, GetFieldsArrayNegation) { testGetFields("{a: {$elemMatch: {b: {$ne: 1}}}}", "", "a.b"); testGetFields("{a: {$all: [{$elemMatch: {b: {$ne: 1}}}]}}", "", "a.b"); } /** * Performs a pre-order traversal of expression tree. Validates * that all tagged nodes contain an instance of RelevantTag. */ void findRelevantTaggedNodePaths(MatchExpression* root, vector* paths) { MatchExpression::TagData* tag = root->getTag(); if (tag) { StringBuilder buf; tag->debugString(&buf); RelevantTag* r = dynamic_cast(tag); if (!r) { mongoutils::str::stream ss; ss << "tag is not instance of RelevantTag. tree: " << root->toString() << "; tag: " << buf.str(); FAIL(ss); } paths->push_back(r->path); } for (size_t i = 0; i < root->numChildren(); ++i) { findRelevantTaggedNodePaths(root->getChild(i), paths); } } /** * Parses a MatchExpression from query string and passes that along with * prefix to rateIndices. * Verifies results against list of expected paths. * For now, we're only interested in which nodes are tagged. * In future, we may expand this test function to include * validate which indices are assigned to a node. */ void testRateIndicesTaggedNodePaths(const char* query, const char* prefix, const char* expectedPathsStr) { // Parse and rate query. Some of the nodes in the rated tree // will be tagged after the rating process. BSONObj obj = fromjson(query); unique_ptr expr(parseMatchExpression(obj)); // Currently, we tag every indexable node even when no compatible // index is available. Hence, it is fine to pass an empty vector of // indices to rateIndices(). vector indices; QueryPlannerIXSelect::rateIndices(expr.get(), prefix, indices); // Retrieve a list of paths embedded in // tagged nodes. vector paths; findRelevantTaggedNodePaths(expr.get(), &paths); // Compare with expected list of paths. // First verify number of paths retrieved. vector expectedPaths = StringSplitter::split(expectedPathsStr, ","); if (paths.size() != expectedPaths.size()) { mongoutils::str::stream ss; ss << "rateIndices(query=" << query << ", prefix=" << prefix << "): unexpected number of tagged nodes found. expected: " << toString(expectedPaths.begin(), expectedPaths.end()) << ". actual: " << toString(paths.begin(), paths.end()); FAIL(ss); } // Next, check that value and order of each element match between the two lists. for (vector::const_iterator i = paths.begin(), j = expectedPaths.begin(); i != paths.end(); i++, j++) { if (*i == *j) { continue; } mongoutils::str::stream ss; ss << "rateIndices(query=" << query << ", prefix=" << prefix << "): unexpected path found. expected: " << *j << " " << toString(expectedPaths.begin(), expectedPaths.end()) << ". actual: " << *i << " " << toString(paths.begin(), paths.end()); FAIL(ss); } } /** * Basic test cases for rateIndices(). * Includes logical operators. */ TEST(QueryPlannerIXSelectTest, RateIndicesTaggedNodePathsBasic) { // Test arguments: query, prefix, comma-delimited list of expected paths testRateIndicesTaggedNodePaths("{}", "", ""); testRateIndicesTaggedNodePaths("{a: 1}", "", "a"); testRateIndicesTaggedNodePaths("{a: 1}", "c.", "c.a"); testRateIndicesTaggedNodePaths("{a: 1, b: 1}", "", "a,b"); testRateIndicesTaggedNodePaths("{a: {$in: [1]}}", "", "a"); testRateIndicesTaggedNodePaths("{$or: [{a: 1}, {b: 1}]}", "", "a,b"); } /** * Array test cases for rateIndices(). */ TEST(QueryPlannerIXSelectTest, RateIndicesTaggedNodePathArray) { testRateIndicesTaggedNodePaths("{a: {$elemMatch: {b: 1}}}", "", "a.b"); testRateIndicesTaggedNodePaths("{a: {$all: [{$elemMatch: {b: 1}}]}}", "", "a.b"); } /** * Negation test cases for rateIndices(). */ TEST(QueryPlannerIXSelectTest, RateIndicesTaggedNodePathsNegation) { testRateIndicesTaggedNodePaths("{a: {$ne: 1}}", "", "a,a"); testRateIndicesTaggedNodePaths("{a: {$nin: [1]}}", "", "a,a"); testRateIndicesTaggedNodePaths("{$nor: [{a: 1}, {b: 1}]}", "", ""); testRateIndicesTaggedNodePaths("{$and: [{a: 1}, {a: {$ne: 2}}]}", "", "a,a,a"); } /** * Array negation test cases for rateIndices(). */ TEST(QueryPlannerIXSelectTest, RateIndicesTaggedNodePathArrayNegation) { testRateIndicesTaggedNodePaths("{a: {$elemMatch: {b: {$ne: 1}}}}", "", "a.b,a.b"); testRateIndicesTaggedNodePaths("{a: {$all: [{$elemMatch: {b: {$ne: 1}}}]}}", "", "a.b,a.b"); } } // namespace