diff options
author | Katherine Wu <katherine.wu@mongodb.com> | 2021-02-01 09:59:12 -0500 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-02-01 22:02:40 +0000 |
commit | 120901099ba270b557054bc8ef06ac87418c0834 (patch) | |
tree | fb2474b3c7621cd8b68325f71eac307d0e1b0518 /src/mongo/db/query | |
parent | 060d4ef8514110ff4d03867b8f704cf9de307905 (diff) | |
download | mongo-120901099ba270b557054bc8ef06ac87418c0834.tar.gz |
SERVER-39943 Create match expressions for aggregation $gt/$gte/$lt/$lte and add to $expr rewrite
Diffstat (limited to 'src/mongo/db/query')
-rw-r--r-- | src/mongo/db/query/canonical_query_encoder.cpp | 14 | ||||
-rw-r--r-- | src/mongo/db/query/index_bounds_builder.cpp | 176 | ||||
-rw-r--r-- | src/mongo/db/query/index_bounds_builder_test.cpp | 384 | ||||
-rw-r--r-- | src/mongo/db/query/index_bounds_builder_test.h | 19 | ||||
-rw-r--r-- | src/mongo/db/query/indexability.h | 6 | ||||
-rw-r--r-- | src/mongo/db/query/plan_cache_indexability.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/query/planner_ixselect.cpp | 7 | ||||
-rw-r--r-- | src/mongo/db/query/sbe_stage_builder_filter.cpp | 33 |
8 files changed, 598 insertions, 43 deletions
diff --git a/src/mongo/db/query/canonical_query_encoder.cpp b/src/mongo/db/query/canonical_query_encoder.cpp index ce2f70ec0fb..fb73aa3e5c2 100644 --- a/src/mongo/db/query/canonical_query_encoder.cpp +++ b/src/mongo/db/query/canonical_query_encoder.cpp @@ -190,7 +190,19 @@ const char* encodeMatchType(MatchExpression::MatchType mt) { return "xp"; case MatchExpression::INTERNAL_EXPR_EQ: - return "ee"; + return "eeq"; + + case MatchExpression::INTERNAL_EXPR_GT: + return "egt"; + + case MatchExpression::INTERNAL_EXPR_GTE: + return "ege"; + + case MatchExpression::INTERNAL_EXPR_LT: + return "elt"; + + case MatchExpression::INTERNAL_EXPR_LTE: + return "ele"; case MatchExpression::INTERNAL_SCHEMA_ALL_ELEM_MATCH_FROM_INDEX: return "internalSchemaAllElemMatchFromIndex"; diff --git a/src/mongo/db/query/index_bounds_builder.cpp b/src/mongo/db/query/index_bounds_builder.cpp index e423f60844c..aa05e986aed 100644 --- a/src/mongo/db/query/index_bounds_builder.cpp +++ b/src/mongo/db/query/index_bounds_builder.cpp @@ -41,7 +41,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/matcher/expression_internal_expr_comparison.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" @@ -626,6 +626,75 @@ void IndexBoundsBuilder::_translatePredicate(const MatchExpression* expr, // There is no need to sort intervals or merge overlapping intervals here since the output // is from one element. translateEquality(node->getData(), index, isHashed, oilOut, tightnessOut); + } else if (MatchExpression::LT == expr->matchType()) { + const LTMatchExpression* node = static_cast<const LTMatchExpression*>(expr); + BSONElement dataElt = node->getData(); + + // Everything is < MaxKey, except for MaxKey. However the bounds need to be inclusive to + // find the array [MaxKey] which is smaller for a comparison but equal in a multikey index. + if (MaxKey == dataElt.type()) { + oilOut->intervals.push_back(allValuesRespectingInclusion( + IndexBounds::makeBoundInclusionFromBoundBools(true, index.multikey))); + *tightnessOut = index.collator || index.multikey ? IndexBoundsBuilder::INEXACT_FETCH + : IndexBoundsBuilder::EXACT; + return; + } + + // Nothing is < NaN. + if (std::isnan(dataElt.numberDouble())) { + *tightnessOut = IndexBoundsBuilder::EXACT; + return; + } + + BSONObjBuilder bob; + buildBoundsForQueryElementForLT(dataElt, index.collator, &bob); + BSONObj dataObj = bob.done().getOwned(); + verify(dataObj.isOwned()); + bool inclusiveBounds = dataElt.type() == BSONType::Array; + Interval interval = + makeRangeInterval(dataObj, + IndexBounds::makeBoundInclusionFromBoundBools( + typeMatch(dataObj) || inclusiveBounds, inclusiveBounds)); + + // If the operand to LT is equal to the lower bound X, the interval [X, X) is invalid + // and should not be added to the bounds. + if (!interval.isNull()) { + oilOut->intervals.push_back(interval); + } + + *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); + } else if (MatchExpression::INTERNAL_EXPR_LT == expr->matchType()) { + const auto* node = static_cast<const InternalExprLTMatchExpression*>(expr); + BSONElement dataElt = node->getData(); + + // Unlike the regular $lt match expression, $_internalExprLt does not make special checks + // for when dataElt is MaxKey or NaN because it doesn't do type bracketing for any operand. + // Another difference is that $internalExprLt predicates on multikey paths will not use an + // index. + tassert(3994304, + "$expr comparison predicates on multikey paths cannot use an index", + !index.pathHasMultikeyComponent(elt.fieldNameStringData())); + + BSONObjBuilder bob; + bob.appendMinKey(""); + CollationIndexKey::collationAwareIndexKeyAppend(dataElt, index.collator, &bob); + BSONObj dataObj = bob.obj(); + + // Generally all intervals for $_internalExprLt will exclude the end key, however because + // null and missing are conflated in the index but treated as distinct values for + // expressions (with missing ordered as less than null), when dataElt is null we must build + // index bounds [MinKey, null] to include missing values and filter out the literal null + // values with an INEXACT_FETCH. + Interval interval = makeRangeInterval( + dataObj, IndexBounds::makeBoundInclusionFromBoundBools(true, dataElt.isNull())); + + // If the operand to $_internalExprLt is equal to the lower bound X, the interval [X, X) is + // invalid and should not be added to the bounds. Because $_internalExprLt doesn't perform + // type bracketing, here we need to avoid adding the interval [MinKey, MinKey). + if (!interval.isNull()) { + oilOut->intervals.push_back(interval); + } + *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); } else if (MatchExpression::LTE == expr->matchType()) { const LTEMatchExpression* node = static_cast<const LTEMatchExpression*>(expr); BSONElement dataElt = node->getData(); @@ -664,40 +733,32 @@ void IndexBoundsBuilder::_translatePredicate(const MatchExpression* expr, oilOut->intervals.push_back(interval); *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); - } else if (MatchExpression::LT == expr->matchType()) { - const LTMatchExpression* node = static_cast<const LTMatchExpression*>(expr); + } else if (MatchExpression::INTERNAL_EXPR_LTE == expr->matchType()) { + const auto* node = static_cast<const InternalExprLTEMatchExpression*>(expr); BSONElement dataElt = node->getData(); - // Everything is < MaxKey, except for MaxKey. However the bounds need to be inclusive to - // find the array [MaxKey] which is smaller for a comparison but equal in a multikey index. - if (MaxKey == dataElt.type()) { - oilOut->intervals.push_back(allValuesRespectingInclusion( - IndexBounds::makeBoundInclusionFromBoundBools(true, index.multikey))); - *tightnessOut = index.collator || index.multikey ? IndexBoundsBuilder::INEXACT_FETCH - : IndexBoundsBuilder::EXACT; - return; - } - - // Nothing is < NaN. - if (std::isnan(dataElt.numberDouble())) { - *tightnessOut = IndexBoundsBuilder::EXACT; - return; - } + // Unlike the regular $lte match expression, $_internalExprLte does not make special checks + // for when dataElt is MaxKey or NaN because it doesn't do type bracketing for any operand. + // Another difference is that $internalExprLte predicates on multikey paths will not use an + // index. + tassert(3994305, + "$expr comparison predicates on multikey paths cannot use an index", + !index.pathHasMultikeyComponent(elt.fieldNameStringData())); BSONObjBuilder bob; - buildBoundsForQueryElementForLT(dataElt, index.collator, &bob); - BSONObj dataObj = bob.done().getOwned(); - verify(dataObj.isOwned()); - bool inclusiveBounds = dataElt.type() == BSONType::Array; - Interval interval = - makeRangeInterval(dataObj, - IndexBounds::makeBoundInclusionFromBoundBools( - typeMatch(dataObj) || inclusiveBounds, inclusiveBounds)); + bob.appendMinKey(""); + CollationIndexKey::collationAwareIndexKeyAppend(dataElt, index.collator, &bob); + BSONObj dataObj = bob.obj(); - // If the operand to LT is equal to the lower bound X, the interval [X, X) is invalid - // and should not be added to the bounds. - if (!interval.isNull()) { - oilOut->intervals.push_back(interval); + Interval interval = makeRangeInterval(dataObj, BoundInclusion::kIncludeBothStartAndEndKeys); + oilOut->intervals.push_back(interval); + + // Expressions treat null and missing as distinct values, with missing ordered as less than + // null. Thus for $_internalExprLte when dataElt is null we can treat the bounds as EXACT, + // since both null and missing values should be included. + if (dataElt.isNull()) { + *tightnessOut = IndexBoundsBuilder::EXACT; + return; } *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); @@ -737,6 +798,41 @@ void IndexBoundsBuilder::_translatePredicate(const MatchExpression* expr, oilOut->intervals.push_back(interval); } *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); + } else if (MatchExpression::INTERNAL_EXPR_GT == expr->matchType()) { + const auto* node = static_cast<const InternalExprGTMatchExpression*>(expr); + BSONElement dataElt = node->getData(); + + // Unlike the regular $gt match expression, $_internalExprGt does not make special checks + // for when dataElt is MinKey or NaN because it doesn't do type bracketing for any operand. + // Another difference is that $internalExprGt predicates on multikey paths will not use an + // index. + tassert(3994302, + "$expr comparison predicates on multikey paths cannot use an index", + !index.pathHasMultikeyComponent(elt.fieldNameStringData())); + + BSONObjBuilder bob; + CollationIndexKey::collationAwareIndexKeyAppend(dataElt, index.collator, &bob); + bob.appendMaxKey(""); + BSONObj dataObj = bob.obj(); + + Interval interval = makeRangeInterval(dataObj, BoundInclusion::kIncludeEndKeyOnly); + + // If the operand to $_internalExprGt is equal to the upper bound X, the interval (X, X] is + // invalid and should not be added to the bounds. Because $_internalExprGt doesn't perform + // type bracketing, here we need to avoid adding the interval (MaxKey, MaxKey]. + if (!interval.isNull()) { + oilOut->intervals.push_back(interval); + } + + // Expressions treat null and missing as distinct values, with missing ordered as less than + // null. Thus for $_internalExprGt when dataElt is null we can treat the bounds as EXACT, + // since both null and missing values should be excluded. + if (dataElt.isNull()) { + *tightnessOut = IndexBoundsBuilder::EXACT; + return; + } + + *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); } else if (MatchExpression::GTE == expr->matchType()) { const GTEMatchExpression* node = static_cast<const GTEMatchExpression*>(expr); BSONElement dataElt = node->getData(); @@ -773,6 +869,26 @@ void IndexBoundsBuilder::_translatePredicate(const MatchExpression* expr, oilOut->intervals.push_back(interval); *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); + } else if (MatchExpression::INTERNAL_EXPR_GTE == expr->matchType()) { + const auto* node = static_cast<const InternalExprGTEMatchExpression*>(expr); + BSONElement dataElt = node->getData(); + + // Unlike the regular $gte match expression, $_internalExprGte does not make special checks + // for when dataElt is MinKey or NaN because it doesn't do type bracketing for any operand. + // Another difference is that $internalExprGte predicates on multikey paths will not use an + // index. + tassert(3994303, + "$expr comparison predicates on multikey paths cannot use an index", + !index.pathHasMultikeyComponent(elt.fieldNameStringData())); + + BSONObjBuilder bob; + CollationIndexKey::collationAwareIndexKeyAppend(dataElt, index.collator, &bob); + bob.appendMaxKey(""); + BSONObj dataObj = bob.obj(); + + Interval interval = makeRangeInterval(dataObj, BoundInclusion::kIncludeBothStartAndEndKeys); + oilOut->intervals.push_back(interval); + *tightnessOut = getInequalityPredicateTightness(interval, dataElt, index); } else if (MatchExpression::REGEX == expr->matchType()) { const RegexMatchExpression* rme = static_cast<const RegexMatchExpression*>(expr); translateRegex(rme, index, oilOut, tightnessOut); diff --git a/src/mongo/db/query/index_bounds_builder_test.cpp b/src/mongo/db/query/index_bounds_builder_test.cpp index 4c141b52ee2..8413c471214 100644 --- a/src/mongo/db/query/index_bounds_builder_test.cpp +++ b/src/mongo/db/query/index_bounds_builder_test.cpp @@ -38,6 +38,7 @@ #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/query/collation/collator_interface_mock.h" #include "mongo/db/query/expression_index.h" +#include "mongo/unittest/death_test.h" #include "mongo/unittest/unittest.h" namespace { @@ -1358,4 +1359,387 @@ TEST_F(IndexBoundsBuilderTest, IntersectizeBasic) { ASSERT_TRUE(oil2 == expectedIntersection); } +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGT) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprGt" << 4)); + auto 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(maxKeyIntObj(4), false, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTNull) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprGt" << BSONNULL)); + auto 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(oil.intervals[0].toString(), "(null, MaxKey]"); + ASSERT_FALSE(oil.intervals[0].startInclusive); + ASSERT_TRUE(oil.intervals[0].endInclusive); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTMaxKeyDoesNotGenerateBounds) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprGt" << MAXKEY)); + auto 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(), 0U); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +DEATH_TEST_F(IndexBoundsBuilderTest, + TranslateInternalExprGTMultikeyPathFails, + "$expr comparison predicates on multikey paths cannot use an index") { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{0U}, {}}); + BSONObj obj = BSON("a" << BSON("$_internalExprGt" << 4)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTNonMultikeyPathOnMultikeyIndexSucceeds) { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{}, {0U}}); + BSONObj obj = BSON("a" << BSON("$_internalExprGt" << 4)); + auto 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(maxKeyIntObj(4), false, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTSubObjectContainingBadValuesSucceeds) { + BSONObj keyPattern = BSON("_id" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj subObj = BSON("subObj" << BSON("a" << BSONUndefined << "b" << BSON_ARRAY("array"))); + BSONObj obj = BSON("_id" << BSON("$_internalExprGt" << subObj)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); + ASSERT_EQUALS(oil.name, "_id"); + ASSERT_EQUALS(oil.intervals.size(), 1U); + ASSERT_EQUALS( + Interval::INTERVAL_EQUALS, + oil.intervals[0].compare(Interval(BSON("" << subObj << "" << MAXKEY), false, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTE) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprGte" << 4)); + auto 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(maxKeyIntObj(4), true, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTENull) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprGte" << BSONNULL)); + auto 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(oil.intervals[0].toString(), "[null, MaxKey]"); + ASSERT_TRUE(oil.intervals[0].startInclusive); + ASSERT_TRUE(oil.intervals[0].endInclusive); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::INEXACT_FETCH); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTEMaxKeyGeneratesBounds) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprGte" << MAXKEY)); + auto 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(oil.intervals[0].toString(), "[MaxKey, MaxKey]"); + ASSERT_TRUE(oil.intervals[0].startInclusive); + ASSERT_TRUE(oil.intervals[0].endInclusive); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +DEATH_TEST_F(IndexBoundsBuilderTest, + TranslateInternalExprGTEMultikeyPathFails, + "$expr comparison predicates on multikey paths cannot use an index") { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{0U}, {}}); + BSONObj obj = BSON("a" << BSON("$_internalExprGte" << 4)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTENonMultikeyPathOnMultikeyIndexSucceeds) { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{}, {0U}}); + BSONObj obj = BSON("a" << BSON("$_internalExprGte" << 4)); + auto 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(maxKeyIntObj(4), true, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprGTESubObjectContainingBadValuesSucceeds) { + BSONObj keyPattern = BSON("_id" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj subObj = BSON("subObj" << BSON("a" << BSONUndefined << "b" << BSON_ARRAY("array"))); + BSONObj obj = BSON("_id" << BSON("$_internalExprGte" << subObj)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); + ASSERT_EQUALS(oil.name, "_id"); + ASSERT_EQUALS(oil.intervals.size(), 1U); + ASSERT_EQUALS( + Interval::INTERVAL_EQUALS, + oil.intervals[0].compare(Interval(BSON("" << subObj << "" << MAXKEY), true, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLT) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprLt" << 4)); + auto 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(minKeyIntObj(4), true, false))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTNull) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprLt" << BSONNULL)); + auto 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(oil.intervals[0].toString(), "[MinKey, null]"); + ASSERT_TRUE(oil.intervals[0].startInclusive); + ASSERT_TRUE(oil.intervals[0].endInclusive); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::INEXACT_FETCH); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTMinKeyDoesNotGenerateBounds) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprLt" << MINKEY)); + auto 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(), 0U); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +DEATH_TEST_F(IndexBoundsBuilderTest, + TranslateInternalExprLTMultikeyPathFails, + "$expr comparison predicates on multikey paths cannot use an index") { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{0U}, {}}); + BSONObj obj = BSON("a" << BSON("$_internalExprLt" << 4)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTNonMultikeyPathOnMultikeyIndexSucceeds) { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{}, {0U}}); + BSONObj obj = BSON("a" << BSON("$_internalExprLt" << 4)); + auto 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(minKeyIntObj(4), true, false))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTSubObjectContainingBadValuesSucceeds) { + BSONObj keyPattern = BSON("_id" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj subObj = BSON("subObj" << BSON("a" << BSONUndefined << "b" << BSON_ARRAY("array"))); + BSONObj obj = BSON("_id" << BSON("$_internalExprLt" << subObj)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); + ASSERT_EQUALS(oil.name, "_id"); + ASSERT_EQUALS(oil.intervals.size(), 1U); + ASSERT_EQUALS( + Interval::INTERVAL_EQUALS, + oil.intervals[0].compare(Interval(BSON("" << MINKEY << "" << subObj), true, false))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTE) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprLte" << 4)); + auto 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(minKeyIntObj(4), true, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTENull) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprLte" << BSONNULL)); + auto 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(oil.intervals[0].toString(), "[MinKey, null]"); + ASSERT_TRUE(oil.intervals[0].startInclusive); + ASSERT_TRUE(oil.intervals[0].endInclusive); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTEMinKeyGeneratesBounds) { + BSONObj keyPattern = BSON("a" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj obj = BSON("a" << BSON("$_internalExprLte" << MINKEY)); + auto 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(oil.intervals[0].toString(), "[MinKey, MinKey]"); + ASSERT_TRUE(oil.intervals[0].startInclusive); + ASSERT_TRUE(oil.intervals[0].endInclusive); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +DEATH_TEST_F(IndexBoundsBuilderTest, + TranslateInternalExprLTEMultikeyPathFails, + "$expr comparison predicates on multikey paths cannot use an index") { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{0U}, {}}); + + BSONObj obj = BSON("a" << BSON("$_internalExprLte" << 4)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTENonMultikeyPathOnMultikeyIndexSucceeds) { + BSONObj keyPattern = BSON("a" << 1 << "b" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildMultikeyIndexEntry(keyPattern, {{}, {0U}}); + BSONObj obj = BSON("a" << BSON("$_internalExprLte" << 4)); + auto 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(minKeyIntObj(4), true, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + +TEST_F(IndexBoundsBuilderTest, TranslateInternalExprLTESubObjectContainingBadValuesSucceeds) { + BSONObj keyPattern = BSON("_id" << 1); + BSONElement elt = keyPattern.firstElement(); + auto testIndex = buildSimpleIndexEntry(keyPattern); + BSONObj subObj = BSON("subObj" << BSON("a" << BSONUndefined << "b" << BSON_ARRAY("array"))); + BSONObj obj = BSON("_id" << BSON("$_internalExprLte" << subObj)); + auto expr = parseMatchExpression(obj); + OrderedIntervalList oil; + IndexBoundsBuilder::BoundsTightness tightness; + IndexBoundsBuilder::translate(expr.get(), elt, testIndex, &oil, &tightness); + ASSERT_EQUALS(oil.name, "_id"); + ASSERT_EQUALS(oil.intervals.size(), 1U); + ASSERT_EQUALS( + Interval::INTERVAL_EQUALS, + oil.intervals[0].compare(Interval(BSON("" << MINKEY << "" << subObj), true, true))); + ASSERT_EQUALS(tightness, IndexBoundsBuilder::EXACT); +} + } // namespace diff --git a/src/mongo/db/query/index_bounds_builder_test.h b/src/mongo/db/query/index_bounds_builder_test.h index f3abf8c212c..421794fecf4 100644 --- a/src/mongo/db/query/index_bounds_builder_test.h +++ b/src/mongo/db/query/index_bounds_builder_test.h @@ -69,6 +69,25 @@ public: } /** + * Make a multikey IndexEntry with the provided key pattern and multikey paths. + */ + IndexEntry buildMultikeyIndexEntry(const BSONObj& kp, const MultikeyPaths& mkp) { + return {kp, + IndexNames::nameToType(IndexNames::findPluginName(kp)), + IndexDescriptor::kLatestIndexVersion, + true, // multikey + mkp, // multikey paths + {}, + false, + false, + IndexEntry::Identifier{"test_multikey"}, + nullptr, + {}, + nullptr, + nullptr}; + } + + /** * Given a list of queries in 'toUnion', translate into index bounds and return * the union of these bounds in the out-parameter 'oilOut'. */ diff --git a/src/mongo/db/query/indexability.h b/src/mongo/db/query/indexability.h index 7442e251c3b..96930df7650 100644 --- a/src/mongo/db/query/indexability.h +++ b/src/mongo/db/query/indexability.h @@ -176,7 +176,11 @@ private: me->matchType() == MatchExpression::GEO_NEAR || me->matchType() == MatchExpression::EXISTS || me->matchType() == MatchExpression::TEXT || - me->matchType() == MatchExpression::INTERNAL_EXPR_EQ; + me->matchType() == MatchExpression::INTERNAL_EXPR_EQ || + me->matchType() == MatchExpression::INTERNAL_EXPR_GT || + me->matchType() == MatchExpression::INTERNAL_EXPR_GTE || + me->matchType() == MatchExpression::INTERNAL_EXPR_LT || + me->matchType() == MatchExpression::INTERNAL_EXPR_LTE; } }; diff --git a/src/mongo/db/query/plan_cache_indexability.cpp b/src/mongo/db/query/plan_cache_indexability.cpp index 216b714dbed..dbf0250966c 100644 --- a/src/mongo/db/query/plan_cache_indexability.cpp +++ b/src/mongo/db/query/plan_cache_indexability.cpp @@ -39,7 +39,7 @@ #include "mongo/db/index/wildcard_key_generator.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_internal_expr_comparison.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" diff --git a/src/mongo/db/query/planner_ixselect.cpp b/src/mongo/db/query/planner_ixselect.cpp index 9e7fc493d77..d9cc7855d19 100644 --- a/src/mongo/db/query/planner_ixselect.cpp +++ b/src/mongo/db/query/planner_ixselect.cpp @@ -40,7 +40,7 @@ #include "mongo/db/index_names.h" #include "mongo/db/matcher/expression_algo.h" #include "mongo/db/matcher/expression_geo.h" -#include "mongo/db/matcher/expression_internal_expr_eq.h" +#include "mongo/db/matcher/expression_internal_expr_comparison.h" #include "mongo/db/matcher/expression_text.h" #include "mongo/db/query/canonical_query_encoder.h" #include "mongo/db/query/collation/collator_interface.h" @@ -383,9 +383,10 @@ bool QueryPlannerIXSelect::_compatible(const BSONElement& keyPatternElt, // We know keyPatternElt.fieldname() == node->path(). MatchExpression::MatchType exprtype = node->matchType(); - if (exprtype == MatchExpression::INTERNAL_EXPR_EQ && + if (ComparisonMatchExpressionBase::isInternalExprComparison(exprtype) && index.pathHasMultikeyComponent(keyPatternElt.fieldNameStringData())) { - // Expression language equality cannot be indexed if the field path has multikey components. + // Expression language comparisons cannot be indexed if the field path has multikey + // components. return false; } diff --git a/src/mongo/db/query/sbe_stage_builder_filter.cpp b/src/mongo/db/query/sbe_stage_builder_filter.cpp index 5b3e6672a15..b33b48efb6b 100644 --- a/src/mongo/db/query/sbe_stage_builder_filter.cpp +++ b/src/mongo/db/query/sbe_stage_builder_filter.cpp @@ -43,7 +43,7 @@ #include "mongo/db/matcher/expression_array.h" #include "mongo/db/matcher/expression_expr.h" #include "mongo/db/matcher/expression_geo.h" -#include "mongo/db/matcher/expression_internal_expr_eq.h" +#include "mongo/db/matcher/expression_internal_expr_comparison.h" #include "mongo/db/matcher/expression_leaf.h" #include "mongo/db/matcher/expression_text.h" #include "mongo/db/matcher/expression_text_noop.h" @@ -700,6 +700,10 @@ public: } void visit(const InMatchExpression* expr) final {} void visit(const InternalExprEqMatchExpression* expr) final {} + void visit(const InternalExprGTMatchExpression* expr) final {} + void visit(const InternalExprGTEMatchExpression* expr) final {} + void visit(const InternalExprLTMatchExpression* expr) final {} + void visit(const InternalExprLTEMatchExpression* expr) final {} void visit(const InternalSchemaAllElemMatchFromIndexMatchExpression* expr) final { unsupportedExpression(expr); } @@ -1185,13 +1189,24 @@ public: LeafTraversalMode::kArrayAndItsElements); } } - + // The following are no-ops. The internal expr comparison match expression are produced + // internally by rewriting an $expr expression to an AND($expr, $_internalExpr[OP]), which can + // later be eliminated by via a conversion into EXACT index bounds, or remains present. In the + // latter case we can simply ignore it, as the result of AND($expr, $_internalExpr[OP]) is equal + // to just $expr. void visit(const InternalExprEqMatchExpression* expr) final { - // This is a no-op. The $_internalExprEq match expression is produced internally by - // rewriting an $expr expression to an AND($expr, $_internalExprEq), which can later be - // eliminated by via a conversion into EXACT index bounds, or remains present. In the latter - // case we can simply ignore it, as the result of AND($expr, $_internalExprEq) is equal to - // just $expr. + generateAlwaysBoolean(_context, true); + } + void visit(const InternalExprGTMatchExpression* expr) final { + generateAlwaysBoolean(_context, true); + } + void visit(const InternalExprGTEMatchExpression* expr) final { + generateAlwaysBoolean(_context, true); + } + void visit(const InternalExprLTMatchExpression* expr) final { + generateAlwaysBoolean(_context, true); + } + void visit(const InternalExprLTEMatchExpression* expr) final { generateAlwaysBoolean(_context, true); } @@ -1388,6 +1403,10 @@ public: void visit(const GeoNearMatchExpression* expr) final {} void visit(const InMatchExpression* expr) final {} void visit(const InternalExprEqMatchExpression* expr) final {} + void visit(const InternalExprGTMatchExpression* expr) final {} + void visit(const InternalExprGTEMatchExpression* expr) final {} + void visit(const InternalExprLTMatchExpression* expr) final {} + void visit(const InternalExprLTEMatchExpression* expr) final {} void visit(const InternalSchemaAllElemMatchFromIndexMatchExpression* expr) final {} void visit(const InternalSchemaAllowedPropertiesMatchExpression* expr) final {} void visit(const InternalSchemaBinDataEncryptedTypeExpression* expr) final {} |