summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharlie Swanson <charlie.swanson@mongodb.com>2015-05-20 17:09:37 -0400
committerCharlie Swanson <charlie.swanson@mongodb.com>2015-07-15 16:24:44 -0400
commitc6e7a0874e5fc2767a5d50f47d0441703fea73ac (patch)
treeae2a86c70fe945ab532500c106ebb2a329aff263
parentb5cabb47a24aa0c4c866a5aaa7c14891cf69c1d5 (diff)
downloadmongo-c6e7a0874e5fc2767a5d50f47d0441703fea73ac.tar.gz
SERVER-8141 Avoid treating arrays as literals in aggregation pipeline
-rw-r--r--jstests/aggregation/bugs/server8141.js48
-rw-r--r--src/mongo/db/pipeline/expression.cpp32
-rw-r--r--src/mongo/db/pipeline/expression.h9
3 files changed, 89 insertions, 0 deletions
diff --git a/jstests/aggregation/bugs/server8141.js b/jstests/aggregation/bugs/server8141.js
new file mode 100644
index 00000000000..d450ec14227
--- /dev/null
+++ b/jstests/aggregation/bugs/server8141.js
@@ -0,0 +1,48 @@
+// SERVER-8141 Avoid treating arrays as literals in aggregation pipeline.
+(function() {
+ 'use strict';
+ var coll = db.exprs_in_arrays;
+ coll.drop();
+
+ assert.writeOK(coll.insert({_id: 0, a: ['foo', 'bar', 'baz'], b: 'bar', c: 'Baz'}));
+
+ // An array of constants should still evaluate to an array of constants.
+ var pipeline = [{$project: {_id: 0, d: ['constant', 1]}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['constant', 1]}]);
+
+ // A field name inside an array should take on the value of that field.
+ pipeline = [{$project: {_id: 0, d: ['$b']}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['bar']}]);
+
+ // An expression inside an array should be evaluated.
+ pipeline = [{$project: {_id: 0, d: [{$toLower: 'FoO'}]}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['foo']}]);
+
+ // Both an expression and a field name inside an array should be evaluated.
+ pipeline = [{$project: {_id: 0, d: ['$b', {$toLower: 'FoO'}]}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['bar', 'foo']}]);
+
+ // A nested array should still be evaluated.
+ pipeline = [{$project: {_id: 0, d: ['$b', 'constant', [1, {$toLower: 'FoO'}]]}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['bar', 'constant', [1, 'foo']]}]);
+
+ // Should still evaluate array elements inside arguments to an expression.
+ pipeline = [{$project: {_id: 0, d: {$setIntersection: ['$a', ['$b']]}}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['bar']}]);
+
+ pipeline = [{$project: {_id: 0, d: {$setIntersection: ['$a', [{$toLower: 'FoO'}]]}}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: ['foo']}]);
+
+ // Nested arrays.
+ pipeline = [{$project: {_id: 0, d: {$setIntersection: [[[1, 'foo', 'bar']],
+ [[1, {$toLower: 'FoO'}, '$b']]]}}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{d: [[1, 'foo', 'bar']]}]);
+
+ coll.drop();
+
+ // Should replace missing values with NULL to preserve indices.
+ assert.writeOK(coll.insert({_id: 1, x: 1, z: 2}));
+
+ pipeline = [{$project: {_id: 0, coordinate: ['$x', '$y', '$z']}}];
+ assert.eq(coll.aggregate(pipeline).toArray(), [{coordinate: [1, null, 2]}]);
+}());
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp
index 63d3aa3ec69..92aab58ad61 100644
--- a/src/mongo/db/pipeline/expression.cpp
+++ b/src/mongo/db/pipeline/expression.cpp
@@ -270,6 +270,10 @@ intrusive_ptr<Expression> Expression::parseObject(BSONObj obj,
fieldName, ExpressionFieldPath::parse(fieldElement.str(), vps));
break;
}
+ case Array:
+ pExpressionObject->addField(fieldName,
+ ExpressionArray::parse(fieldElement, vps));
+ break;
case Bool:
case NumberDouble:
case NumberLong:
@@ -345,6 +349,8 @@ intrusive_ptr<Expression> Expression::parseOperand(BSONElement exprElement,
} else if (type == Object) {
ObjectCtx oCtx(ObjectCtx::DOCUMENT_OK);
return Expression::parseObject(exprElement.Obj(), &oCtx, vps);
+ } else if (type == Array) {
+ return ExpressionArray::parse(exprElement, vps);
} else {
return ExpressionConstant::parse(exprElement, vps);
}
@@ -558,6 +564,32 @@ const char* ExpressionAnyElementTrue::getOpName() const {
return "$anyElementTrue";
}
+/* ---------------------- ExpressionArray --------------------------- */
+
+Value ExpressionArray::evaluateInternal(Variables* vars) const {
+ vector<Value> values;
+ values.reserve(vpOperand.size());
+ for (auto&& expr : vpOperand) {
+ Value elemVal = expr->evaluateInternal(vars);
+ values.push_back(elemVal.missing() ? Value(BSONNULL) : std::move(elemVal));
+ }
+ return Value(std::move(values));
+}
+
+Value ExpressionArray::serialize(bool explain) const {
+ vector<Value> expressions;
+ expressions.reserve(vpOperand.size());
+ for (auto&& expr : vpOperand) {
+ expressions.push_back(expr->serialize(explain));
+ }
+ return Value(std::move(expressions));
+}
+
+const char* ExpressionArray::getOpName() const {
+ // This should never be called, but is needed to inherit from ExpressionNary.
+ return "$array";
+}
+
/* ------------------------- ExpressionArrayElemAt -------------------------- */
Value ExpressionArrayElemAt::evaluateInternal(Variables* vars) const {
diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h
index 8caa59a9d51..c386cbe80b3 100644
--- a/src/mongo/db/pipeline/expression.h
+++ b/src/mongo/db/pipeline/expression.h
@@ -454,6 +454,15 @@ public:
};
+class ExpressionArray final : public ExpressionVariadic<ExpressionArray> {
+public:
+ // virtuals from ExpressionNary
+ Value evaluateInternal(Variables* vars) const final;
+ Value serialize(bool explain) const final;
+ const char* getOpName() const final;
+};
+
+
class ExpressionArrayElemAt final : public ExpressionFixedArity<ExpressionArrayElemAt, 2> {
public:
Value evaluateInternal(Variables* vars) const final;