Skip to content

Commit b33f7eb

Browse files
committed
SERVER-41963 Create an API to apply an $elemMatch projection on a Document path
1 parent 710ccfe commit b33f7eb

File tree

3 files changed

+139
-0
lines changed

3 files changed

+139
-0
lines changed

src/mongo/db/exec/find_projection_executor.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,34 @@ void applyPositionalProjection(const Document& input,
102102
}
103103
}
104104
}
105+
106+
void applyElemMatchProjection(const Document& input,
107+
const MatchExpression& matchExpr,
108+
const FieldPath& path,
109+
MutableDocument* output) {
110+
invariant(output);
111+
invariant(path.getPathLength() == 1);
112+
113+
// Try to find the first matching array element from the 'input' document based on the condition
114+
// specified as 'matchExpr'. If such an element is found, its position within an array will be
115+
// recorded in the 'details' object.
116+
MatchDetails details;
117+
details.requestElemMatchKey();
118+
if (!matchExpr.matchesBSON(input.toBson(), &details)) {
119+
// If there is no match, remove the 'path' from the output document (if it exists, otherwise
120+
// the call below is a no op).
121+
output->remove(path.fullPath());
122+
return;
123+
}
124+
125+
auto val = input[path.fullPath()];
126+
invariant(val.getType() == BSONType::Array);
127+
auto elemMatchKey = details.elemMatchKey();
128+
invariant(details.hasElemMatchKey());
129+
auto matchingElem = extractArrayElement(val, elemMatchKey);
130+
invariant(!matchingElem.missing());
131+
132+
output->setField(path.fullPath(), Value{std::vector<Value>{matchingElem}});
133+
}
105134
} // namespace projection_executor
106135
} // namespace mongo

src/mongo/db/exec/find_projection_executor.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,29 @@ void applyPositionalProjection(const Document& input,
5555
const MatchExpression& matchExpr,
5656
const FieldPath& path,
5757
MutableDocument* output);
58+
/**
59+
* Applies an $elemMatch projection on the array at the given 'path' on the 'input' document. The
60+
* applied projection is stored in the output document. If the output document contains a field
61+
* under which the projection is saved, it will be overwritten with the projection value. The
62+
* 'matchExpr' specifies a condition to locate the first matching element in the array and must
63+
* contain the $elemMatch operator. This function doesn't enforce this requirement and the caller
64+
* must ensure that the valid match expression is passed. For example, given:
65+
*
66+
* - the 'input' document {foo: [{bar: 1}, {bar: 2}, {bar: 3}]}
67+
* - the 'matchExpr' condition {foo: {$elemMatch: {bar: {$gt: 1}}}}
68+
* - and the 'path' of 'foo'
69+
*
70+
* The resulting document will contain the following element: {foo: [{bar: 2}]}
71+
*
72+
* If the 'matchExpr' does not match the input document, the function returns without modifying
73+
* the output document.
74+
*
75+
* Since the $elemMatch projection cannot be used with a nested field, the 'path' value must not
76+
* be a dotted path, otherwise an invariant will be triggered.
77+
*/
78+
void applyElemMatchProjection(const Document& input,
79+
const MatchExpression& matchExpr,
80+
const FieldPath& path,
81+
MutableDocument* output);
5882
} // namespace projection_executor
5983
} // namespace mongo

src/mongo/db/exec/find_projection_executor_test.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,5 +121,91 @@ TEST(PositionalProjection, CanMergeWithExistingFieldsInOutputDocument) {
121121
ASSERT_DOCUMENT_EQ(doc, applyPositional(fromjson("{foo: 3}"), "foo", doc, doc));
122122
}
123123
} // namespace positional_projection_tests
124+
125+
namespace elem_match_projection_tests {
126+
Document applyElemMatch(const BSONObj& match,
127+
const std::string& path,
128+
const Document& input,
129+
const Document& output = {}) {
130+
MutableDocument doc(output);
131+
boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest());
132+
auto matchObj = BSON(path << BSON("$elemMatch" << match));
133+
auto matchExpr = uassertStatusOK(MatchExpressionParser::parse(matchObj, expCtx));
134+
projection_executor::applyElemMatchProjection(input, *matchExpr, path, &doc);
135+
return doc.freeze();
136+
}
137+
138+
TEST(ElemMatchProjection, CorrectlyProjectsNonObjectElement) {
139+
ASSERT_DOCUMENT_EQ(
140+
Document{fromjson("{foo: [4]}")},
141+
applyElemMatch(fromjson("{$in: [4]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")}));
142+
ASSERT_DOCUMENT_EQ(
143+
Document{fromjson("{foo: [4]}")},
144+
applyElemMatch(fromjson("{$nin: [1,2,3]}"), "foo", Document{fromjson("{foo: [1,2,3,4]}")}));
145+
}
146+
147+
TEST(ElemMatchProjection, CorrectlyProjectsObjectElement) {
148+
ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")},
149+
applyElemMatch(fromjson("{bar: {$gte: 5}}"),
150+
"foo",
151+
Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, "
152+
"{bar: 6, z: 6}, {bar: 10, z: 10}]}")}));
153+
}
154+
155+
TEST(ElemMatchProjection, CorrectlyProjectsArrayElement) {
156+
ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: [[3,4]]}")},
157+
applyElemMatch(fromjson("{$gt: [1,2]}"),
158+
"foo",
159+
Document{fromjson("{foo: [[1,2], [3,4]]}")}));
160+
}
161+
162+
TEST(ElemMatchProjection, ProjectsAsEmptyDocumentIfInputIsEmpty) {
163+
ASSERT_DOCUMENT_EQ({}, applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", {}));
164+
}
165+
166+
TEST(ElemMatchProjection, RemovesFieldFromOutputDocumentIfUnableToMatchArrayElement) {
167+
ASSERT_DOCUMENT_EQ({},
168+
applyElemMatch(fromjson("{bar: {$gte: 5}}"),
169+
"foo",
170+
Document{fromjson("{foo: [{bar: 1, z: 1}, "
171+
"{bar: 2, z: 2}]}")}));
172+
auto doc =
173+
Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, "
174+
"{bar: 6, z: 6}, {bar: 10, z: 10}]}")};
175+
ASSERT_DOCUMENT_EQ(Document{fromjson("{bar:1}")},
176+
applyElemMatch(fromjson("{bar: {$gte: 20}}"), "foo", doc, doc));
177+
}
178+
179+
TEST(ElemMatchProjection, CorrectlyProjectsWithMultipleCriteriaInMatchExpression) {
180+
ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: [{bar: 2, z: 2}]}")},
181+
applyElemMatch(fromjson("{bar: {$gt: 1, $lt: 6}}"),
182+
"foo",
183+
Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, "
184+
"{bar: 6, z: 6}, {bar: 10, z: 10}]}")}));
185+
}
186+
187+
TEST(ElemMatchProjection, CanMergeWithExistingFieldsInOutputDocument) {
188+
auto doc =
189+
Document{fromjson("{foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, "
190+
"{bar: 6, z: 6}, {bar: 10, z: 10}]}")};
191+
ASSERT_DOCUMENT_EQ(Document{fromjson("{foo: [{bar: 6, z: 6}]}")},
192+
applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", doc, doc));
193+
194+
doc =
195+
Document{fromjson("{bar: 1, foo: [{bar: 1, z: 1}, {bar: 2, z: 2}, "
196+
"{bar: 6, z: 6}, {bar: 10, z: 10}]}")};
197+
ASSERT_DOCUMENT_EQ(Document{fromjson("{bar:1, foo: [{bar: 6, z: 6}]}")},
198+
applyElemMatch(fromjson("{bar: {$gte: 5}}"), "foo", doc, doc));
199+
}
200+
201+
TEST(ElemMatchProjection, RemovesFieldFromOutputDocumentIfItContainsNumericSubfield) {
202+
auto doc = Document{BSON("foo" << BSON(0 << 3))};
203+
ASSERT_DOCUMENT_EQ({}, applyElemMatch(fromjson("{$gt: 2}"), "foo", doc));
204+
205+
doc = Document{BSON("bar" << 1 << "foo" << BSON(0 << 3))};
206+
ASSERT_DOCUMENT_EQ(Document{fromjson("{bar: 1}")},
207+
applyElemMatch(fromjson("{$gt: 2}"), "foo", doc, doc));
208+
}
209+
} // namespace elem_match_projection_tests
124210
} // namespace projection_executor
125211
} // namespace mongo

0 commit comments

Comments
 (0)