diff options
author | Scott Hernandez <scotthernandez@gmail.com> | 2013-09-27 15:15:18 -0400 |
---|---|---|
committer | Scott Hernandez <scotthernandez@gmail.com> | 2013-09-27 19:56:07 -0400 |
commit | f294acf6512a11ee06ed88555b02947f2fbe2c79 (patch) | |
tree | bfb97297b98368bd093431b31dfd7468b6e0a0c0 | |
parent | 2deab8693871e0d762eb5810c1ea1beefd29dea6 (diff) | |
download | mongo-f294acf6512a11ee06ed88555b02947f2fbe2c79.tar.gz |
SERVER-1534 $min/$max update mods
-rw-r--r-- | src/mongo/db/ops/SConscript | 10 | ||||
-rw-r--r-- | src/mongo/db/ops/modifier_compare.cpp | 170 | ||||
-rw-r--r-- | src/mongo/db/ops/modifier_compare.h | 98 | ||||
-rw-r--r-- | src/mongo/db/ops/modifier_compare_test.cpp | 296 |
4 files changed, 574 insertions, 0 deletions
diff --git a/src/mongo/db/ops/SConscript b/src/mongo/db/ops/SConscript index c75c6dd5182..eb9ccaef935 100644 --- a/src/mongo/db/ops/SConscript +++ b/src/mongo/db/ops/SConscript @@ -53,6 +53,7 @@ env.StaticLibrary( source=[ 'modifier_add_to_set.cpp', 'modifier_bit.cpp', + 'modifier_compare.cpp', 'modifier_inc.cpp', 'modifier_object_replace.cpp', 'modifier_pop.cpp', @@ -88,6 +89,15 @@ env.CppUnitTest( ) env.CppUnitTest( + target='modifier_compare_test', + source='modifier_compare_test.cpp', + LIBDEPS=[ + '$BUILD_DIR/mongo/mutable_bson_test_utils', + 'update', + ], +) + +env.CppUnitTest( target='modifier_inc_test', source='modifier_inc_test.cpp', LIBDEPS=[ diff --git a/src/mongo/db/ops/modifier_compare.cpp b/src/mongo/db/ops/modifier_compare.cpp new file mode 100644 index 00000000000..4942e8f6b1b --- /dev/null +++ b/src/mongo/db/ops/modifier_compare.cpp @@ -0,0 +1,170 @@ +/** + * 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_compare.h" + +#include "mongo/base/error_codes.h" +#include "mongo/bson/mutable/document.h" +#include "mongo/db/ops/field_checker.h" +#include "mongo/db/ops/log_builder.h" +#include "mongo/db/ops/path_support.h" + +namespace mongo { + + + struct ModifierCompare::PreparedState { + + PreparedState(mutablebson::Document& targetDoc) + : doc(targetDoc) + , idxFound(0) + , elemFound(doc.end()) { + } + + // Document that is going to be changed. + mutablebson::Document& doc; + + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; + + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; + + // The replacement string passed in via prepare + std::string pathReplacementString; + }; + + ModifierCompare::ModifierCompare(ModifierCompare::ModifierCompareMode mode) + : _mode(mode) + , _pathReplacementPosition(0) { + } + + ModifierCompare::~ModifierCompare() { + } + + Status ModifierCompare::init(const BSONElement& modExpr, const Options& opts) { + _updatePath.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_updatePath); + if (!status.isOK()) { + return status; + } + + // If a $-positional operator was used, get the index in which it occurred + // and ensure only one occurrence. + size_t foundCount; + fieldchecker::isPositional(_updatePath, &_pathReplacementPosition, &foundCount); + if (_pathReplacementPosition && foundCount > 1) { + return Status(ErrorCodes::BadValue, "too many positional($) elements found."); + } + + // Store value for later. + _val = modExpr; + return Status::OK(); + } + + Status ModifierCompare::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 (_pathReplacementPosition) { + if (matchedField.empty()) { + return Status(ErrorCodes::BadValue, "matched field not provided"); + } + _preparedState->pathReplacementString = matchedField.toString(); + _updatePath.setPart(_pathReplacementPosition, _preparedState->pathReplacementString); + } + + // 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(_updatePath, + 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] = &_updatePath; + + const bool destExists = (_preparedState->elemFound.ok() && + _preparedState->idxFound == (_updatePath.numParts() - 1)); + if (!destExists) { + execInfo->noOp = false; + } + else { + const int compareVal = _preparedState->elemFound.compareWithBSONElement(_val, false); + execInfo->noOp = (compareVal == 0) || + ((_mode == ModifierCompare::MAX) ? + (compareVal > 0) : (compareVal < 0)); + } + + return Status::OK(); + } + + Status ModifierCompare::apply() const { + + const bool destExists = (_preparedState->elemFound.ok() && + _preparedState->idxFound == (_updatePath.numParts() - 1)); + // If there's no need to create any further field part, the $set is simply a value + // assignment. + if (destExists) { + return _preparedState->elemFound.setValueBSONElement(_val); + } + + mutablebson::Document& doc = _preparedState->doc; + StringData lastPart = _updatePath.getPart(_updatePath.numParts() - 1); + // If the element exists and is the same type, then that is what we want to work with + mutablebson::Element elemToSet = doc.makeElementWithNewFieldName(lastPart, _val); + 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(_updatePath, + _preparedState->idxFound, + _preparedState->elemFound, + elemToSet); + } + + Status ModifierCompare::log(LogBuilder* logBuilder) const { + return logBuilder->addToSetsWithNewFieldName(_updatePath.dottedField(), _val); + } + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_compare.h b/src/mongo/db/ops/modifier_compare.h new file mode 100644 index 00000000000..ceec02bdc37 --- /dev/null +++ b/src/mongo/db/ops/modifier_compare.h @@ -0,0 +1,98 @@ +/** + * 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/jsobj.h" +#include "mongo/db/ops/modifier_interface.h" + +namespace mongo { + + class LogBuilder; + + class ModifierCompare : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierCompare); + + public: + + enum ModifierCompareMode { MAX, MIN }; + explicit ModifierCompare(ModifierCompareMode mode = MAX); + + virtual ~ModifierCompare(); + + // + // Modifier interface implementation + // + + /** + * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming from a $set mod such as + * {$set: {<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, const Options& opts); + + /** + * Looks up the field name in the sub-tree rooted at 'root', and binds, if necessary, + * the '$' field part using the 'matchedfield' number. prepare() returns OK and + * fills in 'execInfo' with information of whether this mod is a no-op on 'root' and + * whether it is an in-place candidate. Otherwise, returns a status describing the + * error. + */ + virtual Status prepare(mutablebson::Element root, + const StringData& matchedField, + ExecInfo* execInfo); + + /** + * Applies the prepared mod over the element 'root' specified in the prepare() + * call. Returns OK if successful or a status describing the error. + */ + virtual Status apply() const; + + /** + * Adds a log entry to logRoot corresponding to the operation applied here. Returns OK + * if successful or a status describing the error. + */ + virtual Status log(LogBuilder* logBuilder) const; + + private: + + // Compare mode: min/max + const ModifierCompareMode _mode; + + // Access to each component of fieldName that's the target of this mod. + FieldRef _updatePath; + + // 0 or index for $-positional in _updatePath. + size_t _pathReplacementPosition; + + // Element of the mod expression. + BSONElement _val; + + // The instance of the field in the provided doc. This state is valid after a + // prepare() was issued and until a log() is issued. The document this mod is + // being prepared against must be live throughout all the calls. + struct PreparedState; + scoped_ptr<PreparedState> _preparedState; + }; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_compare_test.cpp b/src/mongo/db/ops/modifier_compare_test.cpp new file mode 100644 index 00000000000..eba04776b1f --- /dev/null +++ b/src/mongo/db/ops/modifier_compare_test.cpp @@ -0,0 +1,296 @@ +/** + * 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/>. + * + * 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/db/ops/modifier_compare.h" + +#include "mongo/base/string_data.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/db/ops/log_builder.h" +#include "mongo/platform/cstdint.h" +#include "mongo/unittest/unittest.h" + +namespace { + + using mongo::BSONObj; + using mongo::LogBuilder; + using mongo::ModifierCompare; + using mongo::ModifierInterface; + using mongo::Status; + using mongo::StringData; + using mongo::fromjson; + using mongo::mutablebson::ConstElement; + using mongo::mutablebson::Document; + using mongo::mutablebson::Element; + + const char kModNameMin[] = "$min"; + const char kModNameMax[] = "$max"; + + /** Helper to build and manipulate a $min/max mod. */ + class Mod { + public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) + : _modObj(modObj) + , _mod((modObj.firstElement().fieldNameStringData() == "$min") ? + ModifierCompare::MIN : + ModifierCompare::MAX) { + const StringData& modName = modObj.firstElement().fieldName(); + ASSERT_OK(_mod.init(modObj[modName].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + } + + Status prepare(Element root, + const StringData& matchedField, + ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); + } + + Status apply() const { + return _mod.apply(); + } + + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); + } + + ModifierCompare& mod() { return _mod; } + + private: + BSONObj _modObj; + ModifierCompare _mod; + }; + + TEST(Init, ValidValues) { + BSONObj modObj; + ModifierCompare mod; + + modObj = fromjson("{ $min : { a : 2 } }"); + ASSERT_OK(mod.init(modObj[kModNameMin].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + modObj = fromjson("{ $max : { a : 1 } }"); + ASSERT_OK(mod.init(modObj[kModNameMax].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + modObj = fromjson("{ $min : { a : {$date : 0 } } }"); + ASSERT_OK(mod.init(modObj[kModNameMin].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + } + + TEST(ExistingNumber, MaxSameNumber) { + Document doc(fromjson("{a: 1 }")); + Mod mod(fromjson("{$max: {a: 1} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(ExistingNumber, MinSameNumber) { + Document doc(fromjson("{a: 1 }")); + Mod mod(fromjson("{$min: {a: 1} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(ExistingNumber, MaxNumberIsLess) { + Document doc(fromjson("{a: 1 }")); + Mod mod(fromjson("{$max: {a: 0} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(ExistingNumber, MinNumberIsMore) { + Document doc(fromjson("{a: 1 }")); + Mod mod(fromjson("{$min: {a: 2} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(ExistingDouble, MaxSameValInt) { + Document doc(fromjson("{a: 1.0 }")); + Mod mod(BSON("$max" << BSON("a" << 1LL))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(ExistingDoubleZero, MaxSameValIntZero) { + Document doc(fromjson("{a: 0.0 }")); + Mod mod(BSON("$max" << BSON("a" << 0LL))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(ExistingDoubleZero, MinSameValIntZero) { + Document doc(fromjson("{a: 0.0 }")); + Mod mod(BSON("$min" << BSON("a" << 0LL))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + } + + TEST(MissingField, MinNumber) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{$min: {a: 0} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(fromjson("{a : 0}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 0 } }"), logDoc); + } + + TEST(ExistingNumber, MinNumber) { + Document doc(fromjson("{a: 1 }")); + Mod mod(fromjson("{$min: {a: 0} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(fromjson("{a : 0}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 0 } }"), logDoc); + } + + TEST(MissingField, MaxNumber) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{$max: {a: 0} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(fromjson("{a : 0}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 0 } }"), logDoc); + } + + TEST(ExistingNumber, MaxNumber) { + Document doc(fromjson("{a: 1 }")); + Mod mod(fromjson("{$max: {a: 2} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(fromjson("{a : 2}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 2 } }"), logDoc); + } + + TEST(ExistingDate, MaxDate) { + Document doc(fromjson("{a: {$date: 0} }")); + Mod mod(fromjson("{$max: {a: {$date: 123123123}} }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(fromjson("{a: {$date: 123123123}}"), doc); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$set: {a: {$date: 123123123}} }"), logDoc); + } + + TEST(ExistingEmbeddedDoc, MaxDoc) { + Document doc(fromjson("{a: {b: 2}}")); + Mod mod(fromjson("{$max: {a: {b: 3}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(fromjson("{a: {b: 3}}}"), doc); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$set: {a: {b: 3}} }"), logDoc); + } + + TEST(ExistingEmbeddedDoc, MaxNumber) { + Document doc(fromjson("{a: {b: 2}}")); + Mod mod(fromjson("{$max: {a: 3}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + } + +} // namespace |