summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Storch <david.storch@10gen.com>2017-12-20 11:21:27 -0500
committerDavid Storch <david.storch@10gen.com>2018-02-01 12:51:48 -0500
commit779beeca39066f939dc4fdbcb05f2e6ed05e99fa (patch)
treeae4519fdfebae3c40a2471896949055874640b82
parent72267ebfbe177cb37f742397f633bc484ebf52c5 (diff)
downloadmongo-779beeca39066f939dc4fdbcb05f2e6ed05e99fa.tar.gz
SERVER-31760 Add index support for InternalExprEqMatchExpression.
(cherry picked from commit 6699621bfb54174c7ee082ee85c62211788942c3) Conflicts: src/mongo/db/matcher/expression_internal_expr_eq.cpp src/mongo/db/matcher/expression_internal_expr_eq.h src/mongo/db/matcher/expression_internal_expr_eq_test.cpp src/mongo/db/matcher/expression_leaf.cpp src/mongo/db/matcher/expression_leaf.h src/mongo/db/matcher/expression_parser.cpp
-rw-r--r--src/mongo/db/index/multikey_paths.h11
-rw-r--r--src/mongo/db/matcher/expression_algo_test.cpp16
-rw-r--r--src/mongo/db/matcher/expression_internal_expr_eq.cpp42
-rw-r--r--src/mongo/db/matcher/expression_internal_expr_eq.h46
-rw-r--r--src/mongo/db/matcher/expression_internal_expr_eq_test.cpp39
-rw-r--r--src/mongo/db/matcher/expression_leaf.cpp87
-rw-r--r--src/mongo/db/matcher/expression_leaf.h118
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp9
-rw-r--r--src/mongo/db/matcher/expression_parser_test.cpp17
-rw-r--r--src/mongo/db/query/index_bounds_builder.cpp9
-rw-r--r--src/mongo/db/query/index_bounds_builder_test.cpp76
-rw-r--r--src/mongo/db/query/indexability.h13
-rw-r--r--src/mongo/db/query/plan_cache.cpp3
-rw-r--r--src/mongo/db/query/plan_cache_indexability.cpp6
-rw-r--r--src/mongo/db/query/plan_cache_indexability_test.cpp25
-rw-r--r--src/mongo/db/query/planner_ixselect.cpp41
-rw-r--r--src/mongo/db/query/planner_ixselect.h8
-rw-r--r--src/mongo/db/query/planner_ixselect_test.cpp192
-rw-r--r--src/mongo/db/query/query_planner_geo_test.cpp114
-rw-r--r--src/mongo/db/query/query_planner_partialidx_test.cpp10
-rw-r--r--src/mongo/db/query/query_planner_test.cpp62
-rw-r--r--src/mongo/db/query/query_planner_text_test.cpp26
22 files changed, 767 insertions, 203 deletions
diff --git a/src/mongo/db/index/multikey_paths.h b/src/mongo/db/index/multikey_paths.h
index 794c9478093..8f8b00c3938 100644
--- a/src/mongo/db/index/multikey_paths.h
+++ b/src/mongo/db/index/multikey_paths.h
@@ -41,6 +41,17 @@ namespace mongo {
// For example, with the index {'a.b': 1, 'a.c': 1} where the paths "a" and "a.b" cause the
// index to be multikey, we'd have a std::vector<std::set<size_t>>{{0U, 1U}, {0U}}.
//
+// Further Examples:
+// Index PathsThatAreMultiKey MultiKeyPaths
+// -------------------- -------------------- --------------------
+// {'a.b': 1, 'a.c': 1} "a", "a.b" {{0U, 1U}, {0U}}
+// {a: 1, b: 1} "b" {{}, {0U}}
+// {a: 1, b: 1} "a" {{0U}, {}}
+// {'a.b.c': 1, d: 1} "a.b.c" {{2U}, {}}
+// {'a.b': 1, c: 1, d: 1} "a.b", "d" {{1U}, {}, {0U}}
+// {a: 1, b: 1} none {{}, {}}
+// {a: 1, b: 1} no multikey metadata {}
+//
// An empty vector is used to represent that the index doesn't support path-level multikey tracking.
using MultikeyPaths = std::vector<std::set<std::size_t>>;
diff --git a/src/mongo/db/matcher/expression_algo_test.cpp b/src/mongo/db/matcher/expression_algo_test.cpp
index 23a6b1dce5a..788cc231144 100644
--- a/src/mongo/db/matcher/expression_algo_test.cpp
+++ b/src/mongo/db/matcher/expression_algo_test.cpp
@@ -709,6 +709,22 @@ TEST(ExpressionAlgoIsSubsetOf, NonMatchingCollationsNoStringComparison) {
ASSERT_TRUE(expression::isSubsetOf(lhs.get(), rhs.get()));
}
+TEST(ExpressionAlgoIsSubsetOf, InternalExprEqIsSubsetOfNothing) {
+ ParsedMatchExpression exprEq("{a: {$_internalExprEq: 0}}");
+ ParsedMatchExpression regularEq("{a: {$eq: 0}}");
+ {
+ ParsedMatchExpression rhs("{a: {$gte: 0}}");
+ ASSERT_FALSE(expression::isSubsetOf(exprEq.get(), rhs.get()));
+ ASSERT_TRUE(expression::isSubsetOf(regularEq.get(), rhs.get()));
+ }
+
+ {
+ ParsedMatchExpression rhs("{a: {$lte: 0}}");
+ ASSERT_FALSE(expression::isSubsetOf(exprEq.get(), rhs.get()));
+ ASSERT_TRUE(expression::isSubsetOf(regularEq.get(), rhs.get()));
+ }
+}
+
TEST(IsIndependent, AndIsIndependentOnlyIfChildrenAre) {
BSONObj matchPredicate = fromjson("{$and: [{a: 1}, {b: 1}]}");
boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
diff --git a/src/mongo/db/matcher/expression_internal_expr_eq.cpp b/src/mongo/db/matcher/expression_internal_expr_eq.cpp
index 20df9e61c07..d3cd93a403a 100644
--- a/src/mongo/db/matcher/expression_internal_expr_eq.cpp
+++ b/src/mongo/db/matcher/expression_internal_expr_eq.cpp
@@ -47,54 +47,18 @@ bool InternalExprEqMatchExpression::matchesSingleElement(const BSONElement& elem
return true;
}
- if (elem.canonicalType() != _rhsElem.canonicalType()) {
+ if (elem.canonicalType() != _rhs.canonicalType()) {
return false;
}
auto comp = BSONElement::compareElements(
- elem, _rhsElem, BSONElement::ComparisonRules::kConsiderFieldName, _collator);
+ elem, _rhs, BSONElement::ComparisonRules::kConsiderFieldName, _collator);
return comp == 0;
}
-void InternalExprEqMatchExpression::debugString(StringBuilder& debug, int level) const {
- _debugAddSpace(debug, level);
- debug << path() << " " << kName << " " << _rhsElem.toString(false);
-
- auto td = getTag();
- if (td) {
- debug << " ";
- td->debugString(&debug);
- }
-
- debug << "\n";
-}
-
-void InternalExprEqMatchExpression::serialize(BSONObjBuilder* builder) const {
- BSONObjBuilder exprObj(builder->subobjStart(path()));
- exprObj.appendAs(_rhsElem, kName);
- exprObj.doneFast();
-}
-
-bool InternalExprEqMatchExpression::equivalent(const MatchExpression* other) const {
- if (other->matchType() != matchType()) {
- return false;
- }
-
- const InternalExprEqMatchExpression* realOther =
- static_cast<const InternalExprEqMatchExpression*>(other);
-
- if (!CollatorInterface::collatorsMatch(_collator, realOther->_collator)) {
- return false;
- }
-
- constexpr StringData::ComparatorInterface* stringComparator = nullptr;
- BSONElementComparator eltCmp(BSONElementComparator::FieldNamesMode::kIgnore, stringComparator);
- return path() == realOther->path() && eltCmp.evaluate(_rhsElem == realOther->_rhsElem);
-}
-
std::unique_ptr<MatchExpression> InternalExprEqMatchExpression::shallowClone() const {
auto clone = stdx::make_unique<InternalExprEqMatchExpression>();
- invariantOK(clone->init(path(), _rhsElem));
+ invariantOK(clone->init(path(), _rhs));
clone->setCollator(_collator);
if (getTag()) {
clone->setTag(getTag()->clone());
diff --git a/src/mongo/db/matcher/expression_internal_expr_eq.h b/src/mongo/db/matcher/expression_internal_expr_eq.h
index 5e8ad02a466..32df310c35d 100644
--- a/src/mongo/db/matcher/expression_internal_expr_eq.h
+++ b/src/mongo/db/matcher/expression_internal_expr_eq.h
@@ -44,49 +44,33 @@ namespace mongo {
* - Equality to null matches literal nulls, but not documents in which the field path is missing or
* undefined.
*
- * - Equality to undefined is legal, and matches either literal undefined, or documents in which the
- * field path is missing.
+ * - Equality to an array is illegal. It is invalid usage to construct a
+ * InternalExprEqMatchExpression node which compares to an array.
*/
-class InternalExprEqMatchExpression final : public LeafMatchExpression {
+class InternalExprEqMatchExpression final : public ComparisonMatchExpressionBase {
public:
static constexpr StringData kName = "$_internalExprEq"_sd;
InternalExprEqMatchExpression()
- : LeafMatchExpression(MatchType::INTERNAL_EXPR_EQ,
- ElementPath::LeafArrayBehavior::kNoTraversal,
- ElementPath::NonLeafArrayBehavior::kMatchSubpath) {}
+ : ComparisonMatchExpressionBase(MatchType::INTERNAL_EXPR_EQ,
+ ElementPath::LeafArrayBehavior::kNoTraversal,
+ ElementPath::NonLeafArrayBehavior::kMatchSubpath) {}
Status init(StringData path, BSONElement value) {
- _rhsElem = value;
+ invariant(value);
+ invariant(value.type() != BSONType::Undefined);
+ invariant(value.type() != BSONType::Array);
+ _rhs = value;
return setPath(path);
}
- bool matchesSingleElement(const BSONElement&, MatchDetails*) const final;
-
- void debugString(StringBuilder& debug, int level) const final;
-
- void serialize(BSONObjBuilder* out) const final;
-
- bool equivalent(const MatchExpression* other) const final;
-
- std::unique_ptr<MatchExpression> shallowClone() const final;
-
-protected:
- /**
- * 'collator' must outlive the InternalExprEqMatchExpression and any clones made of it.
- */
- void _doSetCollator(const CollatorInterface* collator) final {
- _collator = collator;
+ StringData name() const final {
+ return kName;
}
- // Collator used to compare elements. By default, simple binary comparison will be used.
- const CollatorInterface* _collator = nullptr;
-
- BSONElement _rhsElem;
+ bool matchesSingleElement(const BSONElement&, MatchDetails*) const final;
-private:
- ExpressionOptimizerFunc getOptimizer() const final {
- return [](std::unique_ptr<MatchExpression> expression) { return expression; };
- }
+ std::unique_ptr<MatchExpression> shallowClone() const final;
};
+
} // namespace mongo
diff --git a/src/mongo/db/matcher/expression_internal_expr_eq_test.cpp b/src/mongo/db/matcher/expression_internal_expr_eq_test.cpp
index f132cfeb24f..0154b8ba36a 100644
--- a/src/mongo/db/matcher/expression_internal_expr_eq_test.cpp
+++ b/src/mongo/db/matcher/expression_internal_expr_eq_test.cpp
@@ -34,6 +34,7 @@
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/query/collation/collator_interface_mock.h"
#include "mongo/db/query/index_tag.h"
+#include "mongo/unittest/death_test.h"
#include "mongo/unittest/unittest.h"
namespace mongo {
@@ -170,10 +171,12 @@ TEST(InternalExprEqMatchExpression, ComparisonRespectsNewCollationAfterCallingSe
}
TEST(InternalExprEqMatchExpression, CorrectlyMatchesArrayElement) {
- BSONObj operand = BSON("a" << BSON_ARRAY("b" << 5));
+ BSONObj operand = BSON("a.b" << 5);
InternalExprEqMatchExpression eq;
ASSERT_OK(eq.init(operand.firstElement().fieldNameStringData(), operand.firstElement()));
+ ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON("b" << 5))));
+ ASSERT_FALSE(eq.matchesBSON(BSON("a" << BSON("b" << 6))));
ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON_ARRAY("b" << 5))));
ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON_ARRAY("b" << BSON_ARRAY(5)))));
ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON_ARRAY(5 << "b"))));
@@ -205,19 +208,6 @@ TEST(InternalExprEqMatchExpression, CorrectlyMatchesNullElement) {
ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2))));
}
-TEST(InternalExprEqMatchExpression, CorrectlyMatchesUndefined) {
- BSONObj operand = fromjson("{a: undefined}");
-
- InternalExprEqMatchExpression eq;
- ASSERT_OK(eq.init(operand.firstElement().fieldNameStringData(), operand.firstElement()));
- // Expression equality to undefined should match literal undefined and missing, but not null.
- ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSONUndefined)));
- ASSERT_TRUE(eq.matchesBSON(BSONObj()));
- ASSERT_FALSE(eq.matchesBSON(BSON("a" << BSONNULL)));
- ASSERT_FALSE(eq.matchesBSON(BSON("a" << 4)));
- ASSERT_TRUE(eq.matchesBSON(BSON("a" << BSON_ARRAY(1 << 2))));
-}
-
TEST(InternalExprEqMatchExpression, CorrectlyMatchesNaN) {
BSONObj operand = BSON("x" << kNaN);
@@ -315,5 +305,26 @@ TEST(InternalExprEqMatchExpression, EquivalentToClone) {
auto clone = eq.getMatchExpression()->shallowClone();
ASSERT_TRUE(eq.getMatchExpression()->equivalent(clone.get()));
}
+
+DEATH_TEST(InternalExprEqMatchExpression,
+ CannotCompareToArray,
+ "Invariant failure value.type() != BSONType::Array") {
+ auto operand = BSON("a" << BSON_ARRAY(1 << 2));
+ InternalExprEqMatchExpression eq;
+ eq.init(operand.firstElement().fieldNameStringData(), operand.firstElement()).ignore();
+}
+
+DEATH_TEST(InternalExprEqMatchExpression,
+ CannotCompareToUndefined,
+ "Invariant failure value.type() != BSONType::Undefined") {
+ auto operand = BSON("a" << BSONUndefined);
+ InternalExprEqMatchExpression eq;
+ eq.init(operand.firstElement().fieldNameStringData(), operand.firstElement()).ignore();
+}
+
+DEATH_TEST(InternalExprEqMatchExpression, CannotCompareToMissing, "Invariant failure value") {
+ InternalExprEqMatchExpression eq;
+ eq.init("a"_sd, BSONElement()).ignore();
+}
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp
index ba0244b98a0..fb506b4c54e 100644
--- a/src/mongo/db/matcher/expression_leaf.cpp
+++ b/src/mongo/db/matcher/expression_leaf.cpp
@@ -47,11 +47,10 @@
namespace mongo {
-bool ComparisonMatchExpression::equivalent(const MatchExpression* other) const {
+bool ComparisonMatchExpressionBase::equivalent(const MatchExpression* other) const {
if (other->matchType() != matchType())
return false;
- const ComparisonMatchExpression* realOther =
- static_cast<const ComparisonMatchExpression*>(other);
+ auto realOther = static_cast<const ComparisonMatchExpressionBase*>(other);
if (!CollatorInterface::collatorsMatch(_collator, realOther->_collator)) {
return false;
@@ -62,7 +61,25 @@ bool ComparisonMatchExpression::equivalent(const MatchExpression* other) const {
return path() == realOther->path() && eltCmp.evaluate(_rhs == realOther->_rhs);
}
-Status ComparisonMatchExpression::init(StringData path, const BSONElement& rhs) {
+void ComparisonMatchExpressionBase::debugString(StringBuilder& debug, int level) const {
+ _debugAddSpace(debug, level);
+ debug << path() << " " << name();
+ debug << " " << _rhs.toString(false);
+
+ MatchExpression::TagData* td = getTag();
+ if (td) {
+ debug << " ";
+ td->debugString(&debug);
+ }
+
+ debug << "\n";
+}
+
+void ComparisonMatchExpressionBase::serialize(BSONObjBuilder* out) const {
+ out->append(path(), BSON(name() << _rhs));
+}
+
+Status ComparisonMatchExpression::init(StringData path, BSONElement rhs) {
_rhs = rhs;
invariant(_rhs);
@@ -143,63 +160,11 @@ bool ComparisonMatchExpression::matchesSingleElement(const BSONElement& e,
}
}
-void ComparisonMatchExpression::debugString(StringBuilder& debug, int level) const {
- _debugAddSpace(debug, level);
- debug << path() << " ";
- switch (matchType()) {
- case LT:
- debug << "$lt";
- break;
- case LTE:
- debug << "$lte";
- break;
- case EQ:
- debug << "==";
- break;
- case GT:
- debug << "$gt";
- break;
- case GTE:
- debug << "$gte";
- break;
- default:
- invariant(false);
- }
- debug << " " << _rhs.toString(false);
-
- MatchExpression::TagData* td = getTag();
- if (NULL != td) {
- debug << " ";
- td->debugString(&debug);
- }
-
- debug << "\n";
-}
-
-void ComparisonMatchExpression::serialize(BSONObjBuilder* out) const {
- std::string opString = "";
- switch (matchType()) {
- case LT:
- opString = "$lt";
- break;
- case LTE:
- opString = "$lte";
- break;
- case EQ:
- opString = "$eq";
- break;
- case GT:
- opString = "$gt";
- break;
- case GTE:
- opString = "$gte";
- break;
- default:
- invariant(false);
- }
-
- out->append(path(), BSON(opString << _rhs));
-}
+constexpr StringData EqualityMatchExpression::kName;
+constexpr StringData LTMatchExpression::kName;
+constexpr StringData LTEMatchExpression::kName;
+constexpr StringData GTMatchExpression::kName;
+constexpr StringData GTEMatchExpression::kName;
// ---------------
diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h
index 58c1af080eb..700d241bc94 100644
--- a/src/mongo/db/matcher/expression_leaf.h
+++ b/src/mongo/db/matcher/expression_leaf.h
@@ -77,17 +77,28 @@ public:
};
/**
- * EQ, LTE, LT, GT, GTE subclass from ComparisonMatchExpression.
+ * Base class for comparison-like match expression nodes. This includes both the comparison nodes in
+ * the match language ($eq, $gt, $gte, $lt, and $lte), as well as internal comparison nodes like
+ * $_internalExprEq.
*/
-class ComparisonMatchExpression : public LeafMatchExpression {
+class ComparisonMatchExpressionBase : public LeafMatchExpression {
public:
- explicit ComparisonMatchExpression(MatchType type) : LeafMatchExpression(type) {}
-
- Status init(StringData path, const BSONElement& rhs);
+ static bool isEquality(MatchType matchType) {
+ switch (matchType) {
+ case MatchExpression::EQ:
+ case MatchExpression::INTERNAL_EXPR_EQ:
+ return true;
+ default:
+ return false;
+ }
+ }
- virtual ~ComparisonMatchExpression() {}
+ ComparisonMatchExpressionBase(MatchType type,
+ ElementPath::LeafArrayBehavior leafArrBehavior,
+ ElementPath::NonLeafArrayBehavior nonLeafArrBehavior)
+ : LeafMatchExpression(type, leafArrBehavior, nonLeafArrBehavior) {}
- bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
+ virtual ~ComparisonMatchExpressionBase() = default;
virtual void debugString(StringBuilder& debug, int level = 0) const;
@@ -95,6 +106,11 @@ public:
virtual bool equivalent(const MatchExpression* other) const;
+ /**
+ * Returns the name of this MatchExpression.
+ */
+ virtual StringData name() const = 0;
+
const BSONElement& getData() const {
return _rhs;
}
@@ -103,6 +119,30 @@ public:
return _collator;
}
+protected:
+ /**
+ * 'collator' must outlive the ComparisonMatchExpression and any clones made of it.
+ */
+ virtual void _doSetCollator(const CollatorInterface* collator) {
+ _collator = collator;
+ }
+
+ BSONElement _rhs;
+
+ // Collator used to compare elements. By default, simple binary comparison will be used.
+ const CollatorInterface* _collator = nullptr;
+
+private:
+ ExpressionOptimizerFunc getOptimizer() const final {
+ return [](std::unique_ptr<MatchExpression> expression) { return expression; };
+ }
+};
+
+/**
+ * EQ, LTE, LT, GT, GTE subclass from ComparisonMatchExpression.
+ */
+class ComparisonMatchExpression : public ComparisonMatchExpressionBase {
+public:
/**
* Returns true if the MatchExpression is a ComparisonMatchExpression.
*/
@@ -119,28 +159,28 @@ public:
}
}
-protected:
- /**
- * 'collator' must outlive the ComparisonMatchExpression and any clones made of it.
- */
- virtual void _doSetCollator(const CollatorInterface* collator) {
- _collator = collator;
- }
+ explicit ComparisonMatchExpression(MatchType type)
+ : ComparisonMatchExpressionBase(type,
+ ElementPath::LeafArrayBehavior::kTraverse,
+ ElementPath::NonLeafArrayBehavior::kTraverse) {}
- BSONElement _rhs;
+ virtual ~ComparisonMatchExpression() = default;
- // Collator used to compare elements. By default, simple binary comparison will be used.
- const CollatorInterface* _collator = nullptr;
+ Status init(StringData path, BSONElement rhs);
-private:
- ExpressionOptimizerFunc getOptimizer() const final {
- return [](std::unique_ptr<MatchExpression> expression) { return expression; };
- }
+ bool matchesSingleElement(const BSONElement&, MatchDetails* details = nullptr) const final;
};
-class EqualityMatchExpression : public ComparisonMatchExpression {
+class EqualityMatchExpression final : public ComparisonMatchExpression {
public:
+ static constexpr StringData kName = "$eq"_sd;
+
EqualityMatchExpression() : ComparisonMatchExpression(EQ) {}
+
+ StringData name() const final {
+ return kName;
+ }
+
virtual std::unique_ptr<MatchExpression> shallowClone() const {
std::unique_ptr<ComparisonMatchExpression> e = stdx::make_unique<EqualityMatchExpression>();
invariantOK(e->init(path(), _rhs));
@@ -152,9 +192,16 @@ public:
}
};
-class LTEMatchExpression : public ComparisonMatchExpression {
+class LTEMatchExpression final : public ComparisonMatchExpression {
public:
+ static constexpr StringData kName = "$lte"_sd;
+
LTEMatchExpression() : ComparisonMatchExpression(LTE) {}
+
+ StringData name() const final {
+ return kName;
+ }
+
virtual std::unique_ptr<MatchExpression> shallowClone() const {
std::unique_ptr<ComparisonMatchExpression> e = stdx::make_unique<LTEMatchExpression>();
invariantOK(e->init(path(), _rhs));
@@ -166,9 +213,16 @@ public:
}
};
-class LTMatchExpression : public ComparisonMatchExpression {
+class LTMatchExpression final : public ComparisonMatchExpression {
public:
+ static constexpr StringData kName = "$lt"_sd;
+
LTMatchExpression() : ComparisonMatchExpression(LT) {}
+
+ StringData name() const final {
+ return kName;
+ }
+
virtual std::unique_ptr<MatchExpression> shallowClone() const {
std::unique_ptr<ComparisonMatchExpression> e = stdx::make_unique<LTMatchExpression>();
invariantOK(e->init(path(), _rhs));
@@ -180,9 +234,16 @@ public:
}
};
-class GTMatchExpression : public ComparisonMatchExpression {
+class GTMatchExpression final : public ComparisonMatchExpression {
public:
+ static constexpr StringData kName = "$gt"_sd;
+
GTMatchExpression() : ComparisonMatchExpression(GT) {}
+
+ StringData name() const final {
+ return kName;
+ }
+
virtual std::unique_ptr<MatchExpression> shallowClone() const {
std::unique_ptr<ComparisonMatchExpression> e = stdx::make_unique<GTMatchExpression>();
invariantOK(e->init(path(), _rhs));
@@ -196,7 +257,14 @@ public:
class GTEMatchExpression : public ComparisonMatchExpression {
public:
+ static constexpr StringData kName = "$gte"_sd;
+
GTEMatchExpression() : ComparisonMatchExpression(GTE) {}
+
+ StringData name() const final {
+ return kName;
+ }
+
virtual std::unique_ptr<MatchExpression> shallowClone() const {
std::unique_ptr<ComparisonMatchExpression> e = stdx::make_unique<GTEMatchExpression>();
invariantOK(e->init(path(), _rhs));
diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp
index a32ab4d4c19..7e045da0065 100644
--- a/src/mongo/db/matcher/expression_parser.cpp
+++ b/src/mongo/db/matcher/expression_parser.cpp
@@ -1503,6 +1503,8 @@ StatusWithMatchExpression parseSubField(const BSONObj& context,
const ExtensionsCallback* extensionsCallback,
MatchExpressionParser::AllowedFeatureSet allowedFeatures,
DocumentParseLevel currentLevel) {
+ invariant(e);
+
if ("$eq"_sd == e.fieldNameStringData()) {
return parseComparison(name, new EqualityMatchExpression(), e, expCtx, allowedFeatures);
}
@@ -1667,6 +1669,13 @@ StatusWithMatchExpression parseSubField(const BSONObj& context,
str::stream() << "near must be first in: " << context)};
case PathAcceptingKeyword::INTERNAL_EXPR_EQ: {
+ if (e.type() == BSONType::Undefined || e.type() == BSONType::Array) {
+ return {Status(ErrorCodes::BadValue,
+ str::stream() << InternalExprEqMatchExpression::kName
+ << " cannot be used to compare to type: "
+ << typeName(e.type()))};
+ }
+
auto exprEqExpr = stdx::make_unique<InternalExprEqMatchExpression>();
auto status = exprEqExpr->init(name, e);
if (!status.isOK()) {
diff --git a/src/mongo/db/matcher/expression_parser_test.cpp b/src/mongo/db/matcher/expression_parser_test.cpp
index a5904d68982..4343cf60403 100644
--- a/src/mongo/db/matcher/expression_parser_test.cpp
+++ b/src/mongo/db/matcher/expression_parser_test.cpp
@@ -484,12 +484,17 @@ TEST(MatchExpressionParserTest, InternalExprEqParsesCorrectly) {
ASSERT_TRUE(statusWith.getValue()->matchesBSON(fromjson("{a: {b: [5]}}")));
ASSERT_TRUE(statusWith.getValue()->matchesBSON(fromjson("{a: {b: [6]}}")));
ASSERT_FALSE(statusWith.getValue()->matchesBSON(fromjson("{a: {b: 6}}")));
+}
- query = fromjson("{'a.b': {$_internalExprEq: [5]}}");
- statusWith = MatchExpressionParser::parse(query, expCtx);
- ASSERT_OK(statusWith.getStatus());
- ASSERT_TRUE(statusWith.getValue()->matchesBSON(fromjson("{a: {b: [5]}}")));
- ASSERT_TRUE(statusWith.getValue()->matchesBSON(fromjson("{a: {b: [6]}}")));
- ASSERT_FALSE(statusWith.getValue()->matchesBSON(fromjson("{a: {b: 5}}")));
+TEST(MatchesExpressionParserTest, InternalExprEqComparisonToArrayDoesNotParse) {
+ boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ auto query = fromjson("{'a.b': {$_internalExprEq: [5]}}");
+ ASSERT_EQ(MatchExpressionParser::parse(query, expCtx).getStatus(), ErrorCodes::BadValue);
+}
+
+TEST(MatchesExpressionParserTest, InternalExprEqComparisonToUndefinedDoesNotParse) {
+ boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
+ auto query = fromjson("{'a.b': {$_internalExprEq: undefined}}");
+ ASSERT_EQ(MatchExpressionParser::parse(query, expCtx).getStatus(), ErrorCodes::BadValue);
}
} // namespace mongo
diff --git a/src/mongo/db/query/index_bounds_builder.cpp b/src/mongo/db/query/index_bounds_builder.cpp
index 344cb119877..aa91b2c35a2 100644
--- a/src/mongo/db/query/index_bounds_builder.cpp
+++ b/src/mongo/db/query/index_bounds_builder.cpp
@@ -39,6 +39,7 @@
#include "mongo/db/index/expression_params.h"
#include "mongo/db/index/s2_common.h"
#include "mongo/db/matcher/expression_geo.h"
+#include "mongo/db/matcher/expression_internal_expr_eq.h"
#include "mongo/db/query/collation/collation_index_key.h"
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/query/expression_index.h"
@@ -302,8 +303,8 @@ void IndexBoundsBuilder::translate(const MatchExpression* expr,
}
if (isHashed) {
- verify(MatchExpression::EQ == expr->matchType() ||
- MatchExpression::MATCH_IN == expr->matchType());
+ invariant(MatchExpression::MATCH_IN == expr->matchType() ||
+ ComparisonMatchExpressionBase::isEquality(expr->matchType()));
}
if (MatchExpression::ELEM_MATCH_VALUE == expr->matchType()) {
@@ -395,8 +396,8 @@ void IndexBoundsBuilder::translate(const MatchExpression* expr,
} else {
*tightnessOut = IndexBoundsBuilder::INEXACT_FETCH;
}
- } else if (MatchExpression::EQ == expr->matchType()) {
- const EqualityMatchExpression* node = static_cast<const EqualityMatchExpression*>(expr);
+ } else if (ComparisonMatchExpressionBase::isEquality(expr->matchType())) {
+ const auto* node = static_cast<const ComparisonMatchExpressionBase*>(expr);
translateEquality(node->getData(), index, isHashed, oilOut, tightnessOut);
} else if (MatchExpression::LTE == expr->matchType()) {
const LTEMatchExpression* node = static_cast<const LTEMatchExpression*>(expr);
diff --git a/src/mongo/db/query/index_bounds_builder_test.cpp b/src/mongo/db/query/index_bounds_builder_test.cpp
index 8305f91be4b..f865979f3eb 100644
--- a/src/mongo/db/query/index_bounds_builder_test.cpp
+++ b/src/mongo/db/query/index_bounds_builder_test.cpp
@@ -498,6 +498,82 @@ TEST(IndexBoundsBuilderTest, TranslateEqual) {
ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
}
+TEST(IndexBoundsBuilderTest, TranslateExprEqual) {
+ BSONObj keyPattern = BSON("a" << 1);
+ BSONElement elt = keyPattern.firstElement();
+ IndexEntry testIndex{keyPattern};
+ BSONObj obj = BSON("a" << BSON("$_internalExprEq" << 4));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': 4, '': 4}"), true, true)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateExprEqualToStringRespectsCollation) {
+ BSONObj keyPattern = BSON("a" << 1);
+ BSONElement elt = keyPattern.firstElement();
+ CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kReverseString);
+ IndexEntry testIndex{keyPattern};
+ testIndex.collator = &collator;
+
+ BSONObj obj = BSON("a" << BSON("$_internalExprEq"
+ << "foo"));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(
+ Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': 'oof', '': 'oof'}"), true, true)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateExprEqualHashedIndex) {
+ BSONObj keyPattern = fromjson("{a: 'hashed'}");
+ BSONElement elt = keyPattern.firstElement();
+ IndexEntry testIndex{keyPattern};
+ BSONObj obj = BSON("a" << BSON("$_internalExprEq" << 4));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+
+ BSONObj expectedHash = ExpressionMapping::hash(BSON("" << 4).firstElement());
+ BSONObjBuilder intervalBuilder;
+ intervalBuilder.append("", expectedHash.firstElement().numberLong());
+ intervalBuilder.append("", expectedHash.firstElement().numberLong());
+ BSONObj intervalObj = intervalBuilder.obj();
+
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(intervalObj, true, true)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::INEXACT_FETCH);
+}
+
+TEST(IndexBoundsBuilderTest, TranslateExprEqualToNullIsInexactFetch) {
+ BSONObj keyPattern = BSON("a" << 1);
+ BSONElement elt = keyPattern.firstElement();
+ IndexEntry testIndex{keyPattern};
+ BSONObj obj = BSON("a" << BSON("$_internalExprEq" << BSONNULL));
+ unique_ptr<MatchExpression> expr(parseMatchExpression(obj));
+ OrderedIntervalList oil;
+ IndexBoundsBuilder::BoundsTightness tightness;
+ IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness);
+ ASSERT_EQUALS(oil.name, "a");
+ ASSERT_EQUALS(oil.intervals.size(), 1U);
+ ASSERT_EQUALS(Interval::INTERVAL_EQUALS,
+ oil.intervals[0].compare(Interval(fromjson("{'': null, '': null}"), true, true)));
+ ASSERT_EQUALS(tightness, IndexBoundsBuilder::INEXACT_FETCH);
+}
+
TEST(IndexBoundsBuilderTest, TranslateArrayEqualBasic) {
IndexEntry testIndex = IndexEntry(BSONObj());
BSONObj obj = fromjson("{a: [1, 2, 3]}");
diff --git a/src/mongo/db/query/indexability.h b/src/mongo/db/query/indexability.h
index 6861be067f6..f892d91b37c 100644
--- a/src/mongo/db/query/indexability.h
+++ b/src/mongo/db/query/indexability.h
@@ -132,15 +132,6 @@ public:
}
/**
- * Returns true if 'me' is of type EQ, GT, GTE, LT, or LTE.
- */
- static bool isEqualityOrInequality(const MatchExpression* me) {
- return (me->matchType() == MatchExpression::EQ || me->matchType() == MatchExpression::GT ||
- me->matchType() == MatchExpression::GTE || me->matchType() == MatchExpression::LT ||
- me->matchType() == MatchExpression::LTE);
- }
-
- /**
* Returns true if 'elt' is a BSONType for which exact index bounds can be generated.
*/
static bool isExactBoundsGenerating(BSONElement elt) {
@@ -177,7 +168,9 @@ private:
me->matchType() == MatchExpression::TYPE_OPERATOR ||
me->matchType() == MatchExpression::GEO ||
me->matchType() == MatchExpression::GEO_NEAR ||
- me->matchType() == MatchExpression::EXISTS || me->matchType() == MatchExpression::TEXT;
+ me->matchType() == MatchExpression::EXISTS ||
+ me->matchType() == MatchExpression::TEXT ||
+ me->matchType() == MatchExpression::INTERNAL_EXPR_EQ;
}
};
diff --git a/src/mongo/db/query/plan_cache.cpp b/src/mongo/db/query/plan_cache.cpp
index 577cacd3a2b..37de1384fcd 100644
--- a/src/mongo/db/query/plan_cache.cpp
+++ b/src/mongo/db/query/plan_cache.cpp
@@ -177,6 +177,9 @@ const char* encodeMatchType(MatchExpression::MatchType mt) {
case MatchExpression::EXPRESSION:
return "xp";
+ case MatchExpression::INTERNAL_EXPR_EQ:
+ return "ee";
+
case MatchExpression::INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX:
return "internalSchemaAllElemMatchFromIndex";
diff --git a/src/mongo/db/query/plan_cache_indexability.cpp b/src/mongo/db/query/plan_cache_indexability.cpp
index a4646714abd..4576d34c0ed 100644
--- a/src/mongo/db/query/plan_cache_indexability.cpp
+++ b/src/mongo/db/query/plan_cache_indexability.cpp
@@ -34,6 +34,7 @@
#include "mongo/base/owned_pointer_vector.h"
#include "mongo/db/matcher/expression.h"
#include "mongo/db/matcher/expression_algo.h"
+#include "mongo/db/matcher/expression_internal_expr_eq.h"
#include "mongo/db/matcher/expression_leaf.h"
#include "mongo/db/query/collation/collation_index_key.h"
#include "mongo/db/query/collation/collator_interface.h"
@@ -82,9 +83,8 @@ void PlanCacheIndexabilityState::processIndexCollation(const std::string& indexN
for (BSONElement elem : keyPattern) {
_pathDiscriminatorsMap[elem.fieldNameStringData()][indexName].addDiscriminator([collator](
const MatchExpression* queryExpr) {
- if (ComparisonMatchExpression::isComparisonMatchExpression(queryExpr)) {
- const auto* queryExprComparison =
- static_cast<const ComparisonMatchExpression*>(queryExpr);
+ if (const auto* queryExprComparison =
+ dynamic_cast<const ComparisonMatchExpressionBase*>(queryExpr)) {
const bool collatorsMatch =
CollatorInterface::collatorsMatch(queryExprComparison->getCollator(), collator);
const bool isCollatableType =
diff --git a/src/mongo/db/query/plan_cache_indexability_test.cpp b/src/mongo/db/query/plan_cache_indexability_test.cpp
index 5d008f9665a..bebec5ccdeb 100644
--- a/src/mongo/db/query/plan_cache_indexability_test.cpp
+++ b/src/mongo/db/query/plan_cache_indexability_test.cpp
@@ -72,6 +72,12 @@ TEST(PlanCacheIndexabilityTest, SparseIndexSimple) {
disc.isMatchCompatibleWithIndex(parseMatchExpression(BSON("a" << BSONNULL)).get()));
ASSERT_EQ(true,
disc.isMatchCompatibleWithIndex(
+ parseMatchExpression(BSON("a" << BSON("$_internalExprEq" << 1))).get()));
+ ASSERT_EQ(true,
+ disc.isMatchCompatibleWithIndex(
+ parseMatchExpression(BSON("a" << BSON("$_internalExprEq" << BSONNULL))).get()));
+ ASSERT_EQ(true,
+ disc.isMatchCompatibleWithIndex(
parseMatchExpression(BSON("a" << BSON("$in" << BSON_ARRAY(1)))).get()));
ASSERT_EQ(false,
disc.isMatchCompatibleWithIndex(
@@ -329,8 +335,13 @@ TEST(PlanCacheIndexabilityTest, DiscriminatorForCollationIndicatesWhenCollations
ASSERT_EQ(true,
disc.isMatchCompatibleWithIndex(
parseMatchExpression(fromjson("{a: {$in: ['abc', 'xyz']}}"), &collator).get()));
+ ASSERT_EQ(
+ true,
+ disc.isMatchCompatibleWithIndex(
+ parseMatchExpression(fromjson("{a: {$_internalExprEq: 'abc'}}}"), &collator).get()));
- // Expression is not a ComparisonMatchExpression or InMatchExpression.
+ // Expression is not a ComparisonMatchExpression, InternalExprEqMatchExpression or
+ // InMatchExpression.
ASSERT_EQ(true,
disc.isMatchCompatibleWithIndex(
parseMatchExpression(fromjson("{a: {$exists: true}}"), nullptr).get()));
@@ -349,6 +360,18 @@ TEST(PlanCacheIndexabilityTest, DiscriminatorForCollationIndicatesWhenCollations
disc.isMatchCompatibleWithIndex(
parseMatchExpression(fromjson("{a: ['abc', 'xyz']}"), nullptr).get()));
+ // Expression is an InternalExprEqMatchExpression with non-matching collator.
+ ASSERT_EQ(true,
+ disc.isMatchCompatibleWithIndex(
+ parseMatchExpression(fromjson("{a: {$_internalExprEq: 5}}"), nullptr).get()));
+ ASSERT_EQ(false,
+ disc.isMatchCompatibleWithIndex(
+ parseMatchExpression(fromjson("{a: {$_internalExprEq: 'abc'}}"), nullptr).get()));
+ ASSERT_EQ(
+ false,
+ disc.isMatchCompatibleWithIndex(
+ parseMatchExpression(fromjson("{a: {$_internalExprEq: {b: 'abc'}}}"), nullptr).get()));
+
// Expression is an InMatchExpression with non-matching collator.
ASSERT_EQ(true,
disc.isMatchCompatibleWithIndex(
diff --git a/src/mongo/db/query/planner_ixselect.cpp b/src/mongo/db/query/planner_ixselect.cpp
index 98f34186c72..166a0627ec9 100644
--- a/src/mongo/db/query/planner_ixselect.cpp
+++ b/src/mongo/db/query/planner_ixselect.cpp
@@ -39,6 +39,7 @@
#include "mongo/db/matcher/expression_algo.h"
#include "mongo/db/matcher/expression_array.h"
#include "mongo/db/matcher/expression_geo.h"
+#include "mongo/db/matcher/expression_internal_expr_eq.h"
#include "mongo/db/matcher/expression_text.h"
#include "mongo/db/query/collation/collator_interface.h"
#include "mongo/db/query/index_tag.h"
@@ -99,16 +100,14 @@ static bool twoDWontWrap(const Circle& circle, const IndexEntry& index) {
// Checks whether 'node' contains any comparison to an element of type 'type'. Nested objects and
// arrays are not checked recursively. We assume 'node' is bounds-generating or is a recursive child
// of a bounds-generating node, i.e. it does not contain AND, OR, ELEM_MATCH_OBJECT, or NOR.
-// TODO SERVER-23172: Check nested objects and arrays.
static bool boundsGeneratingNodeContainsComparisonToType(MatchExpression* node, BSONType type) {
invariant(node->matchType() != MatchExpression::AND &&
node->matchType() != MatchExpression::OR &&
node->matchType() != MatchExpression::NOR &&
node->matchType() != MatchExpression::ELEM_MATCH_OBJECT);
- if (Indexability::isEqualityOrInequality(node)) {
- const ComparisonMatchExpression* expr = static_cast<const ComparisonMatchExpression*>(node);
- return expr->getData().type() == type;
+ if (const auto* comparisonExpr = dynamic_cast<const ComparisonMatchExpressionBase*>(node)) {
+ return comparisonExpr->getData().type() == type;
}
if (node->matchType() == MatchExpression::MATCH_IN) {
@@ -217,9 +216,19 @@ bool QueryPlannerIXSelect::compatible(const BSONElement& elt,
// We know elt.fieldname() == node->path().
MatchExpression::MatchType exprtype = node->matchType();
+ if (exprtype == MatchExpression::INTERNAL_EXPR_EQ &&
+ indexedFieldHasMultikeyComponents(elt.fieldNameStringData(), index)) {
+ // Expression language equality cannot be indexed if the field path has multikey components.
+ return false;
+ }
+
if (indexedFieldType.empty()) {
// Can't use a sparse index for $eq with a null element, unless the equality is within a
// $elemMatch expression since the latter implies a match on the literal element 'null'.
+ //
+ // We can use a sparse index for $_internalExprEq with a null element. Expression language
+ // equality-to-null semantics are that only literal nulls match. Sparse indexes contain
+ // index keys for literal nulls, but not for missing elements.
if (exprtype == MatchExpression::EQ && index.sparse && !elemMatchChild) {
const EqualityMatchExpression* expr = static_cast<const EqualityMatchExpression*>(node);
if (expr->getData().isNull()) {
@@ -317,7 +326,7 @@ bool QueryPlannerIXSelect::compatible(const BSONElement& elt,
invariant(0);
return true;
} else if (IndexNames::HASHED == indexedFieldType) {
- if (exprtype == MatchExpression::EQ) {
+ if (ComparisonMatchExpressionBase::isEquality(exprtype)) {
return true;
}
if (exprtype == MatchExpression::MATCH_IN) {
@@ -405,8 +414,8 @@ void QueryPlannerIXSelect::rateIndices(MatchExpression* node,
}
verify(NULL == node->getTag());
- RelevantTag* rt = new RelevantTag();
- node->setTag(rt);
+ node->setTag(new RelevantTag());
+ auto rt = static_cast<RelevantTag*>(node->getTag());
rt->path = fullPath;
// TODO: This is slow, with all the string compares.
@@ -466,6 +475,24 @@ void QueryPlannerIXSelect::stripInvalidAssignments(MatchExpression* node,
stripInvalidAssignmentsToPartialIndices(node, indices);
}
+bool QueryPlannerIXSelect::indexedFieldHasMultikeyComponents(StringData indexedField,
+ const IndexEntry& index) {
+ if (index.multikeyPaths.empty()) {
+ // The index has no path-level multikeyness metadata.
+ return index.multikey;
+ }
+
+ size_t pos = 0;
+ for (auto&& key : index.keyPattern) {
+ if (key.fieldNameStringData() == indexedField) {
+ return !index.multikeyPaths[pos].empty();
+ }
+ ++pos;
+ }
+
+ MONGO_UNREACHABLE;
+}
+
namespace {
/**
diff --git a/src/mongo/db/query/planner_ixselect.h b/src/mongo/db/query/planner_ixselect.h
index 7cb34973810..715df2014fe 100644
--- a/src/mongo/db/query/planner_ixselect.h
+++ b/src/mongo/db/query/planner_ixselect.h
@@ -84,8 +84,6 @@ public:
*
* For an index to be useful to a predicate, the index must be compatible (see above).
*
- * If an index is prefixed by the predicate's path, it's always useful.
- *
* If an index is compound but not prefixed by a predicate's path, it's only useful if
* there exists another predicate that 1. will use that index and 2. is related to the
* original predicate by having an AND as a parent.
@@ -131,6 +129,12 @@ public:
static void stripUnneededAssignments(MatchExpression* node,
const std::vector<IndexEntry>& indices);
+ /**
+ * Returns true if the indexed field has any multikey components. Illegal to call unless
+ * 'indexedField' is present in the key pattern for 'index'.
+ */
+ static bool indexedFieldHasMultikeyComponents(StringData indexedField, const IndexEntry& index);
+
private:
/**
* Amend the RelevantTag lists for all predicates in the subtree rooted at 'node' to remove
diff --git a/src/mongo/db/query/planner_ixselect_test.cpp b/src/mongo/db/query/planner_ixselect_test.cpp
index 50053c7659b..a6603f7c681 100644
--- a/src/mongo/db/query/planner_ixselect_test.cpp
+++ b/src/mongo/db/query/planner_ixselect_test.cpp
@@ -37,6 +37,7 @@
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/query/collation/collator_interface_mock.h"
#include "mongo/db/query/index_tag.h"
+#include "mongo/unittest/death_test.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/text.h"
#include <memory>
@@ -457,6 +458,52 @@ TEST(QueryPlannerIXSelectTest, NoStringComparison) {
testRateIndices("{a: 1}", "", &collator, indices, "a", expectedIndices);
}
+TEST(QueryPlannerIXSelectTest, StringInternalExprEqUnequalCollatorsCannotUseIndex) {
+ CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual);
+ IndexEntry index(BSON("a" << 1));
+ CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
+ index.collator = &indexCollator;
+ std::vector<IndexEntry> indices;
+ indices.push_back(index);
+ std::set<size_t> expectedIndices;
+ testRateIndices(
+ "{a: {$_internalExprEq: 'string'}}", "", &collator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, StringInternalExprEqEqualCollatorsCanUseIndex) {
+ CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual);
+ IndexEntry index(BSON("a" << 1));
+ index.collator = &collator;
+ std::vector<IndexEntry> indices;
+ indices.push_back(index);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: 'string'}}", "", &collator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, NestedObjectInternalExprEqUnequalCollatorsCannotUseIndex) {
+ CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual);
+ IndexEntry index(BSON("a" << 1));
+ CollatorInterfaceMock indexCollator(CollatorInterfaceMock::MockType::kReverseString);
+ index.collator = &indexCollator;
+ std::vector<IndexEntry> indices;
+ indices.push_back(index);
+ std::set<size_t> expectedIndices;
+ testRateIndices(
+ "{a: {$_internalExprEq: {b: 'string'}}}", "", &collator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, NestedObjectInternalExprEqEqualCollatorsCanUseIndex) {
+ CollatorInterfaceMock collator(CollatorInterfaceMock::MockType::kAlwaysEqual);
+ IndexEntry index(BSON("a" << 1));
+ index.collator = &collator;
+ std::vector<IndexEntry> indices;
+ indices.push_back(index);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: {b: 'string'}}}", "", &collator, indices, "a", expectedIndices);
+}
+
/**
* $gt string comparison requires matching collator.
*/
@@ -982,4 +1029,149 @@ TEST(QueryPlannerIXSelectTest, NoStringComparisonType) {
}
}
+IndexEntry makeIndexEntry(BSONObj keyPattern, MultikeyPaths multiKeyPaths) {
+ IndexEntry entry{std::move(keyPattern)};
+ entry.multikeyPaths = std::move(multiKeyPaths);
+ entry.multikey = std::any_of(entry.multikeyPaths.cbegin(),
+ entry.multikeyPaths.cend(),
+ [](const auto& entry) { return !entry.empty(); });
+ return entry;
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCannotUseMultiKeyIndex) {
+ IndexEntry entry = makeIndexEntry(BSON("a" << 1), {{0U}});
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices;
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCanUseNonMultikeyFieldOfMultikeyIndex) {
+ IndexEntry entry = makeIndexEntry(BSON("a" << 1 << "b" << 1), {{0U}, {}});
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{b: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "b", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCannotUseMultikeyIndexWithoutPathLevelMultikeyData) {
+ IndexEntry entry{BSON("a" << 1)};
+ entry.multikey = true;
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices;
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCanUseNonMultikeyIndexWithNoPathLevelMultikeyData) {
+ IndexEntry entry{BSON("a" << 1)};
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCanUseHashedIndex) {
+ IndexEntry entry{BSON("a"
+ << "hashed")};
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCannotUseTextIndexPrefix) {
+ IndexEntry entry{BSON("a" << 1 << "_fts"
+ << "text"
+ << "_ftsx"
+ << 1)};
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices;
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCanUseTextIndexSuffix) {
+ IndexEntry entry{BSON("_fts"
+ << "text"
+ << "_ftsx"
+ << 1
+ << "a"
+ << 1)};
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCanUseSparseIndexWithComparisonToNull) {
+ IndexEntry entry{BSON("a" << 1)};
+ entry.sparse = true;
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: null}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, InternalExprEqCanUseSparseIndexWithComparisonToNonNull) {
+ IndexEntry entry{BSON("a" << 1)};
+ entry.sparse = true;
+ std::vector<IndexEntry> indices;
+ indices.push_back(entry);
+ std::set<size_t> expectedIndices = {0};
+ testRateIndices(
+ "{a: {$_internalExprEq: 1}}", "", kSimpleCollator, indices, "a", expectedIndices);
+}
+
+TEST(QueryPlannerIXSelectTest, IndexedFieldHasMultikeyComponents) {
+ auto indexEntry = makeIndexEntry(BSON("a" << 1 << "b.c" << 1), {{}, {}});
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a"_sd, indexEntry));
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("b.c"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a" << 1 << "b" << 1), {{}, {0U}});
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a"_sd, indexEntry));
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("b"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a" << 1 << "b" << 1 << "c.d" << 1), {{}, {}, {1U}});
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a"_sd, indexEntry));
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("b"_sd, indexEntry));
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("c.d"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a.b" << 1 << "a.c" << 1), {{}, {1U}});
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a.b"_sd, indexEntry));
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a.c"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a.b" << 1 << "a.c" << 1), {{0U, 1U}, {0U}});
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a.b"_sd, indexEntry));
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a.c"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a" << 1 << "b" << 1), {{0U}, {}});
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a"_sd, indexEntry));
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("b"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a.b.c" << 1 << "d" << 1), {{1U, 2U}, {}});
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a.b.c"_sd, indexEntry));
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("d"_sd, indexEntry));
+
+ indexEntry = makeIndexEntry(BSON("a.b" << 1 << "c" << 1 << "d" << 1), {{1U}, {}, {0U}});
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("a.b"_sd, indexEntry));
+ ASSERT_FALSE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("c"_sd, indexEntry));
+ ASSERT_TRUE(QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("d"_sd, indexEntry));
+}
+
+DEATH_TEST(QueryPlannerIXSelectTest,
+ IndexedFieldHasMultikeyComponentsPassingInvalidFieldIsFatal,
+ "Invariant failure Hit a MONGO_UNREACHABLE!") {
+ auto indexEntry = makeIndexEntry(BSON("a" << 1), {{}});
+ QueryPlannerIXSelect::indexedFieldHasMultikeyComponents("b"_sd, indexEntry);
+}
+
} // namespace
diff --git a/src/mongo/db/query/query_planner_geo_test.cpp b/src/mongo/db/query/query_planner_geo_test.cpp
index d9474eddae3..15fa04bdf3d 100644
--- a/src/mongo/db/query/query_planner_geo_test.cpp
+++ b/src/mongo/db/query/query_planner_geo_test.cpp
@@ -1652,4 +1652,118 @@ TEST_F(QueryPlannerTest, 2dNearInexactFetchPredicateOverTrailingFieldMultikey) {
"{fetch: {filter: {b: {$exists: true}}, node: {geoNear2d: {a: '2d', b: 1}}}}");
}
+TEST_F(QueryPlannerTest, 2dNearWithInternalExprEqOverTrailingField) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a"
+ << "2d"
+ << "b"
+ << 1));
+
+ runQuery(fromjson("{a: {$near: [0, 0]}, b: {$_internalExprEq: 1}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists("{geoNear2d: {a: '2d', b: 1}}}}");
+}
+
+TEST_F(QueryPlannerTest, 2dNearWithInternalExprEqOverTrailingFieldMultikey) {
+ const bool multikey = true;
+ addIndex(BSON("a"
+ << "2d"
+ << "b"
+ << 1),
+ multikey);
+
+ runQuery(fromjson("{a: {$near: [0, 0]}, b: {$_internalExprEq: 1}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$_internalExprEq: 1}}, node: {geoNear2d: {a: '2d', b: 1}}}}");
+}
+
+TEST_F(QueryPlannerTest, 2dGeoWithinWithInternalExprEqOverTrailingField) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a"
+ << "2d"
+ << "b"
+ << 1));
+
+ runQuery(
+ fromjson("{a: {$within: {$polygon: [[0,0], [2,0], [4,0]]}}, b: {$_internalExprEq: 2}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$within: {$polygon: [[0,0], [2,0], [4,0]]}}}, node:"
+ "{ixscan: {filter: {b: {$_internalExprEq: 2}}, pattern: {a: '2d', b: 1}}}}}");
+}
+
+TEST_F(QueryPlannerTest, 2dsphereNearWithInternalExprEq) {
+ addIndex(BSON("a" << 1 << "b"
+ << "2dsphere"));
+ runQuery(
+ fromjson("{a: {$_internalExprEq: 0}, b: {$near: {$geometry: "
+ "{type: 'Point', coordinates: [2, 2]}}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{geoNear2dsphere: {pattern: {a: 1, b: '2dsphere'}, "
+ "bounds: {a: [[0,0,true,true]], b: [['MinKey','MaxKey',true,true]]}}}");
+}
+
+TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverLeadingField) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1 << "b"
+ << "2dsphere"));
+
+ runQuery(
+ fromjson("{a: {$_internalExprEq: 0}, b: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {b: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}, node: "
+ "{ixscan: {pattern: {a: 1, b: '2dsphere'}, filter: null, bounds:"
+ "{a: [[0,0,true,true]], b: []}}}}}");
+}
+
+TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverLeadingFieldMultikey) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ const bool multikey = true;
+ addIndex(BSON("a" << 1 << "b"
+ << "2dsphere"),
+ multikey);
+
+ runQuery(
+ fromjson("{a: {$_internalExprEq: 0}, b: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}"));
+ assertNumSolutions(0U);
+}
+
+TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverTrailingField) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a"
+ << "2dsphere"
+ << "b"
+ << 1));
+
+ runQuery(
+ fromjson("{b: {$_internalExprEq: 0}, a: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$geoWithin: {$centerSphere: [[0, 0], 10]}}}, node: "
+ "{ixscan: {pattern: {a : '2dsphere', b: 1}, filter: null, bounds:"
+ "{a: [], b: [[0,0,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, 2dsphereNonNearWithInternalExprEqOverTrailingFieldMultikey) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ const bool multikey = true;
+ addIndex(BSON("a"
+ << "2dsphere"
+ << "b"
+ << 1),
+ multikey);
+
+ runQuery(
+ fromjson("{a: {$geoWithin: {$centerSphere: [[0, 0], 10]}}, b: {$_internalExprEq: 0}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$geoWithin: {$centerSphere: [[0,0],10]}}, b: {$_internalExprEq: 0}},"
+ "node: {ixscan: {pattern: {a : '2dsphere', b: 1}, filter: null, bounds:"
+ "{a: [], b: [['MinKey','MaxKey',true,true]]}}}}}");
+}
+
} // namespace
diff --git a/src/mongo/db/query/query_planner_partialidx_test.cpp b/src/mongo/db/query/query_planner_partialidx_test.cpp
index 9626c19d8b3..e56f114b6d4 100644
--- a/src/mongo/db/query/query_planner_partialidx_test.cpp
+++ b/src/mongo/db/query/query_planner_partialidx_test.cpp
@@ -479,5 +479,15 @@ TEST_F(QueryPlannerTest, PartialIndexNoStringComparisonNonMatchingCollators) {
"bounds: {a: [[1, 1, true, true]]}}}}}");
}
+TEST_F(QueryPlannerTest, InternalExprEqCannotUsePartialIndex) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ BSONObj filterObj(fromjson("{a: {$gte: 0}}"));
+ auto filterExpr = parseMatchExpression(filterObj);
+ addIndex(fromjson("{a: 1}"), filterExpr.get());
+
+ runQueryAsCommand(fromjson("{find: 'testns', filter: {a: {$_internalExprEq: 1}}}"));
+ assertNumSolutions(0U);
+}
+
} // namespace
} // namespace mongo
diff --git a/src/mongo/db/query/query_planner_test.cpp b/src/mongo/db/query/query_planner_test.cpp
index 6407ce75786..8ac5cbd0c06 100644
--- a/src/mongo/db/query/query_planner_test.cpp
+++ b/src/mongo/db/query/query_planner_test.cpp
@@ -85,6 +85,24 @@ TEST_F(QueryPlannerTest, EqualityIndexScanWithTrailingFields) {
assertSolutionExists("{fetch: {filter: null, node: {ixscan: {pattern: {x: 1, y: 1}}}}}");
}
+TEST_F(QueryPlannerTest, ExprEqCanUseIndex) {
+ params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
+ addIndex(BSON("a" << 1));
+ runQuery(fromjson("{a: {$_internalExprEq: 1}}"));
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: {pattern: {a: 1}, bounds: {a: "
+ "[[1,1,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExprEqCannotUseMultikeyFieldOfIndex) {
+ MultikeyPaths multikeyPaths{{0U}};
+ addIndex(BSON("a.b" << 1), multikeyPaths);
+ runQuery(fromjson("{'a.b': {$_internalExprEq: 1}}"));
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1, filter: {'a.b': {$_internalExprEq: 1}}}}");
+}
+
// $eq can use a hashed index because it looks for values of type regex;
// it doesn't evaluate the regex itself.
TEST_F(QueryPlannerTest, EqCanUseHashedIndexWithRegex) {
@@ -94,6 +112,28 @@ TEST_F(QueryPlannerTest, EqCanUseHashedIndexWithRegex) {
ASSERT_EQUALS(getNumSolutions(), 2U);
}
+TEST_F(QueryPlannerTest, ExprEqCanUseHashedIndex) {
+ params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
+ addIndex(BSON("a"
+ << "hashed"));
+ runQuery(fromjson("{a: {$_internalExprEq: 1}}"));
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$_internalExprEq: 1}}, node: {ixscan: {filter: null, pattern: {a: "
+ "'hashed'}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExprEqCanUseHashedIndexWithRegex) {
+ params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
+ addIndex(BSON("a"
+ << "hashed"));
+ runQuery(fromjson("{a: {$_internalExprEq: /abc/}}"));
+ ASSERT_EQUALS(getNumSolutions(), 1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$_internalExprEq: /abc/}}, node: {ixscan: {filter: null, pattern: "
+ "{a: 'hashed'}}}}}");
+}
+
//
// indexFilterApplied
// Check that index filter flag is passed from planner params
@@ -2481,6 +2521,28 @@ TEST_F(QueryPlannerTest, SparseIndexForQuery) {
"{filter: null, pattern: {a: 1}}}}}");
}
+TEST_F(QueryPlannerTest, ExprEqCanUseSparseIndex) {
+ params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
+ addIndex(fromjson("{a: 1}"), false, true);
+ runQuery(fromjson("{a: {$_internalExprEq: 1}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: null, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}, bounds: {a: [[1,1,true,true]]}}}}}");
+}
+
+TEST_F(QueryPlannerTest, ExprEqCanUseSparseIndexForEqualityToNull) {
+ params.options &= ~QueryPlannerParams::INCLUDE_COLLSCAN;
+ addIndex(fromjson("{a: 1}"), false, true);
+ runQuery(fromjson("{a: {$_internalExprEq: null}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{fetch: {filter: {a: {$_internalExprEq: null}}, node: {ixscan: "
+ "{filter: null, pattern: {a: 1}, bounds: {a: [[null,null,true,true]]}}}}}");
+}
+
//
// Regex
//
diff --git a/src/mongo/db/query/query_planner_text_test.cpp b/src/mongo/db/query/query_planner_text_test.cpp
index 1488ddd849c..3ebb84bbc6d 100644
--- a/src/mongo/db/query/query_planner_text_test.cpp
+++ b/src/mongo/db/query/query_planner_text_test.cpp
@@ -545,4 +545,30 @@ TEST_F(QueryPlannerTest, InexactFetchPredicateOverTrailingFieldHandledCorrectlyM
"{fetch: {filter: {b: {$exists: true}}, node: {text: {search: 'foo', prefix: {a: 3}}}}}");
}
+TEST_F(QueryPlannerTest, ExprEqCannotUsePrefixOfTextIndex) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("a" << 1 << "_fts"
+ << "text"
+ << "_ftsx"
+ << 1));
+
+ runInvalidQuery(fromjson("{a: {$_internalExprEq: 3}, $text: {$search: 'blah'}}"));
+}
+
+TEST_F(QueryPlannerTest, ExprEqCanUseSuffixOfTextIndex) {
+ params.options = QueryPlannerParams::NO_TABLE_SCAN;
+ addIndex(BSON("_fts"
+ << "text"
+ << "_ftsx"
+ << 1
+ << "a"
+ << 1));
+
+ runQuery(fromjson("{a: {$_internalExprEq: 3}, $text: {$search: 'blah'}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists(
+ "{text: {search: 'blah', prefix: {}, filter: {a: {$_internalExprEq: 3}}}}");
+}
+
} // namespace