/**
* Copyright (C) 2012 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
#include "mongo/platform/basic.h"
#include "mongo/bson/bsonmisc.h"
#include "mongo/config.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/json.h"
#include "mongo/db/pipeline/accumulator.h"
#include "mongo/db/pipeline/document.h"
#include "mongo/db/pipeline/document_value_test_util.h"
#include "mongo/db/pipeline/expression.h"
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/pipeline/value_comparator.h"
#include "mongo/db/query/collation/collator_interface_mock.h"
#include "mongo/dbtests/dbtests.h"
#include "mongo/unittest/unittest.h"
namespace ExpressionTests {
using boost::intrusive_ptr;
using std::initializer_list;
using std::numeric_limits;
using std::pair;
using std::set;
using std::sort;
using std::string;
using std::vector;
using std::list;
/**
* Creates an expression given by 'expressionName' and evaluates it using
* 'operands' as inputs, returning the result.
*/
static Value evaluateExpression(const string& expressionName,
const vector& operands) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
const BSONObj obj = BSON(expressionName << ImplicitValue::convertToValue(operands));
auto expression = Expression::parseExpression(expCtx, obj, vps);
Value result = expression->evaluate(Document());
return result;
}
/**
* Creates an expression which parses named arguments via an object specification, then evaluates it
* and returns the result.
*/
static Value evaluateNamedArgExpression(const string& expressionName, const Document& operand) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
const BSONObj obj = BSON(expressionName << operand);
auto expression = Expression::parseExpression(expCtx, obj, vps);
Value result = expression->evaluate(Document());
return result;
}
/**
* Takes the name of an expression as its first argument and a list of pairs of arguments and
* expected results as its second argument, and asserts that for the given expression the arguments
* evaluate to the expected results.
*/
static void assertExpectedResults(
const string& expression,
initializer_list, ImplicitValue>> operations) {
for (auto&& op : operations) {
try {
Value result = evaluateExpression(expression, op.first);
ASSERT_VALUE_EQ(op.second, result);
ASSERT_EQUALS(op.second.getType(), result.getType());
} catch (...) {
log() << "failed with arguments: " << ImplicitValue::convertToValue(op.first);
throw;
}
}
}
/** Convert BSONObj to a BSONObj with our $const wrappings. */
static BSONObj constify(const BSONObj& obj, bool parentIsArray = false) {
BSONObjBuilder bob;
for (BSONObjIterator itr(obj); itr.more(); itr.next()) {
BSONElement elem = *itr;
if (elem.type() == Object) {
bob << elem.fieldName() << constify(elem.Obj(), false);
} else if (elem.type() == Array && !parentIsArray) {
// arrays within arrays are treated as constant values by the real
// parser
bob << elem.fieldName() << BSONArray(constify(elem.Obj(), true));
} else if (str::equals(elem.fieldName(), "$const") ||
(elem.type() == mongo::String && elem.valuestrsafe()[0] == '$')) {
bob.append(elem);
} else {
bob.append(elem.fieldName(), BSON("$const" << elem));
}
}
return bob.obj();
}
/** Check binary equality, ensuring use of the same numeric types. */
static void assertBinaryEqual(const BSONObj& expected, const BSONObj& actual) {
ASSERT_BSONOBJ_EQ(expected, actual);
ASSERT(expected.binaryEqual(actual));
}
/** Convert Value to a wrapped BSONObj with an empty string field name. */
static BSONObj toBson(const Value& value) {
BSONObjBuilder bob;
value.addToBsonObj(&bob, "");
return bob.obj();
}
/** Convert Expression to BSON. */
static BSONObj expressionToBson(const intrusive_ptr& expression) {
return BSON("" << expression->serialize(false)).firstElement().embeddedObject().getOwned();
}
/** Convert Document to BSON. */
static BSONObj toBson(const Document& document) {
return document.toBson();
}
/** Create a Document from a BSONObj. */
Document fromBson(BSONObj obj) {
return Document(obj);
}
/** Create a Value from a BSONObj. */
Value valueFromBson(BSONObj obj) {
BSONElement element = obj.firstElement();
return Value(element);
}
template
intrusive_ptr makeConstant(T&& val) {
intrusive_ptr expCtx(new ExpressionContextForTest());
return ExpressionConstant::create(expCtx, Value(std::forward(val)));
}
class ExpressionBaseTest : public unittest::Test {
public:
void addOperand(intrusive_ptr expr, Value arg) {
intrusive_ptr expCtx(new ExpressionContextForTest());
expr->addOperand(ExpressionConstant::create(expCtx, arg));
}
};
class ExpressionNaryTestOneArg : public ExpressionBaseTest {
public:
virtual void assertEvaluates(Value input, Value output) {
addOperand(_expr, input);
ASSERT_VALUE_EQ(output, _expr->evaluate(Document()));
ASSERT_EQUALS(output.getType(), _expr->evaluate(Document()).getType());
}
intrusive_ptr _expr;
};
/* ------------------------- NaryExpression -------------------------- */
/** A dummy child of ExpressionNary used for testing. */
class Testable : public ExpressionNary {
public:
virtual Value evaluate(const Document& root) const {
// Just put all the values in a list.
// By default, this is not associative/commutative so the results will change if
// instantiated as commutative or associative and operations are reordered.
vector values;
for (ExpressionVector::const_iterator i = vpOperand.begin(); i != vpOperand.end(); ++i) {
values.push_back((*i)->evaluate(root));
}
return Value(values);
}
virtual const char* getOpName() const {
return "$testable";
}
virtual bool isAssociative() const {
return _isAssociative;
}
virtual bool isCommutative() const {
return _isCommutative;
}
static intrusive_ptr create(bool associative, bool commutative) {
return new Testable(associative, commutative);
}
private:
Testable(bool isAssociative, bool isCommutative)
: ExpressionNary(
boost::intrusive_ptr(new ExpressionContextForTest())),
_isAssociative(isAssociative),
_isCommutative(isCommutative) {}
bool _isAssociative;
bool _isCommutative;
};
class ExpressionNaryTest : public unittest::Test {
public:
virtual void setUp() override {
_notAssociativeNorCommutative = Testable::create(false, false);
_associativeOnly = Testable::create(true, false);
_associativeAndCommutative = Testable::create(true, true);
}
protected:
void assertDependencies(const intrusive_ptr& expr,
const BSONArray& expectedDependencies) {
DepsTracker dependencies;
expr->addDependencies(&dependencies);
BSONArrayBuilder dependenciesBson;
for (set::const_iterator i = dependencies.fields.begin();
i != dependencies.fields.end();
++i) {
dependenciesBson << *i;
}
ASSERT_BSONOBJ_EQ(expectedDependencies, dependenciesBson.arr());
ASSERT_EQUALS(false, dependencies.needWholeDocument);
ASSERT_EQUALS(false, dependencies.getNeedTextScore());
}
void assertContents(const intrusive_ptr& expr, const BSONArray& expectedContents) {
ASSERT_BSONOBJ_EQ(constify(BSON("$testable" << expectedContents)), expressionToBson(expr));
}
void addOperandArrayToExpr(const intrusive_ptr& expr, const BSONArray& operands) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
BSONObjIterator i(operands);
while (i.more()) {
BSONElement element = i.next();
expr->addOperand(Expression::parseOperand(expCtx, element, vps));
}
}
intrusive_ptr _notAssociativeNorCommutative;
intrusive_ptr _associativeOnly;
intrusive_ptr _associativeAndCommutative;
};
TEST_F(ExpressionNaryTest, AddedConstantOperandIsSerialized) {
intrusive_ptr expCtx(new ExpressionContextForTest());
_notAssociativeNorCommutative->addOperand(ExpressionConstant::create(expCtx, Value(9)));
assertContents(_notAssociativeNorCommutative, BSON_ARRAY(9));
}
TEST_F(ExpressionNaryTest, AddedFieldPathOperandIsSerialized) {
intrusive_ptr expCtx(new ExpressionContextForTest());
_notAssociativeNorCommutative->addOperand(ExpressionFieldPath::create(expCtx, "ab.c"));
assertContents(_notAssociativeNorCommutative, BSON_ARRAY("$ab.c"));
}
TEST_F(ExpressionNaryTest, ValidateEmptyDependencies) {
assertDependencies(_notAssociativeNorCommutative, BSONArray());
}
TEST_F(ExpressionNaryTest, ValidateConstantExpressionDependency) {
intrusive_ptr expCtx(new ExpressionContextForTest());
_notAssociativeNorCommutative->addOperand(ExpressionConstant::create(expCtx, Value(1)));
assertDependencies(_notAssociativeNorCommutative, BSONArray());
}
TEST_F(ExpressionNaryTest, ValidateFieldPathExpressionDependency) {
intrusive_ptr expCtx(new ExpressionContextForTest());
_notAssociativeNorCommutative->addOperand(ExpressionFieldPath::create(expCtx, "ab.c"));
assertDependencies(_notAssociativeNorCommutative, BSON_ARRAY("ab.c"));
}
TEST_F(ExpressionNaryTest, ValidateObjectExpressionDependency) {
BSONObj spec = BSON("" << BSON("a"
<< "$x"
<< "q"
<< "$r"));
intrusive_ptr expCtx(new ExpressionContextForTest());
BSONElement specElement = spec.firstElement();
VariablesParseState vps = expCtx->variablesParseState;
_notAssociativeNorCommutative->addOperand(
Expression::parseObject(expCtx, specElement.Obj(), vps));
assertDependencies(_notAssociativeNorCommutative,
BSON_ARRAY("r"
<< "x"));
}
TEST_F(ExpressionNaryTest, SerializationToBsonObj) {
intrusive_ptr expCtx(new ExpressionContextForTest());
_notAssociativeNorCommutative->addOperand(ExpressionConstant::create(expCtx, Value(5)));
ASSERT_BSONOBJ_EQ(BSON("foo" << BSON("$testable" << BSON_ARRAY(BSON("$const" << 5)))),
BSON("foo" << _notAssociativeNorCommutative->serialize(false)));
}
TEST_F(ExpressionNaryTest, SerializationToBsonArr) {
intrusive_ptr expCtx(new ExpressionContextForTest());
_notAssociativeNorCommutative->addOperand(ExpressionConstant::create(expCtx, Value(5)));
ASSERT_BSONOBJ_EQ(constify(BSON_ARRAY(BSON("$testable" << BSON_ARRAY(5)))),
BSON_ARRAY(_notAssociativeNorCommutative->serialize(false)));
}
// Verify that the internal operands are optimized
TEST_F(ExpressionNaryTest, InternalOperandOptimizationIsDone) {
BSONArray spec = BSON_ARRAY(BSON("$and" << BSONArray()) << "$abc");
addOperandArrayToExpr(_notAssociativeNorCommutative, spec);
assertContents(_notAssociativeNorCommutative, spec);
ASSERT(_notAssociativeNorCommutative == _notAssociativeNorCommutative->optimize());
assertContents(_notAssociativeNorCommutative, BSON_ARRAY(true << "$abc"));
}
// Verify that if all the operands are constants, the expression is replaced
// by a constant value equivalent to the expression applied to the operands.
TEST_F(ExpressionNaryTest, AllConstantOperandOptimization) {
BSONArray spec = BSON_ARRAY(1 << 2);
addOperandArrayToExpr(_notAssociativeNorCommutative, spec);
assertContents(_notAssociativeNorCommutative, spec);
intrusive_ptr optimized = _notAssociativeNorCommutative->optimize();
ASSERT(_notAssociativeNorCommutative != optimized);
ASSERT_BSONOBJ_EQ(BSON("$const" << BSON_ARRAY(1 << 2)), expressionToBson(optimized));
}
// Verify that the optimization of grouping constant and non-constant operands
// and then applying the expression to the constant operands to reduce them to
// one constant operand is only applied if the expression is associative and
// commutative.
TEST_F(ExpressionNaryTest, GroupingOptimizationOnNotCommutativeNorAssociative) {
BSONArray spec = BSON_ARRAY(55 << 66 << "$path");
addOperandArrayToExpr(_notAssociativeNorCommutative, spec);
assertContents(_notAssociativeNorCommutative, spec);
intrusive_ptr optimized = _notAssociativeNorCommutative->optimize();
ASSERT(_notAssociativeNorCommutative == optimized);
assertContents(_notAssociativeNorCommutative, spec);
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnAssociativeOnlyFrontOperands) {
BSONArray spec = BSON_ARRAY(55 << 66 << "$path");
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, BSON_ARRAY(BSON_ARRAY(55 << 66) << "$path"));
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnAssociativeOnlyMiddleOperands) {
BSONArray spec = BSON_ARRAY("$path1" << 55 << 66 << "$path");
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, BSON_ARRAY("$path1" << BSON_ARRAY(55 << 66) << "$path"));
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnAssociativeOnlyBackOperands) {
BSONArray spec = BSON_ARRAY("$path" << 55 << 66);
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, BSON_ARRAY("$path" << BSON_ARRAY(55 << 66)));
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnAssociativeOnlyNotExecuteOnSingleConstantsFront) {
BSONArray spec = BSON_ARRAY(55 << "$path");
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, BSON_ARRAY(55 << "$path"));
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnAssociativeOnlyNotExecuteOnSingleConstantsMiddle) {
BSONArray spec = BSON_ARRAY("$path1" << 55 << "$path2");
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, BSON_ARRAY("$path1" << 55 << "$path2"));
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnAssociativeOnlyNotExecuteOnSingleConstantsBack) {
BSONArray spec = BSON_ARRAY("$path" << 55);
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, BSON_ARRAY("$path" << 55));
}
TEST_F(ExpressionNaryTest, GroupingOptimizationOnCommutativeAndAssociative) {
BSONArray spec = BSON_ARRAY(55 << 66 << "$path");
addOperandArrayToExpr(_associativeAndCommutative, spec);
assertContents(_associativeAndCommutative, spec);
intrusive_ptr optimized = _associativeAndCommutative->optimize();
ASSERT(_associativeAndCommutative == optimized);
assertContents(_associativeAndCommutative, BSON_ARRAY("$path" << BSON_ARRAY(55 << 66)));
}
TEST_F(ExpressionNaryTest, FlattenOptimizationNotDoneOnOtherExpressionsForAssociativeExpressions) {
BSONArray spec = BSON_ARRAY(66 << "$path" << BSON("$sum" << BSON_ARRAY("$path" << 2)));
addOperandArrayToExpr(_associativeOnly, spec);
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, spec);
}
TEST_F(ExpressionNaryTest, FlattenOptimizationNotDoneOnSameButNotAssociativeExpression) {
BSONArrayBuilder specBuilder;
intrusive_ptr innerOperand = Testable::create(false, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1" << 101));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
assertContents(_associativeOnly, spec);
}
// Test that if there is an expression of the same type in a non-commutative nor associative
// expression, the inner expression is not expanded.
// {"$testable" : [ { "$testable" : [ 100, "$path1"] }, 99, "$path2"] } is optimized to:
// {"$testable" : [ { "$testable" : [ 100, "$path1"] }, 99, "$path2"] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnNotCommutativeNorAssociative) {
BSONArrayBuilder specBuilder;
intrusive_ptr innerOperand = Testable::create(false, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1"));
specBuilder.append(expressionToBson(innerOperand));
_notAssociativeNorCommutative->addOperand(innerOperand);
addOperandArrayToExpr(_notAssociativeNorCommutative, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_notAssociativeNorCommutative, spec);
intrusive_ptr optimized = _notAssociativeNorCommutative->optimize();
ASSERT(_notAssociativeNorCommutative == optimized);
assertContents(_notAssociativeNorCommutative, spec);
}
// Test that if there is an expression of the same type as the first operand
// in a non-commutative but associative expression, the inner expression is expanded.
// Also, there shouldn't be any grouping of the operands.
// {"$testable" : [ { "$testable" : [ 100, "$path1"] }, 99, "$path2"] } is optimized to:
// {"$testable" : [ 100, "$path1", 99, "$path2"] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnAssociativeOnlyFrontOperandNoGroup) {
BSONArrayBuilder specBuilder;
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1"));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(100 << "$path1" << 99 << "$path2");
assertContents(_associativeOnly, expectedContent);
}
// Test that if there is an expression of the same type as the first operand
// in a non-commutative but associative expression, the inner expression is expanded.
// Partial collapsing optimization should be applied to the operands.
// {"$testable" : [ { "$testable" : [ 100, "$path1", 101] }, 99, "$path2"] } is optimized to:
// {"$testable" : [ 100, "$path1", [101, 99], "$path2"] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnAssociativeOnlyFrontOperandAndGroup) {
BSONArrayBuilder specBuilder;
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1" << 101));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(100 << "$path1" << BSON_ARRAY(101 << 99) << "$path2");
assertContents(_associativeOnly, expectedContent);
}
// Test that if there is an expression of the same type in the middle of the operands
// in a non-commutative but associative expression, the inner expression is expanded.
// Partial collapsing optimization should not be applied to the operands.
// {"$testable" : [ 200, "$path3", { "$testable" : [ 100, "$path1"] }, 99, "$path2"] } is
// optimized to: {"$testable" : [ 200, "$path3", 100, "$path1", 99, "$path2"] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnAssociativeOnlyMiddleOperandNoGroup) {
BSONArrayBuilder specBuilder;
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(200 << "$path3"));
specBuilder << 200 << "$path3";
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1"));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(200 << "$path3" << 100 << "$path1" << 99 << "$path2");
assertContents(_associativeOnly, expectedContent);
}
// Test that if there is an expression of the same type in the middle of the operands
// in a non-commutative but associative expression, the inner expression is expanded.
// Partial collapsing optimization should be applied to the operands.
// {"$testable" : [ 200, "$path3", 201 { "$testable" : [ 100, "$path1", 101] }, 99, "$path2"] } is
// optimized to: {"$testable" : [ 200, "$path3", [201, 100], "$path1", [101, 99], "$path2"] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnAssociativeOnlyMiddleOperandAndGroup) {
BSONArrayBuilder specBuilder;
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(200 << "$path3" << 201));
specBuilder << 200 << "$path3" << 201;
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1" << 101));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(
200 << "$path3" << BSON_ARRAY(201 << 100) << "$path1" << BSON_ARRAY(101 << 99) << "$path2");
assertContents(_associativeOnly, expectedContent);
}
// Test that if there is an expression of the same type in the back of the operands in a
// non-commutative but associative expression, the inner expression is expanded.
// Partial collapsing optimization should not be applied to the operands.
// {"$testable" : [ 200, "$path3", { "$testable" : [ 100, "$path1"] }] } is
// optimized to: {"$testable" : [ 200, "$path3", 100, "$path1"] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnAssociativeOnlyBackOperandNoGroup) {
BSONArrayBuilder specBuilder;
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(200 << "$path3"));
specBuilder << 200 << "$path3";
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1"));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(200 << "$path3" << 100 << "$path1");
assertContents(_associativeOnly, expectedContent);
}
// Test that if there is an expression of the same type in the back of the operands in a
// non-commutative but associative expression, the inner expression is expanded.
// Partial collapsing optimization should be applied to the operands.
// {"$testable" : [ 200, "$path3", 201, { "$testable" : [ 100, "$path1", 101] }] } is
// optimized to: {"$testable" : [ 200, "$path3", [201, 100], "$path1", 101] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnAssociativeOnlyBackOperandAndGroup) {
BSONArrayBuilder specBuilder;
addOperandArrayToExpr(_associativeOnly, BSON_ARRAY(200 << "$path3" << 201));
specBuilder << 200 << "$path3" << 201;
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1" << 101));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent =
BSON_ARRAY(200 << "$path3" << BSON_ARRAY(201 << 100) << "$path1" << 101);
assertContents(_associativeOnly, expectedContent);
}
// Test that if there are two consecutive inner expressions of the same type in a non-commutative
// but associative expression, both expressions are correctly flattened.
// Partial collapsing optimization should not be applied to the operands.
// {"$testable" : [ { "$testable" : [ 100, "$path1"] }, { "$testable" : [ 200, "$path2"] }] } is
// optimized to: {"$testable" : [ 100, "$path1", 200, "$path2"] }
TEST_F(ExpressionNaryTest, FlattenConsecutiveInnerOperandsOptimizationOnAssociativeOnlyNoGroup) {
BSONArrayBuilder specBuilder;
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1"));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
intrusive_ptr innerOperand2 = Testable::create(true, false);
addOperandArrayToExpr(innerOperand2, BSON_ARRAY(200 << "$path2"));
specBuilder.append(expressionToBson(innerOperand2));
_associativeOnly->addOperand(innerOperand2);
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(100 << "$path1" << 200 << "$path2");
assertContents(_associativeOnly, expectedContent);
}
// Test that if there are two consecutive inner expressions of the same type in a non-commutative
// but associative expression, both expressions are correctly flattened.
// Partial collapsing optimization should be applied to the operands.
// {"$testable" : [ { "$testable" : [ 100, "$path1", 101] }, { "$testable" : [ 200, "$path2"] }] }
// is optimized to: {"$testable" : [ 100, "$path1", [ 101, 200], "$path2"] }
TEST_F(ExpressionNaryTest, FlattenConsecutiveInnerOperandsOptimizationOnAssociativeAndGroup) {
BSONArrayBuilder specBuilder;
intrusive_ptr innerOperand = Testable::create(true, false);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1" << 101));
specBuilder.append(expressionToBson(innerOperand));
_associativeOnly->addOperand(innerOperand);
intrusive_ptr innerOperand2 = Testable::create(true, false);
addOperandArrayToExpr(innerOperand2, BSON_ARRAY(200 << "$path2"));
specBuilder.append(expressionToBson(innerOperand2));
_associativeOnly->addOperand(innerOperand2);
BSONArray spec = specBuilder.arr();
assertContents(_associativeOnly, spec);
intrusive_ptr optimized = _associativeOnly->optimize();
ASSERT(_associativeOnly == optimized);
BSONArray expectedContent = BSON_ARRAY(100 << "$path1" << BSON_ARRAY(101 << 200) << "$path2");
assertContents(_associativeOnly, expectedContent);
}
// Test that inner expressions are correctly flattened and constant operands re-arranged and
// collapsed when using a commutative and associative expression.
// {"$testable" : [ 200, "$path3", 201, { "$testable" : [ 100, "$path1", 101] }, 99, "$path2"] } is
// optimized to: {"$testable" : [ "$path3", "$path1", "$path2", [200, 201, [ 100, 101], 99] ] }
TEST_F(ExpressionNaryTest, FlattenInnerOperandsOptimizationOnCommutativeAndAssociative) {
BSONArrayBuilder specBuilder;
addOperandArrayToExpr(_associativeAndCommutative, BSON_ARRAY(200 << "$path3" << 201));
specBuilder << 200 << "$path3" << 201;
intrusive_ptr innerOperand = Testable::create(true, true);
addOperandArrayToExpr(innerOperand, BSON_ARRAY(100 << "$path1" << 101));
specBuilder.append(expressionToBson(innerOperand));
_associativeAndCommutative->addOperand(innerOperand);
addOperandArrayToExpr(_associativeAndCommutative, BSON_ARRAY(99 << "$path2"));
specBuilder << 99 << "$path2";
BSONArray spec = specBuilder.arr();
assertContents(_associativeAndCommutative, spec);
intrusive_ptr optimized = _associativeAndCommutative->optimize();
ASSERT(_associativeAndCommutative == optimized);
BSONArray expectedContent = BSON_ARRAY("$path3"
<< "$path1"
<< "$path2"
<< BSON_ARRAY(200 << 201 << BSON_ARRAY(100 << 101)
<< 99));
assertContents(_associativeAndCommutative, expectedContent);
}
/* ------------------------- ExpressionCeil -------------------------- */
class ExpressionCeilTest : public ExpressionNaryTestOneArg {
public:
virtual void assertEvaluates(Value input, Value output) override {
intrusive_ptr expCtx(new ExpressionContextForTest());
_expr = new ExpressionCeil(expCtx);
ExpressionNaryTestOneArg::assertEvaluates(input, output);
}
};
TEST_F(ExpressionCeilTest, IntArg) {
assertEvaluates(Value(0), Value(0));
assertEvaluates(Value(numeric_limits::min()), Value(numeric_limits::min()));
assertEvaluates(Value(numeric_limits::max()), Value(numeric_limits::max()));
}
TEST_F(ExpressionCeilTest, LongArg) {
assertEvaluates(Value(0LL), Value(0LL));
assertEvaluates(Value(numeric_limits::min()),
Value(numeric_limits::min()));
assertEvaluates(Value(numeric_limits::max()),
Value(numeric_limits::max()));
}
TEST_F(ExpressionCeilTest, DoubleArg) {
assertEvaluates(Value(2.0), Value(2.0));
assertEvaluates(Value(-2.0), Value(-2.0));
assertEvaluates(Value(0.9), Value(1.0));
assertEvaluates(Value(0.1), Value(1.0));
assertEvaluates(Value(-1.2), Value(-1.0));
assertEvaluates(Value(-1.7), Value(-1.0));
// Outside the range of long longs (there isn't enough precision for decimals in this range, so
// ceil should just preserve the number).
double largerThanLong = numeric_limits::max() * 2.0;
assertEvaluates(Value(largerThanLong), Value(largerThanLong));
double smallerThanLong = numeric_limits::min() * 2.0;
assertEvaluates(Value(smallerThanLong), Value(smallerThanLong));
}
TEST_F(ExpressionCeilTest, DecimalArg) {
assertEvaluates(Value(Decimal128("2")), Value(Decimal128("2.0")));
assertEvaluates(Value(Decimal128("-2")), Value(Decimal128("-2.0")));
assertEvaluates(Value(Decimal128("0.9")), Value(Decimal128("1.0")));
assertEvaluates(Value(Decimal128("0.1")), Value(Decimal128("1.0")));
assertEvaluates(Value(Decimal128("-1.2")), Value(Decimal128("-1.0")));
assertEvaluates(Value(Decimal128("-1.7")), Value(Decimal128("-1.0")));
assertEvaluates(Value(Decimal128("1234567889.000000000000000000000001")),
Value(Decimal128("1234567890")));
assertEvaluates(Value(Decimal128("-99999999999999999999999999999.99")),
Value(Decimal128("-99999999999999999999999999999.00")));
assertEvaluates(Value(Decimal128("3.4E-6000")), Value(Decimal128("1")));
}
TEST_F(ExpressionCeilTest, NullArg) {
assertEvaluates(Value(BSONNULL), Value(BSONNULL));
}
/* ------------------------- ExpressionFloor -------------------------- */
class ExpressionFloorTest : public ExpressionNaryTestOneArg {
public:
virtual void assertEvaluates(Value input, Value output) override {
intrusive_ptr expCtx(new ExpressionContextForTest());
_expr = new ExpressionFloor(expCtx);
ExpressionNaryTestOneArg::assertEvaluates(input, output);
}
};
TEST_F(ExpressionFloorTest, IntArg) {
assertEvaluates(Value(0), Value(0));
assertEvaluates(Value(numeric_limits::min()), Value(numeric_limits::min()));
assertEvaluates(Value(numeric_limits::max()), Value(numeric_limits::max()));
}
TEST_F(ExpressionFloorTest, LongArg) {
assertEvaluates(Value(0LL), Value(0LL));
assertEvaluates(Value(numeric_limits::min()),
Value(numeric_limits::min()));
assertEvaluates(Value(numeric_limits::max()),
Value(numeric_limits::max()));
}
TEST_F(ExpressionFloorTest, DoubleArg) {
assertEvaluates(Value(2.0), Value(2.0));
assertEvaluates(Value(-2.0), Value(-2.0));
assertEvaluates(Value(0.9), Value(0.0));
assertEvaluates(Value(0.1), Value(0.0));
assertEvaluates(Value(-1.2), Value(-2.0));
assertEvaluates(Value(-1.7), Value(-2.0));
// Outside the range of long longs (there isn't enough precision for decimals in this range, so
// floor should just preserve the number).
double largerThanLong = numeric_limits::max() * 2.0;
assertEvaluates(Value(largerThanLong), Value(largerThanLong));
double smallerThanLong = numeric_limits::min() * 2.0;
assertEvaluates(Value(smallerThanLong), Value(smallerThanLong));
}
TEST_F(ExpressionFloorTest, DecimalArg) {
assertEvaluates(Value(Decimal128("2")), Value(Decimal128("2.0")));
assertEvaluates(Value(Decimal128("-2")), Value(Decimal128("-2.0")));
assertEvaluates(Value(Decimal128("0.9")), Value(Decimal128("0.0")));
assertEvaluates(Value(Decimal128("0.1")), Value(Decimal128("0.0")));
assertEvaluates(Value(Decimal128("-1.2")), Value(Decimal128("-2.0")));
assertEvaluates(Value(Decimal128("-1.7")), Value(Decimal128("-2.0")));
assertEvaluates(Value(Decimal128("1234567890.000000000000000000000001")),
Value(Decimal128("1234567890")));
assertEvaluates(Value(Decimal128("-99999999999999999999999999999.99")),
Value(Decimal128("-100000000000000000000000000000")));
assertEvaluates(Value(Decimal128("3.4E-6000")), Value(Decimal128("0")));
}
TEST_F(ExpressionFloorTest, NullArg) {
assertEvaluates(Value(BSONNULL), Value(BSONNULL));
}
/* ------------------------ ExpressionRange --------------------------- */
TEST(ExpressionRangeTest, ComputesStandardRange) {
assertExpectedResults("$range", {{{Value(0), Value(3)}, Value(BSON_ARRAY(0 << 1 << 2))}});
}
TEST(ExpressionRangeTest, ComputesRangeWithStep) {
assertExpectedResults("$range",
{{{Value(0), Value(6), Value(2)}, Value(BSON_ARRAY(0 << 2 << 4))}});
}
TEST(ExpressionRangeTest, ComputesReverseRange) {
assertExpectedResults("$range",
{{{Value(0), Value(-3), Value(-1)}, Value(BSON_ARRAY(0 << -1 << -2))}});
}
TEST(ExpressionRangeTest, ComputesRangeWithPositiveAndNegative) {
assertExpectedResults("$range",
{{{Value(-2), Value(3)}, Value(BSON_ARRAY(-2 << -1 << 0 << 1 << 2))}});
}
TEST(ExpressionRangeTest, ComputesEmptyRange) {
assertExpectedResults("$range",
{{{Value(-2), Value(3), Value(-1)}, Value(std::vector())}});
}
TEST(ExpressionRangeTest, ComputesRangeWithSameStartAndEnd) {
assertExpectedResults("$range", {{{Value(20), Value(20)}, Value(std::vector())}});
}
TEST(ExpressionRangeTest, ComputesRangeWithLargeNegativeStep) {
assertExpectedResults("$range",
{{{Value(3), Value(-5), Value(-3)}, Value(BSON_ARRAY(3 << 0 << -3))}});
}
/* ------------------------ ExpressionReverseArray -------------------- */
TEST(ExpressionReverseArrayTest, ReversesNormalArray) {
assertExpectedResults("$reverseArray",
{{{Value(BSON_ARRAY(1 << 2 << 3))}, Value(BSON_ARRAY(3 << 2 << 1))}});
}
TEST(ExpressionReverseArrayTest, ReversesEmptyArray) {
assertExpectedResults("$reverseArray",
{{{Value(std::vector())}, Value(std::vector())}});
}
TEST(ExpressionReverseArrayTest, ReversesOneElementArray) {
assertExpectedResults("$reverseArray", {{{Value(BSON_ARRAY(1))}, Value(BSON_ARRAY(1))}});
}
TEST(ExpressionReverseArrayTest, ReturnsNullWithNullishInput) {
assertExpectedResults(
"$reverseArray",
{{{Value(BSONNULL)}, Value(BSONNULL)}, {{Value(BSONUndefined)}, Value(BSONNULL)}});
}
/* ------------------------- ExpressionTrunc -------------------------- */
class ExpressionTruncTest : public ExpressionNaryTestOneArg {
public:
virtual void assertEvaluates(Value input, Value output) override {
intrusive_ptr expCtx(new ExpressionContextForTest());
_expr = new ExpressionTrunc(expCtx);
ExpressionNaryTestOneArg::assertEvaluates(input, output);
}
};
TEST_F(ExpressionTruncTest, IntArg) {
assertEvaluates(Value(0), Value(0));
assertEvaluates(Value(numeric_limits::min()), Value(numeric_limits::min()));
assertEvaluates(Value(numeric_limits::max()), Value(numeric_limits::max()));
}
TEST_F(ExpressionTruncTest, LongArg) {
assertEvaluates(Value(0LL), Value(0LL));
assertEvaluates(Value(numeric_limits::min()),
Value(numeric_limits::min()));
assertEvaluates(Value(numeric_limits::max()),
Value(numeric_limits::max()));
}
TEST_F(ExpressionTruncTest, DoubleArg) {
assertEvaluates(Value(2.0), Value(2.0));
assertEvaluates(Value(-2.0), Value(-2.0));
assertEvaluates(Value(0.9), Value(0.0));
assertEvaluates(Value(0.1), Value(0.0));
assertEvaluates(Value(-1.2), Value(-1.0));
assertEvaluates(Value(-1.7), Value(-1.0));
// Outside the range of long longs (there isn't enough precision for decimals in this range, so
// should just preserve the number).
double largerThanLong = numeric_limits::max() * 2.0;
assertEvaluates(Value(largerThanLong), Value(largerThanLong));
double smallerThanLong = numeric_limits::min() * 2.0;
assertEvaluates(Value(smallerThanLong), Value(smallerThanLong));
}
TEST_F(ExpressionTruncTest, DecimalArg) {
assertEvaluates(Value(Decimal128("2")), Value(Decimal128("2.0")));
assertEvaluates(Value(Decimal128("-2")), Value(Decimal128("-2.0")));
assertEvaluates(Value(Decimal128("0.9")), Value(Decimal128("0.0")));
assertEvaluates(Value(Decimal128("0.1")), Value(Decimal128("0.0")));
assertEvaluates(Value(Decimal128("-1.2")), Value(Decimal128("-1.0")));
assertEvaluates(Value(Decimal128("-1.7")), Value(Decimal128("-1.0")));
assertEvaluates(Value(Decimal128("123456789.9999999999999999999999999")),
Value(Decimal128("123456789")));
assertEvaluates(Value(Decimal128("-99999999999999999999999999999.99")),
Value(Decimal128("-99999999999999999999999999999.00")));
assertEvaluates(Value(Decimal128("3.4E-6000")), Value(Decimal128("0")));
}
TEST_F(ExpressionTruncTest, NullArg) {
assertEvaluates(Value(BSONNULL), Value(BSONNULL));
}
/* ------------------------- Old-style tests -------------------------- */
namespace Add {
class ExpectedResultBase {
public:
virtual ~ExpectedResultBase() {}
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = new ExpressionAdd(expCtx);
populateOperands(expression);
ASSERT_BSONOBJ_EQ(expectedResult(), toBson(expression->evaluate(Document())));
}
protected:
virtual void populateOperands(intrusive_ptr& expression) = 0;
virtual BSONObj expectedResult() = 0;
};
/** $add with a NULL Document pointer, as called by ExpressionNary::optimize().
*/
class NullDocument {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = new ExpressionAdd(expCtx);
expression->addOperand(ExpressionConstant::create(expCtx, Value(2)));
ASSERT_BSONOBJ_EQ(BSON("" << 2), toBson(expression->evaluate(Document())));
}
};
/** $add without operands. */
class NoOperands : public ExpectedResultBase {
void populateOperands(intrusive_ptr& expression) {}
virtual BSONObj expectedResult() {
return BSON("" << 0);
}
};
/** String type unsupported. */
class String {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = new ExpressionAdd(expCtx);
expression->addOperand(ExpressionConstant::create(expCtx, Value("a"_sd)));
ASSERT_THROWS(expression->evaluate(Document()), AssertionException);
}
};
/** Bool type unsupported. */
class Bool {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = new ExpressionAdd(expCtx);
expression->addOperand(ExpressionConstant::create(expCtx, Value(true)));
ASSERT_THROWS(expression->evaluate(Document()), AssertionException);
}
};
class SingleOperandBase : public ExpectedResultBase {
void populateOperands(intrusive_ptr& expression) {
intrusive_ptr expCtx(new ExpressionContextForTest());
expression->addOperand(ExpressionConstant::create(expCtx, valueFromBson(operand())));
}
BSONObj expectedResult() {
return operand();
}
protected:
virtual BSONObj operand() = 0;
};
/** Single int argument. */
class Int : public SingleOperandBase {
BSONObj operand() {
return BSON("" << 1);
}
};
/** Single long argument. */
class Long : public SingleOperandBase {
BSONObj operand() {
return BSON("" << 5555LL);
}
};
/** Single double argument. */
class Double : public SingleOperandBase {
BSONObj operand() {
return BSON("" << 99.99);
}
};
/** Single Date argument. */
class Date : public SingleOperandBase {
BSONObj operand() {
return BSON("" << Date_t::fromMillisSinceEpoch(12345));
}
};
/** Single null argument. */
class Null : public SingleOperandBase {
BSONObj operand() {
return BSON("" << BSONNULL);
}
BSONObj expectedResult() {
return BSON("" << BSONNULL);
}
};
/** Single undefined argument. */
class Undefined : public SingleOperandBase {
BSONObj operand() {
return fromjson("{'':undefined}");
}
BSONObj expectedResult() {
return BSON("" << BSONNULL);
}
};
class TwoOperandBase : public ExpectedResultBase {
public:
TwoOperandBase() : _reverse() {}
void run() {
ExpectedResultBase::run();
// Now add the operands in the reverse direction.
_reverse = true;
ExpectedResultBase::run();
}
protected:
void populateOperands(intrusive_ptr& expression) {
intrusive_ptr expCtx(new ExpressionContextForTest());
expression->addOperand(
ExpressionConstant::create(expCtx, valueFromBson(_reverse ? operand2() : operand1())));
expression->addOperand(
ExpressionConstant::create(expCtx, valueFromBson(_reverse ? operand1() : operand2())));
}
virtual BSONObj operand1() = 0;
virtual BSONObj operand2() = 0;
private:
bool _reverse;
};
/** Add two ints. */
class IntInt : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 1);
}
BSONObj operand2() {
return BSON("" << 5);
}
BSONObj expectedResult() {
return BSON("" << 6);
}
};
/** Adding two large ints produces a long, not an overflowed int. */
class IntIntNoOverflow : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << numeric_limits::max());
}
BSONObj operand2() {
return BSON("" << numeric_limits::max());
}
BSONObj expectedResult() {
return BSON("" << ((long long)(numeric_limits::max()) + numeric_limits::max()));
}
};
/** Adding an int and a long produces a long. */
class IntLong : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 1);
}
BSONObj operand2() {
return BSON("" << 9LL);
}
BSONObj expectedResult() {
return BSON("" << 10LL);
}
};
/** Adding an int and a long produces a double. */
class IntLongOverflowToDouble : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << numeric_limits::max());
}
BSONObj operand2() {
return BSON("" << numeric_limits::max());
}
BSONObj expectedResult() {
// When the result cannot be represented in a NumberLong, a NumberDouble is returned.
const auto im = numeric_limits::max();
const auto llm = numeric_limits::max();
double result = static_cast(im) + static_cast(llm);
return BSON("" << result);
}
};
/** Adding an int and a double produces a double. */
class IntDouble : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 9);
}
BSONObj operand2() {
return BSON("" << 1.1);
}
BSONObj expectedResult() {
return BSON("" << 10.1);
}
};
/** Adding an int and a Date produces a Date. */
class IntDate : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 6);
}
BSONObj operand2() {
return BSON("" << Date_t::fromMillisSinceEpoch(123450));
}
BSONObj expectedResult() {
return BSON("" << Date_t::fromMillisSinceEpoch(123456));
}
};
/** Adding a long and a double produces a double. */
class LongDouble : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 9LL);
}
BSONObj operand2() {
return BSON("" << 1.1);
}
BSONObj expectedResult() {
return BSON("" << 10.1);
}
};
/** Adding a long and a double does not overflow. */
class LongDoubleNoOverflow : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << numeric_limits::max());
}
BSONObj operand2() {
return BSON("" << double(numeric_limits::max()));
}
BSONObj expectedResult() {
return BSON("" << numeric_limits::max() +
double(numeric_limits::max()));
}
};
/** Adding an int and null. */
class IntNull : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 1);
}
BSONObj operand2() {
return BSON("" << BSONNULL);
}
BSONObj expectedResult() {
return BSON("" << BSONNULL);
}
};
/** Adding a long and undefined. */
class LongUndefined : public TwoOperandBase {
BSONObj operand1() {
return BSON("" << 5LL);
}
BSONObj operand2() {
return fromjson("{'':undefined}");
}
BSONObj expectedResult() {
return BSON("" << BSONNULL);
}
};
} // namespace Add
namespace And {
class ExpectedResultBase {
public:
virtual ~ExpectedResultBase() {}
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
BSONObj specObject = BSON("" << spec());
BSONElement specElement = specObject.firstElement();
VariablesParseState vps = expCtx->variablesParseState;
intrusive_ptr expression = Expression::parseOperand(expCtx, specElement, vps);
ASSERT_BSONOBJ_EQ(constify(spec()), expressionToBson(expression));
ASSERT_BSONOBJ_EQ(BSON("" << expectedResult()),
toBson(expression->evaluate(fromBson(BSON("a" << 1)))));
intrusive_ptr optimized = expression->optimize();
ASSERT_BSONOBJ_EQ(BSON("" << expectedResult()),
toBson(optimized->evaluate(fromBson(BSON("a" << 1)))));
}
protected:
virtual BSONObj spec() = 0;
virtual bool expectedResult() = 0;
};
class OptimizeBase {
public:
virtual ~OptimizeBase() {}
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
BSONObj specObject = BSON("" << spec());
BSONElement specElement = specObject.firstElement();
VariablesParseState vps = expCtx->variablesParseState;
intrusive_ptr expression = Expression::parseOperand(expCtx, specElement, vps);
ASSERT_BSONOBJ_EQ(constify(spec()), expressionToBson(expression));
intrusive_ptr optimized = expression->optimize();
ASSERT_BSONOBJ_EQ(expectedOptimized(), expressionToBson(optimized));
}
protected:
virtual BSONObj spec() = 0;
virtual BSONObj expectedOptimized() = 0;
};
class NoOptimizeBase : public OptimizeBase {
BSONObj expectedOptimized() {
return constify(spec());
}
};
/** $and without operands. */
class NoOperands : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSONArray());
}
bool expectedResult() {
return true;
}
};
/** $and passed 'true'. */
class True : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(true));
}
bool expectedResult() {
return true;
}
};
/** $and passed 'false'. */
class False : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(false));
}
bool expectedResult() {
return false;
}
};
/** $and passed 'true', 'true'. */
class TrueTrue : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(true << true));
}
bool expectedResult() {
return true;
}
};
/** $and passed 'true', 'false'. */
class TrueFalse : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(true << false));
}
bool expectedResult() {
return false;
}
};
/** $and passed 'false', 'true'. */
class FalseTrue : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(false << true));
}
bool expectedResult() {
return false;
}
};
/** $and passed 'false', 'false'. */
class FalseFalse : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(false << false));
}
bool expectedResult() {
return false;
}
};
/** $and passed 'true', 'true', 'true'. */
class TrueTrueTrue : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(true << true << true));
}
bool expectedResult() {
return true;
}
};
/** $and passed 'true', 'true', 'false'. */
class TrueTrueFalse : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(true << true << false));
}
bool expectedResult() {
return false;
}
};
/** $and passed '0', '1'. */
class ZeroOne : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(0 << 1));
}
bool expectedResult() {
return false;
}
};
/** $and passed '1', '2'. */
class OneTwo : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(1 << 2));
}
bool expectedResult() {
return true;
}
};
/** $and passed a field path. */
class FieldPath : public ExpectedResultBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY("$a"));
}
bool expectedResult() {
return true;
}
};
/** A constant expression is optimized to a constant. */
class OptimizeConstantExpression : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(1));
}
BSONObj expectedOptimized() {
return BSON("$const" << true);
}
};
/** A non constant expression is not optimized. */
class NonConstant : public NoOptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY("$a"));
}
};
/** An expression beginning with a single constant is optimized. */
class ConstantNonConstantTrue : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(1 << "$a"));
}
BSONObj expectedOptimized() {
return BSON("$and" << BSON_ARRAY("$a"));
}
// note: using $and as serialization of ExpressionCoerceToBool rather than
// ExpressionAnd
};
class ConstantNonConstantFalse : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(0 << "$a"));
}
BSONObj expectedOptimized() {
return BSON("$const" << false);
}
};
/** An expression with a field path and '1'. */
class NonConstantOne : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY("$a" << 1));
}
BSONObj expectedOptimized() {
return BSON("$and" << BSON_ARRAY("$a"));
}
};
/** An expression with a field path and '0'. */
class NonConstantZero : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY("$a" << 0));
}
BSONObj expectedOptimized() {
return BSON("$const" << false);
}
};
/** An expression with two field paths and '1'. */
class NonConstantNonConstantOne : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY("$a"
<< "$b"
<< 1));
}
BSONObj expectedOptimized() {
return BSON("$and" << BSON_ARRAY("$a"
<< "$b"));
}
};
/** An expression with two field paths and '0'. */
class NonConstantNonConstantZero : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY("$a"
<< "$b"
<< 0));
}
BSONObj expectedOptimized() {
return BSON("$const" << false);
}
};
/** An expression with '0', '1', and a field path. */
class ZeroOneNonConstant : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(0 << 1 << "$a"));
}
BSONObj expectedOptimized() {
return BSON("$const" << false);
}
};
/** An expression with '1', '1', and a field path. */
class OneOneNonConstant : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(1 << 1 << "$a"));
}
BSONObj expectedOptimized() {
return BSON("$and" << BSON_ARRAY("$a"));
}
};
/** Nested $and expressions. */
class Nested : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(1 << BSON("$and" << BSON_ARRAY(1)) << "$a"
<< "$b"));
}
BSONObj expectedOptimized() {
return BSON("$and" << BSON_ARRAY("$a"
<< "$b"));
}
};
/** Nested $and expressions containing a nested value evaluating to false. */
class NestedZero : public OptimizeBase {
BSONObj spec() {
return BSON("$and" << BSON_ARRAY(
1 << BSON("$and" << BSON_ARRAY(BSON("$and" << BSON_ARRAY(0)))) << "$a"
<< "$b"));
}
BSONObj expectedOptimized() {
return BSON("$const" << false);
}
};
} // namespace And
namespace CoerceToBool {
/** Nested expression coerced to true. */
class EvaluateTrue {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr nested = ExpressionConstant::create(expCtx, Value(5));
intrusive_ptr expression = ExpressionCoerceToBool::create(expCtx, nested);
ASSERT(expression->evaluate(Document()).getBool());
}
};
/** Nested expression coerced to false. */
class EvaluateFalse {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr nested = ExpressionConstant::create(expCtx, Value(0));
intrusive_ptr expression = ExpressionCoerceToBool::create(expCtx, nested);
ASSERT(!expression->evaluate(Document()).getBool());
}
};
/** Dependencies forwarded from nested expression. */
class Dependencies {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr nested = ExpressionFieldPath::create(expCtx, "a.b");
intrusive_ptr expression = ExpressionCoerceToBool::create(expCtx, nested);
DepsTracker dependencies;
expression->addDependencies(&dependencies);
ASSERT_EQUALS(1U, dependencies.fields.size());
ASSERT_EQUALS(1U, dependencies.fields.count("a.b"));
ASSERT_EQUALS(false, dependencies.needWholeDocument);
ASSERT_EQUALS(false, dependencies.getNeedTextScore());
}
};
/** Output to BSONObj. */
class AddToBsonObj {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression =
ExpressionCoerceToBool::create(expCtx, ExpressionFieldPath::create(expCtx, "foo"));
// serialized as $and because CoerceToBool isn't an ExpressionNary
assertBinaryEqual(fromjson("{field:{$and:['$foo']}}"), toBsonObj(expression));
}
private:
static BSONObj toBsonObj(const intrusive_ptr& expression) {
return BSON("field" << expression->serialize(false));
}
};
/** Output to BSONArray. */
class AddToBsonArray {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression =
ExpressionCoerceToBool::create(expCtx, ExpressionFieldPath::create(expCtx, "foo"));
// serialized as $and because CoerceToBool isn't an ExpressionNary
assertBinaryEqual(BSON_ARRAY(fromjson("{$and:['$foo']}")), toBsonArray(expression));
}
private:
static BSONArray toBsonArray(const intrusive_ptr& expression) {
BSONArrayBuilder bab;
bab << expression->serialize(false);
return bab.arr();
}
};
// TODO Test optimize(), difficult because a CoerceToBool cannot be output as
// BSON.
} // namespace CoerceToBool
namespace Compare {
class OptimizeBase {
public:
virtual ~OptimizeBase() {}
void run() {
BSONObj specObject = BSON("" << spec());
BSONElement specElement = specObject.firstElement();
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
intrusive_ptr expression = Expression::parseOperand(expCtx, specElement, vps);
intrusive_ptr optimized = expression->optimize();
ASSERT_BSONOBJ_EQ(constify(expectedOptimized()), expressionToBson(optimized));
}
protected:
virtual BSONObj spec() = 0;
virtual BSONObj expectedOptimized() = 0;
};
class FieldRangeOptimize : public OptimizeBase {
BSONObj expectedOptimized() {
return spec();
}
};
class NoOptimize : public OptimizeBase {
BSONObj expectedOptimized() {
return spec();
}
};
/** Check expected result for expressions depending on constants. */
class ExpectedResultBase : public OptimizeBase {
public:
void run() {
OptimizeBase::run();
BSONObj specObject = BSON("" << spec());
BSONElement specElement = specObject.firstElement();
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
intrusive_ptr expression = Expression::parseOperand(expCtx, specElement, vps);
// Check expression spec round trip.
ASSERT_BSONOBJ_EQ(constify(spec()), expressionToBson(expression));
// Check evaluation result.
ASSERT_BSONOBJ_EQ(expectedResult(), toBson(expression->evaluate(Document())));
// Check that the result is the same after optimizing.
intrusive_ptr optimized = expression->optimize();
ASSERT_BSONOBJ_EQ(expectedResult(), toBson(optimized->evaluate(Document())));
}
protected:
virtual BSONObj spec() = 0;
virtual BSONObj expectedResult() = 0;
private:
virtual BSONObj expectedOptimized() {
return BSON("$const" << expectedResult().firstElement());
}
};
class ExpectedTrue : public ExpectedResultBase {
BSONObj expectedResult() {
return BSON("" << true);
}
};
class ExpectedFalse : public ExpectedResultBase {
BSONObj expectedResult() {
return BSON("" << false);
}
};
class ParseError {
public:
virtual ~ParseError() {}
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
BSONObj specObject = BSON("" << spec());
BSONElement specElement = specObject.firstElement();
VariablesParseState vps = expCtx->variablesParseState;
ASSERT_THROWS(Expression::parseOperand(expCtx, specElement, vps), AssertionException);
}
protected:
virtual BSONObj spec() = 0;
};
/** $eq with first < second. */
class EqLt : public ExpectedFalse {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1 << 2));
}
};
/** $eq with first == second. */
class EqEq : public ExpectedTrue {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1 << 1));
}
};
/** $eq with first > second. */
class EqGt : public ExpectedFalse {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1 << 0));
}
};
/** $ne with first < second. */
class NeLt : public ExpectedTrue {
BSONObj spec() {
return BSON("$ne" << BSON_ARRAY(1 << 2));
}
};
/** $ne with first == second. */
class NeEq : public ExpectedFalse {
BSONObj spec() {
return BSON("$ne" << BSON_ARRAY(1 << 1));
}
};
/** $ne with first > second. */
class NeGt : public ExpectedTrue {
BSONObj spec() {
return BSON("$ne" << BSON_ARRAY(1 << 0));
}
};
/** $gt with first < second. */
class GtLt : public ExpectedFalse {
BSONObj spec() {
return BSON("$gt" << BSON_ARRAY(1 << 2));
}
};
/** $gt with first == second. */
class GtEq : public ExpectedFalse {
BSONObj spec() {
return BSON("$gt" << BSON_ARRAY(1 << 1));
}
};
/** $gt with first > second. */
class GtGt : public ExpectedTrue {
BSONObj spec() {
return BSON("$gt" << BSON_ARRAY(1 << 0));
}
};
/** $gte with first < second. */
class GteLt : public ExpectedFalse {
BSONObj spec() {
return BSON("$gte" << BSON_ARRAY(1 << 2));
}
};
/** $gte with first == second. */
class GteEq : public ExpectedTrue {
BSONObj spec() {
return BSON("$gte" << BSON_ARRAY(1 << 1));
}
};
/** $gte with first > second. */
class GteGt : public ExpectedTrue {
BSONObj spec() {
return BSON("$gte" << BSON_ARRAY(1 << 0));
}
};
/** $lt with first < second. */
class LtLt : public ExpectedTrue {
BSONObj spec() {
return BSON("$lt" << BSON_ARRAY(1 << 2));
}
};
/** $lt with first == second. */
class LtEq : public ExpectedFalse {
BSONObj spec() {
return BSON("$lt" << BSON_ARRAY(1 << 1));
}
};
/** $lt with first > second. */
class LtGt : public ExpectedFalse {
BSONObj spec() {
return BSON("$lt" << BSON_ARRAY(1 << 0));
}
};
/** $lte with first < second. */
class LteLt : public ExpectedTrue {
BSONObj spec() {
return BSON("$lte" << BSON_ARRAY(1 << 2));
}
};
/** $lte with first == second. */
class LteEq : public ExpectedTrue {
BSONObj spec() {
return BSON("$lte" << BSON_ARRAY(1 << 1));
}
};
/** $lte with first > second. */
class LteGt : public ExpectedFalse {
BSONObj spec() {
return BSON("$lte" << BSON_ARRAY(1 << 0));
}
};
/** $cmp with first < second. */
class CmpLt : public ExpectedResultBase {
BSONObj spec() {
return BSON("$cmp" << BSON_ARRAY(1 << 2));
}
BSONObj expectedResult() {
return BSON("" << -1);
}
};
/** $cmp with first == second. */
class CmpEq : public ExpectedResultBase {
BSONObj spec() {
return BSON("$cmp" << BSON_ARRAY(1 << 1));
}
BSONObj expectedResult() {
return BSON("" << 0);
}
};
/** $cmp with first > second. */
class CmpGt : public ExpectedResultBase {
BSONObj spec() {
return BSON("$cmp" << BSON_ARRAY(1 << 0));
}
BSONObj expectedResult() {
return BSON("" << 1);
}
};
/** $cmp results are bracketed to an absolute value of 1. */
class CmpBracketed : public ExpectedResultBase {
BSONObj spec() {
return BSON("$cmp" << BSON_ARRAY("z"
<< "a"));
}
BSONObj expectedResult() {
return BSON("" << 1);
}
};
/** Zero operands provided. */
class ZeroOperands : public ParseError {
BSONObj spec() {
return BSON("$ne" << BSONArray());
}
};
/** One operand provided. */
class OneOperand : public ParseError {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1));
}
};
/** Three operands provided. */
class ThreeOperands : public ParseError {
BSONObj spec() {
return BSON("$gt" << BSON_ARRAY(2 << 3 << 4));
}
};
/** Incompatible types can be compared. */
class IncompatibleTypes {
public:
void run() {
BSONObj specObject = BSON("" << BSON("$ne" << BSON_ARRAY("a" << 1)));
BSONElement specElement = specObject.firstElement();
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
intrusive_ptr expression = Expression::parseOperand(expCtx, specElement, vps);
ASSERT_VALUE_EQ(expression->evaluate(Document()), Value(true));
}
};
/**
* An expression depending on constants is optimized to a constant via
* ExpressionNary::optimize().
*/
class OptimizeConstants : public OptimizeBase {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1 << 1));
}
BSONObj expectedOptimized() {
return BSON("$const" << true);
}
};
/** $cmp is not optimized. */
class NoOptimizeCmp : public NoOptimize {
BSONObj spec() {
return BSON("$cmp" << BSON_ARRAY(1 << "$a"));
}
};
/** $ne is not optimized. */
class NoOptimizeNe : public NoOptimize {
BSONObj spec() {
return BSON("$ne" << BSON_ARRAY(1 << "$a"));
}
};
/** No optimization is performend without a constant. */
class NoOptimizeNoConstant : public NoOptimize {
BSONObj spec() {
return BSON("$ne" << BSON_ARRAY("$a"
<< "$b"));
}
};
/** No optimization is performend without an immediate field path. */
class NoOptimizeWithoutFieldPath : public NoOptimize {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(BSON("$and" << BSON_ARRAY("$a")) << 1));
}
};
/** No optimization is performend without an immediate field path. */
class NoOptimizeWithoutFieldPathReverse : public NoOptimize {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1 << BSON("$and" << BSON_ARRAY("$a"))));
}
};
/** An equality expression is optimized. */
class OptimizeEq : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY("$a" << 1));
}
};
/** A reverse sense equality expression is optimized. */
class OptimizeEqReverse : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$eq" << BSON_ARRAY(1 << "$a"));
}
};
/** A $lt expression is optimized. */
class OptimizeLt : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$lt" << BSON_ARRAY("$a" << 1));
}
};
/** A reverse sense $lt expression is optimized. */
class OptimizeLtReverse : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$lt" << BSON_ARRAY(1 << "$a"));
}
};
/** A $lte expression is optimized. */
class OptimizeLte : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$lte" << BSON_ARRAY("$b" << 2));
}
};
/** A reverse sense $lte expression is optimized. */
class OptimizeLteReverse : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$lte" << BSON_ARRAY(2 << "$b"));
}
};
/** A $gt expression is optimized. */
class OptimizeGt : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$gt" << BSON_ARRAY("$b" << 2));
}
};
/** A reverse sense $gt expression is optimized. */
class OptimizeGtReverse : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$gt" << BSON_ARRAY(2 << "$b"));
}
};
/** A $gte expression is optimized. */
class OptimizeGte : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$gte" << BSON_ARRAY("$b" << 2));
}
};
/** A reverse sense $gte expression is optimized. */
class OptimizeGteReverse : public FieldRangeOptimize {
BSONObj spec() {
return BSON("$gte" << BSON_ARRAY(2 << "$b"));
}
};
} // namespace Compare
namespace Constant {
/** Create an ExpressionConstant from a Value. */
class Create {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value(5));
assertBinaryEqual(BSON("" << 5), toBson(expression->evaluate(Document())));
}
};
/** Create an ExpressionConstant from a BsonElement. */
class CreateFromBsonElement {
public:
void run() {
BSONObj spec = BSON("IGNORED_FIELD_NAME"
<< "foo");
intrusive_ptr expCtx(new ExpressionContextForTest());
BSONElement specElement = spec.firstElement();
VariablesParseState vps = expCtx->variablesParseState;
intrusive_ptr expression = ExpressionConstant::parse(expCtx, specElement, vps);
assertBinaryEqual(BSON(""
<< "foo"),
toBson(expression->evaluate(Document())));
}
};
/** No optimization is performed. */
class Optimize {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value(5));
// An attempt to optimize returns the Expression itself.
ASSERT_EQUALS(expression, expression->optimize());
}
};
/** No dependencies. */
class Dependencies {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value(5));
DepsTracker dependencies;
expression->addDependencies(&dependencies);
ASSERT_EQUALS(0U, dependencies.fields.size());
ASSERT_EQUALS(false, dependencies.needWholeDocument);
ASSERT_EQUALS(false, dependencies.getNeedTextScore());
}
};
/** Output to BSONObj. */
class AddToBsonObj {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value(5));
// The constant is replaced with a $ expression.
assertBinaryEqual(BSON("field" << BSON("$const" << 5)), toBsonObj(expression));
}
private:
static BSONObj toBsonObj(const intrusive_ptr& expression) {
return BSON("field" << expression->serialize(false));
}
};
/** Output to BSONArray. */
class AddToBsonArray {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value(5));
// The constant is copied out as is.
assertBinaryEqual(constify(BSON_ARRAY(5)), toBsonArray(expression));
}
private:
static BSONObj toBsonArray(const intrusive_ptr& expression) {
BSONArrayBuilder bab;
bab << expression->serialize(false);
return bab.obj();
}
};
TEST(ExpressionConstantTest, ConstantOfValueMissingRemovesField) {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value());
assertBinaryEqual(BSONObj(), toBson(expression->evaluate(Document{{"foo", Value("bar"_sd)}})));
}
TEST(ExpressionConstantTest, ConstantOfValueMissingSerializesToRemoveSystemVar) {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionConstant::create(expCtx, Value());
assertBinaryEqual(BSON("field"
<< "$$REMOVE"),
BSON("field" << expression->serialize(false)));
}
} // namespace Constant
TEST(ExpressionFromAccumulators, Avg) {
assertExpectedResults("$avg",
{// $avg ignores non-numeric inputs.
{{Value("string"_sd), Value(BSONNULL), Value(), Value(3)}, Value(3.0)},
// $avg always returns a double.
{{Value(10LL), Value(20LL)}, Value(15.0)},
// $avg returns null when no arguments are provided.
{{}, Value(BSONNULL)}});
}
TEST(ExpressionFromAccumulators, Max) {
assertExpectedResults("$max",
{// $max treats non-numeric inputs as valid arguments.
{{Value(1), Value(BSONNULL), Value(), Value("a"_sd)}, Value("a"_sd)},
{{Value("a"_sd), Value("b"_sd)}, Value("b"_sd)},
// $max always preserves the type of the result.
{{Value(10LL), Value(0.0), Value(5)}, Value(10LL)},
// $max returns null when no arguments are provided.
{{}, Value(BSONNULL)}});
}
TEST(ExpressionFromAccumulators, Min) {
assertExpectedResults("$min",
{// $min treats non-numeric inputs as valid arguments.
{{Value("string"_sd)}, Value("string"_sd)},
{{Value(1), Value(BSONNULL), Value(), Value("a"_sd)}, Value(1)},
{{Value("a"_sd), Value("b"_sd)}, Value("a"_sd)},
// $min always preserves the type of the result.
{{Value(0LL), Value(20.0), Value(10)}, Value(0LL)},
// $min returns null when no arguments are provided.
{{}, Value(BSONNULL)}});
}
TEST(ExpressionFromAccumulators, Sum) {
assertExpectedResults(
"$sum",
{// $sum ignores non-numeric inputs.
{{Value("string"_sd), Value(BSONNULL), Value(), Value(3)}, Value(3)},
// If any argument is a double, $sum returns a double
{{Value(10LL), Value(10.0)}, Value(20.0)},
// If no arguments are doubles and an argument is a long, $sum returns a long
{{Value(10LL), Value(10)}, Value(20LL)},
// $sum returns 0 when no arguments are provided.
{{}, Value(0)}});
}
TEST(ExpressionFromAccumulators, StdDevPop) {
assertExpectedResults("$stdDevPop",
{// $stdDevPop ignores non-numeric inputs.
{{Value("string"_sd), Value(BSONNULL), Value(), Value(3)}, Value(0.0)},
// $stdDevPop always returns a double.
{{Value(1LL), Value(3LL)}, Value(1.0)},
// $stdDevPop returns null when no arguments are provided.
{{}, Value(BSONNULL)}});
}
TEST(ExpressionFromAccumulators, StdDevSamp) {
assertExpectedResults(
"$stdDevSamp",
{// $stdDevSamp ignores non-numeric inputs.
{{Value("string"_sd), Value(BSONNULL), Value(), Value(3)}, Value(BSONNULL)},
// $stdDevSamp always returns a double.
{{Value(1LL), Value(2LL), Value(3LL)}, Value(1.0)},
// $stdDevSamp returns null when no arguments are provided.
{{}, Value(BSONNULL)}});
}
TEST(ExpressionPowTest, NegativeOneRaisedToNegativeOddExponentShouldOutPutNegativeOne) {
assertExpectedResults("$pow",
{
{{Value(-1), Value(-1)}, Value(-1)},
{{Value(-1), Value(-2)}, Value(1)},
{{Value(-1), Value(-3)}, Value(-1)},
{{Value(-1LL), Value(0LL)}, Value(1LL)},
{{Value(-1LL), Value(-1LL)}, Value(-1LL)},
{{Value(-1LL), Value(-2LL)}, Value(1LL)},
{{Value(-1LL), Value(-3LL)}, Value(-1LL)},
{{Value(-1LL), Value(-4LL)}, Value(1LL)},
{{Value(-1LL), Value(-5LL)}, Value(-1LL)},
});
}
namespace FieldPath {
/** The provided field path does not pass validation. */
class Invalid {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
ASSERT_THROWS(ExpressionFieldPath::create(expCtx, ""), AssertionException);
}
};
TEST(FieldPath, NoOptimizationForRootFieldPathWithDottedPath) {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression =
ExpressionFieldPath::parse(expCtx, "$$ROOT.x.y", expCtx->variablesParseState);
// An attempt to optimize returns the Expression itself.
ASSERT_EQUALS(expression, expression->optimize());
}
TEST(FieldPath, NoOptimizationForCurrentFieldPathWithDottedPath) {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression =
ExpressionFieldPath::parse(expCtx, "$$CURRENT.x.y", expCtx->variablesParseState);
// An attempt to optimize returns the Expression itself.
ASSERT_EQUALS(expression, expression->optimize());
}
TEST(FieldPath, RemoveOptimizesToMissingValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression =
ExpressionFieldPath::parse(expCtx, "$$REMOVE", expCtx->variablesParseState);
auto optimizedExpr = expression->optimize();
ASSERT_VALUE_EQ(Value(), optimizedExpr->evaluate(Document(BSON("x" << BSON("y" << 123)))));
}
TEST(FieldPath, NoOptimizationOnNormalPath) {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a");
// An attempt to optimize returns the Expression itself.
ASSERT_EQUALS(expression, expression->optimize());
}
TEST(FieldPath, OptimizeOnVariableWithConstantScalarValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
expCtx->variables.setConstantValue(varId, Value(123));
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
ASSERT_TRUE(dynamic_cast(optimizedExpr.get()));
}
TEST(FieldPath, OptimizeOnVariableWithConstantArrayValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
expCtx->variables.setConstantValue(varId, Value(BSON_ARRAY(1 << 2 << 3)));
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
auto constantExpr = dynamic_cast(optimizedExpr.get());
ASSERT_TRUE(constantExpr);
ASSERT_VALUE_EQ(Value(BSON_ARRAY(1 << 2 << 3)), constantExpr->getValue());
}
TEST(FieldPath, OptimizeToEmptyArrayOnNumericalPathComponentAndConstantArrayValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
expCtx->variables.setConstantValue(varId, Value(BSON_ARRAY(1 << 2 << 3)));
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar.1", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
auto constantExpr = dynamic_cast(optimizedExpr.get());
ASSERT_TRUE(constantExpr);
ASSERT_VALUE_EQ(Value(BSONArray()), constantExpr->getValue());
}
TEST(FieldPath, OptimizeOnVariableWithConstantValueAndDottedPath) {
intrusive_ptr expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
expCtx->variables.setConstantValue(varId, Value(Document{{"x", Document{{"y", 123}}}}));
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar.x.y", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
auto constantExpr = dynamic_cast(optimizedExpr.get());
ASSERT_TRUE(constantExpr);
ASSERT_VALUE_EQ(Value(123), constantExpr->getValue());
}
TEST(FieldPath, NoOptimizationOnVariableWithNoValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
expCtx->variablesParseState.defineVariable("userVar");
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
ASSERT_FALSE(dynamic_cast(optimizedExpr.get()));
}
TEST(FieldPath, NoOptimizationOnVariableWithMissingValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
expCtx->variables.setValue(varId, Value());
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
ASSERT_FALSE(dynamic_cast(optimizedExpr.get()));
}
TEST(FieldPath, ScalarVariableWithDottedFieldPathOptimizesToConstantMissingValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
auto varId = expCtx->variablesParseState.defineVariable("userVar");
expCtx->variables.setConstantValue(varId, Value(123));
auto expr = ExpressionFieldPath::parse(expCtx, "$$userVar.x.y", expCtx->variablesParseState);
ASSERT_TRUE(dynamic_cast(expr.get()));
auto optimizedExpr = expr->optimize();
auto constantExpr = dynamic_cast(optimizedExpr.get());
ASSERT_TRUE(constantExpr);
ASSERT_VALUE_EQ(Value(), constantExpr->getValue());
}
/** The field path itself is a dependency. */
class Dependencies {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
DepsTracker dependencies;
expression->addDependencies(&dependencies);
ASSERT_EQUALS(1U, dependencies.fields.size());
ASSERT_EQUALS(1U, dependencies.fields.count("a.b"));
ASSERT_EQUALS(false, dependencies.needWholeDocument);
ASSERT_EQUALS(false, dependencies.getNeedTextScore());
}
};
/** Field path target field is missing. */
class Missing {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a");
assertBinaryEqual(fromjson("{}"), toBson(expression->evaluate(Document())));
}
};
/** Simple case where the target field is present. */
class Present {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a");
assertBinaryEqual(fromjson("{'':123}"),
toBson(expression->evaluate(fromBson(BSON("a" << 123)))));
}
};
/** Target field parent is null. */
class NestedBelowNull {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{}"),
toBson(expression->evaluate(fromBson(fromjson("{a:null}")))));
}
};
/** Target field parent is undefined. */
class NestedBelowUndefined {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{}"),
toBson(expression->evaluate(fromBson(fromjson("{a:undefined}")))));
}
};
/** Target field parent is missing. */
class NestedBelowMissing {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{}"),
toBson(expression->evaluate(fromBson(fromjson("{z:1}")))));
}
};
/** Target field parent is an integer. */
class NestedBelowInt {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{}"), toBson(expression->evaluate(fromBson(BSON("a" << 2)))));
}
};
/** A value in a nested object. */
class NestedValue {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(BSON("" << 55),
toBson(expression->evaluate(fromBson(BSON("a" << BSON("b" << 55))))));
}
};
/** Target field within an empty object. */
class NestedBelowEmptyObject {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{}"),
toBson(expression->evaluate(fromBson(BSON("a" << BSONObj())))));
}
};
/** Target field within an empty array. */
class NestedBelowEmptyArray {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(BSON("" << BSONArray()),
toBson(expression->evaluate(fromBson(BSON("a" << BSONArray())))));
}
};
/** Target field within an array containing null. */
class NestedBelowArrayWithNull {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{'':[]}"),
toBson(expression->evaluate(fromBson(fromjson("{a:[null]}")))));
}
};
/** Target field within an array containing undefined. */
class NestedBelowArrayWithUndefined {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{'':[]}"),
toBson(expression->evaluate(fromBson(fromjson("{a:[undefined]}")))));
}
};
/** Target field within an array containing an integer. */
class NestedBelowArrayWithInt {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{'':[]}"),
toBson(expression->evaluate(fromBson(fromjson("{a:[1]}")))));
}
};
/** Target field within an array. */
class NestedWithinArray {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{'':[9]}"),
toBson(expression->evaluate(fromBson(fromjson("{a:[{b:9}]}")))));
}
};
/** Multiple value types within an array. */
class MultipleArrayValues {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b");
assertBinaryEqual(fromjson("{'':[9,20]}"),
toBson(expression->evaluate(
fromBson(fromjson("{a:[{b:9},null,undefined,{g:4},{b:20},{}]}")))));
}
};
/** Expanding values within nested arrays. */
class ExpandNestedArrays {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b.c");
assertBinaryEqual(fromjson("{'':[[1,2],3,[4],[[5]],[6,7]]}"),
toBson(expression->evaluate(fromBson(fromjson("{a:[{b:[{c:1},{c:2}]},"
"{b:{c:3}},"
"{b:[{c:4}]},"
"{b:[{c:[5]}]},"
"{b:{c:[6,7]}}]}")))));
}
};
/** Add to a BSONObj. */
class AddToBsonObj {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b.c");
assertBinaryEqual(BSON("foo"
<< "$a.b.c"),
BSON("foo" << expression->serialize(false)));
}
};
/** Add to a BSONArray. */
class AddToBsonArray {
public:
void run() {
intrusive_ptr expCtx(new ExpressionContextForTest());
intrusive_ptr expression = ExpressionFieldPath::create(expCtx, "a.b.c");
BSONArrayBuilder bab;
bab << expression->serialize(false);
assertBinaryEqual(BSON_ARRAY("$a.b.c"), bab.arr());
}
};
} // namespace FieldPath
namespace Object {
using mongo::ExpressionObject;
template
Document literal(T&& value) {
return Document{{"$const", Value(std::forward(value))}};
}
//
// Parsing.
//
TEST(ExpressionObjectParse, ShouldAcceptEmptyObject) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object = ExpressionObject::parse(expCtx, BSONObj(), vps);
ASSERT_VALUE_EQ(Value(Document{}), object->serialize(false));
}
TEST(ExpressionObjectParse, ShouldAcceptLiteralsAsValues) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object = ExpressionObject::parse(expCtx,
BSON("a" << 5 << "b"
<< "string"
<< "c"
<< BSONNULL),
vps);
auto expectedResult =
Value(Document{{"a", literal(5)}, {"b", literal("string"_sd)}, {"c", literal(BSONNULL)}});
ASSERT_VALUE_EQ(expectedResult, object->serialize(false));
}
TEST(ExpressionObjectParse, ShouldAccept_idAsFieldName) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object = ExpressionObject::parse(expCtx, BSON("_id" << 5), vps);
auto expectedResult = Value(Document{{"_id", literal(5)}});
ASSERT_VALUE_EQ(expectedResult, object->serialize(false));
}
TEST(ExpressionObjectParse, ShouldAcceptFieldNameContainingDollar) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object = ExpressionObject::parse(expCtx, BSON("a$b" << 5), vps);
auto expectedResult = Value(Document{{"a$b", literal(5)}});
ASSERT_VALUE_EQ(expectedResult, object->serialize(false));
}
TEST(ExpressionObjectParse, ShouldAcceptNestedObjects) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object =
ExpressionObject::parse(expCtx, fromjson("{a: {b: 1}, c: {d: {e: 1, f: 1}}}"), vps);
auto expectedResult =
Value(Document{{"a", Document{{"b", literal(1)}}},
{"c", Document{{"d", Document{{"e", literal(1)}, {"f", literal(1)}}}}}});
ASSERT_VALUE_EQ(expectedResult, object->serialize(false));
}
TEST(ExpressionObjectParse, ShouldAcceptArrays) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object = ExpressionObject::parse(expCtx, fromjson("{a: [1, 2]}"), vps);
auto expectedResult =
Value(Document{{"a", vector{Value(literal(1)), Value(literal(2))}}});
ASSERT_VALUE_EQ(expectedResult, object->serialize(false));
}
TEST(ObjectParsing, ShouldAcceptExpressionAsValue) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
auto object = ExpressionObject::parse(expCtx, BSON("a" << BSON("$and" << BSONArray())), vps);
ASSERT_VALUE_EQ(object->serialize(false),
Value(Document{{"a", Document{{"$and", BSONArray()}}}}));
}
//
// Error cases.
//
TEST(ExpressionObjectParse, ShouldRejectDottedFieldNames) {
intrusive_ptr expCtx(new ExpressionContextForTest());
VariablesParseState vps = expCtx->variablesParseState;
ASSERT_THROWS(ExpressionObject::parse(expCtx, BSON("a.b" << 1), vps), AssertionException);
ASSERT_THROWS(ExpressionObject::parse(expCtx, BSON("c" << 3 << "a.b" << 1), vps),
AssertionException);
ASSERT_THROWS(ExpressionObject::parse(expCtx, BSON("a.b" << 1 << "c" << 3), vps),
AssertionException);
}
TEST(ExpressionObjectParse, ShouldRejectDuplicateFieldNames) {
intrusive_ptr