diff options
author | Charlie Swanson <charlie.swanson@mongodb.com> | 2015-05-20 17:09:37 -0400 |
---|---|---|
committer | Charlie Swanson <charlie.swanson@mongodb.com> | 2015-07-15 16:24:44 -0400 |
commit | c6e7a0874e5fc2767a5d50f47d0441703fea73ac (patch) | |
tree | ae2a86c70fe945ab532500c106ebb2a329aff263 | |
parent | b5cabb47a24aa0c4c866a5aaa7c14891cf69c1d5 (diff) | |
download | mongo-c6e7a0874e5fc2767a5d50f47d0441703fea73ac.tar.gz |
SERVER-8141 Avoid treating arrays as literals in aggregation pipeline
-rw-r--r-- | jstests/aggregation/bugs/server8141.js | 48 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 32 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 9 |
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; |