diff options
author | Andrew Morrow <acm@10gen.com> | 2013-05-06 17:22:08 -0400 |
---|---|---|
committer | Andrew Morrow <acm@10gen.com> | 2013-05-07 17:18:30 -0400 |
commit | 3049ab5ae2b3e672f17a001be0f7cdf7e599a9fb (patch) | |
tree | ca6558d87e37bfd54182827460b2b8ed93b19da4 /src | |
parent | 6932776c728f65860c88813626efe94513fbf83b (diff) | |
download | mongo-3049ab5ae2b3e672f17a001be0f7cdf7e599a9fb.tar.gz |
SERVER-7174 Implement $inc on new update framework
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/ops/SConscript | 10 | ||||
-rw-r--r-- | src/mongo/db/ops/modifier_inc.cpp | 266 | ||||
-rw-r--r-- | src/mongo/db/ops/modifier_inc.h | 73 | ||||
-rw-r--r-- | src/mongo/db/ops/modifier_inc_test.cpp | 401 |
4 files changed, 750 insertions, 0 deletions
diff --git a/src/mongo/db/ops/SConscript b/src/mongo/db/ops/SConscript index 4c35518a886..ccd175ff2c6 100644 --- a/src/mongo/db/ops/SConscript +++ b/src/mongo/db/ops/SConscript @@ -39,6 +39,7 @@ env.CppUnitTest( env.StaticLibrary( target='update', source=[ + 'modifier_inc.cpp', 'modifier_set.cpp', 'modifier_unset.cpp', ], @@ -48,6 +49,15 @@ env.StaticLibrary( ) env.CppUnitTest( + target='modifier_inc_test', + source='modifier_inc_test.cpp', + LIBDEPS=[ + '$BUILD_DIR/mongo/mutable_bson_test_utils', + 'update', + ], +) + +env.CppUnitTest( target='modifier_set_test', source='modifier_set_test.cpp', LIBDEPS=[ diff --git a/src/mongo/db/ops/modifier_inc.cpp b/src/mongo/db/ops/modifier_inc.cpp new file mode 100644 index 00000000000..00a773f6329 --- /dev/null +++ b/src/mongo/db/ops/modifier_inc.cpp @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2013 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 <http://www.gnu.org/licenses/>. + */ + +#include "mongo/db/ops/modifier_inc.h" + +#include "mongo/base/error_codes.h" +#include "mongo/bson/mutable/document.h" +#include "mongo/db/ops/field_checker.h" +#include "mongo/db/ops/path_support.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { + + using mongoutils::str::stream; + + struct ModifierInc::PreparedState { + + PreparedState(mutablebson::Document& doc) + : doc(doc) + , idxFound(0) + , elemFound(doc.end()) + , boundDollar("") + , newValue() + , inPlace(false) + , noOp(false) { + } + + // Document that is going to be changed. + mutablebson::Document& doc; + + // Index in _fieldRef for which an Element exist in the document. + int32_t idxFound; + + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; + + // Value to bind to a $-positional field, if one is provided. + std::string boundDollar; + + // Value to be applied + SafeNum newValue; + + // This $inc is in-place? + bool inPlace; + + // This $inc is a no-op? + bool noOp; + }; + + ModifierInc::ModifierInc() + : ModifierInterface () + , _fieldRef() + , _posDollar(0) + , _val() { + } + + ModifierInc::~ModifierInc() { + } + + Status ModifierInc::init(const BSONElement& modExpr) { + + // + // field name analysis + // + + // Perform standard field name and updateable checks. + _fieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fieldRef); + if (! status.isOK()) { + return status; + } + + // If a $-positional operator was used, get the index in which it occurred. + fieldchecker::isPositional(_fieldRef, &_posDollar); + + // + // value analysis + // + + if (!modExpr.isNumber()) { + // TODO: Context for mod error messages would be helpful + // include mod code, etc. + return Status(ErrorCodes::BadValue, + "Cannot increment with non-numeric argument"); + } + + _val = modExpr; + dassert(_val.isValid()); + + return Status::OK(); + } + + Status ModifierInc::prepare(mutablebson::Element root, + const StringData& matchedField, + ExecInfo* execInfo) { + + _preparedState.reset(new PreparedState(root.getDocument())); + + // If we have a $-positional field, it is time to bind it to an actual field part. + if (_posDollar) { + if (matchedField.empty()) { + return Status(ErrorCodes::BadValue, "matched field not provided"); + } + _preparedState->boundDollar = matchedField.toString(); + _fieldRef.setPart(_posDollar, _preparedState->boundDollar); + } + + // Locate the field name in 'root'. Note that we may not have all the parts in the path + // in the doc -- which is fine. Our goal now is merely to reason about whether this mod + // apply is a noOp or whether is can be in place. The remaining path, if missing, will + // be created during the apply. + Status status = pathsupport::findLongestPrefix(_fieldRef, + root, + &_preparedState->idxFound, + &_preparedState->elemFound); + + // FindLongestPrefix may say the path does not exist at all, which is fine here, or + // that the path was not viable or otherwise wrong, in which case, the mod cannot + // proceed. + if (status.code() == ErrorCodes::NonExistentPath) { + _preparedState->elemFound = root.getDocument().end(); + } + else if (!status.isOK()) { + return status; + } + + // We register interest in the field name. The driver needs this info to sort out if + // there is any conflict among mods. + execInfo->fieldRef[0] = &_fieldRef; + + // Capture the value we are going to write. At this point, there may not be a value + // against which to operate, so the result will be simply _val. + _preparedState->newValue = _val; + + // + // in-place and no-op logic + // + // If the field path is not fully present, then this mod cannot be in place, nor is a + // noOp. + if (!_preparedState->elemFound.ok() || + _preparedState->idxFound < static_cast<int32_t>(_fieldRef.numParts() - 1)) { + return Status::OK(); + } + + // If the value being $inc'ed is the same as the one already in the doc, than this is a + // noOp. + if (!_preparedState->elemFound.isNumeric()) + return Status(ErrorCodes::BadValue, + "invalid attempt to increment a non-numeric field"); + + const SafeNum currentValue = _preparedState->elemFound.getValueSafeNum(); + + // TODO: Is it possible for the current value to be EOO? + + // Update newValue w.r.t to the current value of the found element. + _preparedState->newValue += currentValue; + + // If the result of the addition is invalid, we must return an error. + if (!_preparedState->newValue.isValid()) + return Status(ErrorCodes::BadValue, + "Failed to increment current value"); + + // If the values are identical (same type, same value), then this is a no-op, and + // therefore in-place as well. + if (_preparedState->newValue.isIdentical(currentValue)) { + _preparedState->noOp = execInfo->noOp = true; + _preparedState->inPlace = execInfo->inPlace = true; + return Status::OK(); + } + + // If the types are the same, this can be done in place. + // + // TODO: Potentially, cases where $inc results in a mixed type of the same size could + // be in-place as well, but we don't currently handle them. + if (_preparedState->newValue.type() == currentValue.type()) { + _preparedState->inPlace = execInfo->inPlace = true; + } + + return Status::OK(); + } + + Status ModifierInc::apply() const { + dassert(_preparedState->noOp == false); + + // If there's no need to create any further field part, the $inc is simply a value + // assignment. + if (_preparedState->elemFound.ok() && + _preparedState->idxFound == static_cast<int32_t>(_fieldRef.numParts() - 1)) { + return _preparedState->elemFound.setValueSafeNum(_preparedState->newValue); + } + + dassert(_preparedState->inPlace == false); + + // + // Complete document path logic + // + + // Creates the final element that's going to be $set in 'doc'. + mutablebson::Document& doc = _preparedState->doc; + StringData lastPart = _fieldRef.getPart(_fieldRef.numParts() - 1); + mutablebson::Element elemToSet = doc.makeElementSafeNum(lastPart, _preparedState->newValue); + if (!elemToSet.ok()) { + return Status(ErrorCodes::InternalError, "can't create new element"); + } + + // Now, we can be in two cases here, as far as attaching the element being set goes: + // (a) none of the parts in the element's path exist, or (b) some parts of the path + // exist but not all. + if (!_preparedState->elemFound.ok()) { + _preparedState->elemFound = doc.root(); + _preparedState->idxFound = 0; + } + else { + _preparedState->idxFound++; + } + + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + return pathsupport::createPathAt(_fieldRef, + _preparedState->idxFound, + _preparedState->elemFound, + elemToSet); + } + + Status ModifierInc::log(mutablebson::Element logRoot) const { + + // We'd like to create an entry such as {$set: {<fieldname>: <value>}} under 'logRoot'. + // We start by creating the {$set: ...} Element. + mutablebson::Document& doc = logRoot.getDocument(); + mutablebson::Element setElement = doc.makeElementObject("$set"); + if (!setElement.ok()) { + return Status(ErrorCodes::InternalError, "cannot append log entry for $set mod"); + } + + // Then we create the {<fieldname>: <value>} Element. + mutablebson::Element logElement = doc.makeElementSafeNum( + _fieldRef.dottedField(), + _preparedState->newValue); + + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, "cannot append details for $set mod"); + } + + // Now, we attach the {<fieldname>: <value>} Element under the {$set: ...} one. + Status status = setElement.pushBack(logElement); + if (!status.isOK()) { + return status; + } + + // And attach the result under the 'logRoot' Element provided. + return logRoot.pushBack(setElement); + } + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_inc.h b/src/mongo/db/ops/modifier_inc.h new file mode 100644 index 00000000000..5dc9f8fae95 --- /dev/null +++ b/src/mongo/db/ops/modifier_inc.h @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2013 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 <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <boost/scoped_ptr.hpp> +#include <string> + +#include "mongo/base/disallow_copying.h" +#include "mongo/bson/mutable/element.h" +#include "mongo/db/field_ref.h" +#include "mongo/db/ops/modifier_interface.h" + +namespace mongo { + + class ModifierInc : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierInc); + + public: + + ModifierInc(); + virtual ~ModifierInc(); + + /** + * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming from a $inc mod such as + * {$inc: {<fieldname: <value>}}. init() extracts the field name and the value to be + * assigned to it from 'modExpr'. It returns OK if successful or a status describing + * the error. + */ + virtual Status init(const BSONElement& modExpr); + + /** Evaluates the validity of applying $inc to the identified node, and computes + * effects, handling upcasting and overflow as necessary. + */ + virtual Status prepare(mutablebson::Element root, + const StringData& matchedField, + ExecInfo* execInfo); + + /** Updates the node passed in prepare with the results of the $inc */ + virtual Status apply() const; + + /** Converts the result of the $inc into an equivalent $set under logRoot */ + virtual Status log(mutablebson::Element logRoot) const; + + private: + + // Access to each component of fieldName that's the target of this mod. + FieldRef _fieldRef; + + // 0 or index for $-positional in _fieldRef. + size_t _posDollar; + + // Element of the $set expression. + SafeNum _val; + + struct PreparedState; + scoped_ptr<PreparedState> _preparedState; + }; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_inc_test.cpp b/src/mongo/db/ops/modifier_inc_test.cpp new file mode 100644 index 00000000000..ad78567cbdf --- /dev/null +++ b/src/mongo/db/ops/modifier_inc_test.cpp @@ -0,0 +1,401 @@ +/** + * Copyright (C) 2013 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 <http://www.gnu.org/licenses/>. + */ + + +#include "mongo/db/ops/modifier_inc.h" + +#include "mongo/base/status.h" +#include "mongo/base/string_data.h" +#include "mongo/bson/mutable/algorithm.h" +#include "mongo/bson/mutable/document.h" +#include "mongo/bson/mutable/mutable_bson_test_utils.h" +#include "mongo/db/jsobj.h" +#include "mongo/db/json.h" +#include "mongo/platform/cstdint.h" +#include "mongo/unittest/unittest.h" + +namespace { + + using mongo::BSONObj; + using mongo::ModifierInc; + using mongo::ModifierInterface; + using mongo::NumberInt; + using mongo::Status; + using mongo::StringData; + using mongo::fromjson; + using mongo::mutablebson::checkDoc; + using mongo::mutablebson::ConstElement; + using mongo::mutablebson::Document; + using mongo::mutablebson::Element; + using mongo::mutablebson::countChildren; + + /** Helper to build and manipulate a $inc mod. */ + class Mod { + public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) + : _modObj(modObj) + , _mod() { + ASSERT_OK(_mod.init(_modObj["$inc"].embeddedObject().firstElement())); + } + + Status prepare(Element root, + const StringData& matchedField, + ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); + } + + Status apply() const { + return _mod.apply(); + } + + Status log(Element logRoot) const { + return _mod.log(logRoot); + } + + ModifierInc& mod() { return _mod; } + + private: + BSONObj _modObj; + ModifierInc _mod; + }; + + TEST(Init, FailToInitWithInvalidValue) { + BSONObj modObj; + ModifierInc mod; + + // String is an invalid increment argument + modObj = fromjson("{ $inc : { a : '' } }"); + ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement())); + + // Object is an invalid increment argument + modObj = fromjson("{ $inc : { a : {} } }"); + ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement())); + + // Array is an invalid increment argument + modObj = fromjson("{ $inc : { a : [] } }"); + ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement())); + } + + TEST(Init, InitParsesNumberInt) { + Mod incMod(BSON("$inc" << BSON("a" << static_cast<int>(1)))); + } + + TEST(Init, InitParsesNumberLong) { + Mod incMod(BSON("$inc" << BSON("a" << static_cast<long long>(1)))); + } + + TEST(Init, InitParsesNumberDouble) { + Mod incMod(BSON("$inc" << BSON("a" << 1.0))); + } + + TEST(SimpleMod, PrepareSimpleOK) { + Document doc(fromjson("{ a : 1 }")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.inPlace); + ASSERT_FALSE(execInfo.noOp); + } + + TEST(SimpleMod, PrepareSimpleNonNumericObject) { + Document doc(fromjson("{ a : {} }")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(incMod.prepare(doc.root(), "", &execInfo)); + } + + TEST(SimpleMod, PrepareSimpleNonNumericArray) { + + Document doc(fromjson("{ a : [] }")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(incMod.prepare(doc.root(), "", &execInfo)); + } + + TEST(SimpleMod, PrepareSimpleNonNumericString) { + Document doc(fromjson("{ a : '' }")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(incMod.prepare(doc.root(), "", &execInfo)); + } + + TEST(SimpleMod, ApplyAndLogEmptyDocument) { + Document doc(fromjson("{}")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.inPlace); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : 1 }"))); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { a : 1 } }"))); + } + + TEST(SimpleMod, ApplyAndLogSimpleDocument) { + Document doc(fromjson("{ a : 2 }")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.inPlace); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : 3 }"))); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { a : 3 } }"))); + } + + TEST(DottedMod, ApplyAndLogSimpleDocument) { + Document doc(fromjson("{ a : { b : 2 } }")); + Mod incMod(fromjson("{ $inc: { 'a.b' : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.inPlace); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : { b : 3 } }"))); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { 'a.b' : 3 } }"))); + } + + TEST(InPlace, IntToInt) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod incMod(BSON("$inc" << BSON("a" << static_cast<int>(1)))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + } + + TEST(InPlace, LongToLong) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod incMod(BSON("$inc" << BSON("a" << static_cast<long long>(1)))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + } + + TEST(InPlace, DoubleToDouble) { + Document doc(BSON("a" << 1.0)); + Mod incMod(BSON("$inc" << BSON("a" << 1.0 ))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + } + + TEST(NoOp, Int) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod incMod(BSON("$inc" << BSON("a" << static_cast<int>(0)))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + } + + TEST(NoOp, Long) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod incMod(BSON("$inc" << BSON("a" << static_cast<long long>(0)))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + } + + TEST(NoOp, Double) { + Document doc(BSON("a" << 1.0)); + Mod incMod(BSON("$inc" << BSON("a" << 0.0))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + } + + TEST(Upcasting, UpcastIntToLong) { + // Checks that $inc : NumberLong(0) turns a NumberInt into a NumberLong and logs it + // correctly. + Document doc(BSON("a" << static_cast<int>(1))); + ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); + + Mod incMod(BSON("$inc" << BSON("a" << static_cast<long long>(0)))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_FALSE(execInfo.inPlace); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : 1 }"))); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { a : 1 } }"))); + ASSERT_EQUALS(mongo::NumberLong, logDoc.root()["$set"]["a"].getType()); + } + + TEST(Upcasting, UpcastIntToDouble) { + // Checks that $inc : 0.0 turns a NumberInt into a NumberDouble and logs it + // correctly. + Document doc(BSON("a" << static_cast<int>(1))); + ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); + + Mod incMod(fromjson("{ $inc : { a : 0.0 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_FALSE(execInfo.inPlace); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : 1.0 }"))); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { a : 1.0 } }"))); + ASSERT_EQUALS(mongo::NumberDouble, logDoc.root()["$set"]["a"].getType()); + } + + TEST(Upcasting, UpcastLongToDouble) { + // Checks that $inc : 0.0 turns a NumberLong into a NumberDouble and logs it + // correctly. + Document doc(BSON("a" << static_cast<long long>(1))); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); + + Mod incMod(fromjson("{ $inc : { a : 0.0 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_FALSE(execInfo.inPlace); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : 1.0 }"))); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { a : 1.0 } }"))); + ASSERT_EQUALS(mongo::NumberDouble, logDoc.root()["$set"]["a"].getType()); + } + + TEST(Upcasting, DoublesStayDoubles) { + // Checks that $inc : 0 doesn't change a NumberDouble away from double + Document doc(fromjson("{ a : 1.0 }")); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Mod incMod(fromjson("{ $inc : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_TRUE(execInfo.inPlace); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, fromjson("{ a : 2.0 }"))); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Document logDoc; + ASSERT_OK(incMod.log(logDoc.root())); + ASSERT_TRUE(checkDoc(logDoc, fromjson("{ $set : { a : 2.0 } }"))); + ASSERT_EQUALS(mongo::NumberDouble, logDoc.root()["$set"]["a"].getType()); + } + + // The only interesting overflow cases are int->long via increment: we never overflow to + // double, and we never decrease precision on decrement. + + TEST(Spilling, OverflowIntToLong) { + const int initial_value = std::numeric_limits<int32_t>::max(); + + Document doc(BSON("a" << static_cast<int>(initial_value))); + ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); + + Mod incMod(fromjson("{ $inc : { a : 1 } }")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_FALSE(execInfo.inPlace); + + const long long target_value = static_cast<long long>(initial_value) + 1; + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, BSON("a" << target_value))); + } + + TEST(Spilling, UnderflowIntToLong) { + const int initial_value = std::numeric_limits<int32_t>::min(); + + Document doc(BSON("a" << static_cast<int>(initial_value))); + ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); + + Mod incMod(fromjson("{ $inc : { a : -1 } }")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_FALSE(execInfo.inPlace); + + const long long target_value = static_cast<long long>(initial_value) - 1; + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc, BSON("a" << target_value))); + } + + TEST(Lifecycle, IncModCanBeReused) { + Document doc1(fromjson("{ a : 1 }")); + Document doc2(fromjson("{ a : 1 }")); + + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc1.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.inPlace); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc1, fromjson("{ a : 2 }"))); + + ASSERT_OK(incMod.prepare(doc2.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.inPlace); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(checkDoc(doc2, fromjson("{ a : 2 }"))); + } + +} // namespace |