summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimour Katchaounov <timour.katchaounov@mongodb.com>2021-11-22 11:41:21 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2022-01-12 07:48:16 +0000
commit4b8b91c2ff91f538eede6b6c875656d697a8a916 (patch)
treedb56d29d45b7d79dc28ea1361fbee243366bab39
parentf2fc34bdec288cf3b90bf926d6a6c77631f4fa10 (diff)
downloadmongo-4b8b91c2ff91f538eede6b6c875656d697a8a916.tar.gz
SERVER-57588 Inconsistent query results when an array position is indexed whose value is an array
(cherry picked from commit 90699509e15b33fda10832e79efcd158aee1f0eb)
-rw-r--r--etc/backports_required_for_multiversion_tests.yml2
-rw-r--r--jstests/core/match_numeric_components.js449
-rw-r--r--src/mongo/db/query/planner_ixselect.cpp10
-rw-r--r--src/mongo/db/query/query_planner_array_test.cpp41
4 files changed, 502 insertions, 0 deletions
diff --git a/etc/backports_required_for_multiversion_tests.yml b/etc/backports_required_for_multiversion_tests.yml
index dd877b77bd3..82662d7f1f7 100644
--- a/etc/backports_required_for_multiversion_tests.yml
+++ b/etc/backports_required_for_multiversion_tests.yml
@@ -122,6 +122,8 @@ all:
test_file: jstests/aggregation/range.js
- ticket: SERVER-53335
test_file: jstests/sharding/collation_shard_targeting_hashed_shard_key.js
+ - ticket: SERVER-57588
+ test_file: jstests/core/match_numeric_components.js
- ticket: SERVER-60682
test_file: jstests/sharding/coordinate_txn_commit_with_tickets_exhausted.js
- ticket: SERVER-60685
diff --git a/jstests/core/match_numeric_components.js b/jstests/core/match_numeric_components.js
new file mode 100644
index 00000000000..7f20b3710ab
--- /dev/null
+++ b/jstests/core/match_numeric_components.js
@@ -0,0 +1,449 @@
+/**
+ * Tests behavior of the match language when using numeric path components.
+ */
+(function() {
+const coll = db.match_numeric_components;
+coll.drop();
+
+const kDocs = [
+ {_id: 0, "a": 42},
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 3, "a": [[42]]},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 6, "a": {"0": {"0": 42}}},
+ {_id: 7, "a": [[[42]]]},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 9, "a": [{"0": [42]}]},
+ {_id: 10, "a": [{"0": {"0": 42}}]},
+ {_id: 11, "a": {"0": [[42]]}},
+ {_id: 12, "a": {"0": [{"0": 42}]}},
+ {_id: 13, "a": {"0": {"0": [42]}}},
+ {_id: 14, "a": {"0": {"0": {"0": 42}}}},
+ {_id: 15, "a": [[[[42]]]]},
+ {_id: 16, "a": [[[{"0": 42}]]]},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 18, "a": [[{"0": {"0": 42}}]]},
+ {_id: 19, "a": [{"0": [[42]]}]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 21, "a": [{"0": {"0": [42]}}]},
+ {_id: 22, "a": [{"0": {"0": {"0": 42}}}]},
+ {_id: 23, "a": {"0": [[[42]]]}},
+ {_id: 24, "a": {"0": [[{"0": 42}]]}},
+ {_id: 25, "a": {"0": [{"0": [42]}]}},
+ {_id: 26, "a": {"0": [{"0": {"0": 42}}]}},
+ {_id: 27, "a": {"0": {"0": [[42]]}}},
+ {_id: 28, "a": {"0": {"0": [{"0": 42}]}}},
+ {_id: 29, "a": {"0": {"0": {"0": [42]}}}},
+ {_id: 30, "a": {"0": {"0": {"0": {"0": 42}}}}},
+];
+
+assert.commandWorked(coll.insert(kDocs));
+
+{
+ const res = coll.find({"a.0": 42}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using $ne.
+{
+ const res = coll.find({"a.0": {$ne: 42}}).toArray();
+ const expected = [
+ {_id: 0, "a": 42},
+ {_id: 3, "a": [[42]]},
+ {_id: 6, "a": {"0": {"0": 42}}},
+ {_id: 7, "a": [[[42]]]},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 10, "a": [{"0": {"0": 42}}]},
+ {_id: 11, "a": {"0": [[42]]}},
+ {_id: 12, "a": {"0": [{"0": 42}]}},
+ {_id: 13, "a": {"0": {"0": [42]}}},
+ {_id: 14, "a": {"0": {"0": {"0": 42}}}},
+ {_id: 15, "a": [[[[42]]]]},
+ {_id: 16, "a": [[[{"0": 42}]]]},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 18, "a": [[{"0": {"0": 42}}]]},
+ {_id: 19, "a": [{"0": [[42]]}]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 21, "a": [{"0": {"0": [42]}}]},
+ {_id: 22, "a": [{"0": {"0": {"0": 42}}}]},
+ {_id: 23, "a": {"0": [[[42]]]}},
+ {_id: 24, "a": {"0": [[{"0": 42}]]}},
+ {_id: 25, "a": {"0": [{"0": [42]}]}},
+ {_id: 26, "a": {"0": [{"0": {"0": 42}}]}},
+ {_id: 27, "a": {"0": {"0": [[42]]}}},
+ {_id: 28, "a": {"0": {"0": [{"0": 42}]}}},
+ {_id: 29, "a": {"0": {"0": {"0": [42]}}}},
+ {_id: 30, "a": {"0": {"0": {"0": {"0": 42}}}}},
+ ];
+ assert.sameMembers(res, expected);
+}
+
+{
+ const res = coll.find({"a.0.0": 42}).toArray();
+ const expected = [
+ {_id: 3, "a": [[42]]},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 6, "a": {"0": {"0": 42}}},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 9, "a": [{"0": [42]}]},
+ {_id: 10, "a": [{"0": {"0": 42}}]},
+ {_id: 12, "a": {"0": [{"0": 42}]}},
+ {_id: 13, "a": {"0": {"0": [42]}}},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 21, "a": [{"0": {"0": [42]}}]},
+ {_id: 25, "a": {"0": [{"0": [42]}]}}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using a comparison.
+{
+ const res = coll.find({"a.0": {$gt: 41}}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using $in.
+{
+ const res = coll.find({"a.0": {$in: [41, 42, 43]}}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using $nin.
+{
+ const res = coll.find({"a.0": {$nin: [41, 42, 43]}}).toArray();
+ const expected = [
+ {_id: 0, "a": 42},
+ {_id: 3, "a": [[42]]},
+ {_id: 6, "a": {"0": {"0": 42}}},
+ {_id: 7, "a": [[[42]]]},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 10, "a": [{"0": {"0": 42}}]},
+ {_id: 11, "a": {"0": [[42]]}},
+ {_id: 12, "a": {"0": [{"0": 42}]}},
+ {_id: 13, "a": {"0": {"0": [42]}}},
+ {_id: 14, "a": {"0": {"0": {"0": 42}}}},
+ {_id: 15, "a": [[[[42]]]]},
+ {_id: 16, "a": [[[{"0": 42}]]]},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 18, "a": [[{"0": {"0": 42}}]]},
+ {_id: 19, "a": [{"0": [[42]]}]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 21, "a": [{"0": {"0": [42]}}]},
+ {_id: 22, "a": [{"0": {"0": {"0": 42}}}]},
+ {_id: 23, "a": {"0": [[[42]]]}},
+ {_id: 24, "a": {"0": [[{"0": 42}]]}},
+ {_id: 25, "a": {"0": [{"0": [42]}]}},
+ {_id: 26, "a": {"0": [{"0": {"0": 42}}]}},
+ {_id: 27, "a": {"0": {"0": [[42]]}}},
+ {_id: 28, "a": {"0": {"0": [{"0": 42}]}}},
+ {_id: 29, "a": {"0": {"0": {"0": [42]}}}},
+ {_id: 30, "a": {"0": {"0": {"0": {"0": 42}}}}},
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using $exists with true and false.
+{
+ let res = coll.find({"a.0": {$exists: false}}).toArray();
+ let expected = [{_id: 0, "a": 42}];
+
+ assert.sameMembers(res, expected);
+
+ res = coll.find({"a.0": {$exists: true}}).toArray();
+ expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 3, "a": [[42]]},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 6, "a": {"0": {"0": 42}}},
+ {_id: 7, "a": [[[42]]]},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 9, "a": [{"0": [42]}]},
+ {_id: 10, "a": [{"0": {"0": 42}}]},
+ {_id: 11, "a": {"0": [[42]]}},
+ {_id: 12, "a": {"0": [{"0": 42}]}},
+ {_id: 13, "a": {"0": {"0": [42]}}},
+ {_id: 14, "a": {"0": {"0": {"0": 42}}}},
+ {_id: 15, "a": [[[[42]]]]},
+ {_id: 16, "a": [[[{"0": 42}]]]},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 18, "a": [[{"0": {"0": 42}}]]},
+ {_id: 19, "a": [{"0": [[42]]}]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 21, "a": [{"0": {"0": [42]}}]},
+ {_id: 22, "a": [{"0": {"0": {"0": 42}}}]},
+ {_id: 23, "a": {"0": [[[42]]]}},
+ {_id: 24, "a": {"0": [[{"0": 42}]]}},
+ {_id: 25, "a": {"0": [{"0": [42]}]}},
+ {_id: 26, "a": {"0": [{"0": {"0": 42}}]}},
+ {_id: 27, "a": {"0": {"0": [[42]]}}},
+ {_id: 28, "a": {"0": {"0": [{"0": 42}]}}},
+ {_id: 29, "a": {"0": {"0": {"0": [42]}}}},
+ {_id: 30, "a": {"0": {"0": {"0": {"0": 42}}}}},
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using $elemMatch.
+{
+ const res = coll.find({a: {$elemMatch: {"0.0": {$eq: 42}}}}).toArray();
+ const expected = [
+ {_id: 7, "a": [[[42]]]},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 9, "a": [{"0": [42]}]},
+ {_id: 10, "a": [{"0": {"0": 42}}]},
+ {_id: 16, "a": [[[{"0": 42}]]]},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 21, "a": [{"0": {"0": [42]}}]},
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using top-level $and.
+{
+ const res = coll.find({_id: {$lt: 15}, "a.0": {$gt: 41}}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]},
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// $all with equality
+{
+ const res = coll.find({"a.0": {$all: [42]}}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// $all with $elemMatch
+{
+ const res = coll.find({"a.0": {$all: [{$elemMatch: {0: 42}}]}}).toArray();
+ const expected = [
+ {_id: 7, "a": [[[42]]]},
+ {_id: 8, "a": [[{"0": 42}]]},
+ {_id: 11, "a": {"0": [[42]]}},
+ {_id: 12, "a": {"0": [{"0": 42}]}},
+ {_id: 15, "a": [[[[42]]]]},
+ {_id: 17, "a": [[{"0": [42]}]]},
+ {_id: 19, "a": [{"0": [[42]]}]},
+ {_id: 20, "a": [{"0": [{"0": 42}]}]},
+ {_id: 23, "a": {"0": [[[42]]]}},
+ {_id: 25, "a": {"0": [{"0": [42]}]}},
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// Using an expression.
+{
+ const res = coll.find({"a.0": {$type: "number"}}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+{
+ const res = coll.find({"a.0": {$mod: [42, 0]}}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+// In a $or.
+{
+ const res = coll.find({$or: [{"a.0": 42}, {"notARealField": 123}]}).toArray();
+ const expected = [
+ {_id: 1, "a": [42]},
+ {_id: 2, "a": {"0": 42}},
+ {_id: 4, "a": [{"0": 42}]},
+ {_id: 5, "a": {"0": [42]}},
+ {_id: 9, "a": [{"0": [42]}]}
+ ];
+
+ assert.sameMembers(res, expected);
+}
+
+const coll2 = db.match_numeric_components2;
+coll2.drop();
+
+const kRegexDocs =
+ [{_id: 1, "b": "hello"}, {_id: 2, "b": {"0": "hello"}}, {_id: 3, "b": ["hello", "abc", "abc"]}];
+
+assert.commandWorked(coll2.insert(kRegexDocs));
+
+// Regexes are often something of a special case.
+{
+ const res = coll2.find({"b.0": {$regex: "hello"}}).toArray();
+ const expected = [{_id: 2, "b": {"0": "hello"}}, {_id: 3, "b": ["hello", "abc", "abc"]}];
+
+ assert.sameMembers(res, expected);
+}
+
+// $all with regexes.
+{
+ const res = coll2.find({"b.0": {$all: [/^hello/]}}).toArray();
+ const expected = [{_id: 2, "b": {"0": "hello"}}, {_id: 3, "b": ["hello", "abc", "abc"]}];
+ assert.sameMembers(res, expected);
+}
+
+// $not with regex.
+{
+ const res = coll2.find({"b.0": {$not: /^h/}}).toArray();
+ const expected = [{_id: 1, "b": "hello"}];
+ assert.sameMembers(res, expected);
+}
+
+// The tests below indirectly make sure that an index scan is not chosen when $elemMatch is
+// against a indexed positional path component because it is not possible to generate index
+// bounds from the $elemMatch conditions. If an index scan is chosen, then the corresponding
+// queries would produce a wrong result.
+// More precisely for an index like {"a.0": 1} and a document {a: [[1, 2, 3]]}, the nested array is
+// not unwound during index key generation. That is, there is a single index key {"": [1, 2, 3]}
+// rather than three separate index keys, {"": 1}, {"": 2}, {"": 3}. This precludes the ability to
+// generate index bounds for $elemMatch predicates on "a.0" because "a.0" refers to the whole array
+// [1, 2, 3], and not its individual members.
+{
+ // Test with $in.
+ assert(coll.drop());
+ const expectedDoc = {_id: 42, "a": [["b"], ["c"]]};
+ assert.commandWorked(coll.insert(expectedDoc));
+ const query = {"a.0": {$elemMatch: {$in: ["b"]}}};
+
+ const res1 = coll.find(query).toArray();
+ assert.commandWorked(coll.createIndex({"a.0": 1}));
+ const res2 = coll.find(query).toArray();
+ assert.sameMembers([expectedDoc], res1);
+ assert.sameMembers([expectedDoc], res2);
+}
+
+// Tests with equality. Add some data for the next few tests.
+coll.drop();
+assert.commandWorked(coll.insert({_id: 0, f0: 'zz', f1: 5}));
+assert.commandWorked(coll.insert({_id: 1, f0: 'zz', f1: [3, 5]}));
+assert.commandWorked(coll.insert({_id: 4, f0: 'zz', f1: [3, 5, [7, 9]]}));
+assert.commandWorked(coll.insert({_id: 2, f0: 'zz', f1: [[3, 5], [5, 7]]}));
+assert.commandWorked(coll.insert({_id: 3, f0: 'zz', f1: [[[0], [3, 5]], [[0], [5, 7]]]}));
+
+{
+ const res1 = coll.find({'f1.0': {$elemMatch: {$eq: 5}}}).toArray();
+ const res2 = coll.find({'f1.0': {$elemMatch: {$eq: [3, 5]}}}).toArray();
+ assert.commandWorked(coll.createIndex({'f1.0': 1}));
+ const res3 = coll.find({'f1.0': {$elemMatch: {$eq: 5}}}).toArray();
+ const res4 = coll.find({'f1.0': {$elemMatch: {$eq: [3, 5]}}}).toArray();
+ const expected1 = [{_id: 2, f0: 'zz', f1: [[3, 5], [5, 7]]}];
+ const expected2 = [{_id: 3, f0: 'zz', f1: [[[0], [3, 5]], [[0], [5, 7]]]}];
+ assert.sameMembers(expected1, res1);
+ assert.sameMembers(expected1, res3);
+ assert.sameMembers(expected2, res2);
+ assert.sameMembers(expected2, res4);
+ assert.commandWorked(coll.dropIndex({'f1.0': 1}));
+}
+
+{
+ // Compound index.
+ const res1 = coll.find({'f0': 'zz', 'f1.0': {$elemMatch: {$eq: 5}}}).toArray();
+ assert.commandWorked(coll.createIndex({'f0': 1, 'f1.0': 1}));
+ const res2 = coll.find({'f0': 'zz', 'f1.0': {$elemMatch: {$eq: 5}}}).toArray();
+ const expected = [{_id: 2, f0: 'zz', f1: [[3, 5], [5, 7]]}];
+ assert.sameMembers(expected, res1);
+ assert.sameMembers(expected, res2);
+ assert.commandWorked(coll.dropIndex({'f0': 1, 'f1.0': 1}));
+}
+
+{
+ // Two-levels of array nesting.
+ const res1 = coll.find({'f1.0.1': {$elemMatch: {$eq: 3}}}).toArray();
+ assert.commandWorked(coll.createIndex({'f1.0.1': 1}));
+ const res2 = coll.find({'f1.0.1': {$elemMatch: {$eq: 3}}}).toArray();
+ const expected = [{_id: 3, f0: 'zz', f1: [[[0], [3, 5]], [[0], [5, 7]]]}];
+ assert.sameMembers(expected, res1);
+ assert.sameMembers(expected, res2);
+}
+
+{
+ assert(coll.drop());
+ assert.commandWorked(coll.createIndex({'f1.0': 1}));
+ assert.commandWorked(coll.insert({_id: 1, f1: [[42, 5], [77, 99]]}));
+
+ const res1 = coll.find({'f1.0': {$elemMatch: {$eq: 5}}}).toArray();
+ assert.sameMembers([{_id: 1, f1: [[42, 5], [77, 99]]}], res1);
+
+ // Object with numeric field component, and no nested arrays.
+ assert.commandWorked(coll.insert({_id: 2, f1: {0: [42, 5], 1: [77, 99]}}));
+ const res2 = coll.find({'f1.0': {$elemMatch: {$eq: 5}}}).toArray();
+ assert.sameMembers(
+ [{_id: 1, f1: [[42, 5], [77, 99]]}, {_id: 2, f1: {'0': [42, 5], '1': [77, 99]}}], res2);
+}
+
+{
+ assert(coll.drop());
+ assert.commandWorked(coll.createIndex({'0': 1}));
+
+ assert.commandWorked(coll.insert({_id: 1, '0': [42, 5]}));
+ const res1 = coll.find({'0': {$elemMatch: {$eq: 5}}}).toArray();
+ assert.sameMembers([{'0': [42, 5], _id: 1}], res1);
+
+ assert.commandWorked(coll.createIndex({'0.1': 1}));
+ assert.commandWorked(coll.insert({_id: 2, '0': {0: [42], 1: [5]}}));
+ const res2 = coll.find({'0.1': {$elemMatch: {$eq: 5}}}).toArray();
+ assert.sameMembers([{'0': {'0': [42], '1': [5]}, _id: 2}], res2);
+}
+})();
diff --git a/src/mongo/db/query/planner_ixselect.cpp b/src/mongo/db/query/planner_ixselect.cpp
index c6f1e093f12..3443ba16581 100644
--- a/src/mongo/db/query/planner_ixselect.cpp
+++ b/src/mongo/db/query/planner_ixselect.cpp
@@ -465,6 +465,16 @@ bool QueryPlannerIXSelect::_compatible(const BSONElement& keyPatternElt,
newContext.fullPathToParentElemMatch = fullPathToNode;
newContext.innermostParentElemMatch = static_cast<ElemMatchValueMatchExpression*>(node);
+ FieldRef path(fullPathToNode);
+ // If the index path has at least two components, and the last component of the path is
+ // numeric, this component could be an array index because the preceding path component
+ // may contain an array. Currently it is not known whether the preceding path component
+ // could be an array because indexes which positionally index array elements are not
+ // considered multikey.
+ if (path.numParts() > 1 && path.isNumericPathComponentStrict(path.numParts() - 1)) {
+ return false;
+ }
+
auto* children = node->getChildVector();
if (!std::all_of(children->begin(), children->end(), [&](MatchExpression* child) {
const auto newPath = fullPathToNode.toString() + child->path();
diff --git a/src/mongo/db/query/query_planner_array_test.cpp b/src/mongo/db/query/query_planner_array_test.cpp
index 718c6ad7444..a8ffff7fe58 100644
--- a/src/mongo/db/query/query_planner_array_test.cpp
+++ b/src/mongo/db/query/query_planner_array_test.cpp
@@ -2566,6 +2566,47 @@ TEST_F(QueryPlannerTest,
"}}}}}}}");
}
+// Test for older versions of indexes where it is possible to have empty MultikeyPaths,
+// but still the index is considered multikey. In that case the index should not be
+// considered to evaluate an $elemMatch predicate with a positional path component.
+TEST_F(QueryPlannerTest, ElemMatchValueMultikeyIndexEmptyMultikeyPaths) {
+ addIndex(BSON("a.0" << 1), true);
+ runQuery(fromjson("{'a.0': {$elemMatch: {$eq: 42}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, ElemMatchValuePositionalIndexPath1) {
+ MultikeyPaths multikeyPaths{std::set<size_t>{}};
+ addIndex(BSON("f1.0" << 1), multikeyPaths);
+ runQuery(fromjson("{'f1.0': {$elemMatch: {$eq: 42}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, ElemMatchValuePositionalIndexPath2) {
+ MultikeyPaths multikeyPaths{{0U}};
+ addIndex(BSON("f1.0" << 1), multikeyPaths);
+ runQuery(fromjson("{'f1.0': {$elemMatch: {$eq: 42}}}"));
+
+ assertNumSolutions(1U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+}
+
+TEST_F(QueryPlannerTest, ElemMatchValuePositionalIndexPath3) {
+ MultikeyPaths multikeyPaths{std::set<size_t>{}};
+ addIndex(BSON("0" << 1), multikeyPaths);
+ runQuery(fromjson("{'0': {$elemMatch: {$eq: 42}}}"));
+
+ assertNumSolutions(2U);
+ assertSolutionExists("{cscan: {dir: 1}}");
+ assertSolutionExists(
+ "{fetch: {node: {ixscan: {pattern: {'0': 1}, bounds: "
+ "{'0': [[42,42,true,true]]}}}}}");
+}
+
TEST_F(QueryPlannerTest, CompoundIndexBoundsDottedNotEqualsNullMultiKey) {
const bool isMultiKey = true;
addIndex(BSON("a.b" << 1 << "c.d" << 1), isMultiKey);