summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSvilen Mihaylov <svilen.mihaylov@mongodb.com>2020-09-01 11:01:11 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-09-03 20:15:36 +0000
commit39ce43a37c0e9bcdbaf04bbe76e9a75fefec7dd3 (patch)
treedb460e590703c965d246a39c2f0c4246fb8406ee
parent37276b21f4dbd66f913e8d49577fd4b1c4eafbf9 (diff)
downloadmongo-39ce43a37c0e9bcdbaf04bbe76e9a75fefec7dd3.tar.gz
SERVER-49766 Indexed and non-indexed collections return different results for null query
-rw-r--r--jstests/core/null_query_semantics.js21
-rw-r--r--src/mongo/db/query/canonical_query_encoder.cpp27
-rw-r--r--src/mongo/db/query/canonical_query_encoder.h7
-rw-r--r--src/mongo/db/query/planner_ixselect.cpp6
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;