diff options
author | Svilen Mihaylov <svilen.mihaylov@mongodb.com> | 2020-09-01 11:01:11 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-09-03 20:15:36 +0000 |
commit | 39ce43a37c0e9bcdbaf04bbe76e9a75fefec7dd3 (patch) | |
tree | db460e590703c965d246a39c2f0c4246fb8406ee | |
parent | 37276b21f4dbd66f913e8d49577fd4b1c4eafbf9 (diff) | |
download | mongo-39ce43a37c0e9bcdbaf04bbe76e9a75fefec7dd3.tar.gz |
SERVER-49766 Indexed and non-indexed collections return different results for null query
-rw-r--r-- | jstests/core/null_query_semantics.js | 21 | ||||
-rw-r--r-- | src/mongo/db/query/canonical_query_encoder.cpp | 27 | ||||
-rw-r--r-- | src/mongo/db/query/canonical_query_encoder.h | 7 | ||||
-rw-r--r-- | src/mongo/db/query/planner_ixselect.cpp | 6 |
4 files changed, 61 insertions, 0 deletions
diff --git a/jstests/core/null_query_semantics.js b/jstests/core/null_query_semantics.js index e13e0f5d0a8..7c1e3f5ff3b 100644 --- a/jstests/core/null_query_semantics.js +++ b/jstests/core/null_query_semantics.js @@ -260,6 +260,27 @@ function testNotEqualsNullSemantics() { assert(resultsEq(projectResults, extractAValues(expected)), tojson(projectResults)); }()); + // Test the semantics of the query {a: {$not: {$gte: null}}}. + (function testBasicNotEqualsGTENullQuery() { + const noProjectResults = coll.find({a: {$not: {$gte: null}}}).toArray(); + const expected = [ + {_id: "a_double_array", a: [[]]}, + {_id: "a_empty_array", a: []}, + {_id: "a_empty_subobject", a: {}}, + {_id: "a_number", a: 4}, + {_id: "a_object_array_all_b_nulls", a: [{b: null}, {b: undefined}, {b: null}, {}]}, + {_id: "a_object_array_no_b_nulls", a: [{b: 1}, {b: 3}, {b: "string"}]}, + {_id: "a_object_array_some_b_nulls", a: [{b: null}, {b: 3}, {b: null}]}, + {_id: "a_object_array_some_b_undefined", a: [{b: undefined}, {b: 3}]}, + {_id: "a_object_array_some_b_missing", a: [{b: 3}, {}]}, + {_id: "a_subobject_b_not_null", a: {b: "hi"}}, + {_id: "a_subobject_b_null", a: {b: null}}, + {_id: "a_subobject_b_undefined", a: {b: undefined}}, + {_id: "a_value_array_no_nulls", a: [1, "string", 4]}, + ]; + assert(resultsEq(noProjectResults, expected), tojson(noProjectResults)); + }()); + // Test the semantics of the query {a: {$nin: [null, <number>]}}. (function testNotInNullQuery() { const query = {a: {$nin: [null, 75]}}; diff --git a/src/mongo/db/query/canonical_query_encoder.cpp b/src/mongo/db/query/canonical_query_encoder.cpp index 487a8ad2198..ce2f70ec0fb 100644 --- a/src/mongo/db/query/canonical_query_encoder.cpp +++ b/src/mongo/db/query/canonical_query_encoder.cpp @@ -42,6 +42,27 @@ #include "mongo/logv2/log.h" namespace mongo { + +bool isQueryNegatingGTEorLTENull(const mongo::MatchExpression* tree) { + // If the query predicate is null, do not reuse the plan since empty arrays ([]) are + // encoded as 'null' in the index. Thus we cannot safely invert the index bounds since 'null' + // has special comparison semantics. + if (tree->matchType() != MatchExpression::NOT) { + return false; + } + + const MatchExpression* child = tree->getChild(0); + switch (child->matchType()) { + case MatchExpression::GTE: + case MatchExpression::LTE: + return static_cast<const ComparisonMatchExpression*>(child)->getData().type() == + BSONType::jstNULL; + + default: + return false; + } +} + namespace { // Delimiters for cache key encoding. @@ -423,6 +444,12 @@ void encodeKeyForMatch(const MatchExpression* tree, StringBuilder* keyBuilder) { else if (tree->isLTMaxKey()) *keyBuilder << "max"; + // If the query predicate involves comparison to null, do not reuse the plan since empty arrays + // ([]) are encoded as null in the index. Thus we cannot safely invert the index bounds. + if (isQueryNegatingGTEorLTENull(tree)) { + *keyBuilder << "not_gte_lte_null"; + } + // Traverse child nodes. // Enclose children in []. if (tree->numChildren() > 0) { diff --git a/src/mongo/db/query/canonical_query_encoder.h b/src/mongo/db/query/canonical_query_encoder.h index 73c0eff5fa7..ca9d91bcace 100644 --- a/src/mongo/db/query/canonical_query_encoder.h +++ b/src/mongo/db/query/canonical_query_encoder.h @@ -32,6 +32,13 @@ #include "mongo/db/query/plan_cache.h" namespace mongo { + +/** + * Returns true if the query predicate involves a negation of a LTE or GTE comparison to 'null'. + */ +bool isQueryNegatingGTEorLTENull(const mongo::MatchExpression* tree); + + namespace canonical_query_encoder { /** * Encode the given CanonicalQuery into a string representation which represents the shape of the diff --git a/src/mongo/db/query/planner_ixselect.cpp b/src/mongo/db/query/planner_ixselect.cpp index 47e1df683f5..ff0a4753e17 100644 --- a/src/mongo/db/query/planner_ixselect.cpp +++ b/src/mongo/db/query/planner_ixselect.cpp @@ -42,6 +42,7 @@ #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/canonical_query_encoder.h" #include "mongo/db/query/collation/collator_interface.h" #include "mongo/db/query/index_tag.h" #include "mongo/db/query/indexability.h" @@ -481,6 +482,11 @@ bool QueryPlannerIXSelect::_compatible(const BSONElement& keyPatternElt, } } + if (index.pathHasMultikeyComponent(keyPatternElt.fieldNameStringData()) && !index.sparse && + isQueryNegatingGTEorLTENull(node)) { + return false; + } + // If this is an $elemMatch value, make sure _all_ of the children can use the index. if (node->matchType() == MatchExpression::ELEM_MATCH_VALUE) { ElemMatchContext newContext; |