Skip to content

Commit

Permalink
SERVER-10689 Aggregation now supports the switch expression.
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminmurphy committed Apr 15, 2016
1 parent 63d021f commit 77aaa54
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 1 deletion.
1 change: 1 addition & 0 deletions buildscripts/resmokeconfig/suites/aggregation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ selector:
roots:
- jstests/aggregation/*.js
- jstests/aggregation/bugs/*.js
- jstests/aggregation/expressions/*.js

executor:
js_test:
Expand Down
1 change: 1 addition & 0 deletions buildscripts/resmokeconfig/suites/aggregation_auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ selector:
roots:
- jstests/aggregation/*.js
- jstests/aggregation/bugs/*.js
- jstests/aggregation/expressions/*.js
exclude_files:
# Skip any tests that run with auth explicitly.
- jstests/aggregation/*[aA]uth*.js
Expand Down
1 change: 1 addition & 0 deletions buildscripts/resmokeconfig/suites/aggregation_ese.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ selector:
roots:
- jstests/aggregation/*.js
- jstests/aggregation/bugs/*.js
- jstests/aggregation/expressions/*.js
- src/mongo/db/modules/*/jstests/aggregation/*.js
exclude_files:
# Skip any tests that run with auth explicitly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ selector:
roots:
- jstests/aggregation/*.js
- jstests/aggregation/bugs/*.js
- jstests/aggregation/expressions/*.js
exclude_files:
- jstests/aggregation/bugs/server18198.js # Uses a mocked mongo client to test read preference.
- jstests/aggregation/mongos_slaveok.js # Majority read on secondary requires afterOpTime.
Expand All @@ -28,4 +29,4 @@ executor:
enableTestCommands: 1
num_nodes: 2
# Needs to be set for any ephemeral or no-journaling storage engine
write_concern_majority_journal_default: false
write_concern_majority_journal_default: false
150 changes: 150 additions & 0 deletions jstests/aggregation/expressions/switch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// In SERVER-10689, the $switch expression was introduced. In this file, we test the functionality
// of the expression.

(function() {
"use strict";

var coll = db.switch;
coll.drop();

// Insert an empty document so that something can flow through the pipeline.
coll.insert({});

// Ensure that a branch is correctly evaluated.
var pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch": {
"branches": [{"case": {"$eq": [1, 1]}, "then": "one is equal to one!"}],
}
}
}
};
var res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": "one is equal to one!"});

// Ensure that the first branch which matches is chosen.
pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch": {
"branches": [
{"case": {"$eq": [1, 1]}, "then": "one is equal to one!"},
{"case": {"$eq": [2, 2]}, "then": "two is equal to two!"}
],
}
}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": "one is equal to one!"});

// Ensure that the default is chosen if no case matches.
pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch": {
"branches": [{"case": {"$eq": [1, 2]}, "then": "one is equal to two!"}],
"default": "no case matched."
}
}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": "no case matched."});

// Ensure that nullish values are treated as false when they are a "case", and are null
// otherwise.
pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch": {
"branches": [{"case": null, "then": "Null was true!"}],
"default": "No case matched."
}
}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": "No case matched."});

pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch": {
"branches": [{"case": "$missingField", "then": "Null was true!"}],
"default": "No case matched."
}
}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": "No case matched."});

pipeline = {
"$project": {
"_id": 0,
"output":
{"$switch": {"branches": [{"case": true, "then": null}], "default": false}}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": null});

pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch":
{"branches": [{"case": true, "then": "$missingField"}], "default": false}
}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {});

pipeline = {
"$project": {
"_id": 0,
"output":
{"$switch": {"branches": [{"case": null, "then": false}], "default": null}}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {"output": null});

pipeline = {
"$project": {
"_id": 0,
"output": {
"$switch":
{"branches": [{"case": null, "then": false}], "default": "$missingField"}
}
}
};
res = coll.aggregate(pipeline).toArray();

assert.eq(res.length, 1);
assert.eq(res[0], {});
}());
71 changes: 71 additions & 0 deletions jstests/aggregation/expressions/switch_errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SERVER-10689 introduced the $switch expression. In this file, we test the error cases of the
// expression.
load("jstests/aggregation/extras/utils.js"); // For assertErrorCode.

(function() {
"use strict";

var coll = db.switch;
coll.drop();

var pipeline = {
"$project": {"output": {"$switch": "not an object"}}
};
assertErrorCode(coll, pipeline, 40060, "$switch requires an object as an argument.");

pipeline = {
"$project": {"output": {"$switch": {"branches": "not an array"}}}
};
assertErrorCode(coll, pipeline, 40061, "$switch requires 'branches' to be an array.");

pipeline = {
"$project": {"output": {"$switch": {"branches": ["not an object"]}}}
};
assertErrorCode(coll, pipeline, 40062, "$switch requires each branch to be an object.");

pipeline = {
"$project": {"output": {"$switch": {"branches": [{}]}}}
};
assertErrorCode(coll, pipeline, 40064, "$switch requires each branch have a 'case'.");

pipeline = {
"$project": {
"output": {
"$switch": {
"branches": [{
"case": 1,
}]
}
}
}
};
assertErrorCode(coll, pipeline, 40065, "$switch requires each branch have a 'then'.");

pipeline = {
"$project":
{"output": {"$switch": {"branches": [{"case": true, "then": false, "badKey": 1}]}}}
};
assertErrorCode(coll, pipeline, 40063, "$switch found a branch with an unknown argument");

pipeline = {
"$project": {"output": {"$switch": {"notAnArgument": 1}}}
};
assertErrorCode(coll, pipeline, 40067, "$switch found an unknown argument");

pipeline = {
"$project": {"output": {"$switch": {"branches": []}}}
};
assertErrorCode(coll, pipeline, 40068, "$switch requires at least one branch");

pipeline = {
"$project": {"output": {"$switch": {}}}
};
assertErrorCode(coll, pipeline, 40068, "$switch requires at least one branch");

coll.insert({x: 1});
pipeline = {
"$project":
{"output": {"$switch": {"branches": [{"case": {"$eq": ["$x", 0]}, "then": 1}]}}}
};
assertErrorCode(coll, pipeline, 40066, "$switch has no default and an input matched no case");
}());
130 changes: 130 additions & 0 deletions src/mongo/db/pipeline/expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,136 @@ const char* ExpressionSubtract::getOpName() const {
return "$subtract";
}

/* ------------------------- ExpressionSwitch ------------------------------ */

REGISTER_EXPRESSION(switch, ExpressionSwitch::parse);
const char* ExpressionSwitch::getOpName() const {
return "$switch";
}

Value ExpressionSwitch::evaluateInternal(Variables* vars) const {
for (auto&& branch : _branches) {
Value caseExpression(branch.first->evaluateInternal(vars));

if (caseExpression.coerceToBool()) {
return branch.second->evaluateInternal(vars);
}
}

uassert(40066,
"$switch could not find a matching branch for an input, and no default was specified.",
_default);

return _default->evaluateInternal(vars);
}

boost::intrusive_ptr<Expression> ExpressionSwitch::parse(BSONElement expr,
const VariablesParseState& vps) {
uassert(40060,
str::stream() << "$switch requires an object as an argument, found: "
<< typeName(expr.type()),
expr.type() == Object);

intrusive_ptr<ExpressionSwitch> expression(new ExpressionSwitch());

for (auto&& elem : expr.Obj()) {
auto field = elem.fieldNameStringData();

if (field == "branches") {
// Parse each branch separately.
uassert(40061,
str::stream() << "$switch expected an array for 'branches', found: "
<< typeName(elem.type()),
elem.type() == Array);

for (auto&& branch : elem.Array()) {
uassert(40062,
str::stream() << "$switch expected each branch to be an object, found: "
<< typeName(branch.type()),
branch.type() == Object);

ExpressionPair branchExpression;

for (auto&& branchElement : branch.Obj()) {
auto branchField = branchElement.fieldNameStringData();

if (branchField == "case") {
branchExpression.first = parseOperand(branchElement, vps);
} else if (branchField == "then") {
branchExpression.second = parseOperand(branchElement, vps);
} else {
uasserted(40063,
str::stream() << "$switch found an unknown argument to a branch: "
<< branchField);
}
}

uassert(40064,
"$switch requires each branch have a 'case' expression",
branchExpression.first);
uassert(40065,
"$switch requires each branch have a 'then' expression.",
branchExpression.second);

expression->_branches.push_back(branchExpression);
}
} else if (field == "default") {
// Optional, arbitrary expression.
expression->_default = parseOperand(elem, vps);
} else {
uasserted(40067, str::stream() << "$switch found an unknown argument: " << field);
}
}

uassert(40068, "$switch requires at least one branch.", !expression->_branches.empty());

return expression;
}

void ExpressionSwitch::addDependencies(DepsTracker* deps, std::vector<std::string>* path) const {
for (auto&& branch : _branches) {
branch.first->addDependencies(deps, path);
branch.second->addDependencies(deps, path);
}

if (_default) {
_default->addDependencies(deps, path);
}
}

boost::intrusive_ptr<Expression> ExpressionSwitch::optimize() {
if (_default) {
_default = _default->optimize();
}

std::transform(_branches.begin(),
_branches.end(),
_branches.begin(),
[](ExpressionPair branch) -> ExpressionPair {
return {branch.first->optimize(), branch.second->optimize()};
});

return this;
}

Value ExpressionSwitch::serialize(bool explain) const {
std::vector<Value> serializedBranches;
serializedBranches.reserve(_branches.size());

for (auto&& branch : _branches) {
serializedBranches.push_back(Value(Document{{"case", branch.first->serialize(explain)},
{"then", branch.second->serialize(explain)}}));
}

if (_default) {
return Value(Document{{"$switch",
Document{{"branches", Value(serializedBranches)},
{"default", _default->serialize(explain)}}}});
}

return Value(Document{{"$switch", Document{{"branches", Value(serializedBranches)}}}});
}

/* ------------------------- ExpressionToLower ----------------------------- */

Value ExpressionToLower::evaluateInternal(Variables* vars) const {
Expand Down
Loading

0 comments on commit 77aaa54

Please sign in to comment.