diff options
-rw-r--r-- | jstests/aggregation/expressions/in.js | 156 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 23 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 7 |
3 files changed, 186 insertions, 0 deletions
diff --git a/jstests/aggregation/expressions/in.js b/jstests/aggregation/expressions/in.js new file mode 100644 index 00000000000..7839c95d32e --- /dev/null +++ b/jstests/aggregation/expressions/in.js @@ -0,0 +1,156 @@ +// SERVER-6146 introduced the $in expression to aggregation. In this file, we test the functionality +// and error cases of the expression. +load("jstests/aggregation/extras/utils.js"); // For assertErrorCode. + +(function() { + "use strict"; + + var coll = db.in; + coll.drop(); + + function testExpression(options) { + var pipeline = { + $project: {included: {$in: ["$elementField", {$literal: options.array}]}} + }; + + coll.drop(); + assert.writeOK(coll.insert({elementField: options.element})); + var res = coll.aggregate(pipeline).toArray(); + assert.eq(res.length, 1); + assert.eq(res[0].included, options.elementIsIncluded); + + if (options.queryFormShouldBeEquivalent) { + var query = { + elementField: {$in: options.array} + }; + res = coll.find(query).toArray(); + + if (options.elementIsIncluded) { + assert.eq(res.length, 1); + } else { + assert.eq(res.length, 0); + } + } + } + + testExpression({ + element: 1, + array: [1, 2, 3], + elementIsIncluded: true, + queryFormShouldBeEquivalent: true + }); + + testExpression({ + element: "A", + array: ["a", "A", "a"], + elementIsIncluded: true, + queryFormShouldBeEquivalent: true + }); + + testExpression({ + element: {a: 1}, + array: [{b: 1}, 2], + elementIsIncluded: false, + queryFormShouldBeEquivalent: true + }); + + testExpression({ + element: {a: 1}, + array: [{a: 1}], + elementIsIncluded: true, + queryFormShouldBeEquivalent: true + }); + + testExpression({ + element: [1, 2], + array: [[2, 1]], + elementIsIncluded: false, + queryFormShouldBeEquivalent: true + }); + + testExpression({ + element: [1, 2], + array: [[1, 2]], + elementIsIncluded: true, + queryFormShouldBeEquivalent: true + }); + + testExpression( + {element: 1, array: [], elementIsIncluded: false, queryFormShouldBeEquivalent: true}); + + // Aggregation's $in has parity with query's $in except with regexes matching string values and + // equality semantics with array values. + + testExpression({ + element: "abc", + array: [/a/, /b/, /c/], + elementIsIncluded: false, + queryFormShouldBeEquivalent: false + }); + + testExpression({ + element: /a/, + array: ["a", "b", "c"], + elementIsIncluded: false, + queryFormShouldBeEquivalent: false + }); + + testExpression({ + element: [], + array: [1, 2, 3], + elementIsIncluded: false, + queryFormShouldBeEquivalent: false + }); + + testExpression({ + element: [1], + array: [1, 2, 3], + elementIsIncluded: false, + queryFormShouldBeEquivalent: false + }); + + testExpression({ + element: [1, 2], + array: [1, 2, 3], + elementIsIncluded: false, + queryFormShouldBeEquivalent: false + }); + + coll.drop(); + coll.insert({}); + + var pipeline = { + $project: {included: {$in: [[1, 2], 1]}} + }; + assertErrorCode(coll, pipeline, 40081, "$in requires an array as a second argument"); + + pipeline = { + $project: {included: {$in: [1, null]}} + }; + assertErrorCode(coll, pipeline, 40081, "$in requires an array as a second argument"); + + pipeline = { + $project: {included: {$in: [1, "$notAField"]}} + }; + assertErrorCode(coll, pipeline, 40081, "$in requires an array as a second argument"); + + pipeline = { + $project: {included: {$in: null}} + }; + assertErrorCode(coll, pipeline, 16020, "$in requires two arguments"); + + pipeline = { + $project: {included: {$in: [1]}} + }; + assertErrorCode(coll, pipeline, 16020, "$in requires two arguments"); + + pipeline = { + $project: {included: {$in: []}} + }; + assertErrorCode(coll, pipeline, 16020, "$in requires two arguments"); + + pipeline = { + $project: {included: {$in: [1, 2, 3]}} + }; + assertErrorCode(coll, pipeline, 16020, "$in requires two arguments"); +}()); diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 9eb2497cd27..465c98aa450 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -2095,6 +2095,29 @@ const char* ExpressionIfNull::getOpName() const { return "$ifNull"; } +/* ----------------------- ExpressionIn ---------------------------- */ + +Value ExpressionIn::evaluateInternal(Variables* vars) const { + Value argument(vpOperand[0]->evaluateInternal(vars)); + Value arrayOfValues(vpOperand[1]->evaluateInternal(vars)); + + uassert(40081, + str::stream() << "$in requires an array as a second argument, found: " + << typeName(arrayOfValues.getType()), + arrayOfValues.isArray()); + for (auto&& value : arrayOfValues.getArray()) { + if (argument == value) { + return Value(true); + } + } + return Value(false); +} + +REGISTER_EXPRESSION(in, ExpressionIn::parse); +const char* ExpressionIn::getOpName() const { + return "$in"; +} + /* ----------------------- ExpressionLn ---------------------------- */ Value ExpressionLn::evaluateNumericArg(const Value& numericArg) const { diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 1642ee8cb89..f17fd5a93d6 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -852,6 +852,13 @@ public: }; +class ExpressionIn final : public ExpressionFixedArity<ExpressionIn, 2> { +public: + Value evaluateInternal(Variables* vars) const final; + const char* getOpName() const final; +}; + + class ExpressionLet final : public Expression { public: boost::intrusive_ptr<Expression> optimize() final; |