From bb62b1223f72c3ad8af0b77019c2ce3390d677a1 Mon Sep 17 00:00:00 2001 From: Jason Rassi Date: Thu, 5 Nov 2015 17:46:37 -0500 Subject: SERVER-19510 Refactor $text match expression parsing - Text parsing no longer uses dedicated callback handle; responsibility moved to ExtensionsCallback. - Introduces TextMatchExpressionBase (new base class for existing class TextMatchExpression and new class TextNoOpMatchExpression). --- src/mongo/base/error_codes.err | 2 +- src/mongo/db/exec/projection.h | 2 +- src/mongo/db/exec/stagedebug_cmd.cpp | 5 +- src/mongo/db/fts/fts_query.cpp | 3 - src/mongo/db/fts/fts_query.h | 3 - src/mongo/db/matcher/SConscript | 17 +- src/mongo/db/matcher/expression_parser.cpp | 83 ++++++-- src/mongo/db/matcher/expression_parser.h | 26 ++- src/mongo/db/matcher/expression_parser_text.cpp | 109 ---------- .../db/matcher/expression_parser_text_test.cpp | 139 ------------- src/mongo/db/matcher/expression_text.cpp | 91 +++------ src/mongo/db/matcher/expression_text.h | 43 +--- src/mongo/db/matcher/expression_text_base.cpp | 86 ++++++++ src/mongo/db/matcher/expression_text_base.h | 100 +++++++++ src/mongo/db/matcher/expression_text_noop.cpp | 58 ++++++ src/mongo/db/matcher/expression_text_noop.h | 44 ++++ src/mongo/db/matcher/extensions_callback_noop.cpp | 16 ++ src/mongo/db/matcher/extensions_callback_noop.h | 12 +- src/mongo/db/matcher/extensions_callback_real.cpp | 15 ++ src/mongo/db/matcher/extensions_callback_real.h | 11 +- src/mongo/db/ops/modifier_pull.cpp | 4 +- src/mongo/db/ops/update_driver.cpp | 6 +- src/mongo/db/pipeline/document_source_match.cpp | 5 +- src/mongo/db/query/canonical_query_test.cpp | 3 +- src/mongo/db/query/query_planner_test_fixture.cpp | 9 +- src/mongo/dbtests/SConscript | 2 +- src/mongo/dbtests/expression_where_test.cpp | 116 ----------- .../dbtests/extensions_callback_real_test.cpp | 227 +++++++++++++++++++++ src/mongo/dbtests/query_stage_subplan.cpp | 4 +- 29 files changed, 712 insertions(+), 529 deletions(-) delete mode 100644 src/mongo/db/matcher/expression_parser_text.cpp delete mode 100644 src/mongo/db/matcher/expression_parser_text_test.cpp create mode 100644 src/mongo/db/matcher/expression_text_base.cpp create mode 100644 src/mongo/db/matcher/expression_text_base.h create mode 100644 src/mongo/db/matcher/expression_text_noop.cpp create mode 100644 src/mongo/db/matcher/expression_text_noop.h delete mode 100644 src/mongo/dbtests/expression_where_test.cpp create mode 100644 src/mongo/dbtests/extensions_callback_real_test.cpp diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err index 494b953aab7..966797e6944 100644 --- a/src/mongo/base/error_codes.err +++ b/src/mongo/base/error_codes.err @@ -80,7 +80,7 @@ error_code("OperationIncomplete", 77) error_code("CommandResultSchemaViolation", 78) error_code("UnknownReplWriteConcern", 79) error_code("RoleDataInconsistent", 80) -error_code("NoWhereParseContext", 81) +error_code("NoMatchParseContext", 81) error_code("NoProgressMade", 82) error_code("RemoteResultsUnavailable", 83) error_code("DuplicateKeyValue", 84) diff --git a/src/mongo/db/exec/projection.h b/src/mongo/db/exec/projection.h index 389abcc2123..87a3f97a2fd 100644 --- a/src/mongo/db/exec/projection.h +++ b/src/mongo/db/exec/projection.h @@ -66,7 +66,7 @@ struct ProjectionStageParams { // from. Otherwise, this field is ignored. BSONObj coveredKeyObj; - // Used for creating context for the $where clause processing. Not owned. + // Used for creating context for the match extensions processing. Not owned. const MatchExpressionParser::ExtensionsCallback* extensionsCallback; }; diff --git a/src/mongo/db/exec/stagedebug_cmd.cpp b/src/mongo/db/exec/stagedebug_cmd.cpp index 261df368104..623745f0047 100644 --- a/src/mongo/db/exec/stagedebug_cmd.cpp +++ b/src/mongo/db/exec/stagedebug_cmd.cpp @@ -54,6 +54,7 @@ #include "mongo/db/index/fts_access_method.h" #include "mongo/db/jsobj.h" #include "mongo/db/matcher/expression_parser.h" +#include "mongo/db/matcher/expression_text_base.h" #include "mongo/db/matcher/extensions_callback_real.h" #include "mongo/db/query/plan_executor.h" #include "mongo/stdx/memory.h" @@ -460,8 +461,8 @@ public: if (!params.query.parse(search, fam->getSpec().defaultLanguage().str().c_str(), - fts::FTSQuery::caseSensitiveDefault, - fts::FTSQuery::diacriticSensitiveDefault, + TextMatchExpressionBase::kCaseSensitiveDefault, + TextMatchExpressionBase::kDiacriticSensitiveDefault, fam->getSpec().getTextIndexVersion()).isOK()) { return NULL; } diff --git a/src/mongo/db/fts/fts_query.cpp b/src/mongo/db/fts/fts_query.cpp index f162481066b..3e77701f79f 100644 --- a/src/mongo/db/fts/fts_query.cpp +++ b/src/mongo/db/fts/fts_query.cpp @@ -49,9 +49,6 @@ using std::string; using std::stringstream; using std::vector; -const bool FTSQuery::caseSensitiveDefault = false; -const bool FTSQuery::diacriticSensitiveDefault = false; - Status FTSQuery::parse(const string& query, StringData language, bool caseSensitive, diff --git a/src/mongo/db/fts/fts_query.h b/src/mongo/db/fts/fts_query.h index ea1882e4baf..ae05100ea37 100644 --- a/src/mongo/db/fts/fts_query.h +++ b/src/mongo/db/fts/fts_query.h @@ -89,9 +89,6 @@ public: BSONObj toBSON() const; - static const bool caseSensitiveDefault; - static const bool diacriticSensitiveDefault; - private: void _addTerms(FTSTokenizer* tokenizer, const std::string& tokens, bool negated); diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index b4b62c29b55..506e2ba1cbc 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -32,16 +32,19 @@ env.Library( 'expression_leaf.cpp', 'expression_parser.cpp', 'expression_parser_tree.cpp', + 'expression_text_base.cpp', + 'expression_text_noop.cpp', 'expression_tree.cpp', 'expression_where_base.cpp', 'expression_where_noop.cpp', 'extensions_callback_noop.cpp', 'match_details.cpp', 'matchable.cpp', - "matcher.cpp", + 'matcher.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/base', + '$BUILD_DIR/mongo/bson/util/bson_extract', '$BUILD_DIR/mongo/db/common', '$BUILD_DIR/third_party/shim_pcrecpp', 'path', @@ -122,7 +125,6 @@ env.Library( target='expressions_text', source=[ 'expression_text.cpp', - 'expression_parser_text.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/db/fts/base', @@ -130,16 +132,6 @@ env.Library( ], ) -env.CppUnitTest( - target='expression_text_test', - source=[ - 'expression_parser_text_test.cpp', - ], - LIBDEPS=[ - 'expressions_text', - ], -) - env.Library( target='expressions_mongod_only', source=[ @@ -150,5 +142,6 @@ env.Library( '$BUILD_DIR/mongo/db/auth/authorization_manager_global', '$BUILD_DIR/mongo/scripting/scripting_server', 'expressions', + 'expressions_text', ], ) diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp index afbff931422..6692e41aead 100644 --- a/src/mongo/db/matcher/expression_parser.cpp +++ b/src/mongo/db/matcher/expression_parser.cpp @@ -33,6 +33,7 @@ #include "mongo/bson/bsonmisc.h" #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/util/bson_extract.h" #include "mongo/db/matcher/expression_array.h" #include "mongo/db/matcher/expression_leaf.h" #include "mongo/db/matcher/expression_tree.h" @@ -332,10 +333,7 @@ StatusWithMatchExpression MatchExpressionParser::_parse(const BSONObj& obj, int return s; root->add(s.getValue().release()); } else if (mongoutils::str::equals("text", rest)) { - if (e.type() != Object) { - return {Status(ErrorCodes::BadValue, "$text expects an object")}; - } - StatusWithMatchExpression s = expressionParserTextCallback(e.Obj()); + StatusWithMatchExpression s = _extensionsCallback->parseText(e); if (!s.isOK()) { return s; } @@ -957,7 +955,12 @@ StatusWith> MatchExpressionParser::_parseBitPositionsArray StatusWithMatchExpression MatchExpressionParser::ExtensionsCallback::parseWhere( BSONElement where) const { - return {Status(ErrorCodes::NoWhereParseContext, "no context for parsing $where")}; + return {Status(ErrorCodes::NoMatchParseContext, "no context for parsing $where")}; +} + +StatusWithMatchExpression MatchExpressionParser::ExtensionsCallback::parseText( + BSONElement text) const { + return {Status(ErrorCodes::NoMatchParseContext, "no context for parsing $text")}; } StatusWith @@ -985,6 +988,68 @@ MatchExpressionParser::ExtensionsCallback::extractWhereMatchExpressionParams(BSO return params; } +StatusWith +MatchExpressionParser::ExtensionsCallback::extractTextMatchExpressionParams(BSONElement text) { + TextMatchExpressionBase::TextParams params; + if (text.type() != Object) { + return {ErrorCodes::BadValue, "$text expects an object"}; + } + BSONObj queryObj = text.Obj(); + + // + // Parse required fields. + // + + Status queryStatus = bsonExtractStringField(queryObj, "$search", ¶ms.query); + if (!queryStatus.isOK()) { + return queryStatus; + } + + // + // Parse optional fields. + // + + int expectedFieldCount = 1; + + Status languageStatus = bsonExtractStringField(queryObj, "$language", ¶ms.language); + if (languageStatus == ErrorCodes::TypeMismatch) { + return languageStatus; + } else if (languageStatus == ErrorCodes::NoSuchKey) { + params.language = string(); + } else { + invariantOK(languageStatus); + expectedFieldCount++; + } + + Status caseSensitiveStatus = + bsonExtractBooleanField(queryObj, "$caseSensitive", ¶ms.caseSensitive); + if (caseSensitiveStatus == ErrorCodes::TypeMismatch) { + return caseSensitiveStatus; + } else if (caseSensitiveStatus == ErrorCodes::NoSuchKey) { + params.caseSensitive = TextMatchExpressionBase::kCaseSensitiveDefault; + } else { + invariantOK(caseSensitiveStatus); + expectedFieldCount++; + } + + Status diacriticSensitiveStatus = + bsonExtractBooleanField(queryObj, "$diacriticSensitive", ¶ms.diacriticSensitive); + if (diacriticSensitiveStatus == ErrorCodes::TypeMismatch) { + return diacriticSensitiveStatus; + } else if (diacriticSensitiveStatus == ErrorCodes::NoSuchKey) { + params.diacriticSensitive = TextMatchExpressionBase::kDiacriticSensitiveDefault; + } else { + invariantOK(diacriticSensitiveStatus); + expectedFieldCount++; + } + + if (queryObj.nFields() != expectedFieldCount) { + return {ErrorCodes::BadValue, "extra fields in $text"}; + } + + return {std::move(params)}; +} + // Geo StatusWithMatchExpression expressionParserGeoCallbackDefault(const char* name, int type, @@ -993,12 +1058,4 @@ StatusWithMatchExpression expressionParserGeoCallbackDefault(const char* name, } MatchExpressionParserGeoCallback expressionParserGeoCallback = expressionParserGeoCallbackDefault; - -// Text -StatusWithMatchExpression expressionParserTextCallbackDefault(const BSONObj& queryObj) { - return {Status(ErrorCodes::BadValue, "$text not linked in")}; -} - -MatchExpressionParserTextCallback expressionParserTextCallback = - expressionParserTextCallbackDefault; } diff --git a/src/mongo/db/matcher/expression_parser.h b/src/mongo/db/matcher/expression_parser.h index 4b7bf4a751c..5af324291da 100644 --- a/src/mongo/db/matcher/expression_parser.h +++ b/src/mongo/db/matcher/expression_parser.h @@ -35,6 +35,7 @@ #include "mongo/db/matcher/expression.h" #include "mongo/db/matcher/expression_leaf.h" #include "mongo/db/matcher/expression_tree.h" +#include "mongo/db/matcher/expression_text_base.h" #include "mongo/db/matcher/expression_where_base.h" #include "mongo/stdx/functional.h" @@ -47,20 +48,29 @@ typedef StatusWith> StatusWithMatchExpression; class MatchExpressionParser { public: /** - * In general, expression parsing and matching should not require context, but the $where - * clause is an exception in that it needs to read the sys.js collection. + * Certain match clauses (the "extension" clauses, namely $text and $where) require context in + * order to perform parsing. This context is captured inside of an ExtensionsCallback object. * - * The default behaviour is to return an error status that $where context is not present. - * - * Do not use this class to pass-in generic context as it should only be used for $where. + * The default implementations of parseText() and parseWhere() simply return an error Status. + * Instead of constructing an ExtensionsCallback object directly, an instance of one of the + * derived classes (ExtensionsCallbackReal or ExtensionsCallbackNoop) should generally be used + * instead. */ class ExtensionsCallback { public: + virtual StatusWithMatchExpression parseText(BSONElement text) const; + virtual StatusWithMatchExpression parseWhere(BSONElement where) const; virtual ~ExtensionsCallback() {} protected: + /** + * Helper method which extracts parameters from the given $text element. + */ + static StatusWith extractTextMatchExpressionParams( + BSONElement text); + /** * Helper method which extracts parameters from the given $where element. */ @@ -170,7 +180,7 @@ private: // The maximum allowed depth of a query tree. Just to guard against stack overflow. static const int kMaximumTreeDepth; - // Performs parsing for the $where clause. We do not own this pointer - it has to live + // Performs parsing for the match extensions. We do not own this pointer - it has to live // as long as the parser is active. const ExtensionsCallback* _extensionsCallback; }; @@ -178,8 +188,4 @@ private: typedef stdx::function MatchExpressionParserGeoCallback; extern MatchExpressionParserGeoCallback expressionParserGeoCallback; - -typedef stdx::function - MatchExpressionParserTextCallback; -extern MatchExpressionParserTextCallback expressionParserTextCallback; } diff --git a/src/mongo/db/matcher/expression_parser_text.cpp b/src/mongo/db/matcher/expression_parser_text.cpp deleted file mode 100644 index 6968dc6c0cb..00000000000 --- a/src/mongo/db/matcher/expression_parser_text.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// expression_parser_text.cpp - -/** - * 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. - */ - -#include "mongo/base/init.h" -#include "mongo/db/fts/fts_language.h" -#include "mongo/db/fts/fts_spec.h" -#include "mongo/db/jsobj.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/db/matcher/expression_text.h" - -namespace mongo { - -using std::unique_ptr; -using std::string; - -StatusWithMatchExpression expressionParserTextCallbackReal(const BSONObj& queryObj) { - // Validate queryObj, but defer construction of FTSQuery (which requires access to the - // target namespace) until stage building time. - - int expectedFieldCount = 1; - - if (mongo::String != queryObj["$search"].type()) { - return StatusWithMatchExpression(ErrorCodes::TypeMismatch, - "$search requires a string value"); - } - - string language = ""; - BSONElement languageElt = queryObj["$language"]; - if (!languageElt.eoo()) { - expectedFieldCount++; - if (mongo::String != languageElt.type()) { - return StatusWithMatchExpression(ErrorCodes::TypeMismatch, - "$language requires a string value"); - } - language = languageElt.String(); - Status status = fts::FTSLanguage::make(language, fts::TEXT_INDEX_VERSION_2).getStatus(); - if (!status.isOK()) { - return StatusWithMatchExpression(ErrorCodes::BadValue, - "$language specifies unsupported language"); - } - } - string query = queryObj["$search"].String(); - - BSONElement caseSensitiveElt = queryObj["$caseSensitive"]; - bool caseSensitive = fts::FTSQuery::caseSensitiveDefault; - if (!caseSensitiveElt.eoo()) { - expectedFieldCount++; - if (mongo::Bool != caseSensitiveElt.type()) { - return StatusWithMatchExpression(ErrorCodes::TypeMismatch, - "$caseSensitive requires a boolean value"); - } - caseSensitive = caseSensitiveElt.trueValue(); - } - - BSONElement diacriticSensitiveElt = queryObj["$diacriticSensitive"]; - bool diacriticSensitive = fts::FTSQuery::diacriticSensitiveDefault; - if (!diacriticSensitiveElt.eoo()) { - expectedFieldCount++; - if (mongo::Bool != diacriticSensitiveElt.type()) { - return StatusWithMatchExpression(ErrorCodes::TypeMismatch, - "$diacriticSensitive requires a boolean value"); - } - diacriticSensitive = diacriticSensitiveElt.trueValue(); - } - - if (queryObj.nFields() != expectedFieldCount) { - return StatusWithMatchExpression(ErrorCodes::BadValue, "extra fields in $text"); - } - - unique_ptr e(new TextMatchExpression()); - Status s = e->init(query, language, caseSensitive, diacriticSensitive); - if (!s.isOK()) { - return StatusWithMatchExpression(s); - } - return {std::move(e)}; -} - -MONGO_INITIALIZER(MatchExpressionParserText)(::mongo::InitializerContext* context) { - expressionParserTextCallback = expressionParserTextCallbackReal; - return Status::OK(); -} -} diff --git a/src/mongo/db/matcher/expression_parser_text_test.cpp b/src/mongo/db/matcher/expression_parser_text_test.cpp deleted file mode 100644 index 3d51604f7ef..00000000000 --- a/src/mongo/db/matcher/expression_parser_text_test.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// expression_parser_text_test.cpp - -/** - * 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. - */ - -#include "mongo/unittest/unittest.h" - -#include "mongo/db/matcher/expression_parser.h" - -#include "mongo/db/jsobj.h" -#include "mongo/db/json.h" -#include "mongo/db/matcher/expression.h" -#include "mongo/db/matcher/expression_text.h" - -namespace mongo { - -TEST(MatchExpressionParserText, Basic) { - BSONObj query = fromjson("{$text: {$search:\"awesome\", $language:\"english\"}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_TRUE(result.isOK()); - - ASSERT_EQUALS(MatchExpression::TEXT, result.getValue()->matchType()); - std::unique_ptr textExp( - static_cast(result.getValue().release())); - ASSERT_EQUALS(textExp->getQuery(), "awesome"); - ASSERT_EQUALS(textExp->getLanguage(), "english"); - ASSERT_EQUALS(textExp->getCaseSensitive(), fts::FTSQuery::caseSensitiveDefault); - ASSERT_EQUALS(textExp->getDiacriticSensitive(), fts::FTSQuery::diacriticSensitiveDefault); -} - -TEST(MatchExpressionParserText, LanguageError) { - BSONObj query = fromjson("{$text: {$search:\"awesome\", $language:\"spanglish\"}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_FALSE(result.isOK()); -} - -TEST(MatchExpressionParserText, CaseSensitiveTrue) { - BSONObj query = fromjson("{$text: {$search:\"awesome\", $caseSensitive: true}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_TRUE(result.isOK()); - - ASSERT_EQUALS(MatchExpression::TEXT, result.getValue()->matchType()); - std::unique_ptr textExp( - static_cast(result.getValue().release())); - ASSERT_EQUALS(textExp->getCaseSensitive(), true); -} - -TEST(MatchExpressionParserText, CaseSensitiveFalse) { - BSONObj query = fromjson("{$text: {$search:\"awesome\", $caseSensitive: false}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_TRUE(result.isOK()); - - ASSERT_EQUALS(MatchExpression::TEXT, result.getValue()->matchType()); - std::unique_ptr textExp( - static_cast(result.getValue().release())); - ASSERT_EQUALS(textExp->getCaseSensitive(), false); -} - -TEST(MatchExpressionParserText, CaseSensitiveError) { - BSONObj query = fromjson("{$text:{$search:\"awesome\", $caseSensitive: 0}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_FALSE(result.isOK()); -} - -TEST(MatchExpressionParserText, DiacriticSensitiveTrue) { - BSONObj query = fromjson("{$text: {$search:\"awesome\", $diacriticSensitive: true}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_TRUE(result.isOK()); - - ASSERT_EQUALS(MatchExpression::TEXT, result.getValue()->matchType()); - std::unique_ptr textExp( - static_cast(result.getValue().release())); - ASSERT_EQUALS(textExp->getDiacriticSensitive(), true); -} - -TEST(MatchExpressionParserText, DiacriticSensitiveFalse) { - BSONObj query = fromjson("{$text: {$search:\"awesome\", $diacriticSensitive: false}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_TRUE(result.isOK()); - - ASSERT_EQUALS(MatchExpression::TEXT, result.getValue()->matchType()); - std::unique_ptr textExp( - static_cast(result.getValue().release())); - ASSERT_EQUALS(textExp->getDiacriticSensitive(), false); -} - -TEST(MatchExpressionParserText, DiacriticSensitiveError) { - BSONObj query = fromjson("{$text:{$search:\"awesome\", $diacriticSensitive: 0}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_FALSE(result.isOK()); -} - -TEST(MatchExpressionParserText, DiacriticSensitiveAndCaseSensitiveTrue) { - BSONObj query = - fromjson("{$text: {$search:\"awesome\", $diacriticSensitive: true, $caseSensitive: true}}"); - - StatusWithMatchExpression result = MatchExpressionParser::parse(query); - ASSERT_TRUE(result.isOK()); - - ASSERT_EQUALS(MatchExpression::TEXT, result.getValue()->matchType()); - std::unique_ptr textExp( - static_cast(result.getValue().release())); - ASSERT_EQUALS(textExp->getDiacriticSensitive(), true); - ASSERT_EQUALS(textExp->getCaseSensitive(), true); -} -} diff --git a/src/mongo/db/matcher/expression_text.cpp b/src/mongo/db/matcher/expression_text.cpp index b5ccd9f40bd..b371357970d 100644 --- a/src/mongo/db/matcher/expression_text.cpp +++ b/src/mongo/db/matcher/expression_text.cpp @@ -29,82 +29,41 @@ */ #include "mongo/platform/basic.h" + #include "mongo/db/matcher/expression_text.h" + +#include "mongo/db/fts/fts_language.h" #include "mongo/stdx/memory.h" namespace mongo { -using std::string; -using std::unique_ptr; -using stdx::make_unique; - -Status TextMatchExpression::init(const string& query, - const string& language, - bool caseSensitive, - bool diacriticSensitive) { - _query = query; - _language = language; - _caseSensitive = caseSensitive; - _diacriticSensitive = diacriticSensitive; - return initPath("_fts"); -} - -bool TextMatchExpression::matchesSingleElement(const BSONElement& e) const { - // See ops/update.cpp. - // This node is removed by the query planner. It's only ever called if we're getting an - // elemMatchKey. - return true; -} +TextMatchExpression::TextMatchExpression(TextParams params) + : TextMatchExpressionBase(std::move(params)) {} -void TextMatchExpression::debugString(StringBuilder& debug, int level) const { - _debugAddSpace(debug, level); - debug << "TEXT : query=" << _query << ", language=" << _language - << ", caseSensitive=" << _caseSensitive << ", diacriticSensitive=" << _diacriticSensitive - << ", tag="; - MatchExpression::TagData* td = getTag(); - if (NULL != td) { - td->debugString(&debug); - } else { - debug << "NULL"; +Status TextMatchExpression::init() { + // Validate language, but defer construction of FTSQuery (which requires access to the target + // namespace) until stage building time. + if (!getLanguage().empty()) { + if (!fts::FTSLanguage::make(getLanguage(), fts::TEXT_INDEX_VERSION_2).isOK()) { + return {ErrorCodes::BadValue, "$language specifies unsupported language"}; + } } - debug << "\n"; -} - -void TextMatchExpression::toBSON(BSONObjBuilder* out) const { - out->append("$text", - BSON("$search" << _query << "$language" << _language << "$caseSensitive" - << _caseSensitive << "$diacriticSensitive" << _diacriticSensitive)); -} -bool TextMatchExpression::equivalent(const MatchExpression* other) const { - if (matchType() != other->matchType()) { - return false; - } - const TextMatchExpression* realOther = static_cast(other); - - // TODO This is way too crude. It looks for string equality, but it should be looking for - // common parsed form - if (realOther->getQuery() != _query) { - return false; - } - if (realOther->getLanguage() != _language) { - return false; - } - if (realOther->getCaseSensitive() != _caseSensitive) { - return false; - } - if (realOther->getDiacriticSensitive() != _diacriticSensitive) { - return false; - } - return true; + return initPath("_fts"); } -unique_ptr TextMatchExpression::shallowClone() const { - unique_ptr next = make_unique(); - next->init(_query, _language, _caseSensitive, _diacriticSensitive); +std::unique_ptr TextMatchExpression::shallowClone() const { + TextParams params; + params.query = getQuery(); + params.language = getLanguage(); + params.caseSensitive = getCaseSensitive(); + params.diacriticSensitive = getDiacriticSensitive(); + auto expr = stdx::make_unique(std::move(params)); + expr->init(); if (getTag()) { - next->setTag(getTag()->clone()); + expr->setTag(getTag()->clone()); } - return std::move(next); -} + return std::move(expr); } + +} // namespace mongo diff --git a/src/mongo/db/matcher/expression_text.h b/src/mongo/db/matcher/expression_text.h index 371ad037111..dfe4cbd8204 100644 --- a/src/mongo/db/matcher/expression_text.h +++ b/src/mongo/db/matcher/expression_text.h @@ -30,50 +30,17 @@ #pragma once -#include "mongo/db/fts/fts_query.h" -#include "mongo/db/matcher/expression.h" -#include "mongo/db/matcher/expression_leaf.h" +#include "mongo/db/matcher/expression_text_base.h" namespace mongo { -class TextMatchExpression : public LeafMatchExpression { +class TextMatchExpression : public TextMatchExpressionBase { public: - TextMatchExpression() : LeafMatchExpression(TEXT) {} - virtual ~TextMatchExpression() {} + TextMatchExpression(TextParams params); - Status init(const std::string& query, - const std::string& language, - bool caseSensitive, - bool diacriticSensitive); + Status init(); - virtual bool matchesSingleElement(const BSONElement& e) const; - - virtual void debugString(StringBuilder& debug, int level = 0) const; - - virtual void toBSON(BSONObjBuilder* out) const; - - virtual bool equivalent(const MatchExpression* other) const; - - virtual std::unique_ptr shallowClone() const; - - const std::string& getQuery() const { - return _query; - } - const std::string& getLanguage() const { - return _language; - } - bool getCaseSensitive() const { - return _caseSensitive; - } - bool getDiacriticSensitive() const { - return _diacriticSensitive; - } - -private: - std::string _query; - std::string _language; - bool _caseSensitive; - bool _diacriticSensitive; + std::unique_ptr shallowClone() const final; }; } // namespace mongo diff --git a/src/mongo/db/matcher/expression_text_base.cpp b/src/mongo/db/matcher/expression_text_base.cpp new file mode 100644 index 00000000000..04b62ecd6b1 --- /dev/null +++ b/src/mongo/db/matcher/expression_text_base.cpp @@ -0,0 +1,86 @@ +/** + * Copyright (C) 2015 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/matcher/expression_text_base.h" + +namespace mongo { + +const bool TextMatchExpressionBase::kCaseSensitiveDefault = false; +const bool TextMatchExpressionBase::kDiacriticSensitiveDefault = false; + +TextMatchExpressionBase::TextMatchExpressionBase(TextParams params) + : LeafMatchExpression(TEXT), + _query(std::move(params.query)), + _language(std::move(params.language)), + _caseSensitive(params.caseSensitive), + _diacriticSensitive(params.diacriticSensitive) {} + +void TextMatchExpressionBase::debugString(StringBuilder& debug, int level) const { + _debugAddSpace(debug, level); + debug << "TEXT : query=" << _query << ", language=" << _language + << ", caseSensitive=" << _caseSensitive << ", diacriticSensitive=" << _diacriticSensitive + << ", tag="; + MatchExpression::TagData* td = getTag(); + if (NULL != td) { + td->debugString(&debug); + } else { + debug << "NULL"; + } + debug << "\n"; +} + +void TextMatchExpressionBase::toBSON(BSONObjBuilder* out) const { + out->append("$text", + BSON("$search" << _query << "$language" << _language << "$caseSensitive" + << _caseSensitive << "$diacriticSensitive" << _diacriticSensitive)); +} + +bool TextMatchExpressionBase::equivalent(const MatchExpression* other) const { + if (matchType() != other->matchType()) { + return false; + } + const TextMatchExpressionBase* realOther = static_cast(other); + + if (realOther->getQuery() != _query) { + return false; + } + if (realOther->getLanguage() != _language) { + return false; + } + if (realOther->getCaseSensitive() != _caseSensitive) { + return false; + } + if (realOther->getDiacriticSensitive() != _diacriticSensitive) { + return false; + } + return true; +} + +} // namespace mongo diff --git a/src/mongo/db/matcher/expression_text_base.h b/src/mongo/db/matcher/expression_text_base.h new file mode 100644 index 00000000000..8ca25cf60eb --- /dev/null +++ b/src/mongo/db/matcher/expression_text_base.h @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2015 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. + */ + +#pragma once + +#include "mongo/db/matcher/expression_leaf.h" + +namespace mongo { + +/** + * Common base class for $text match expression implementations. + */ +class TextMatchExpressionBase : public LeafMatchExpression { +public: + struct TextParams { + std::string query; + std::string language; + bool caseSensitive; + bool diacriticSensitive; + }; + + static const bool kCaseSensitiveDefault; + static const bool kDiacriticSensitiveDefault; + + TextMatchExpressionBase(TextParams params); + + const std::string& getQuery() const { + return _query; + } + + const std::string& getLanguage() const { + return _language; + } + + bool getCaseSensitive() const { + return _caseSensitive; + } + + bool getDiacriticSensitive() const { + return _diacriticSensitive; + } + + // + // Methods inherited from MatchExpression. + // + + bool matchesSingleElement(const BSONElement& e) const final { + // Text match expressions force the selection of the text index and always generate EXACT + // index bounds (which causes the MatchExpression node to be trimmed), so we don't currently + // implement any explicit text matching logic here. SERVER-17648 tracks the work to + // implement a real text matcher. + // + // TODO: simply returning 'true' here isn't quite correct. First, we should be overriding + // matches() instead of matchesSingleElement(), because the latter is only ever called if + // the matched document has an element with path "_fts". Second, there are scenarios where + // we will use the untrimmed expression tree for matching (for example, when deciding + // whether or not to retry an operation that throws WriteConflictException); in those cases, + // we won't be able to correctly determine whether or not the object matches the expression. + return true; + } + + void debugString(StringBuilder& debug, int level = 0) const final; + + void toBSON(BSONObjBuilder* out) const final; + + bool equivalent(const MatchExpression* other) const final; + +private: + const std::string _query; + const std::string _language; + const bool _caseSensitive; + const bool _diacriticSensitive; +}; + +} // namespace mongo diff --git a/src/mongo/db/matcher/expression_text_noop.cpp b/src/mongo/db/matcher/expression_text_noop.cpp new file mode 100644 index 00000000000..7d2fb5c84b7 --- /dev/null +++ b/src/mongo/db/matcher/expression_text_noop.cpp @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2015 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/matcher/expression_text_noop.h" + +#include "mongo/stdx/memory.h" + +namespace mongo { + +TextNoOpMatchExpression::TextNoOpMatchExpression(TextParams params) + : TextMatchExpressionBase(std::move(params)) {} + +Status TextNoOpMatchExpression::init() { + return initPath("_fts"); +} + +std::unique_ptr TextNoOpMatchExpression::shallowClone() const { + TextParams params; + params.query = getQuery(); + params.language = getLanguage(); + params.caseSensitive = getCaseSensitive(); + params.diacriticSensitive = getDiacriticSensitive(); + auto expr = stdx::make_unique(std::move(params)); + expr->init(); + if (getTag()) { + expr->setTag(getTag()->clone()); + } + return std::move(expr); +} + +} // namespace mongo diff --git a/src/mongo/db/matcher/expression_text_noop.h b/src/mongo/db/matcher/expression_text_noop.h new file mode 100644 index 00000000000..de02bcfa3cf --- /dev/null +++ b/src/mongo/db/matcher/expression_text_noop.h @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2015 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. + */ + +#pragma once + +#include "mongo/db/matcher/expression_text_base.h" + +namespace mongo { + +class TextNoOpMatchExpression : public TextMatchExpressionBase { +public: + TextNoOpMatchExpression(TextParams params); + + Status init(); + + std::unique_ptr shallowClone() const final; +}; + +} // namespace mongo diff --git a/src/mongo/db/matcher/extensions_callback_noop.cpp b/src/mongo/db/matcher/extensions_callback_noop.cpp index ca51ae22b76..e5941f73562 100644 --- a/src/mongo/db/matcher/extensions_callback_noop.cpp +++ b/src/mongo/db/matcher/extensions_callback_noop.cpp @@ -30,10 +30,26 @@ #include "mongo/db/matcher/extensions_callback_noop.h" +#include "mongo/db/matcher/expression_text_noop.h" #include "mongo/db/matcher/expression_where_noop.h" namespace mongo { +StatusWithMatchExpression ExtensionsCallbackNoop::parseText(BSONElement text) const { + auto textParams = extractTextMatchExpressionParams(text); + if (!textParams.isOK()) { + return textParams.getStatus(); + } + + auto expr = stdx::make_unique(std::move(textParams.getValue())); + Status initStatus = expr->init(); + if (!initStatus.isOK()) { + return initStatus; + } + + return {std::move(expr)}; +} + StatusWithMatchExpression ExtensionsCallbackNoop::parseWhere(BSONElement where) const { auto whereParams = extractWhereMatchExpressionParams(where); if (!whereParams.isOK()) { diff --git a/src/mongo/db/matcher/extensions_callback_noop.h b/src/mongo/db/matcher/extensions_callback_noop.h index 9ac019e3947..b66955add43 100644 --- a/src/mongo/db/matcher/extensions_callback_noop.h +++ b/src/mongo/db/matcher/extensions_callback_noop.h @@ -33,10 +33,20 @@ namespace mongo { /** - * This is just a pass-through implementation, used by sharding only. + * ExtensionsCallbackNoop does not capture any context, and produces "no op" expressions that can't + * be used for matching. It should be used when parsing context is not available (for example, when + * the relevant namespace does not exist, or in mongos). */ class ExtensionsCallbackNoop : public MatchExpressionParser::ExtensionsCallback { public: + /** + * Returns a TextNoOpMatchExpression, or an error Status if parsing fails. + */ + StatusWithMatchExpression parseText(BSONElement text) const final; + + /** + * Returns a WhereNoOpMatchExpression, or an error Status if parsing fails. + */ StatusWithMatchExpression parseWhere(BSONElement where) const final; }; diff --git a/src/mongo/db/matcher/extensions_callback_real.cpp b/src/mongo/db/matcher/extensions_callback_real.cpp index e07a098fc87..161e0fb2e79 100644 --- a/src/mongo/db/matcher/extensions_callback_real.cpp +++ b/src/mongo/db/matcher/extensions_callback_real.cpp @@ -30,6 +30,7 @@ #include "mongo/db/matcher/extensions_callback_real.h" +#include "mongo/db/matcher/expression_text.h" #include "mongo/db/matcher/expression_where.h" #include "mongo/db/namespace_string.h" @@ -38,6 +39,20 @@ namespace mongo { ExtensionsCallbackReal::ExtensionsCallbackReal(OperationContext* txn, const NamespaceString* nss) : _txn(txn), _nss(nss) {} +StatusWithMatchExpression ExtensionsCallbackReal::parseText(BSONElement text) const { + auto textParams = extractTextMatchExpressionParams(text); + if (!textParams.isOK()) { + return textParams.getStatus(); + } + + auto exp = stdx::make_unique(std::move(textParams.getValue())); + Status status = exp->init(); + if (!status.isOK()) { + return status; + } + return {std::move(exp)}; +} + StatusWithMatchExpression ExtensionsCallbackReal::parseWhere(BSONElement where) const { auto whereParams = extractWhereMatchExpressionParams(where); if (!whereParams.isOK()) { diff --git a/src/mongo/db/matcher/extensions_callback_real.h b/src/mongo/db/matcher/extensions_callback_real.h index b820a300dec..6d4caf5c286 100644 --- a/src/mongo/db/matcher/extensions_callback_real.h +++ b/src/mongo/db/matcher/extensions_callback_real.h @@ -35,7 +35,8 @@ namespace mongo { class NamespaceString; /** - * This implementation is used for the server-side code. + * ExtensionsCallbackReal uses the provided OperationContext and namespace to capture context + * necessary for parsing $text and $where clauses. */ class ExtensionsCallbackReal : public MatchExpressionParser::ExtensionsCallback { public: @@ -48,6 +49,14 @@ public: */ ExtensionsCallbackReal(OperationContext* txn, const NamespaceString* nss); + /** + * Returns a TextMatchExpression, or an error Status if parsing fails. + */ + StatusWithMatchExpression parseText(BSONElement text) const final; + + /** + * Returns a WhereMatchExpression, or an error Status if parsing fails. + */ StatusWithMatchExpression parseWhere(BSONElement where) const final; private: diff --git a/src/mongo/db/ops/modifier_pull.cpp b/src/mongo/db/ops/modifier_pull.cpp index 2772b7632b0..530ecfffb5e 100644 --- a/src/mongo/db/ops/modifier_pull.cpp +++ b/src/mongo/db/ops/modifier_pull.cpp @@ -115,8 +115,8 @@ Status ModifierPull::init(const BSONElement& modExpr, const Options& opts, bool* _exprObj = _exprElt.wrap(""); } - // Build the matcher around the object we built above. Currently, we do not allow - // $pull operations to contain $where clauses, so preserving this behaviour. + // Build the matcher around the object we built above. Currently, we do not allow $pull + // operations to contain $text/$where clauses, so preserving this behaviour. StatusWithMatchExpression parseResult = MatchExpressionParser::parse(_exprObj, MatchExpressionParser::ExtensionsCallback()); if (!parseResult.isOK()) { diff --git a/src/mongo/db/ops/update_driver.cpp b/src/mongo/db/ops/update_driver.cpp index 251d2aabae4..c0c11082a2e 100644 --- a/src/mongo/db/ops/update_driver.cpp +++ b/src/mongo/db/ops/update_driver.cpp @@ -170,9 +170,9 @@ inline Status UpdateDriver::addAndParse(const modifiertable::ModifierType type, Status UpdateDriver::populateDocumentWithQueryFields(const BSONObj& query, const vector* immutablePaths, mutablebson::Document& doc) const { - // We canonicalize the query to collapse $and/$or, and the first arg (ns) is not needed - // Also, because this is for the upsert case, where we insert a new document if one was - // not found, the $where clause does not make sense, hence empty ExtensionsCallback. + // We canonicalize the query to collapse $and/$or, and the first arg (ns) is not needed. Also, + // because this is for the upsert case, where we insert a new document if one was not found, the + // $where/$text clauses do not make sense, hence empty ExtensionsCallback. auto statusWithCQ = CanonicalQuery::canonicalize(NamespaceString(""), query, ExtensionsCallbackNoop()); if (!statusWithCQ.isOK()) { diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index e300114f0a7..0458a7474be 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -31,6 +31,7 @@ #include #include "mongo/db/jsobj.h" +#include "mongo/db/matcher/extensions_callback_noop.h" #include "mongo/db/matcher/matcher.h" #include "mongo/db/pipeline/document.h" #include "mongo/db/pipeline/document_source.h" @@ -96,7 +97,7 @@ bool DocumentSourceMatch::coalesce(const intrusive_ptr& nextSour // Replace our matcher with the $and of ours and theirs. matcher.reset(new Matcher(BSON("$and" << BSON_ARRAY(getQuery() << otherMatch->getQuery())), - MatchExpressionParser::ExtensionsCallback())); + ExtensionsCallbackNoop())); return true; } @@ -359,6 +360,6 @@ BSONObj DocumentSourceMatch::getQuery() const { DocumentSourceMatch::DocumentSourceMatch(const BSONObj& query, const intrusive_ptr& pExpCtx) : DocumentSource(pExpCtx), - matcher(new Matcher(query.getOwned(), MatchExpressionParser::ExtensionsCallback())), + matcher(new Matcher(query.getOwned(), ExtensionsCallbackNoop())), _isTextQuery(isTextQuery(query)) {} } diff --git a/src/mongo/db/query/canonical_query_test.cpp b/src/mongo/db/query/canonical_query_test.cpp index eff6201663d..ec650b1f1cc 100644 --- a/src/mongo/db/query/canonical_query_test.cpp +++ b/src/mongo/db/query/canonical_query_test.cpp @@ -29,6 +29,7 @@ #include "mongo/db/query/canonical_query.h" #include "mongo/db/json.h" +#include "mongo/db/matcher/extensions_callback_noop.h" #include "mongo/db/namespace_string.h" #include "mongo/unittest/unittest.h" @@ -46,7 +47,7 @@ static const NamespaceString nss("testdb.testcoll"); * and return the MatchExpression*. */ MatchExpression* parseMatchExpression(const BSONObj& obj) { - StatusWithMatchExpression status = MatchExpressionParser::parse(obj); + StatusWithMatchExpression status = MatchExpressionParser::parse(obj, ExtensionsCallbackNoop()); if (!status.isOK()) { mongoutils::str::stream ss; ss << "failed to parse query: " << obj.toString() diff --git a/src/mongo/db/query/query_planner_test_fixture.cpp b/src/mongo/db/query/query_planner_test_fixture.cpp index 8d559f24c7f..4be1ee2f140 100644 --- a/src/mongo/db/query/query_planner_test_fixture.cpp +++ b/src/mongo/db/query/query_planner_test_fixture.cpp @@ -176,7 +176,8 @@ void QueryPlannerTest::runQueryFull(const BSONObj& query, minObj, maxObj, snapshot, - false); // explain + false, // explain + ExtensionsCallbackNoop()); ASSERT_OK(statusWithCQ.getStatus()); ASSERT_OK(QueryPlanner::plan(*statusWithCQ.getValue(), params, &solns.mutableVector())); @@ -241,7 +242,8 @@ void QueryPlannerTest::runInvalidQueryFull(const BSONObj& query, minObj, maxObj, snapshot, - false); // explain + false, // explain + ExtensionsCallbackNoop()); ASSERT_OK(statusWithCQ.getStatus()); Status s = QueryPlanner::plan(*statusWithCQ.getValue(), params, &solns.mutableVector()); @@ -257,8 +259,7 @@ void QueryPlannerTest::runQueryAsCommand(const BSONObj& cmdObj) { std::unique_ptr lpq( assertGet(LiteParsedQuery::makeFromFindCommand(nss, cmdObj, isExplain))); - ExtensionsCallbackNoop extensionsCallback; - auto statusWithCQ = CanonicalQuery::canonicalize(lpq.release(), extensionsCallback); + auto statusWithCQ = CanonicalQuery::canonicalize(lpq.release(), ExtensionsCallbackNoop()); ASSERT_OK(statusWithCQ.getStatus()); Status s = QueryPlanner::plan(*statusWithCQ.getValue(), params, &solns.mutableVector()); diff --git a/src/mongo/dbtests/SConscript b/src/mongo/dbtests/SConscript index b353987ac96..05456b37e6a 100644 --- a/src/mongo/dbtests/SConscript +++ b/src/mongo/dbtests/SConscript @@ -69,7 +69,7 @@ dbtest = env.Program( 'directclienttests.cpp', 'documentsourcetests.cpp', 'executor_registry.cpp', - 'expression_where_test.cpp', + 'extensions_callback_real_test.cpp', 'gle_test.cpp', 'indexcatalogtests.cpp', 'indexupdatetests.cpp', diff --git a/src/mongo/dbtests/expression_where_test.cpp b/src/mongo/dbtests/expression_where_test.cpp deleted file mode 100644 index 047027022e0..00000000000 --- a/src/mongo/dbtests/expression_where_test.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (C) 2015 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/platform/basic.h" - -#include "mongo/db/json.h" -#include "mongo/db/matcher/expression.h" -#include "mongo/db/matcher/expression_parser.h" -#include "mongo/db/matcher/expression_where.h" -#include "mongo/db/matcher/extensions_callback_real.h" -#include "mongo/db/namespace_string.h" -#include "mongo/db/operation_context_impl.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace { - -const NamespaceString nss("unittests.expression_where_test"); - -TEST(ExpressionWhere, WhereExpressionsWithSameScopeHaveSameBSONRepresentation) { - OperationContextImpl txn; - const char code[] = "function(){ return a; }"; - - BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); - auto expr1 = unittest::assertGet( - MatchExpressionParser::parse(query1, ExtensionsCallbackReal(&txn, &nss))); - BSONObjBuilder builder1; - expr1->toBSON(&builder1); - - BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); - auto expr2 = unittest::assertGet( - MatchExpressionParser::parse(query2, ExtensionsCallbackReal(&txn, &nss))); - BSONObjBuilder builder2; - expr2->toBSON(&builder2); - - ASSERT_EQ(builder1.obj(), builder2.obj()); -} - -TEST(ExpressionWhere, WhereExpressionsWithDifferentScopesHaveDifferentBSONRepresentations) { - OperationContextImpl txn; - const char code[] = "function(){ return a; }"; - - BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); - auto expr1 = unittest::assertGet( - MatchExpressionParser::parse(query1, ExtensionsCallbackReal(&txn, &nss))); - BSONObjBuilder builder1; - expr1->toBSON(&builder1); - - BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << false))); - auto expr2 = unittest::assertGet( - MatchExpressionParser::parse(query2, ExtensionsCallbackReal(&txn, &nss))); - BSONObjBuilder builder2; - expr2->toBSON(&builder2); - - ASSERT_NE(builder1.obj(), builder2.obj()); -} - -TEST(ExpressionWhere, WhereExpressionsWithSameScopeAreEquivalent) { - OperationContextImpl txn; - const char code[] = "function(){ return a; }"; - - BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); - auto expr1 = unittest::assertGet( - MatchExpressionParser::parse(query1, ExtensionsCallbackReal(&txn, &nss))); - - BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); - auto expr2 = unittest::assertGet( - MatchExpressionParser::parse(query2, ExtensionsCallbackReal(&txn, &nss))); - - ASSERT(expr1->equivalent(expr2.get())); - ASSERT(expr2->equivalent(expr1.get())); -} - -TEST(ExpressionWhere, WhereExpressionsWithDifferentScopesAreNotEquivalent) { - OperationContextImpl txn; - const char code[] = "function(){ return a; }"; - - BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); - auto expr1 = unittest::assertGet( - MatchExpressionParser::parse(query1, ExtensionsCallbackReal(&txn, &nss))); - - BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << false))); - auto expr2 = unittest::assertGet( - MatchExpressionParser::parse(query2, ExtensionsCallbackReal(&txn, &nss))); - - ASSERT_FALSE(expr1->equivalent(expr2.get())); - ASSERT_FALSE(expr2->equivalent(expr1.get())); -} - -} // namespace -} // namespace mongo diff --git a/src/mongo/dbtests/extensions_callback_real_test.cpp b/src/mongo/dbtests/extensions_callback_real_test.cpp new file mode 100644 index 00000000000..b4c2291810f --- /dev/null +++ b/src/mongo/dbtests/extensions_callback_real_test.cpp @@ -0,0 +1,227 @@ +/** + * Copyright (C) 2015 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/db/json.h" +#include "mongo/db/matcher/expression_text.h" +#include "mongo/db/matcher/extensions_callback_real.h" +#include "mongo/db/namespace_string.h" +#include "mongo/db/operation_context_impl.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +const NamespaceString nss("unittests.extensions_callback_real_test"); + +// +// $text parsing tests. +// + +TEST(ExtensionsCallbackReal, TextBasic) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text: {$search:\"awesome\", $language:\"english\"}}"); + auto expr = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement())); + + ASSERT_EQUALS(MatchExpression::TEXT, expr->matchType()); + std::unique_ptr textExpr( + static_cast(expr.release())); + ASSERT_EQUALS(textExpr->getQuery(), "awesome"); + ASSERT_EQUALS(textExpr->getLanguage(), "english"); + ASSERT_EQUALS(textExpr->getCaseSensitive(), TextMatchExpressionBase::kCaseSensitiveDefault); + ASSERT_EQUALS(textExpr->getDiacriticSensitive(), + TextMatchExpressionBase::kDiacriticSensitiveDefault); +} + +TEST(ExtensionsCallbackReal, TextLanguageError) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text: {$search:\"awesome\", $language:\"spanglish\"}}"); + StatusWithMatchExpression result = + ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement()); + + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(ExtensionsCallbackReal, TextCaseSensitiveTrue) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text: {$search:\"awesome\", $caseSensitive: true}}"); + auto expr = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement())); + + ASSERT_EQUALS(MatchExpression::TEXT, expr->matchType()); + std::unique_ptr textExpr( + static_cast(expr.release())); + ASSERT_EQUALS(textExpr->getCaseSensitive(), true); +} + +TEST(ExtensionsCallbackReal, TextCaseSensitiveFalse) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text: {$search:\"awesome\", $caseSensitive: false}}"); + auto expr = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement())); + + ASSERT_EQUALS(MatchExpression::TEXT, expr->matchType()); + std::unique_ptr textExpr( + static_cast(expr.release())); + ASSERT_EQUALS(textExpr->getCaseSensitive(), false); +} + +TEST(ExtensionsCallbackReal, TextCaseSensitiveError) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text:{$search:\"awesome\", $caseSensitive: 0}}"); + StatusWithMatchExpression result = + ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement()); + + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(ExtensionsCallbackReal, TextDiacriticSensitiveTrue) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text: {$search:\"awesome\", $diacriticSensitive: true}}"); + auto expr = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement())); + + ASSERT_EQUALS(MatchExpression::TEXT, expr->matchType()); + std::unique_ptr textExpr( + static_cast(expr.release())); + ASSERT_EQUALS(textExpr->getDiacriticSensitive(), true); +} + +TEST(ExtensionsCallbackReal, TextDiacriticSensitiveFalse) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text: {$search:\"awesome\", $diacriticSensitive: false}}"); + auto expr = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement())); + + ASSERT_EQUALS(MatchExpression::TEXT, expr->matchType()); + std::unique_ptr textExpr( + static_cast(expr.release())); + ASSERT_EQUALS(textExpr->getDiacriticSensitive(), false); +} + +TEST(ExtensionsCallbackReal, TextDiacriticSensitiveError) { + OperationContextImpl txn; + BSONObj query = fromjson("{$text:{$search:\"awesome\", $diacriticSensitive: 0}}"); + StatusWithMatchExpression result = + ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement()); + + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(ExtensionsCallbackReal, TextDiacriticSensitiveAndCaseSensitiveTrue) { + OperationContextImpl txn; + BSONObj query = + fromjson("{$text: {$search:\"awesome\", $diacriticSensitive: true, $caseSensitive: true}}"); + auto expr = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseText(query.firstElement())); + + ASSERT_EQUALS(MatchExpression::TEXT, expr->matchType()); + std::unique_ptr textExpr( + static_cast(expr.release())); + ASSERT_EQUALS(textExpr->getDiacriticSensitive(), true); + ASSERT_EQUALS(textExpr->getCaseSensitive(), true); +} + +// +// $where parsing tests. +// + +TEST(ExtensionsCallbackReal, WhereExpressionsWithSameScopeHaveSameBSONRepresentation) { + OperationContextImpl txn; + const char code[] = "function(){ return a; }"; + + BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); + auto expr1 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query1.firstElement())); + BSONObjBuilder builder1; + expr1->toBSON(&builder1); + + BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); + auto expr2 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query2.firstElement())); + BSONObjBuilder builder2; + expr2->toBSON(&builder2); + + ASSERT_EQ(builder1.obj(), builder2.obj()); +} + +TEST(ExtensionsCallbackReal, WhereExpressionsWithDifferentScopesHaveDifferentBSONRepresentations) { + OperationContextImpl txn; + const char code[] = "function(){ return a; }"; + + BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); + auto expr1 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query1.firstElement())); + BSONObjBuilder builder1; + expr1->toBSON(&builder1); + + BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << false))); + auto expr2 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query2.firstElement())); + BSONObjBuilder builder2; + expr2->toBSON(&builder2); + + ASSERT_NE(builder1.obj(), builder2.obj()); +} + +TEST(ExtensionsCallbackReal, WhereExpressionsWithSameScopeAreEquivalent) { + OperationContextImpl txn; + const char code[] = "function(){ return a; }"; + + BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); + auto expr1 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query1.firstElement())); + + BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); + auto expr2 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query2.firstElement())); + + ASSERT(expr1->equivalent(expr2.get())); + ASSERT(expr2->equivalent(expr1.get())); +} + +TEST(ExtensionsCallbackReal, WhereExpressionsWithDifferentScopesAreNotEquivalent) { + OperationContextImpl txn; + const char code[] = "function(){ return a; }"; + + BSONObj query1 = BSON("$where" << BSONCodeWScope(code, BSON("a" << true))); + auto expr1 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query1.firstElement())); + + BSONObj query2 = BSON("$where" << BSONCodeWScope(code, BSON("a" << false))); + auto expr2 = + unittest::assertGet(ExtensionsCallbackReal(&txn, &nss).parseWhere(query2.firstElement())); + + ASSERT_FALSE(expr1->equivalent(expr2.get())); + ASSERT_FALSE(expr2->equivalent(expr1.get())); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/dbtests/query_stage_subplan.cpp b/src/mongo/dbtests/query_stage_subplan.cpp index fd27b7c8b19..624405619a4 100644 --- a/src/mongo/dbtests/query_stage_subplan.cpp +++ b/src/mongo/dbtests/query_stage_subplan.cpp @@ -37,6 +37,7 @@ #include "mongo/db/exec/subplan.h" #include "mongo/db/jsobj.h" #include "mongo/db/json.h" +#include "mongo/db/matcher/extensions_callback_noop.h" #include "mongo/db/operation_context_impl.h" #include "mongo/db/query/canonical_query.h" #include "mongo/db/query/get_executor.h" @@ -74,7 +75,8 @@ protected: auto lpq = unittest::assertGet(LiteParsedQuery::makeFromFindCommand(nss, cmdObj, isExplain)); - auto cq = unittest::assertGet(CanonicalQuery::canonicalize(lpq.release())); + auto cq = unittest::assertGet( + CanonicalQuery::canonicalize(lpq.release(), ExtensionsCallbackNoop())); return cq; } -- cgit v1.2.1