summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Storch <david.storch@10gen.com>2014-04-21 10:32:51 -0400
committerDan Pasette <dan@mongodb.com>2014-04-21 13:53:23 -0400
commite17c3e6dc06362c268836f2d3289c42e14b81e04 (patch)
treedf543b121c1159d38e07046e6c522450f6f3e7f8
parent14e0ba91a9897018c6a0232eb25ab96f1c451a4f (diff)
downloadmongo-e17c3e6dc06362c268836f2d3289c42e14b81e04.tar.gz
SERVER-13664 never detach filters from inside elemMatch object
(cherry picked from commit 15bd7092e059a66ee74b65da9186c29300b70b2a)
-rw-r--r--jstests/core/arrayfindb.js26
-rw-r--r--src/mongo/db/query/planner_access.cpp16
-rw-r--r--src/mongo/db/query/query_planner_test.cpp41
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) {