From 80455be8a4c1e6e63e7236a51668b0e446748b91 Mon Sep 17 00:00:00 2001 From: Jason Rassi Date: Thu, 9 Jan 2014 17:19:12 -0500 Subject: SERVER-12153 SERVER-12171 Restrict valid query expressions with $text CanonicalQuery now refuses to canonicalize queries that contain either: - >1 $text clause - $text inside a $nor clause --- src/mongo/db/query/SConscript | 1 + src/mongo/db/query/canonical_query.cpp | 32 +++++++++++- src/mongo/db/query/canonical_query_test.cpp | 76 +++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript index 0e60cb05970..f010afcdf15 100644 --- a/src/mongo/db/query/SConscript +++ b/src/mongo/db/query/SConscript @@ -99,6 +99,7 @@ env.CppUnitTest( ], LIBDEPS=[ "query_planner", + "$BUILD_DIR/mongo/expressions_text", ], ) diff --git a/src/mongo/db/query/canonical_query.cpp b/src/mongo/db/query/canonical_query.cpp index 7668ab93f5f..af758af0b5a 100644 --- a/src/mongo/db/query/canonical_query.cpp +++ b/src/mongo/db/query/canonical_query.cpp @@ -30,6 +30,7 @@ #include "mongo/db/jsobj.h" #include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/query/query_planner_common.h" namespace mongo { @@ -172,12 +173,39 @@ namespace mongo { return sum; } + /** + * Does 'root' have a subtree of type 'subtreeType' with a node of type 'childType' inside? + */ + bool hasNodeInSubtree(MatchExpression* root, MatchExpression::MatchType childType, + MatchExpression::MatchType subtreeType) { + if (subtreeType == root->matchType()) { + return QueryPlannerCommon::hasNode(root, childType); + } + for (size_t i = 0; i < root->numChildren(); ++i) { + if (hasNodeInSubtree(root->getChild(i), childType, subtreeType)) { + return true; + } + } + return false; + } + // static Status CanonicalQuery::isValid(MatchExpression* root) { // Analysis below should be done after squashing the tree to make it clearer. - // TODO: We want $text in root level or within rooted AND for consistent text score - // availability. + // There can only be one TEXT. If there is a TEXT, it cannot appear inside a NOR. + // + // Note that the query grammar (as enforced by the MatchExpression parser) forbids TEXT + // inside of value-expression clauses like NOT, so we don't check those here. + size_t numText = countNodes(root, MatchExpression::TEXT); + if (numText > 1) { + return Status(ErrorCodes::BadValue, "Too many text expressions"); + } + else if (1 == numText) { + if (hasNodeInSubtree(root, MatchExpression::TEXT, MatchExpression::NOR)) { + return Status(ErrorCodes::BadValue, "text expression not allowed in nor"); + } + } // There can only be one NEAR. If there is a NEAR, it must be either the root or the root // must be an AND and its child must be a NEAR. diff --git a/src/mongo/db/query/canonical_query_test.cpp b/src/mongo/db/query/canonical_query_test.cpp index e95ea3a79b9..bc1be6ed5cd 100644 --- a/src/mongo/db/query/canonical_query_test.cpp +++ b/src/mongo/db/query/canonical_query_test.cpp @@ -47,6 +47,82 @@ namespace { return StatusWithMatchExpression(CanonicalQuery::normalizeTree(swme.getValue())); } + TEST(CanonicalQueryTest, IsValidText) { + auto_ptr me; + StatusWithMatchExpression swme(Status::OK()); + + // Valid: regular TEXT. + swme = parseNormalize("{$text: {$search: 's'}}"); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_OK(CanonicalQuery::isValid(me.get())); + + // Valid: TEXT inside OR. + swme = parseNormalize( + "{$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + "]}" + ); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_OK(CanonicalQuery::isValid(me.get())); + + // Valid: TEXT outside NOR. + swme = parseNormalize("{$text: {$search: 's'}, $nor: [{a: 1}, {b: 1}]}"); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_OK(CanonicalQuery::isValid(me.get())); + + // Invalid: TEXT inside NOR. + swme = parseNormalize("{$nor: [{$text: {$search: 's'}}, {a: 1}]}"); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_NOT_OK(CanonicalQuery::isValid(me.get())); + + // Invalid: TEXT inside NOR. + swme = parseNormalize( + "{$nor: [" + " {$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + " ]}," + " {a: 2}" + "]}" + ); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_NOT_OK(CanonicalQuery::isValid(me.get())); + + // Invalid: >1 TEXT. + swme = parseNormalize( + "{$and: [" + " {$text: {$search: 's'}}," + " {$text: {$search: 't'}}" + "]}" + ); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_NOT_OK(CanonicalQuery::isValid(me.get())); + + // Invalid: >1 TEXT. + swme = parseNormalize( + "{$and: [" + " {$or: [" + " {$text: {$search: 's'}}," + " {a: 1}" + " ]}," + " {$or: [" + " {$text: {$search: 't'}}," + " {b: 1}" + " ]}" + "]}" + ); + ASSERT_OK(swme.getStatus()); + me.reset(swme.getValue()); + ASSERT_NOT_OK(CanonicalQuery::isValid(me.get())); + } + TEST(CanonicalQueryTest, IsValidGeo) { auto_ptr me; StatusWithMatchExpression swme(Status::OK()); -- cgit v1.2.1