diff options
author | David Storch <david.storch@10gen.com> | 2017-12-20 11:21:27 -0500 |
---|---|---|
committer | David Storch <david.storch@10gen.com> | 2018-02-01 12:51:48 -0500 |
commit | 779beeca39066f939dc4fdbcb05f2e6ed05e99fa (patch) | |
tree | ae4519fdfebae3c40a2471896949055874640b82 | |
parent | 72267ebfbe177cb37f742397f633bc484ebf52c5 (diff) | |
download | mongo-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
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 |