diff options
author | David Storch <david.storch@10gen.com> | 2014-04-21 10:32:51 -0400 |
---|---|---|
committer | Dan Pasette <dan@mongodb.com> | 2014-04-21 13:53:23 -0400 |
commit | e17c3e6dc06362c268836f2d3289c42e14b81e04 (patch) | |
tree | df543b121c1159d38e07046e6c522450f6f3e7f8 | |
parent | 14e0ba91a9897018c6a0232eb25ab96f1c451a4f (diff) | |
download | mongo-e17c3e6dc06362c268836f2d3289c42e14b81e04.tar.gz |
SERVER-13664 never detach filters from inside elemMatch object
(cherry picked from commit 15bd7092e059a66ee74b65da9186c29300b70b2a)
-rw-r--r-- | jstests/core/arrayfindb.js | 26 | ||||
-rw-r--r-- | src/mongo/db/query/planner_access.cpp | 16 | ||||
-rw-r--r-- | src/mongo/db/query/query_planner_test.cpp | 41 |
3 files changed, 81 insertions, 2 deletions
diff --git a/jstests/core/arrayfindb.js b/jstests/core/arrayfindb.js new file mode 100644 index 00000000000..ad1a86be142 --- /dev/null +++ b/jstests/core/arrayfindb.js @@ -0,0 +1,26 @@ +// Test $elemMatch object with complex embedded expressions. + +var t = db.jstests_arrayfindb; +t.drop(); + +// Case #1: Ensure correct matching for $elemMatch with an embedded $and (SERVER-13664). +t.save({a: [{b: 1, c: 25}, {a: 3, b: 59}]}); +assert.eq(0, t.find({a: {$elemMatch: {b: {$gte: 2, $lt: 4}, c: 25}}}).itcount(), + "Case #1: wrong number of results returned -- unindexed"); + +t.ensureIndex({"a.b": 1, "a.c": 1}); +assert.eq(0, t.find({a: {$elemMatch: {b: {$gte: 2, $lt: 4}, c: 25}}}).itcount(), + "Case #1: wrong number of results returned -- indexed"); + +// Case #2: Ensure correct matching for $elemMatch with an embedded $or. +t.drop(); +t.save({a: [{b: 1}, {c: 1}]}); +t.save({a: [{b: 2}, {c: 1}]}); +t.save({a: [{b: 1}, {c: 2}]}); +assert.eq(2, t.find({a: {$elemMatch: {$or: [{b: 2}, {c: 2}]}}}).itcount(), + "Case #2: wrong number of results returned -- unindexed"); + +t.ensureIndex({"a.b": 1}); +t.ensureIndex({"a.c": 1}); +assert.eq(2, t.find({a: {$elemMatch: {$or: [{b: 2}, {c: 2}]}}}).itcount(), + "Case #2: wrong number of results returned -- indexed"); diff --git a/src/mongo/db/query/planner_access.cpp b/src/mongo/db/query/planner_access.cpp index 5f9a06d5a8c..0dc1913ec57 100644 --- a/src/mongo/db/query/planner_access.cpp +++ b/src/mongo/db/query/planner_access.cpp @@ -684,7 +684,13 @@ namespace mongo { mergeWithLeafNode(child, indices[currentIndexNumber], ixtag->pos, &tightness, currentScan.get(), root->matchType()); - if (tightness == IndexBoundsBuilder::EXACT) { + if (inArrayOperator) { + // We're inside an array operator. The entire array operator expression + // should always be affixed as a filter. We keep 'curChild' in the $and + // for affixing later. + ++curChild; + } + else if (tightness == IndexBoundsBuilder::EXACT) { root->getChildVector()->erase(root->getChildVector()->begin() + curChild); delete child; @@ -748,7 +754,13 @@ namespace mongo { currentScan.reset(makeLeafNode(query, indices[currentIndexNumber], ixtag->pos, child, &tightness)); - if (tightness == IndexBoundsBuilder::EXACT && !inArrayOperator) { + if (inArrayOperator) { + // We're inside an array operator. The entire array operator expression + // should always be affixed as a filter. We keep 'curChild' in the $and + // for affixing later. + ++curChild; + } + else if (tightness == IndexBoundsBuilder::EXACT) { // The bounds answer the predicate, and we can remove the expression from the // root. NOTE(opt): Erasing entry 0, 1, 2, ... could be kind of n^2, maybe // optimize later. diff --git a/src/mongo/db/query/query_planner_test.cpp b/src/mongo/db/query/query_planner_test.cpp index eab24dec88d..83092ddb49e 100644 --- a/src/mongo/db/query/query_planner_test.cpp +++ b/src/mongo/db/query/query_planner_test.cpp @@ -1303,6 +1303,47 @@ namespace { "{ixscan: {filter: null, pattern: {a: 1}}}}}"); } + // SERVER-13664 + TEST_F(QueryPlannerTest, ElemMatchEmbeddedAnd) { + // true means multikey + addIndex(BSON("a.b" << 1 << "a.c" << 1), true); + runQuery(fromjson("{a: {$elemMatch: {b: {$gte: 2, $lt: 4}, c: 25}}}")); + + assertNumSolutions(2U); + assertSolutionExists("{cscan: {dir: 1}}"); + assertSolutionExists("{fetch: {filter: {a:{$elemMatch:{b:{$gte:2,$lt: 4},c:25}}}, node: " + "{ixscan: {filter: null, pattern: {'a.b': 1, 'a.c': 1}, " + "bounds: {'a.b': [[-Infinity,4,true,false]], " + "'a.c': [[25,25,true,true]]}}}}}"); + } + + // SERVER-13664 + TEST_F(QueryPlannerTest, ElemMatchEmbeddedOr) { + // true means multikey + addIndex(BSON("a.b" << 1), true); + // true means multikey + addIndex(BSON("a.c" << 1), true); + runQuery(fromjson("{a: {$elemMatch: {$or: [{b: 3}, {c: 4}]}}}")); + + assertNumSolutions(2U); + assertSolutionExists("{cscan: {dir: 1}}"); + assertSolutionExists("{fetch: {filter: {a:{$elemMatch:{$or:[{b:3},{c:4}]}}}, " + "node: {or: {nodes: [" + "{ixscan: {filter: null, pattern: {'a.b': 1}}}, " + "{ixscan: {filter: null, pattern: {'a.c': 1}}}]}}}}"); + } + + // SERVER-13664 + TEST_F(QueryPlannerTest, ElemMatchEmbeddedRegex) { + addIndex(BSON("a.b" << 1)); + runQuery(fromjson("{a: {$elemMatch: {b: /foo/}}}")); + + assertNumSolutions(2U); + assertSolutionExists("{cscan: {dir: 1}}"); + assertSolutionExists("{fetch: {filter: {a:{$elemMatch:{b:/foo/}}}, node: " + "{ixscan: {filter: null, pattern: {'a.b': 1}}}}}"); + } + // $not can appear as a value operator inside of an elemMatch (value). We shouldn't crash if we // see it. TEST_F(QueryPlannerTest, ElemMatchWithNotInside) { |