diff options
author | Alya Berciu <alyacarina@gmail.com> | 2021-03-30 16:29:12 +0100 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-04-21 10:33:05 +0000 |
commit | 8736544c4a2f4fbd38792f656408f7b807b786d7 (patch) | |
tree | 954c7234c94454e41a1714b41718e241ded9e353 | |
parent | c90de3e65fff76ead7b8ca4664c3f34b0c913e04 (diff) | |
download | mongo-8736544c4a2f4fbd38792f656408f7b807b786d7.tar.gz |
SERVER-30417: Implement $getField expression
Co-authored-by: Katherine Wu <katherine.wu@mongodb.com>
-rw-r--r-- | jstests/aggregation/expressions/expression_get_field.js | 220 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 80 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 62 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 25 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_visitor.h | 2 | ||||
-rw-r--r-- | src/mongo/db/query/sbe_stage_builder_expression.cpp | 6 |
6 files changed, 395 insertions, 0 deletions
diff --git a/jstests/aggregation/expressions/expression_get_field.js b/jstests/aggregation/expressions/expression_get_field.js new file mode 100644 index 00000000000..991a2cd7766 --- /dev/null +++ b/jstests/aggregation/expressions/expression_get_field.js @@ -0,0 +1,220 @@ +/** + * Tests basic functionality of the $getField expression. + */ +(function() { +"use strict"; + +load("jstests/aggregation/extras/utils.js"); // For assertArrayEq. + +const coll = db.expression_get_field; +coll.drop(); + +for (let i = 0; i < 2; i++) { + assert.commandWorked(coll.insert({ + _id: i, + x: i, + y: "c", + "a$b": "foo", + "a.b": "bar", + "a.$b": 5, + ".xy": i, + ".$xz": i, + "..zz": i, + c: {d: "x"}, + })); +} + +// Test that $getField fails with the provided 'code' for invalid arguments 'getFieldArgs'. +function assertGetFieldFailedWithCode(getFieldArgs, code) { + const error = + assert.throws(() => coll.aggregate([{$project: {test: {$getField: getFieldArgs}}}])); + assert.commandFailedWithCode(error, code); +} + +// Test that $getField returns the 'expected' results for the given arguments 'getFieldArgs'. +function assertGetFieldResultsEq(getFieldArgs, expected) { + assertPipelineResultsEq([{$project: {_id: 1, test: {$getField: getFieldArgs}}}], expected); +} + +// Test the given 'pipeline' returns the 'expected' results. +function assertPipelineResultsEq(pipeline, expected) { + const actual = coll.aggregate(pipeline).toArray(); + assertArrayEq({actual, expected}); +} + +const isDotsAndDollarsEnabled = db.adminCommand({getParameter: 1, featureFlagDotsAndDollars: 1}) + .featureFlagDotsAndDollars.value; + +if (!isDotsAndDollarsEnabled) { + // Verify that $getField is not available if the feature flag is set to false and don't + // run the rest of the test. + assertGetFieldFailedWithCode({field: "a", from: {a: "b"}}, 31325); + return; +} + +// Test that $getField fails with a document missing named arguments. +assertGetFieldFailedWithCode({from: {a: "b"}}, 3041702); +assertGetFieldFailedWithCode({field: "a"}, 3041703); + +// Test that $getField fails with a document with one or more arguments of incorrect type. +assertGetFieldFailedWithCode({field: true, from: {a: "b"}}, 3041704); +assertGetFieldFailedWithCode({field: {"a": 1}, from: {"a": 1}}, 3041704); +assertGetFieldFailedWithCode({field: "a", from: true}, 3041705); +assertGetFieldFailedWithCode(5, 3041704); +assertGetFieldFailedWithCode(true, 3041704); +assertGetFieldFailedWithCode({$add: [2, 3]}, 3041704); + +// Test that $getField fails with a document with invalid arguments. +assertGetFieldFailedWithCode({field: "a", from: {a: "b"}, unknown: true}, 3041701); + +// Test that $getField returns the correct value from the provided object. +assertGetFieldResultsEq({field: "a", from: {a: "b"}}, [{_id: 0, test: "b"}, {_id: 1, test: "b"}]); + +// Test that $getField returns the correct value from the $$ROOT object. +assertGetFieldResultsEq(null, [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq("a", [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. +assertGetFieldResultsEq("a$b", [{_id: 0, test: "foo"}, {_id: 1, test: "foo"}]); +assertGetFieldResultsEq("a.b", [{_id: 0, test: "bar"}, {_id: 1, test: "bar"}]); +assertGetFieldResultsEq("x", [{_id: 0, test: 0}, {_id: 1, test: 1}]); +assertGetFieldResultsEq("a.$b", [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq(".xy", [{_id: 0, test: 0}, {_id: 1, test: 1}]); +assertGetFieldResultsEq(".$xz", [{_id: 0, test: 0}, {_id: 1, test: 1}]); +assertGetFieldResultsEq("..zz", [{_id: 0, test: 0}, {_id: 1, test: 1}]); + +// Test that $getField returns the correct value from the $$ROOT object when field is an expression. +assertGetFieldResultsEq({$concat: ["a", "b"]}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. +assertGetFieldResultsEq({$concat: ["a", {$const: "$"}, "b"]}, + [{_id: 0, test: "foo"}, {_id: 1, test: "foo"}]); +assertGetFieldResultsEq({$cond: [true, null, "x"]}, [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq({$cond: [false, null, "x"]}, [{_id: 0, test: 0}, {_id: 1, test: 1}]); + +// Test that $getField treats dotted fields as key literals instead of field paths. Note that it is +// necessary to use $const in places, otherwise object field validation would reject some of these +// field names. +assertGetFieldResultsEq({field: "a.b", from: {$const: {"a.b": "b"}}}, + [{_id: 0, test: "b"}, {_id: 1, test: "b"}]); +assertGetFieldResultsEq({field: ".ab", from: {$const: {".ab": "b"}}}, + [{_id: 0, test: "b"}, {_id: 1, test: "b"}]); +assertGetFieldResultsEq({field: "ab.", from: {$const: {"ab.": "b"}}}, + [{_id: 0, test: "b"}, {_id: 1, test: "b"}]); +assertGetFieldResultsEq({field: "a.b.c", from: {$const: {"a.b.c": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: "a.b.c", from: {a: {b: {c: 5}}}}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. +assertGetFieldResultsEq({field: {$concat: ["a.b", ".", "c"]}, from: {$const: {"a.b.c": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: {$concat: ["a.b", ".", "c"]}, from: {a: {b: {c: 5}}}}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. + +// Test that $getField works with fields that contain '$'. +assertGetFieldResultsEq({field: "a$b", from: {"a$b": "b"}}, + [{_id: 0, test: "b"}, {_id: 1, test: "b"}]); +assertGetFieldResultsEq({field: "a$b.b", from: {$const: {"a$b.b": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: {$const: "a$b.b"}, from: {$const: {"a$b.b": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: {$const: "$b.b"}, from: {$const: {"$b.b": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: {$const: "$b"}, from: {$const: {"$b": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: {$const: "$.ab"}, from: {$const: {"$.ab": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: {$const: "$$xz"}, from: {$const: {"$$xz": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); + +// Test null and missing cases. +assertGetFieldResultsEq({field: "a", from: null}, [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq({field: null, from: {a: 1}}, [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq({field: "a", from: {b: 2, c: 3}}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. +assertGetFieldResultsEq({field: "a", from: {a: null, b: 2, c: 3}}, + [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq({field: {$const: "$a"}, from: {$const: {"$a": null, b: 2, c: 3}}}, + [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq({field: "a", from: {}}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. + +// These should return null because "$a.b" evaluates to a field path expression which returns a +// nullish value (so the expression should return null), as there is no $a.b field path. +assertGetFieldResultsEq({field: "$a.b", from: {$const: {"$a.b": 5}}}, + [{_id: 0, test: null}, {_id: 1, test: null}]); +assertGetFieldResultsEq("$a.b", [{_id: 0, test: null}, {_id: 1, test: null}]); + +// When the field path does actually resolve to a field, the value of that field should be used. + +// The fieldpath $y resolves to "c" in $$ROOT. +assertGetFieldResultsEq({field: "$y", from: {$const: {"c": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: "$y", from: {$const: {"a": 5}}}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. +assertGetFieldResultsEq("$y", [{_id: 0, test: {d: "x"}}, {_id: 1, test: {d: "x"}}]); + +// The fieldpath $c.d resolves to "x" in $$ROOT. +assertGetFieldResultsEq({field: "$c.d", from: {$const: {"x": 5}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq({field: "$c.d", from: {$const: {"y": 5}}}, + [{_id: 0}, {_id: 1}]); // The test field should evaluate to missing. +assertGetFieldResultsEq("$c.d", [{_id: 0, test: 0}, {_id: 1, test: 1}]); + +// $x resolves to a number, so this should fail. +assertGetFieldFailedWithCode({field: "$x", from: {$const: {"c": 5}}}, 3041704); +assertGetFieldFailedWithCode("$x", 3041704); + +// Test case where $getField stages are nested. +assertGetFieldResultsEq( + {field: "a", from: {$getField: {field: "b.c", from: {$const: {"b.c": {a: 5}}}}}}, + [{_id: 0, test: 5}, {_id: 1, test: 5}]); +assertGetFieldResultsEq( + {field: "x", from: {$getField: {field: "b.c", from: {$const: {"b.c": {a: 5}}}}}}, + [{_id: 0}, {_id: 1}]); +assertGetFieldResultsEq( + {field: "a", from: {$getField: {field: "b.d", from: {$const: {"b.c": {a: 5}}}}}}, + [{_id: 0, test: null}, {_id: 1, test: null}]); + +// Test case when a dotted/dollar path is within an array. +assertGetFieldResultsEq({ + field: {$const: "a$b"}, + from: {$arrayElemAt: [[{$const: {"a$b": 1}}, {$const: {"a$b": 2}}], 0]} +}, + [{_id: 0, test: 1}, {_id: 1, test: 1}]); +assertGetFieldResultsEq({ + field: {$const: "a.."}, + from: {$arrayElemAt: [[{$const: {"a..": 1}}, {$const: {"a..": 2}}], 1]} +}, + [{_id: 0, test: 2}, {_id: 1, test: 2}]); + +// Test $getField expression with other pipeline stages. + +assertPipelineResultsEq( + [ + {$match: {$expr: {$eq: [{$getField: "_id"}, {$getField: ".$xz"}]}}}, + {$project: {aa: {$getField: ".$xz"}, "_id": 1}}, + ], + [{_id: 0, aa: 0}, {_id: 1, aa: 1}]); + +assertPipelineResultsEq([{$match: {$expr: {$ne: [{$getField: "_id"}, {$getField: ".$xz"}]}}}], []); +assertPipelineResultsEq( + [ + {$match: {$expr: {$ne: [{$getField: "_id"}, {$getField: "a.b"}]}}}, + {$project: {"a": {$getField: "x"}, "b": {$getField: {$const: "a.b"}}}} + ], + [{_id: 0, a: 0, b: "bar"}, {_id: 1, a: 1, b: "bar"}]); + +assertPipelineResultsEq( + [ + {$addFields: {aa: {$getField: {$const: "a.b"}}}}, + {$project: {aa: 1, _id: 1}}, + ], + [{_id: 0, aa: "bar"}, {_id: 1, aa: "bar"}]); + +assertPipelineResultsEq( + [ + {$bucket: {groupBy: {$getField: {$const: "a.b"}}, boundaries: ["aaa", "bar", "zzz"]}} + ], // We should get one bucket here ("bar") with two documents. + [{_id: "bar", count: 2}]); +assertPipelineResultsEq([{ + $bucket: {groupBy: {$getField: "x"}, boundaries: [0, 1, 2, 3, 4]} + }], // We should get two buckets here for the two possible values of x. + [{_id: 0, count: 1}, {_id: 1, count: 1}]); +})();
\ No newline at end of file diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 894ef5893aa..4fc2b25e7ef 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -7187,6 +7187,86 @@ void ExpressionDateTrunc::_doAddDependencies(DepsTracker* deps) const { } } +/* -------------------------- ExpressionGetField ------------------------------ */ +REGISTER_FEATURE_FLAG_GUARDED_EXPRESSION(getField, + ExpressionGetField::parse, + feature_flags::gFeatureFlagDotsAndDollars); + +intrusive_ptr<Expression> ExpressionGetField::parse(ExpressionContext* const expCtx, + BSONElement expr, + const VariablesParseState& vps) { + boost::intrusive_ptr<Expression> fieldExpr; + boost::intrusive_ptr<Expression> fromExpr; + + if (expr.type() == BSONType::Object) { + for (auto&& elem : expr.embeddedObject()) { + const auto fieldName = elem.fieldNameStringData(); + if (!fieldExpr && !fromExpr && fieldName[0] == '$') { + // This may be an expression, so we should treat it as such. + fieldExpr = Expression::parseOperand(expCtx, expr, vps); + fromExpr = ExpressionFieldPath::parse(expCtx, "$$ROOT", vps); + break; + } else if (fieldName == "field"_sd) { + fieldExpr = Expression::parseOperand(expCtx, elem, vps); + } else if (fieldName == "from"_sd) { + fromExpr = Expression::parseOperand(expCtx, elem, vps); + } else { + uasserted(3041701, + str::stream() + << kExpressionName << " found an unknown argument: " << fieldName); + } + } + } else { + fieldExpr = Expression::parseOperand(expCtx, expr, vps); + fromExpr = ExpressionFieldPath::parse(expCtx, "$$ROOT", vps); + } + + uassert(3041702, + str::stream() << kExpressionName << " requires 'field' to be specified", + fieldExpr); + uassert( + 3041703, str::stream() << kExpressionName << " requires 'from' to be specified", fromExpr); + + return make_intrusive<ExpressionGetField>(expCtx, fieldExpr, fromExpr); +} + +Value ExpressionGetField::evaluate(const Document& root, Variables* variables) const { + auto fieldValue = _field->evaluate(root, variables); + if (fieldValue.nullish()) { + return Value(BSONNULL); + } + + auto fromValue = _from->evaluate(root, variables); + if (fromValue.nullish()) { + return Value(BSONNULL); + } + + uassert(3041704, + str::stream() << kExpressionName << " requires 'field' to evaluate to type String", + fieldValue.getType() == BSONType::String); + + uassert(3041705, + str::stream() << kExpressionName << " requires 'from' to evaluate to type Object", + fromValue.getType() == BSONType::Object); + + return fromValue.getDocument().getField(fieldValue.getString()); +} + +intrusive_ptr<Expression> ExpressionGetField::optimize() { + return intrusive_ptr<Expression>(this); +} + +void ExpressionGetField::_doAddDependencies(DepsTracker* deps) const { + _from->addDependencies(deps); + _field->addDependencies(deps); +} + +Value ExpressionGetField::serialize(const bool explain) const { + return Value(Document{{"$getField"_sd, + Document{{"field"_sd, _field->serialize(explain)}, + {"from"_sd, _from->serialize(explain)}}}}); +} + MONGO_INITIALIZER(expressionParserMap)(InitializerContext*) { // Nothing to do. This initializer exists to tie together all the individual initializers // defined by REGISTER_EXPRESSION / REGISTER_EXPRESSION_WITH_MIN_VERSION. diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index f89e45a46a1..e0571735a18 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -50,6 +50,7 @@ #include "mongo/db/pipeline/field_path.h" #include "mongo/db/pipeline/variables.h" #include "mongo/db/query/datetime/date_time_support.h" +#include "mongo/db/query/query_feature_flags_gen.h" #include "mongo/db/query/sort_pattern.h" #include "mongo/db/server_options.h" #include "mongo/util/intrusive_counter.h" @@ -78,6 +79,25 @@ class DocumentSource; } /** + * Registers a Parser so it can be called from parseExpression and friends (but only if + * 'featureFlag' is enabled). + * + * As an example, if your expression looks like {"$foo": [1,2,3]} and should be flag-guarded by + * feature_flags::gFoo, you would add this line: + * REGISTER_FEATURE_FLAG_GUARDED_EXPRESSION(foo, ExpressionFoo::parse, feature_flags::gFoo); + * + * An expression registered this way can be used in any featureCompatibilityVersion. + */ +#define REGISTER_FEATURE_FLAG_GUARDED_EXPRESSION(key, parser, featureFlag) \ + MONGO_INITIALIZER_GENERAL( \ + addToExpressionParserMap_##key, ("default"), ("expressionParserMap")) \ + (InitializerContext*) { \ + if (featureFlag.isEnabledAndIgnoreFCV()) { \ + Expression::registerExpression("$" #key, (parser), boost::none); \ + } \ + } + +/** * Registers a Parser so it can be called from parseExpression and friends. Use this version if your * expression can only be persisted to a catalog data structure in a feature compatibility version * >= X. @@ -3472,4 +3492,46 @@ private: // Accepted BSON type: String. If not specified, "sunday" is used. boost::intrusive_ptr<Expression>& _startOfWeek; }; + +class ExpressionGetField final : public Expression { +public: + static boost::intrusive_ptr<Expression> parse(ExpressionContext* const expCtx, + BSONElement exprElement, + const VariablesParseState& vps); + + /** + * Constructs a $getField expression where 'field' is an expression resolving to a string Value + * (or null) and 'from' is an expression resolving to an object Value (or null). + * + * If either 'field' or 'from' is nullish, $getField evaluates to null. Furthermore, if 'from' + * does not contain 'field', then $getField returns missing. + */ + ExpressionGetField(ExpressionContext* const expCtx, + boost::intrusive_ptr<Expression> field, + boost::intrusive_ptr<Expression> from) + : Expression(expCtx, {std::move(field), std::move(from)}), + _field(_children[0]), + _from(_children[1]) { + expCtx->sbeCompatible = false; + } + + Value serialize(const bool explain) const final; + + Value evaluate(const Document& root, Variables* variables) const final; + + boost::intrusive_ptr<Expression> optimize() final; + + void acceptVisitor(ExpressionVisitor* visitor) final { + return visitor->visit(this); + } + + static constexpr auto kExpressionName = "$getField"_sd; + +protected: + void _doAddDependencies(DepsTracker* deps) const final override; + +private: + boost::intrusive_ptr<Expression>& _field; + boost::intrusive_ptr<Expression>& _from; +}; } // namespace mongo diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index 41bb7c2df80..0d1d9dd041f 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -3127,4 +3127,29 @@ TEST(ExpressionSubtractTest, OverflowLong) { ASSERT_EQ(result.getDouble(), static_cast<double>(minLong) * -1); } +TEST(ExpressionGetFieldTest, GetFieldSerializesStringArgumentCorrectly) { + auto expCtx = ExpressionContextForTest{}; + VariablesParseState vps = expCtx.variablesParseState; + BSONObj expr = fromjson("{$meta: \"foo\"}"); + auto expression = ExpressionGetField::parse(&expCtx, expr.firstElement(), vps); + ASSERT_BSONOBJ_EQ(BSON("ignoredField" << BSON("$getField" << BSON("field" << BSON("$const" + << "foo") + << "from" + << "$$ROOT"))), + BSON("ignoredField" << expression->serialize(false))); +} + +TEST(ExpressionGetFieldTest, GetFieldSerializesCorrectly) { + auto expCtx = ExpressionContextForTest{}; + VariablesParseState vps = expCtx.variablesParseState; + BSONObj expr = fromjson("{$meta: {\"field\": \"foo\", \"from\": {a: 1}}}"); + auto expression = ExpressionGetField::parse(&expCtx, expr.firstElement(), vps); + ASSERT_BSONOBJ_EQ(BSON("ignoredField" + << BSON("$getField" + << BSON("field" << BSON("$const" + << "foo") + << "from" << BSON("a" << BSON("$const" << 1))))), + BSON("ignoredField" << expression->serialize(false))); +} + } // namespace ExpressionTests diff --git a/src/mongo/db/pipeline/expression_visitor.h b/src/mongo/db/pipeline/expression_visitor.h index 819e51a03d5..b32ce2ce55d 100644 --- a/src/mongo/db/pipeline/expression_visitor.h +++ b/src/mongo/db/pipeline/expression_visitor.h @@ -157,6 +157,7 @@ class ExpressionDateDiff; class ExpressionDateAdd; class ExpressionDateSubtract; class ExpressionDateTrunc; +class ExpressionGetField; class AccumulatorAvg; class AccumulatorMax; @@ -311,6 +312,7 @@ public: virtual void visit(ExpressionToHashedIndexKey*) = 0; virtual void visit(ExpressionDateAdd*) = 0; virtual void visit(ExpressionDateSubtract*) = 0; + virtual void visit(ExpressionGetField*) = 0; }; } // namespace mongo diff --git a/src/mongo/db/query/sbe_stage_builder_expression.cpp b/src/mongo/db/query/sbe_stage_builder_expression.cpp index fff1f93de32..4d6af930619 100644 --- a/src/mongo/db/query/sbe_stage_builder_expression.cpp +++ b/src/mongo/db/query/sbe_stage_builder_expression.cpp @@ -485,6 +485,7 @@ public: void visit(ExpressionToHashedIndexKey* expr) final {} void visit(ExpressionDateAdd* expr) final {} void visit(ExpressionDateSubtract* expr) final {} + void visit(ExpressionGetField* expr) final {} private: void visitMultiBranchLogicExpression(Expression* expr, sbe::EPrimBinary::Op logicOp) { @@ -684,6 +685,7 @@ public: void visit(ExpressionToHashedIndexKey* expr) final {} void visit(ExpressionDateAdd* expr) final {} void visit(ExpressionDateSubtract* expr) final {} + void visit(ExpressionGetField* expr) final {} private: void visitMultiBranchLogicExpression(Expression* expr, sbe::EPrimBinary::Op logicOp) { @@ -2647,6 +2649,10 @@ public: generateDateArithmeticsExpression(expr, "dateSubtract"); } + void visit(ExpressionGetField* expr) final { + unsupportedExpression("$getField"); + } + private: /** * Shared logic for $and, $or. Converts each child into an EExpression that evaluates to Boolean |