/** * 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 . * * 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_pull.h" #include "mongo/base/error_codes.h" #include "mongo/bson/mutable/algorithm.h" #include "mongo/db/matcher/expression_parser.h" #include "mongo/db/ops/field_checker.h" #include "mongo/db/ops/log_builder.h" #include "mongo/db/ops/path_support.h" #include "mongo/util/mongoutils/str.h" namespace mongo { namespace mb = mutablebson; namespace str = mongoutils::str; struct ModifierPull::PreparedState { PreparedState(mb::Document& doc) : doc(doc), idxFound(0), elemFound(doc.end()), elementsToRemove(), noOp(false) {} // Document that is going to be changed. mb::Document& doc; // Index in _fieldRef for which an Element exist in the document. size_t idxFound; // Element corresponding to _fieldRef[0.._idxFound]. mb::Element elemFound; // Values to be removed. std::vector elementsToRemove; // True if this update is a no-op bool noOp; }; ModifierPull::ModifierPull() : ModifierInterface(), _fieldRef(), _posDollar(0), _exprElt(), _exprObj(), _matchExpr(), _matcherOnPrimitive(false), _preparedState() {} ModifierPull::~ModifierPull() {} Status ModifierPull::init(const BSONElement& modExpr, const Options& opts, bool* positional) { // 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 // and ensure only one occurrence. size_t foundCount; bool foundDollar = fieldchecker::isPositional(_fieldRef, &_posDollar, &foundCount); if (positional) *positional = foundDollar; if (foundDollar && foundCount > 1) { return Status(ErrorCodes::BadValue, str::stream() << "Too many positional (i.e. '$') elements found in path '" << _fieldRef.dottedField() << "'"); } _exprElt = modExpr; // If the element in the mod is actually an object or a regular expression, we need to // build a matcher, instead of just doing an equality comparision. if ((_exprElt.type() == mongo::Object) || (_exprElt.type() == mongo::RegEx)) { if (_exprElt.type() == Object) { _exprObj = _exprElt.embeddedObject(); // If not is not a query operator, then it is a primitive. _matcherOnPrimitive = (_exprObj.firstElement().getGtLtOp() != 0); // If the object is primitive then wrap it up into an object. if (_matcherOnPrimitive) _exprObj = BSON("" << _exprObj); } else { // For a regex, we also need to wrap and treat like a primitive. _matcherOnPrimitive = true; _exprObj = _exprElt.wrap(""); } // Build the matcher around the object we built above. Currently, we do not allow // $pull operations to contain $where clauses, so preserving this behaviour. StatusWithMatchExpression parseResult = MatchExpressionParser::parse(_exprObj, MatchExpressionParser::WhereCallback()); if (!parseResult.isOK()) return parseResult.getStatus(); _matchExpr.reset(parseResult.getValue()); } return Status::OK(); } Status ModifierPull::prepare(mb::Element root, 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, str::stream() << "The positional operator did not find the match " "needed from the query. Unexpanded update: " << _fieldRef.dottedField()); } _fieldRef.setPart(_posDollar, matchedField); } // Locate the field name in 'root'. 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; if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { // If no target element exists, then there is nothing to do here. _preparedState->noOp = execInfo->noOp = true; return Status::OK(); } // This operation only applies to arrays if (_preparedState->elemFound.getType() != mongo::Array) return Status(ErrorCodes::BadValue, "Cannot apply $pull to a non-array value"); // If the array is empty, there is nothing to pull, so this is a noop. if (!_preparedState->elemFound.hasChildren()) { _preparedState->noOp = execInfo->noOp = true; return Status::OK(); } // Walk the values in the array mb::Element cursor = _preparedState->elemFound.leftChild(); while (cursor.ok()) { if (isMatch(cursor)) _preparedState->elementsToRemove.push_back(cursor); cursor = cursor.rightSibling(); } // If we didn't find any elements to add, then this is a no-op, and therefore in place. if (_preparedState->elementsToRemove.empty()) { _preparedState->noOp = execInfo->noOp = true; } return Status::OK(); } Status ModifierPull::apply() const { dassert(_preparedState->noOp == false); dassert(_preparedState->elemFound.ok() && _preparedState->idxFound == (_fieldRef.numParts() - 1)); std::vector::const_iterator where = _preparedState->elementsToRemove.begin(); const std::vector::const_iterator end = _preparedState->elementsToRemove.end(); for (; where != end; ++where) const_cast(*where).remove(); return Status::OK(); } Status ModifierPull::log(LogBuilder* logBuilder) const { mb::Document& doc = logBuilder->getDocument(); if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { // If we didn't find the element that we wanted to pull from, we log an unset for // that element. return logBuilder->addToUnsets(_fieldRef.dottedField()); } else { // TODO: This is copied more or less identically from $push. As a result, it copies the // behavior in $push that relies on 'apply' having been called unless this is a no-op. // TODO We can log just a positional unset in several cases. For now, let's just log // the full resulting array. // We'd like to create an entry such as {$set: {: []}} under // 'logRoot'. We start by creating the {$set: ...} Element. // Then we create the {:[]} Element, that is, an empty array. mb::Element logElement = doc.makeElementArray(_fieldRef.dottedField()); if (!logElement.ok()) { return Status(ErrorCodes::InternalError, "cannot create details for $pull mod"); } mb::Element curr = _preparedState->elemFound.leftChild(); while (curr.ok()) { dassert(curr.hasValue()); // We need to copy each array entry from the resulting document to the log // document. mb::Element currCopy = doc.makeElementWithNewFieldName(StringData(), curr.getValue()); if (!currCopy.ok()) { return Status(ErrorCodes::InternalError, "could create copy element"); } Status status = logElement.pushBack(currCopy); if (!status.isOK()) { return Status(ErrorCodes::BadValue, "could not append entry for $pull log"); } curr = curr.rightSibling(); } return logBuilder->addToSets(logElement); } } bool ModifierPull::isMatch(mutablebson::ConstElement element) { // TODO: We are assuming that 'element' hasValue is true. That might be OK if the // conflict detection logic will prevent us from ever seeing a deserialized element, // but are we sure about that? dassert(element.hasValue()); if (!_matchExpr) return (element.compareWithBSONElement(_exprElt, false) == 0); if (_matcherOnPrimitive) { // TODO: This is kinda slow. BSONObj candidate = element.getValue().wrap(""); return _matchExpr->matchesBSON(candidate); } if (element.getType() != Object) return false; return _matchExpr->matchesBSON(element.getValueObject()); } } // namespace mongo