/**
* Copyright (C) 2013 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 .
*
* 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/commands/plan_cache_commands.h
*/
#include "mongo/db/commands/plan_cache_commands.h"
#include
#include "mongo/db/json.h"
#include "mongo/db/matcher/extensions_callback_disallow_extensions.h"
#include "mongo/db/operation_context_noop.h"
#include "mongo/db/query/plan_ranker.h"
#include "mongo/db/query/query_solution.h"
#include "mongo/db/query/query_test_service_context.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/mongoutils/str.h"
using namespace mongo;
namespace {
using std::string;
using std::unique_ptr;
using std::vector;
static const NamespaceString nss("test.collection");
/**
* Tests for planCacheListQueryShapes
*/
/**
* Utility function to get list of keys in the cache.
*/
std::vector getShapes(const PlanCache& planCache) {
BSONObjBuilder bob;
ASSERT_OK(PlanCacheListQueryShapes::list(planCache, &bob));
BSONObj resultObj = bob.obj();
BSONElement shapesElt = resultObj.getField("shapes");
ASSERT_EQUALS(shapesElt.type(), mongo::Array);
vector shapesEltArray = shapesElt.Array();
vector shapes;
for (vector::const_iterator i = shapesEltArray.begin(); i != shapesEltArray.end();
++i) {
const BSONElement& elt = *i;
ASSERT_TRUE(elt.isABSONObj());
BSONObj obj = elt.Obj();
// Check required fields.
// query
BSONElement queryElt = obj.getField("query");
ASSERT_TRUE(queryElt.isABSONObj());
// sort
BSONElement sortElt = obj.getField("sort");
ASSERT_TRUE(sortElt.isABSONObj());
// projection
BSONElement projectionElt = obj.getField("projection");
ASSERT_TRUE(projectionElt.isABSONObj());
// All fields OK. Append to vector.
shapes.push_back(obj.getOwned());
}
return shapes;
}
/**
* Utility function to create a SolutionCacheData
*/
SolutionCacheData* createSolutionCacheData() {
unique_ptr scd(new SolutionCacheData());
scd->tree.reset(new PlanCacheIndexTree());
return scd.release();
}
/**
* Utility function to create a PlanRankingDecision
*/
PlanRankingDecision* createDecision(size_t numPlans) {
unique_ptr why(new PlanRankingDecision());
for (size_t i = 0; i < numPlans; ++i) {
CommonStats common("COLLSCAN");
unique_ptr stats(new PlanStageStats(common, STAGE_COLLSCAN));
stats->specific.reset(new CollectionScanStats());
why->stats.mutableVector().push_back(stats.release());
why->scores.push_back(0U);
why->candidateOrder.push_back(i);
}
return why.release();
}
TEST(PlanCacheCommandsTest, planCacheListQueryShapesEmpty) {
PlanCache empty;
vector shapes = getShapes(empty);
ASSERT_TRUE(shapes.empty());
}
TEST(PlanCacheCommandsTest, planCacheListQueryShapesOneKey) {
QueryTestServiceContext serviceContext;
auto txn = serviceContext.makeOperationContext();
// Create a canonical query
auto lpq = stdx::make_unique(nss);
lpq->setFilter(fromjson("{a: 1}"));
auto statusWithCQ = CanonicalQuery::canonicalize(
txn.get(), std::move(lpq), ExtensionsCallbackDisallowExtensions());
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr cq = std::move(statusWithCQ.getValue());
// Plan cache with one entry
PlanCache planCache;
QuerySolution qs;
qs.cacheData.reset(createSolutionCacheData());
std::vector solns;
solns.push_back(&qs);
planCache.add(*cq, solns, createDecision(1U));
vector shapes = getShapes(planCache);
ASSERT_EQUALS(shapes.size(), 1U);
ASSERT_EQUALS(shapes[0].getObjectField("query"), cq->getQueryObj());
ASSERT_EQUALS(shapes[0].getObjectField("sort"), cq->getParsed().getSort());
ASSERT_EQUALS(shapes[0].getObjectField("projection"), cq->getParsed().getProj());
}
/**
* Tests for planCacheClear
*/
TEST(PlanCacheCommandsTest, planCacheClearAllShapes) {
QueryTestServiceContext serviceContext;
auto txn = serviceContext.makeOperationContext();
// Create a canonical query
auto lpq = stdx::make_unique(nss);
lpq->setFilter(fromjson("{a: 1}"));
auto statusWithCQ = CanonicalQuery::canonicalize(
txn.get(), std::move(lpq), ExtensionsCallbackDisallowExtensions());
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr cq = std::move(statusWithCQ.getValue());
// Plan cache with one entry
PlanCache planCache;
QuerySolution qs;
qs.cacheData.reset(createSolutionCacheData());
std::vector solns;
solns.push_back(&qs);
planCache.add(*cq, solns, createDecision(1U));
ASSERT_EQUALS(getShapes(planCache).size(), 1U);
// Clear cache and confirm number of keys afterwards.
ASSERT_OK(PlanCacheClear::clear(txn.get(), &planCache, nss.ns(), BSONObj()));
ASSERT_EQUALS(getShapes(planCache).size(), 0U);
}
/**
* Tests for PlanCacheCommand::makeCacheKey
* Mostly validation on the input parameters
*/
TEST(PlanCacheCommandsTest, Canonicalize) {
// Invalid parameters
PlanCache planCache;
OperationContextNoop txn;
// Missing query field
ASSERT_NOT_OK(PlanCacheCommand::canonicalize(&txn, nss.ns(), fromjson("{}")).getStatus());
// Query needs to be an object
ASSERT_NOT_OK(
PlanCacheCommand::canonicalize(&txn, nss.ns(), fromjson("{query: 1}")).getStatus());
// Sort needs to be an object
ASSERT_NOT_OK(PlanCacheCommand::canonicalize(&txn, nss.ns(), fromjson("{query: {}, sort: 1}"))
.getStatus());
// Bad query (invalid sort order)
ASSERT_NOT_OK(
PlanCacheCommand::canonicalize(&txn, nss.ns(), fromjson("{query: {}, sort: {a: 0}}"))
.getStatus());
// Valid parameters
auto statusWithCQ =
PlanCacheCommand::canonicalize(&txn, nss.ns(), fromjson("{query: {a: 1, b: 1}}"));
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr query = std::move(statusWithCQ.getValue());
// Equivalent query should generate same key.
statusWithCQ =
PlanCacheCommand::canonicalize(&txn, nss.ns(), fromjson("{query: {b: 1, a: 1}}"));
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr equivQuery = std::move(statusWithCQ.getValue());
ASSERT_EQUALS(planCache.computeKey(*query), planCache.computeKey(*equivQuery));
// Sort query should generate different key from unsorted query.
statusWithCQ = PlanCacheCommand::canonicalize(
&txn, nss.ns(), fromjson("{query: {a: 1, b: 1}, sort: {a: 1, b: 1}}"));
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr sortQuery1 = std::move(statusWithCQ.getValue());
ASSERT_NOT_EQUALS(planCache.computeKey(*query), planCache.computeKey(*sortQuery1));
// Confirm sort arguments are properly delimited (SERVER-17158)
statusWithCQ = PlanCacheCommand::canonicalize(
&txn, nss.ns(), fromjson("{query: {a: 1, b: 1}, sort: {aab: 1}}"));
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr sortQuery2 = std::move(statusWithCQ.getValue());
ASSERT_NOT_EQUALS(planCache.computeKey(*sortQuery1), planCache.computeKey(*sortQuery2));
// Changing order and/or value of predicates should not change key
statusWithCQ = PlanCacheCommand::canonicalize(
&txn, nss.ns(), fromjson("{query: {b: 3, a: 3}, sort: {a: 1, b: 1}}"));
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr sortQuery3 = std::move(statusWithCQ.getValue());
ASSERT_EQUALS(planCache.computeKey(*sortQuery1), planCache.computeKey(*sortQuery3));
// Projected query should generate different key from unprojected query.
statusWithCQ = PlanCacheCommand::canonicalize(
&txn, nss.ns(), fromjson("{query: {a: 1, b: 1}, projection: {_id: 0, a: 1}}"));
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr projectionQuery = std::move(statusWithCQ.getValue());
ASSERT_NOT_EQUALS(planCache.computeKey(*query), planCache.computeKey(*projectionQuery));
}
/**
* Tests for planCacheClear (single query shape)
*/
TEST(PlanCacheCommandsTest, planCacheClearInvalidParameter) {
PlanCache planCache;
OperationContextNoop txn;
// Query field type must be BSON object.
ASSERT_NOT_OK(PlanCacheClear::clear(&txn, &planCache, nss.ns(), fromjson("{query: 12345}")));
ASSERT_NOT_OK(
PlanCacheClear::clear(&txn, &planCache, nss.ns(), fromjson("{query: /keyisnotregex/}")));
// Query must pass canonicalization.
ASSERT_NOT_OK(PlanCacheClear::clear(
&txn, &planCache, nss.ns(), fromjson("{query: {a: {$no_such_op: 1}}}")));
// Sort present without query is an error.
ASSERT_NOT_OK(PlanCacheClear::clear(&txn, &planCache, nss.ns(), fromjson("{sort: {a: 1}}")));
// Projection present without query is an error.
ASSERT_NOT_OK(PlanCacheClear::clear(
&txn, &planCache, nss.ns(), fromjson("{projection: {_id: 0, a: 1}}")));
}
TEST(PlanCacheCommandsTest, planCacheClearUnknownKey) {
PlanCache planCache;
OperationContextNoop txn;
ASSERT_OK(PlanCacheClear::clear(&txn, &planCache, nss.ns(), fromjson("{query: {a: 1}}")));
}
TEST(PlanCacheCommandsTest, planCacheClearOneKey) {
QueryTestServiceContext serviceContext;
auto txn = serviceContext.makeOperationContext();
// Create 2 canonical queries.
auto lpqA = stdx::make_unique(nss);
lpqA->setFilter(fromjson("{a: 1}"));
auto statusWithCQA = CanonicalQuery::canonicalize(
txn.get(), std::move(lpqA), ExtensionsCallbackDisallowExtensions());
ASSERT_OK(statusWithCQA.getStatus());
auto lpqB = stdx::make_unique(nss);
lpqB->setFilter(fromjson("{b: 1}"));
unique_ptr cqA = std::move(statusWithCQA.getValue());
auto statusWithCQB = CanonicalQuery::canonicalize(
txn.get(), std::move(lpqB), ExtensionsCallbackDisallowExtensions());
ASSERT_OK(statusWithCQB.getStatus());
unique_ptr cqB = std::move(statusWithCQB.getValue());
// Create plan cache with 2 entries.
PlanCache planCache;
QuerySolution qs;
qs.cacheData.reset(createSolutionCacheData());
std::vector solns;
solns.push_back(&qs);
planCache.add(*cqA, solns, createDecision(1U));
planCache.add(*cqB, solns, createDecision(1U));
// Check keys in cache before dropping {b: 1}
vector shapesBefore = getShapes(planCache);
ASSERT_EQUALS(shapesBefore.size(), 2U);
BSONObj shapeA =
BSON("query" << cqA->getQueryObj() << "sort" << cqA->getParsed().getSort() << "projection"
<< cqA->getParsed().getProj());
BSONObj shapeB =
BSON("query" << cqB->getQueryObj() << "sort" << cqB->getParsed().getSort() << "projection"
<< cqB->getParsed().getProj());
ASSERT_TRUE(std::find(shapesBefore.begin(), shapesBefore.end(), shapeA) != shapesBefore.end());
ASSERT_TRUE(std::find(shapesBefore.begin(), shapesBefore.end(), shapeB) != shapesBefore.end());
// Drop {b: 1} from cache. Make sure {a: 1} is still in cache afterwards.
BSONObjBuilder bob;
ASSERT_OK(PlanCacheClear::clear(
txn.get(), &planCache, nss.ns(), BSON("query" << cqB->getQueryObj())));
vector shapesAfter = getShapes(planCache);
ASSERT_EQUALS(shapesAfter.size(), 1U);
ASSERT_EQUALS(shapesAfter[0], shapeA);
}
/**
* Tests for planCacheListPlans
*/
/**
* Function to extract plan ID from BSON element.
* Validates planID during extraction.
* Each BSON element contains an embedded BSON object with the following layout:
* {
* plan: ,
* details: ,
* reason: ,
* feedback: ,
* source:
* }
* Compilation note: GCC 4.4 has issues with getPlan() declared as a function object.
*/
BSONObj getPlan(const BSONElement& elt) {
ASSERT_TRUE(elt.isABSONObj());
BSONObj obj = elt.Obj();
// Check required fields.
// details
BSONElement detailsElt = obj.getField("details");
ASSERT_TRUE(detailsElt.isABSONObj());
// reason
BSONElement reasonElt = obj.getField("reason");
ASSERT_TRUE(reasonElt.isABSONObj());
// feedback
BSONElement feedbackElt = obj.getField("feedback");
ASSERT_TRUE(feedbackElt.isABSONObj());
return obj.getOwned();
}
/**
* Utility function to get list of plan IDs for a query in the cache.
*/
vector getPlans(const PlanCache& planCache,
const BSONObj& query,
const BSONObj& sort,
const BSONObj& projection) {
OperationContextNoop txn;
BSONObjBuilder bob;
BSONObj cmdObj = BSON("query" << query << "sort" << sort << "projection" << projection);
ASSERT_OK(PlanCacheListPlans::list(&txn, planCache, nss.ns(), cmdObj, &bob));
BSONObj resultObj = bob.obj();
BSONElement plansElt = resultObj.getField("plans");
ASSERT_EQUALS(plansElt.type(), mongo::Array);
vector planEltArray = plansElt.Array();
ASSERT_FALSE(planEltArray.empty());
vector plans(planEltArray.size());
std::transform(planEltArray.begin(), planEltArray.end(), plans.begin(), getPlan);
return plans;
}
TEST(PlanCacheCommandsTest, planCacheListPlansInvalidParameter) {
PlanCache planCache;
BSONObjBuilder ignored;
OperationContextNoop txn;
// Missing query field is not ok.
ASSERT_NOT_OK(PlanCacheListPlans::list(&txn, planCache, nss.ns(), BSONObj(), &ignored));
// Query field type must be BSON object.
ASSERT_NOT_OK(
PlanCacheListPlans::list(&txn, planCache, nss.ns(), fromjson("{query: 12345}"), &ignored));
ASSERT_NOT_OK(PlanCacheListPlans::list(
&txn, planCache, nss.ns(), fromjson("{query: /keyisnotregex/}"), &ignored));
}
TEST(PlanCacheCommandsTest, planCacheListPlansUnknownKey) {
// Leave the plan cache empty.
PlanCache planCache;
OperationContextNoop txn;
BSONObjBuilder ignored;
ASSERT_OK(
PlanCacheListPlans::list(&txn, planCache, nss.ns(), fromjson("{query: {a: 1}}"), &ignored));
}
TEST(PlanCacheCommandsTest, planCacheListPlansOnlyOneSolutionTrue) {
QueryTestServiceContext serviceContext;
auto txn = serviceContext.makeOperationContext();
// Create a canonical query
auto lpq = stdx::make_unique(nss);
lpq->setFilter(fromjson("{a: 1}"));
auto statusWithCQ = CanonicalQuery::canonicalize(
txn.get(), std::move(lpq), ExtensionsCallbackDisallowExtensions());
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr cq = std::move(statusWithCQ.getValue());
// Plan cache with one entry
PlanCache planCache;
QuerySolution qs;
qs.cacheData.reset(createSolutionCacheData());
std::vector solns;
solns.push_back(&qs);
planCache.add(*cq, solns, createDecision(1U));
vector plans = getPlans(
planCache, cq->getQueryObj(), cq->getParsed().getSort(), cq->getParsed().getProj());
ASSERT_EQUALS(plans.size(), 1U);
}
TEST(PlanCacheCommandsTest, planCacheListPlansOnlyOneSolutionFalse) {
QueryTestServiceContext serviceContext;
auto txn = serviceContext.makeOperationContext();
// Create a canonical query
auto lpq = stdx::make_unique(nss);
lpq->setFilter(fromjson("{a: 1}"));
auto statusWithCQ = CanonicalQuery::canonicalize(
txn.get(), std::move(lpq), ExtensionsCallbackDisallowExtensions());
ASSERT_OK(statusWithCQ.getStatus());
unique_ptr cq = std::move(statusWithCQ.getValue());
// Plan cache with one entry
PlanCache planCache;
QuerySolution qs;
qs.cacheData.reset(createSolutionCacheData());
// Add cache entry with 2 solutions.
std::vector solns;
solns.push_back(&qs);
solns.push_back(&qs);
planCache.add(*cq, solns, createDecision(2U));
vector plans = getPlans(
planCache, cq->getQueryObj(), cq->getParsed().getSort(), cq->getParsed().getProj());
ASSERT_EQUALS(plans.size(), 2U);
}
} // namespace