summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/aggregation/expressions/in.js156
-rw-r--r--src/mongo/db/pipeline/expression.cpp23
-rw-r--r--src/mongo/db/pipeline/expression.h7
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;