summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Wahlin <james@mongodb.com>2019-03-08 11:11:39 -0500
committerJames Wahlin <james@mongodb.com>2019-03-08 15:21:54 -0500
commit023d3450d39c62a3cc70f6cdc4a52a7f486aeb37 (patch)
tree3e24c2e5086c94fd4087126c3cd02615d51d859b
parentfe0c50c6fc5faed1402e7cefc02470d79f7c1fe5 (diff)
downloadmongo-023d3450d39c62a3cc70f6cdc4a52a7f486aeb37.tar.gz
SERVER-39771 TextMatchExpression crashes instead of serializing in some cases
(cherry picked from commit 8a9a6071b9aac873689cd777dfb4ed6bbad74e04)
-rw-r--r--jstests/core/fts_querylang.js188
-rw-r--r--src/mongo/db/matcher/expression_serialization_test.cpp16
-rw-r--r--src/mongo/db/matcher/expression_tree.cpp11
3 files changed, 111 insertions, 104 deletions
diff --git a/jstests/core/fts_querylang.js b/jstests/core/fts_querylang.js
index 5fb1b8e606c..de27b65ba5b 100644
--- a/jstests/core/fts_querylang.js
+++ b/jstests/core/fts_querylang.js
@@ -1,104 +1,86 @@
+// Test the $text query operator.
// @tags: [requires_non_retryable_writes]
-
-// Test $text query operator.
-
-var t = db.getSiblingDB("test").getCollection("fts_querylang");
-var cursor;
-var results;
-
-t.drop();
-
-t.insert({_id: 0, unindexedField: 0, a: "textual content"});
-t.insert({_id: 1, unindexedField: 1, a: "additional content"});
-t.insert({_id: 2, unindexedField: 2, a: "irrelevant content"});
-t.ensureIndex({a: "text"});
-
-// Test text query with no results.
-assert.eq(false, t.find({$text: {$search: "words"}}).hasNext());
-
-// Test basic text query.
-results = t.find({$text: {$search: "textual content -irrelevant"}}).toArray();
-assert.eq(results.length, 2);
-assert.neq(results[0]._id, 2);
-assert.neq(results[1]._id, 2);
-
-// Test sort with basic text query.
-results =
- t.find({$text: {$search: "textual content -irrelevant"}}).sort({unindexedField: 1}).toArray();
-assert.eq(results.length, 2);
-assert.eq(results[0]._id, 0);
-assert.eq(results[1]._id, 1);
-
-// Test skip with basic text query.
-results = t.find({$text: {$search: "textual content -irrelevant"}})
- .sort({unindexedField: 1})
- .skip(1)
- .toArray();
-assert.eq(results.length, 1);
-assert.eq(results[0]._id, 1);
-
-// Test limit with basic text query.
-results = t.find({$text: {$search: "textual content -irrelevant"}})
- .sort({unindexedField: 1})
- .limit(1)
- .toArray();
-assert.eq(results.length, 1);
-assert.eq(results[0]._id, 0);
-
-// TODO Test basic text query with sort, once sort is enabled in the new query framework.
-
-// TODO Test basic text query with projection, once projection is enabled in the new query
-// framework.
-
-// Test $and of basic text query with indexed expression.
-results = t.find({$text: {$search: "content -irrelevant"}, _id: 1}).toArray();
-assert.eq(results.length, 1);
-assert.eq(results[0]._id, 1);
-
-// Test $and of basic text query with indexed expression, and bad language
-assert.throws(function() {
- t.find({$text: {$search: "content -irrelevant", $language: "spanglish"}, _id: 1}).itcount();
-});
-
-// Test $and of basic text query with unindexed expression.
-results = t.find({$text: {$search: "content -irrelevant"}, unindexedField: 1}).toArray();
-assert.eq(results.length, 1);
-assert.eq(results[0]._id, 1);
-
-// TODO Test invalid inputs for $text, $search, $language.
-
-// Test $language.
-cursor = t.find({$text: {$search: "contents", $language: "none"}});
-assert.eq(false, cursor.hasNext());
-
-cursor = t.find({$text: {$search: "contents", $language: "EN"}});
-assert.eq(true, cursor.hasNext());
-
-cursor = t.find({$text: {$search: "contents", $language: "spanglish"}});
-assert.throws(function() {
- cursor.next();
-});
-
-// TODO Test $and of basic text query with geo expression.
-
-// Test update with $text.
-t.update({$text: {$search: "textual content -irrelevant"}}, {$set: {b: 1}}, {multi: true});
-assert.eq(2, t.find({b: 1}).itcount(), 'incorrect number of documents updated');
-
-// TODO Test remove with $text, once it is enabled with the new query framework.
-
-// TODO Test count with $text, once it is enabled with the new query framework.
-
-// TODO Test findAndModify with $text, once it is enabled with the new query framework.
-
-// TODO Test aggregate with $text, once it is enabled with the new query framework.
-
-// TODO Test that old query framework rejects $text queries.
-
-// TODO Test that $text fails without a text index.
-
-// TODO Test that $text accepts a hint of the text index.
-
-// TODO Test that $text fails if a different index is hinted.
-
-// TODO Test $text with {$natural:1} sort, {$natural:1} hint.
+(function() {
+ "use strict";
+
+ const coll = db.getCollection("fts_querylang");
+ coll.drop();
+
+ assert.commandWorked(coll.insert({_id: 0, unindexedField: 0, a: "textual content"}));
+ assert.commandWorked(coll.insert({_id: 1, unindexedField: 1, a: "additional content"}));
+ assert.commandWorked(coll.insert({_id: 2, unindexedField: 2, a: "irrelevant content"}));
+ assert.commandWorked(coll.createIndex({a: "text"}));
+
+ // Test text query with no results.
+ assert.eq(false, coll.find({$text: {$search: "words"}}).hasNext());
+
+ // Test basic text query.
+ let results = coll.find({$text: {$search: "textual content -irrelevant"}}).toArray();
+ assert.eq(results.length, 2, results);
+ assert.neq(results[0]._id, 2, results);
+ assert.neq(results[1]._id, 2, results);
+
+ // Test sort with basic text query.
+ results = coll.find({$text: {$search: "textual content -irrelevant"}})
+ .sort({unindexedField: 1})
+ .toArray();
+ assert.eq(results.length, 2, results);
+ assert.eq(results[0]._id, 0, results);
+ assert.eq(results[1]._id, 1, results);
+
+ // Test skip with basic text query.
+ results = coll.find({$text: {$search: "textual content -irrelevant"}})
+ .sort({unindexedField: 1})
+ .skip(1)
+ .toArray();
+ assert.eq(results.length, 1, results);
+ assert.eq(results[0]._id, 1, results);
+
+ // Test limit with basic text query.
+ results = coll.find({$text: {$search: "textual content -irrelevant"}})
+ .sort({unindexedField: 1})
+ .limit(1)
+ .toArray();
+ assert.eq(results.length, 1, results);
+ assert.eq(results[0]._id, 0, results);
+
+ // Test $and of basic text query with indexed expression.
+ results = coll.find({$text: {$search: "content -irrelevant"}, _id: 1}).toArray();
+ assert.eq(results.length, 1, results);
+ assert.eq(results[0]._id, 1, results);
+
+ // Test $and of basic text query with indexed expression and bad language.
+ assert.commandFailedWithCode(assert.throws(function() {
+ coll.find({$text: {$search: "content -irrelevant", $language: "spanglish"}, _id: 1})
+ .itcount();
+ }),
+ ErrorCodes.BadValue);
+
+ // Test $and of basic text query with unindexed expression.
+ results = coll.find({$text: {$search: "content -irrelevant"}, unindexedField: 1}).toArray();
+ assert.eq(results.length, 1, results);
+ assert.eq(results[0]._id, 1, results);
+
+ // Test $language.
+ let cursor = coll.find({$text: {$search: "contents", $language: "none"}});
+ assert.eq(false, cursor.hasNext());
+
+ cursor = coll.find({$text: {$search: "contents", $language: "EN"}});
+ assert.eq(true, cursor.hasNext());
+
+ cursor = coll.find({$text: {$search: "contents", $language: "spanglish"}});
+ assert.commandFailedWithCode(assert.throws(function() {
+ cursor.next();
+ }),
+ ErrorCodes.BadValue);
+
+ // Test update with $text.
+ coll.update({$text: {$search: "textual content -irrelevant"}}, {$set: {b: 1}}, {multi: true});
+ assert.eq(2, coll.find({b: 1}).itcount(), 'incorrect number of documents updated');
+
+ // $text cannot be contained within a $nor.
+ assert.commandFailedWithCode(assert.throws(function() {
+ coll.find({$nor: [{$text: {$search: 'a'}}]}).itcount();
+ }),
+ ErrorCodes.BadValue);
+}());
diff --git a/src/mongo/db/matcher/expression_serialization_test.cpp b/src/mongo/db/matcher/expression_serialization_test.cpp
index 63f238593ae..74a8d3b8f74 100644
--- a/src/mongo/db/matcher/expression_serialization_test.cpp
+++ b/src/mongo/db/matcher/expression_serialization_test.cpp
@@ -1377,6 +1377,22 @@ TEST(SerializeBasic, ExpressionTextSerializesCorrectly) {
ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression()));
}
+TEST(SerializeBasic, ExpressionNorWithTextSerializesCorrectly) {
+ boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ Matcher original(fromjson("{$nor: [{$text: {$search: 'x'}}]}"),
+ expCtx,
+ ExtensionsCallbackNoop(),
+ MatchExpressionParser::kAllowAllSpecialFeatures);
+ Matcher reserialized(serialize(original.getMatchExpression()),
+ expCtx,
+ ExtensionsCallbackNoop(),
+ MatchExpressionParser::kAllowAllSpecialFeatures);
+ ASSERT_BSONOBJ_EQ(*reserialized.getQuery(),
+ fromjson("{$nor: [{$text: {$search: 'x', $language: '', $caseSensitive: "
+ "false, $diacriticSensitive: false}}]}"));
+ ASSERT_BSONOBJ_EQ(*reserialized.getQuery(), serialize(reserialized.getMatchExpression()));
+}
+
TEST(SerializeBasic, ExpressionTextWithDefaultLanguageSerializesCorrectly) {
boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
Matcher original(fromjson("{$text: {$search: 'a', $caseSensitive: false}}"),
diff --git a/src/mongo/db/matcher/expression_tree.cpp b/src/mongo/db/matcher/expression_tree.cpp
index c24749ea7b3..2229f602895 100644
--- a/src/mongo/db/matcher/expression_tree.cpp
+++ b/src/mongo/db/matcher/expression_tree.cpp
@@ -37,6 +37,7 @@
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/db/matcher/expression_always_boolean.h"
#include "mongo/db/matcher/expression_path.h"
+#include "mongo/db/matcher/expression_text_base.h"
namespace mongo {
@@ -324,6 +325,14 @@ void NotMatchExpression::debugString(StringBuilder& debug, int level) const {
boost::optional<StringData> NotMatchExpression::getPathIfNotWithSinglePathMatchExpressionTree(
MatchExpression* exp) {
if (auto pathMatch = dynamic_cast<PathMatchExpression*>(exp)) {
+ if (dynamic_cast<TextMatchExpressionBase*>(exp)) {
+ // While TextMatchExpressionBase derives from PathMatchExpression, text match
+ // expressions cannot be serialized in the same manner as other PathMatchExpression
+ // derivatives. This is because the path for a TextMatchExpression is embedded within
+ // the $text object, whereas for other PathMatchExpressions it is on the left-hand-side,
+ // for example {x: {$eq: 1}}.
+ return boost::none;
+ }
return pathMatch->path();
}
@@ -331,7 +340,7 @@ boost::optional<StringData> NotMatchExpression::getPathIfNotWithSinglePathMatchE
boost::optional<StringData> path;
for (size_t i = 0; i < exp->numChildren(); ++i) {
auto pathMatchChild = dynamic_cast<PathMatchExpression*>(exp->getChild(i));
- if (!pathMatchChild) {
+ if (!pathMatchChild || dynamic_cast<TextMatchExpressionBase*>(exp->getChild(i))) {
return boost::none;
}