Skip to content
This repository was archived by the owner on Oct 31, 2023. It is now read-only.

Commit 8736544

Browse files
alyacbkaywux
authored andcommitted
SERVER-30417: Implement $getField expression
Co-authored-by: Katherine Wu <[email protected]>
1 parent c90de3e commit 8736544

File tree

6 files changed

+395
-0
lines changed

6 files changed

+395
-0
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/**
2+
* Tests basic functionality of the $getField expression.
3+
*/
4+
(function() {
5+
"use strict";
6+
7+
load("jstests/aggregation/extras/utils.js"); // For assertArrayEq.
8+
9+
const coll = db.expression_get_field;
10+
coll.drop();
11+
12+
for (let i = 0; i < 2; i++) {
13+
assert.commandWorked(coll.insert({
14+
_id: i,
15+
x: i,
16+
y: "c",
17+
"a$b": "foo",
18+
"a.b": "bar",
19+
"a.$b": 5,
20+
".xy": i,
21+
".$xz": i,
22+
"..zz": i,
23+
c: {d: "x"},
24+
}));
25+
}
26+
27+
// Test that $getField fails with the provided 'code' for invalid arguments 'getFieldArgs'.
28+
function assertGetFieldFailedWithCode(getFieldArgs, code) {
29+
const error =
30+
assert.throws(() => coll.aggregate([{$project: {test: {$getField: getFieldArgs}}}]));
31+
assert.commandFailedWithCode(error, code);
32+
}
33+
34+
// Test that $getField returns the 'expected' results for the given arguments 'getFieldArgs'.
35+
function assertGetFieldResultsEq(getFieldArgs, expected) {
36+
assertPipelineResultsEq([{$project: {_id: 1, test: {$getField: getFieldArgs}}}], expected);
37+
}
38+
39+
// Test the given 'pipeline' returns the 'expected' results.
40+
function assertPipelineResultsEq(pipeline, expected) {
41+
const actual = coll.aggregate(pipeline).toArray();
42+
assertArrayEq({actual, expected});
43+
}
44+
45+
const isDotsAndDollarsEnabled = db.adminCommand({getParameter: 1, featureFlagDotsAndDollars: 1})
46+
.featureFlagDotsAndDollars.value;
47+
48+
if (!isDotsAndDollarsEnabled) {
49+
// Verify that $getField is not available if the feature flag is set to false and don't
50+
// run the rest of the test.
51+
assertGetFieldFailedWithCode({field: "a", from: {a: "b"}}, 31325);
52+
return;
53+
}
54+
55+
// Test that $getField fails with a document missing named arguments.
56+
assertGetFieldFailedWithCode({from: {a: "b"}}, 3041702);
57+
assertGetFieldFailedWithCode({field: "a"}, 3041703);
58+
59+
// Test that $getField fails with a document with one or more arguments of incorrect type.
60+
assertGetFieldFailedWithCode({field: true, from: {a: "b"}}, 3041704);
61+
assertGetFieldFailedWithCode({field: {"a": 1}, from: {"a": 1}}, 3041704);
62+
assertGetFieldFailedWithCode({field: "a", from: true}, 3041705);
63+
assertGetFieldFailedWithCode(5, 3041704);
64+
assertGetFieldFailedWithCode(true, 3041704);
65+
assertGetFieldFailedWithCode({$add: [2, 3]}, 3041704);
66+
67+
// Test that $getField fails with a document with invalid arguments.
68+
assertGetFieldFailedWithCode({field: "a", from: {a: "b"}, unknown: true}, 3041701);
69+
70+
// Test that $getField returns the correct value from the provided object.
71+
assertGetFieldResultsEq({field: "a", from: {a: "b"}}, [{_id: 0, test: "b"}, {_id: 1, test: "b"}]);
72+
73+
// Test that $getField returns the correct value from the $$ROOT object.
74+
assertGetFieldResultsEq(null, [{_id: 0, test: null}, {_id: 1, test: null}]);
75+
assertGetFieldResultsEq("a", [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
76+
assertGetFieldResultsEq("a$b", [{_id: 0, test: "foo"}, {_id: 1, test: "foo"}]);
77+
assertGetFieldResultsEq("a.b", [{_id: 0, test: "bar"}, {_id: 1, test: "bar"}]);
78+
assertGetFieldResultsEq("x", [{_id: 0, test: 0}, {_id: 1, test: 1}]);
79+
assertGetFieldResultsEq("a.$b", [{_id: 0, test: 5}, {_id: 1, test: 5}]);
80+
assertGetFieldResultsEq(".xy", [{_id: 0, test: 0}, {_id: 1, test: 1}]);
81+
assertGetFieldResultsEq(".$xz", [{_id: 0, test: 0}, {_id: 1, test: 1}]);
82+
assertGetFieldResultsEq("..zz", [{_id: 0, test: 0}, {_id: 1, test: 1}]);
83+
84+
// Test that $getField returns the correct value from the $$ROOT object when field is an expression.
85+
assertGetFieldResultsEq({$concat: ["a", "b"]},
86+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
87+
assertGetFieldResultsEq({$concat: ["a", {$const: "$"}, "b"]},
88+
[{_id: 0, test: "foo"}, {_id: 1, test: "foo"}]);
89+
assertGetFieldResultsEq({$cond: [true, null, "x"]}, [{_id: 0, test: null}, {_id: 1, test: null}]);
90+
assertGetFieldResultsEq({$cond: [false, null, "x"]}, [{_id: 0, test: 0}, {_id: 1, test: 1}]);
91+
92+
// Test that $getField treats dotted fields as key literals instead of field paths. Note that it is
93+
// necessary to use $const in places, otherwise object field validation would reject some of these
94+
// field names.
95+
assertGetFieldResultsEq({field: "a.b", from: {$const: {"a.b": "b"}}},
96+
[{_id: 0, test: "b"}, {_id: 1, test: "b"}]);
97+
assertGetFieldResultsEq({field: ".ab", from: {$const: {".ab": "b"}}},
98+
[{_id: 0, test: "b"}, {_id: 1, test: "b"}]);
99+
assertGetFieldResultsEq({field: "ab.", from: {$const: {"ab.": "b"}}},
100+
[{_id: 0, test: "b"}, {_id: 1, test: "b"}]);
101+
assertGetFieldResultsEq({field: "a.b.c", from: {$const: {"a.b.c": 5}}},
102+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
103+
assertGetFieldResultsEq({field: "a.b.c", from: {a: {b: {c: 5}}}},
104+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
105+
assertGetFieldResultsEq({field: {$concat: ["a.b", ".", "c"]}, from: {$const: {"a.b.c": 5}}},
106+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
107+
assertGetFieldResultsEq({field: {$concat: ["a.b", ".", "c"]}, from: {a: {b: {c: 5}}}},
108+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
109+
110+
// Test that $getField works with fields that contain '$'.
111+
assertGetFieldResultsEq({field: "a$b", from: {"a$b": "b"}},
112+
[{_id: 0, test: "b"}, {_id: 1, test: "b"}]);
113+
assertGetFieldResultsEq({field: "a$b.b", from: {$const: {"a$b.b": 5}}},
114+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
115+
assertGetFieldResultsEq({field: {$const: "a$b.b"}, from: {$const: {"a$b.b": 5}}},
116+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
117+
assertGetFieldResultsEq({field: {$const: "$b.b"}, from: {$const: {"$b.b": 5}}},
118+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
119+
assertGetFieldResultsEq({field: {$const: "$b"}, from: {$const: {"$b": 5}}},
120+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
121+
assertGetFieldResultsEq({field: {$const: "$.ab"}, from: {$const: {"$.ab": 5}}},
122+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
123+
assertGetFieldResultsEq({field: {$const: "$$xz"}, from: {$const: {"$$xz": 5}}},
124+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
125+
126+
// Test null and missing cases.
127+
assertGetFieldResultsEq({field: "a", from: null}, [{_id: 0, test: null}, {_id: 1, test: null}]);
128+
assertGetFieldResultsEq({field: null, from: {a: 1}}, [{_id: 0, test: null}, {_id: 1, test: null}]);
129+
assertGetFieldResultsEq({field: "a", from: {b: 2, c: 3}},
130+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
131+
assertGetFieldResultsEq({field: "a", from: {a: null, b: 2, c: 3}},
132+
[{_id: 0, test: null}, {_id: 1, test: null}]);
133+
assertGetFieldResultsEq({field: {$const: "$a"}, from: {$const: {"$a": null, b: 2, c: 3}}},
134+
[{_id: 0, test: null}, {_id: 1, test: null}]);
135+
assertGetFieldResultsEq({field: "a", from: {}},
136+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
137+
138+
// These should return null because "$a.b" evaluates to a field path expression which returns a
139+
// nullish value (so the expression should return null), as there is no $a.b field path.
140+
assertGetFieldResultsEq({field: "$a.b", from: {$const: {"$a.b": 5}}},
141+
[{_id: 0, test: null}, {_id: 1, test: null}]);
142+
assertGetFieldResultsEq("$a.b", [{_id: 0, test: null}, {_id: 1, test: null}]);
143+
144+
// When the field path does actually resolve to a field, the value of that field should be used.
145+
146+
// The fieldpath $y resolves to "c" in $$ROOT.
147+
assertGetFieldResultsEq({field: "$y", from: {$const: {"c": 5}}},
148+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
149+
assertGetFieldResultsEq({field: "$y", from: {$const: {"a": 5}}},
150+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
151+
assertGetFieldResultsEq("$y", [{_id: 0, test: {d: "x"}}, {_id: 1, test: {d: "x"}}]);
152+
153+
// The fieldpath $c.d resolves to "x" in $$ROOT.
154+
assertGetFieldResultsEq({field: "$c.d", from: {$const: {"x": 5}}},
155+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
156+
assertGetFieldResultsEq({field: "$c.d", from: {$const: {"y": 5}}},
157+
[{_id: 0}, {_id: 1}]); // The test field should evaluate to missing.
158+
assertGetFieldResultsEq("$c.d", [{_id: 0, test: 0}, {_id: 1, test: 1}]);
159+
160+
// $x resolves to a number, so this should fail.
161+
assertGetFieldFailedWithCode({field: "$x", from: {$const: {"c": 5}}}, 3041704);
162+
assertGetFieldFailedWithCode("$x", 3041704);
163+
164+
// Test case where $getField stages are nested.
165+
assertGetFieldResultsEq(
166+
{field: "a", from: {$getField: {field: "b.c", from: {$const: {"b.c": {a: 5}}}}}},
167+
[{_id: 0, test: 5}, {_id: 1, test: 5}]);
168+
assertGetFieldResultsEq(
169+
{field: "x", from: {$getField: {field: "b.c", from: {$const: {"b.c": {a: 5}}}}}},
170+
[{_id: 0}, {_id: 1}]);
171+
assertGetFieldResultsEq(
172+
{field: "a", from: {$getField: {field: "b.d", from: {$const: {"b.c": {a: 5}}}}}},
173+
[{_id: 0, test: null}, {_id: 1, test: null}]);
174+
175+
// Test case when a dotted/dollar path is within an array.
176+
assertGetFieldResultsEq({
177+
field: {$const: "a$b"},
178+
from: {$arrayElemAt: [[{$const: {"a$b": 1}}, {$const: {"a$b": 2}}], 0]}
179+
},
180+
[{_id: 0, test: 1}, {_id: 1, test: 1}]);
181+
assertGetFieldResultsEq({
182+
field: {$const: "a.."},
183+
from: {$arrayElemAt: [[{$const: {"a..": 1}}, {$const: {"a..": 2}}], 1]}
184+
},
185+
[{_id: 0, test: 2}, {_id: 1, test: 2}]);
186+
187+
// Test $getField expression with other pipeline stages.
188+
189+
assertPipelineResultsEq(
190+
[
191+
{$match: {$expr: {$eq: [{$getField: "_id"}, {$getField: ".$xz"}]}}},
192+
{$project: {aa: {$getField: ".$xz"}, "_id": 1}},
193+
],
194+
[{_id: 0, aa: 0}, {_id: 1, aa: 1}]);
195+
196+
assertPipelineResultsEq([{$match: {$expr: {$ne: [{$getField: "_id"}, {$getField: ".$xz"}]}}}], []);
197+
assertPipelineResultsEq(
198+
[
199+
{$match: {$expr: {$ne: [{$getField: "_id"}, {$getField: "a.b"}]}}},
200+
{$project: {"a": {$getField: "x"}, "b": {$getField: {$const: "a.b"}}}}
201+
],
202+
[{_id: 0, a: 0, b: "bar"}, {_id: 1, a: 1, b: "bar"}]);
203+
204+
assertPipelineResultsEq(
205+
[
206+
{$addFields: {aa: {$getField: {$const: "a.b"}}}},
207+
{$project: {aa: 1, _id: 1}},
208+
],
209+
[{_id: 0, aa: "bar"}, {_id: 1, aa: "bar"}]);
210+
211+
assertPipelineResultsEq(
212+
[
213+
{$bucket: {groupBy: {$getField: {$const: "a.b"}}, boundaries: ["aaa", "bar", "zzz"]}}
214+
], // We should get one bucket here ("bar") with two documents.
215+
[{_id: "bar", count: 2}]);
216+
assertPipelineResultsEq([{
217+
$bucket: {groupBy: {$getField: "x"}, boundaries: [0, 1, 2, 3, 4]}
218+
}], // We should get two buckets here for the two possible values of x.
219+
[{_id: 0, count: 1}, {_id: 1, count: 1}]);
220+
})();

src/mongo/db/pipeline/expression.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7187,6 +7187,86 @@ void ExpressionDateTrunc::_doAddDependencies(DepsTracker* deps) const {
71877187
}
71887188
}
71897189

7190+
/* -------------------------- ExpressionGetField ------------------------------ */
7191+
REGISTER_FEATURE_FLAG_GUARDED_EXPRESSION(getField,
7192+
ExpressionGetField::parse,
7193+
feature_flags::gFeatureFlagDotsAndDollars);
7194+
7195+
intrusive_ptr<Expression> ExpressionGetField::parse(ExpressionContext* const expCtx,
7196+
BSONElement expr,
7197+
const VariablesParseState& vps) {
7198+
boost::intrusive_ptr<Expression> fieldExpr;
7199+
boost::intrusive_ptr<Expression> fromExpr;
7200+
7201+
if (expr.type() == BSONType::Object) {
7202+
for (auto&& elem : expr.embeddedObject()) {
7203+
const auto fieldName = elem.fieldNameStringData();
7204+
if (!fieldExpr && !fromExpr && fieldName[0] == '$') {
7205+
// This may be an expression, so we should treat it as such.
7206+
fieldExpr = Expression::parseOperand(expCtx, expr, vps);
7207+
fromExpr = ExpressionFieldPath::parse(expCtx, "$$ROOT", vps);
7208+
break;
7209+
} else if (fieldName == "field"_sd) {
7210+
fieldExpr = Expression::parseOperand(expCtx, elem, vps);
7211+
} else if (fieldName == "from"_sd) {
7212+
fromExpr = Expression::parseOperand(expCtx, elem, vps);
7213+
} else {
7214+
uasserted(3041701,
7215+
str::stream()
7216+
<< kExpressionName << " found an unknown argument: " << fieldName);
7217+
}
7218+
}
7219+
} else {
7220+
fieldExpr = Expression::parseOperand(expCtx, expr, vps);
7221+
fromExpr = ExpressionFieldPath::parse(expCtx, "$$ROOT", vps);
7222+
}
7223+
7224+
uassert(3041702,
7225+
str::stream() << kExpressionName << " requires 'field' to be specified",
7226+
fieldExpr);
7227+
uassert(
7228+
3041703, str::stream() << kExpressionName << " requires 'from' to be specified", fromExpr);
7229+
7230+
return make_intrusive<ExpressionGetField>(expCtx, fieldExpr, fromExpr);
7231+
}
7232+
7233+
Value ExpressionGetField::evaluate(const Document& root, Variables* variables) const {
7234+
auto fieldValue = _field->evaluate(root, variables);
7235+
if (fieldValue.nullish()) {
7236+
return Value(BSONNULL);
7237+
}
7238+
7239+
auto fromValue = _from->evaluate(root, variables);
7240+
if (fromValue.nullish()) {
7241+
return Value(BSONNULL);
7242+
}
7243+
7244+
uassert(3041704,
7245+
str::stream() << kExpressionName << " requires 'field' to evaluate to type String",
7246+
fieldValue.getType() == BSONType::String);
7247+
7248+
uassert(3041705,
7249+
str::stream() << kExpressionName << " requires 'from' to evaluate to type Object",
7250+
fromValue.getType() == BSONType::Object);
7251+
7252+
return fromValue.getDocument().getField(fieldValue.getString());
7253+
}
7254+
7255+
intrusive_ptr<Expression> ExpressionGetField::optimize() {
7256+
return intrusive_ptr<Expression>(this);
7257+
}
7258+
7259+
void ExpressionGetField::_doAddDependencies(DepsTracker* deps) const {
7260+
_from->addDependencies(deps);
7261+
_field->addDependencies(deps);
7262+
}
7263+
7264+
Value ExpressionGetField::serialize(const bool explain) const {
7265+
return Value(Document{{"$getField"_sd,
7266+
Document{{"field"_sd, _field->serialize(explain)},
7267+
{"from"_sd, _from->serialize(explain)}}}});
7268+
}
7269+
71907270
MONGO_INITIALIZER(expressionParserMap)(InitializerContext*) {
71917271
// Nothing to do. This initializer exists to tie together all the individual initializers
71927272
// defined by REGISTER_EXPRESSION / REGISTER_EXPRESSION_WITH_MIN_VERSION.

src/mongo/db/pipeline/expression.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
#include "mongo/db/pipeline/field_path.h"
5151
#include "mongo/db/pipeline/variables.h"
5252
#include "mongo/db/query/datetime/date_time_support.h"
53+
#include "mongo/db/query/query_feature_flags_gen.h"
5354
#include "mongo/db/query/sort_pattern.h"
5455
#include "mongo/db/server_options.h"
5556
#include "mongo/util/intrusive_counter.h"
@@ -77,6 +78,25 @@ class DocumentSource;
7778
Expression::registerExpression("$" #key, (parser), boost::none); \
7879
}
7980

81+
/**
82+
* Registers a Parser so it can be called from parseExpression and friends (but only if
83+
* 'featureFlag' is enabled).
84+
*
85+
* As an example, if your expression looks like {"$foo": [1,2,3]} and should be flag-guarded by
86+
* feature_flags::gFoo, you would add this line:
87+
* REGISTER_FEATURE_FLAG_GUARDED_EXPRESSION(foo, ExpressionFoo::parse, feature_flags::gFoo);
88+
*
89+
* An expression registered this way can be used in any featureCompatibilityVersion.
90+
*/
91+
#define REGISTER_FEATURE_FLAG_GUARDED_EXPRESSION(key, parser, featureFlag) \
92+
MONGO_INITIALIZER_GENERAL( \
93+
addToExpressionParserMap_##key, ("default"), ("expressionParserMap")) \
94+
(InitializerContext*) { \
95+
if (featureFlag.isEnabledAndIgnoreFCV()) { \
96+
Expression::registerExpression("$" #key, (parser), boost::none); \
97+
} \
98+
}
99+
80100
/**
81101
* Registers a Parser so it can be called from parseExpression and friends. Use this version if your
82102
* expression can only be persisted to a catalog data structure in a feature compatibility version
@@ -3472,4 +3492,46 @@ class ExpressionDateTrunc final : public Expression {
34723492
// Accepted BSON type: String. If not specified, "sunday" is used.
34733493
boost::intrusive_ptr<Expression>& _startOfWeek;
34743494
};
3495+
3496+
class ExpressionGetField final : public Expression {
3497+
public:
3498+
static boost::intrusive_ptr<Expression> parse(ExpressionContext* const expCtx,
3499+
BSONElement exprElement,
3500+
const VariablesParseState& vps);
3501+
3502+
/**
3503+
* Constructs a $getField expression where 'field' is an expression resolving to a string Value
3504+
* (or null) and 'from' is an expression resolving to an object Value (or null).
3505+
*
3506+
* If either 'field' or 'from' is nullish, $getField evaluates to null. Furthermore, if 'from'
3507+
* does not contain 'field', then $getField returns missing.
3508+
*/
3509+
ExpressionGetField(ExpressionContext* const expCtx,
3510+
boost::intrusive_ptr<Expression> field,
3511+
boost::intrusive_ptr<Expression> from)
3512+
: Expression(expCtx, {std::move(field), std::move(from)}),
3513+
_field(_children[0]),
3514+
_from(_children[1]) {
3515+
expCtx->sbeCompatible = false;
3516+
}
3517+
3518+
Value serialize(const bool explain) const final;
3519+
3520+
Value evaluate(const Document& root, Variables* variables) const final;
3521+
3522+
boost::intrusive_ptr<Expression> optimize() final;
3523+
3524+
void acceptVisitor(ExpressionVisitor* visitor) final {
3525+
return visitor->visit(this);
3526+
}
3527+
3528+
static constexpr auto kExpressionName = "$getField"_sd;
3529+
3530+
protected:
3531+
void _doAddDependencies(DepsTracker* deps) const final override;
3532+
3533+
private:
3534+
boost::intrusive_ptr<Expression>& _field;
3535+
boost::intrusive_ptr<Expression>& _from;
3536+
};
34753537
} // namespace mongo

0 commit comments

Comments
 (0)