diff options
Diffstat (limited to 'src/mongo/db/ops')
74 files changed, 12563 insertions, 13075 deletions
diff --git a/src/mongo/db/ops/delete.cpp b/src/mongo/db/ops/delete.cpp index ecf4f752873..86ed88395b5 100644 --- a/src/mongo/db/ops/delete.cpp +++ b/src/mongo/db/ops/delete.cpp @@ -38,41 +38,41 @@ namespace mongo { - /* ns: namespace, e.g. <database>.<collection> - pattern: the "where" clause / criteria - justOne: stop after 1 match - god: allow access to system namespaces, and don't yield - */ - long long deleteObjects(OperationContext* txn, - Database* db, - StringData ns, - BSONObj pattern, - PlanExecutor::YieldPolicy policy, - bool justOne, - bool god, - bool fromMigrate) { - NamespaceString nsString(ns); - DeleteRequest request(nsString); - request.setQuery(pattern); - request.setMulti(!justOne); - request.setGod(god); - request.setFromMigrate(fromMigrate); - request.setYieldPolicy(policy); +/* ns: namespace, e.g. <database>.<collection> + pattern: the "where" clause / criteria + justOne: stop after 1 match + god: allow access to system namespaces, and don't yield +*/ +long long deleteObjects(OperationContext* txn, + Database* db, + StringData ns, + BSONObj pattern, + PlanExecutor::YieldPolicy policy, + bool justOne, + bool god, + bool fromMigrate) { + NamespaceString nsString(ns); + DeleteRequest request(nsString); + request.setQuery(pattern); + request.setMulti(!justOne); + request.setGod(god); + request.setFromMigrate(fromMigrate); + request.setYieldPolicy(policy); - Collection* collection = NULL; - if (db) { - collection = db->getCollection(nsString.ns()); - } + Collection* collection = NULL; + if (db) { + collection = db->getCollection(nsString.ns()); + } - ParsedDelete parsedDelete(txn, &request); - uassertStatusOK(parsedDelete.parseRequest()); + ParsedDelete parsedDelete(txn, &request); + uassertStatusOK(parsedDelete.parseRequest()); - PlanExecutor* rawExec; - uassertStatusOK(getExecutorDelete(txn, collection, &parsedDelete, &rawExec)); - std::unique_ptr<PlanExecutor> exec(rawExec); + PlanExecutor* rawExec; + uassertStatusOK(getExecutorDelete(txn, collection, &parsedDelete, &rawExec)); + std::unique_ptr<PlanExecutor> exec(rawExec); - uassertStatusOK(exec->executePlan()); - return DeleteStage::getNumDeleted(exec.get()); - } + uassertStatusOK(exec->executePlan()); + return DeleteStage::getNumDeleted(exec.get()); +} } // namespace mongo diff --git a/src/mongo/db/ops/delete.h b/src/mongo/db/ops/delete.h index 563734428ab..b4b7345d70b 100644 --- a/src/mongo/db/ops/delete.h +++ b/src/mongo/db/ops/delete.h @@ -36,16 +36,15 @@ namespace mongo { - class Database; - class OperationContext; - - long long deleteObjects(OperationContext* txn, - Database* db, - StringData ns, - BSONObj pattern, - PlanExecutor::YieldPolicy policy, - bool justOne, - bool god = false, - bool fromMigrate = false); - +class Database; +class OperationContext; + +long long deleteObjects(OperationContext* txn, + Database* db, + StringData ns, + BSONObj pattern, + PlanExecutor::YieldPolicy policy, + bool justOne, + bool god = false, + bool fromMigrate = false); } diff --git a/src/mongo/db/ops/delete_request.h b/src/mongo/db/ops/delete_request.h index 27ea4f2bbb9..8ab27dab596 100644 --- a/src/mongo/db/ops/delete_request.h +++ b/src/mongo/db/ops/delete_request.h @@ -36,52 +36,91 @@ namespace mongo { - class DeleteRequest { - MONGO_DISALLOW_COPYING(DeleteRequest); - public: - explicit DeleteRequest(const NamespaceString& nsString) : - _nsString(nsString), - _multi(false), - _god(false), - _fromMigrate(false), - _isExplain(false), - _returnDeleted(false), - _yieldPolicy(PlanExecutor::YIELD_MANUAL) {} +class DeleteRequest { + MONGO_DISALLOW_COPYING(DeleteRequest); - void setQuery(const BSONObj& query) { _query = query; } - void setProj(const BSONObj& proj) { _proj = proj; } - void setSort(const BSONObj& sort) { _sort = sort; } - void setMulti(bool multi = true) { _multi = multi; } - void setGod(bool god = true) { _god = god; } - void setFromMigrate(bool fromMigrate = true) { _fromMigrate = fromMigrate; } - void setExplain(bool isExplain = true) { _isExplain = isExplain; } - void setReturnDeleted(bool returnDeleted = true) { _returnDeleted = returnDeleted; } - void setYieldPolicy(PlanExecutor::YieldPolicy yieldPolicy) { _yieldPolicy = yieldPolicy; } +public: + explicit DeleteRequest(const NamespaceString& nsString) + : _nsString(nsString), + _multi(false), + _god(false), + _fromMigrate(false), + _isExplain(false), + _returnDeleted(false), + _yieldPolicy(PlanExecutor::YIELD_MANUAL) {} - const NamespaceString& getNamespaceString() const { return _nsString; } - const BSONObj& getQuery() const { return _query; } - const BSONObj& getProj() const { return _proj; } - const BSONObj& getSort() const { return _sort; } - bool isMulti() const { return _multi; } - bool isGod() const { return _god; } - bool isFromMigrate() const { return _fromMigrate; } - bool isExplain() const { return _isExplain; } - bool shouldReturnDeleted() const { return _returnDeleted; } - PlanExecutor::YieldPolicy getYieldPolicy() const { return _yieldPolicy; } + void setQuery(const BSONObj& query) { + _query = query; + } + void setProj(const BSONObj& proj) { + _proj = proj; + } + void setSort(const BSONObj& sort) { + _sort = sort; + } + void setMulti(bool multi = true) { + _multi = multi; + } + void setGod(bool god = true) { + _god = god; + } + void setFromMigrate(bool fromMigrate = true) { + _fromMigrate = fromMigrate; + } + void setExplain(bool isExplain = true) { + _isExplain = isExplain; + } + void setReturnDeleted(bool returnDeleted = true) { + _returnDeleted = returnDeleted; + } + void setYieldPolicy(PlanExecutor::YieldPolicy yieldPolicy) { + _yieldPolicy = yieldPolicy; + } - std::string toString() const; + const NamespaceString& getNamespaceString() const { + return _nsString; + } + const BSONObj& getQuery() const { + return _query; + } + const BSONObj& getProj() const { + return _proj; + } + const BSONObj& getSort() const { + return _sort; + } + bool isMulti() const { + return _multi; + } + bool isGod() const { + return _god; + } + bool isFromMigrate() const { + return _fromMigrate; + } + bool isExplain() const { + return _isExplain; + } + bool shouldReturnDeleted() const { + return _returnDeleted; + } + PlanExecutor::YieldPolicy getYieldPolicy() const { + return _yieldPolicy; + } - private: - const NamespaceString& _nsString; - BSONObj _query; - BSONObj _proj; - BSONObj _sort; - bool _multi; - bool _god; - bool _fromMigrate; - bool _isExplain; - bool _returnDeleted; - PlanExecutor::YieldPolicy _yieldPolicy; - }; + std::string toString() const; + +private: + const NamespaceString& _nsString; + BSONObj _query; + BSONObj _proj; + BSONObj _sort; + bool _multi; + bool _god; + bool _fromMigrate; + bool _isExplain; + bool _returnDeleted; + PlanExecutor::YieldPolicy _yieldPolicy; +}; } // namespace mongo diff --git a/src/mongo/db/ops/field_checker.cpp b/src/mongo/db/ops/field_checker.cpp index da6607ac229..0c71c7e5d07 100644 --- a/src/mongo/db/ops/field_checker.cpp +++ b/src/mongo/db/ops/field_checker.cpp @@ -34,51 +34,50 @@ namespace mongo { - using mongoutils::str::stream; +using mongoutils::str::stream; namespace fieldchecker { - Status isUpdatable(const FieldRef& field) { - const size_t numParts = field.numParts(); +Status isUpdatable(const FieldRef& field) { + const size_t numParts = field.numParts(); - if (numParts == 0) { - return Status(ErrorCodes::EmptyFieldName, - "An empty update path is not valid."); - } + if (numParts == 0) { + return Status(ErrorCodes::EmptyFieldName, "An empty update path is not valid."); + } - for (size_t i = 0; i != numParts; ++i) { - const StringData part = field.getPart(i); + for (size_t i = 0; i != numParts; ++i) { + const StringData part = field.getPart(i); - if (part.empty()) { - return Status(ErrorCodes::EmptyFieldName, - mongoutils::str::stream() << "The update path '" - << field.dottedField() + if (part.empty()) { + return Status(ErrorCodes::EmptyFieldName, + mongoutils::str::stream() + << "The update path '" << field.dottedField() << "' contains an empty field name, which is not allowed."); - } } - - return Status::OK(); } - bool isPositional(const FieldRef& fieldRef, size_t* pos, size_t* count) { + return Status::OK(); +} - // 'count' is optional. - size_t dummy; - if (count == NULL) { - count = &dummy; - } +bool isPositional(const FieldRef& fieldRef, size_t* pos, size_t* count) { + // 'count' is optional. + size_t dummy; + if (count == NULL) { + count = &dummy; + } - *count = 0; - size_t size = fieldRef.numParts(); - for (size_t i=0; i<size; i++) { - StringData fieldPart = fieldRef.getPart(i); - if ((fieldPart.size() == 1) && (fieldPart[0] == '$')) { - if (*count == 0) *pos = i; - (*count)++; - } + *count = 0; + size_t size = fieldRef.numParts(); + for (size_t i = 0; i < size; i++) { + StringData fieldPart = fieldRef.getPart(i); + if ((fieldPart.size() == 1) && (fieldPart[0] == '$')) { + if (*count == 0) + *pos = i; + (*count)++; } - return *count > 0; } + return *count > 0; +} -} // namespace fieldchecker -} // namespace mongo +} // namespace fieldchecker +} // namespace mongo diff --git a/src/mongo/db/ops/field_checker.h b/src/mongo/db/ops/field_checker.h index 544ea131074..772034d4773 100644 --- a/src/mongo/db/ops/field_checker.h +++ b/src/mongo/db/ops/field_checker.h @@ -32,28 +32,28 @@ namespace mongo { - class FieldRef; - - namespace fieldchecker { - - /** - * Returns OK if all the below conditions on 'field' are valid: - * + Non-empty - * + Does not start or end with a '.' - * Otherwise returns a code indicating cause of failure. - */ - Status isUpdatable(const FieldRef& field); - - /** - * Returns true, the position 'pos' of the first $-sign if present in 'fieldRef', and - * how many other $-signs were found in 'count'. Otherwise return false. - * - * Note: - * isPositional assumes that the field is updatable. Call isUpdatable() above to - * verify. - */ - bool isPositional(const FieldRef& fieldRef, size_t* pos, size_t* count = NULL); - - } // namespace fieldchecker - -} // namespace mongo +class FieldRef; + +namespace fieldchecker { + +/** + * Returns OK if all the below conditions on 'field' are valid: + * + Non-empty + * + Does not start or end with a '.' + * Otherwise returns a code indicating cause of failure. + */ +Status isUpdatable(const FieldRef& field); + +/** + * Returns true, the position 'pos' of the first $-sign if present in 'fieldRef', and + * how many other $-signs were found in 'count'. Otherwise return false. + * + * Note: + * isPositional assumes that the field is updatable. Call isUpdatable() above to + * verify. + */ +bool isPositional(const FieldRef& fieldRef, size_t* pos, size_t* count = NULL); + +} // namespace fieldchecker + +} // namespace mongo diff --git a/src/mongo/db/ops/field_checker_test.cpp b/src/mongo/db/ops/field_checker_test.cpp index 470197e5e39..5ee608688e9 100644 --- a/src/mongo/db/ops/field_checker_test.cpp +++ b/src/mongo/db/ops/field_checker_test.cpp @@ -35,72 +35,72 @@ namespace { - using mongo::ErrorCodes; - using mongo::FieldRef; - using mongo::fieldchecker::isUpdatable; - using mongo::fieldchecker::isPositional; - using mongo::Status; - - TEST(IsUpdatable, Basics) { - FieldRef fieldRef("x"); - ASSERT_OK(isUpdatable(fieldRef)); - } - - TEST(IsUpdatable, DottedFields) { - FieldRef fieldRef("x.y.z"); - ASSERT_OK(isUpdatable(fieldRef)); - } - - TEST(IsUpdatable, EmptyFields) { - FieldRef fieldRef(""); - ASSERT_NOT_OK(isUpdatable(fieldRef)); - - FieldRef fieldRefDot("."); - ASSERT_NOT_OK(isUpdatable(fieldRefDot)); - - /* TODO: Re-enable after review - FieldRef fieldRefDollar; - fieldRefDollar.parse("$"); - ASSERT_NOT_OK(isUpdatable(fieldRefDollar)); +using mongo::ErrorCodes; +using mongo::FieldRef; +using mongo::fieldchecker::isUpdatable; +using mongo::fieldchecker::isPositional; +using mongo::Status; + +TEST(IsUpdatable, Basics) { + FieldRef fieldRef("x"); + ASSERT_OK(isUpdatable(fieldRef)); +} + +TEST(IsUpdatable, DottedFields) { + FieldRef fieldRef("x.y.z"); + ASSERT_OK(isUpdatable(fieldRef)); +} + +TEST(IsUpdatable, EmptyFields) { + FieldRef fieldRef(""); + ASSERT_NOT_OK(isUpdatable(fieldRef)); + + FieldRef fieldRefDot("."); + ASSERT_NOT_OK(isUpdatable(fieldRefDot)); + + /* TODO: Re-enable after review + FieldRef fieldRefDollar; + fieldRefDollar.parse("$"); + ASSERT_NOT_OK(isUpdatable(fieldRefDollar)); */ - FieldRef fieldRefADot("a."); - ASSERT_NOT_OK(isUpdatable(fieldRefADot)); - - FieldRef fieldRefDotB(".b"); - ASSERT_NOT_OK(isUpdatable(fieldRefDotB)); - - FieldRef fieldRefEmptyMiddle; - fieldRefEmptyMiddle.parse("a..b"); - ASSERT_NOT_OK(isUpdatable(fieldRefEmptyMiddle)); - } - - // Positional checks - TEST(isPositional, EntireArrayItem) { - FieldRef fieldRefPositional("a.$"); - size_t pos; - size_t count; - ASSERT_TRUE(isPositional(fieldRefPositional, &pos, &count)); - ASSERT_EQUALS(pos, 1u); - ASSERT_EQUALS(count, 1u); - } - - TEST(isPositional, ArraySubObject) { - FieldRef fieldRefPositional("a.$.b"); - size_t pos; - size_t count; - ASSERT_TRUE(isPositional(fieldRefPositional, &pos, &count)); - ASSERT_EQUALS(pos, 1u); - ASSERT_EQUALS(count, 1u); - } - - TEST(isPositional, MultiplePositional) { - FieldRef fieldRefPositional("a.$.b.$.c"); - size_t pos; - size_t count; - ASSERT_TRUE(isPositional(fieldRefPositional, &pos, &count)); - ASSERT_EQUALS(pos, 1u); - ASSERT_EQUALS(count, 2u); - } -} // unnamed namespace + FieldRef fieldRefADot("a."); + ASSERT_NOT_OK(isUpdatable(fieldRefADot)); + + FieldRef fieldRefDotB(".b"); + ASSERT_NOT_OK(isUpdatable(fieldRefDotB)); + + FieldRef fieldRefEmptyMiddle; + fieldRefEmptyMiddle.parse("a..b"); + ASSERT_NOT_OK(isUpdatable(fieldRefEmptyMiddle)); +} + +// Positional checks +TEST(isPositional, EntireArrayItem) { + FieldRef fieldRefPositional("a.$"); + size_t pos; + size_t count; + ASSERT_TRUE(isPositional(fieldRefPositional, &pos, &count)); + ASSERT_EQUALS(pos, 1u); + ASSERT_EQUALS(count, 1u); +} + +TEST(isPositional, ArraySubObject) { + FieldRef fieldRefPositional("a.$.b"); + size_t pos; + size_t count; + ASSERT_TRUE(isPositional(fieldRefPositional, &pos, &count)); + ASSERT_EQUALS(pos, 1u); + ASSERT_EQUALS(count, 1u); +} + +TEST(isPositional, MultiplePositional) { + FieldRef fieldRefPositional("a.$.b.$.c"); + size_t pos; + size_t count; + ASSERT_TRUE(isPositional(fieldRefPositional, &pos, &count)); + ASSERT_EQUALS(pos, 1u); + ASSERT_EQUALS(count, 2u); +} +} // unnamed namespace diff --git a/src/mongo/db/ops/insert.cpp b/src/mongo/db/ops/insert.cpp index 38a2a478043..827bdc69c39 100644 --- a/src/mongo/db/ops/insert.cpp +++ b/src/mongo/db/ops/insert.cpp @@ -34,174 +34,174 @@ namespace mongo { - using std::string; - - using namespace mongoutils; - - StatusWith<BSONObj> fixDocumentForInsert( const BSONObj& doc ) { - if ( doc.objsize() > BSONObjMaxUserSize ) - return StatusWith<BSONObj>( ErrorCodes::BadValue, - str::stream() - << "object to insert too large" - << ". size in bytes: " << doc.objsize() - << ", max size: " << BSONObjMaxUserSize ); - - bool firstElementIsId = doc.firstElement().fieldNameStringData() == "_id"; - bool hasTimestampToFix = false; - { - BSONObjIterator i( doc ); - while ( i.more() ) { - BSONElement e = i.next(); - - if ( e.type() == bsonTimestamp && e.timestampValue() == 0 ) { - // we replace Timestamp(0,0) at the top level with a correct value - // in the fast pass, we just mark that we want to swap - hasTimestampToFix = true; - } - - const char* fieldName = e.fieldName(); - - if ( fieldName[0] == '$' ) { - return StatusWith<BSONObj>( ErrorCodes::BadValue, - str::stream() - << "Document can't have $ prefixed field names: " - << e.fieldName() ); - } - - // check no regexp for _id (SERVER-9502) - // also, disallow undefined and arrays - if ( str::equals( fieldName, "_id") ) { - if ( e.type() == RegEx ) { - return StatusWith<BSONObj>( ErrorCodes::BadValue, - "can't use a regex for _id" ); - } - if ( e.type() == Undefined ) { - return StatusWith<BSONObj>( ErrorCodes::BadValue, - "can't use a undefined for _id" ); - } - if ( e.type() == Array ) { - return StatusWith<BSONObj>( ErrorCodes::BadValue, - "can't use an array for _id" ); - } - if ( e.type() == Object ) { - BSONObj o = e.Obj(); - Status s = o.storageValidEmbedded(); - if ( !s.isOK() ) - return StatusWith<BSONObj>( s ); - } - } +using std::string; + +using namespace mongoutils; + +StatusWith<BSONObj> fixDocumentForInsert(const BSONObj& doc) { + if (doc.objsize() > BSONObjMaxUserSize) + return StatusWith<BSONObj>(ErrorCodes::BadValue, + str::stream() << "object to insert too large" + << ". size in bytes: " << doc.objsize() + << ", max size: " << BSONObjMaxUserSize); + + bool firstElementIsId = doc.firstElement().fieldNameStringData() == "_id"; + bool hasTimestampToFix = false; + { + BSONObjIterator i(doc); + while (i.more()) { + BSONElement e = i.next(); + if (e.type() == bsonTimestamp && e.timestampValue() == 0) { + // we replace Timestamp(0,0) at the top level with a correct value + // in the fast pass, we just mark that we want to swap + hasTimestampToFix = true; } - } - - if ( firstElementIsId && !hasTimestampToFix ) - return StatusWith<BSONObj>( BSONObj() ); - bool hadId = firstElementIsId; + const char* fieldName = e.fieldName(); - BSONObjIterator i( doc ); - - BSONObjBuilder b( doc.objsize() + 16 ); - if ( firstElementIsId ) { - b.append( doc.firstElement() ); - i.next(); - } - else { - BSONElement e = doc["_id"]; - if ( e.type() ) { - b.append( e ); - hadId = true; + if (fieldName[0] == '$') { + return StatusWith<BSONObj>(ErrorCodes::BadValue, + str::stream() + << "Document can't have $ prefixed field names: " + << e.fieldName()); } - else { - b.appendOID( "_id", NULL, true ); - } - } - while ( i.more() ) { - BSONElement e = i.next(); - if ( hadId && e.fieldNameStringData() == "_id" ) { - // no-op - } - else if ( e.type() == bsonTimestamp && e.timestampValue() == 0 ) { - b.append( e.fieldName(), getNextGlobalTimestamp() ); - } - else { - b.append( e ); + // check no regexp for _id (SERVER-9502) + // also, disallow undefined and arrays + if (str::equals(fieldName, "_id")) { + if (e.type() == RegEx) { + return StatusWith<BSONObj>(ErrorCodes::BadValue, "can't use a regex for _id"); + } + if (e.type() == Undefined) { + return StatusWith<BSONObj>(ErrorCodes::BadValue, + "can't use a undefined for _id"); + } + if (e.type() == Array) { + return StatusWith<BSONObj>(ErrorCodes::BadValue, "can't use an array for _id"); + } + if (e.type() == Object) { + BSONObj o = e.Obj(); + Status s = o.storageValidEmbedded(); + if (!s.isOK()) + return StatusWith<BSONObj>(s); + } } } - return StatusWith<BSONObj>( b.obj() ); } - Status userAllowedWriteNS( StringData ns ) { - return userAllowedWriteNS( nsToDatabaseSubstring( ns ), nsToCollectionSubstring( ns ) ); - } + if (firstElementIsId && !hasTimestampToFix) + return StatusWith<BSONObj>(BSONObj()); - Status userAllowedWriteNS( const NamespaceString& ns ) { - return userAllowedWriteNS( ns.db(), ns.coll() ); - } + bool hadId = firstElementIsId; + + BSONObjIterator i(doc); - Status userAllowedWriteNS( StringData db, StringData coll ) { - if ( coll == "system.profile" ) { - return Status( ErrorCodes::BadValue, - str::stream() << "cannot write to '" << db << ".system.profile'" ); + BSONObjBuilder b(doc.objsize() + 16); + if (firstElementIsId) { + b.append(doc.firstElement()); + i.next(); + } else { + BSONElement e = doc["_id"]; + if (e.type()) { + b.append(e); + hadId = true; + } else { + b.appendOID("_id", NULL, true); } - return userAllowedCreateNS( db, coll ); } - Status userAllowedCreateNS( StringData db, StringData coll ) { - // validity checking - - if ( db.size() == 0 ) - return Status( ErrorCodes::BadValue, "db cannot be blank" ); - - if ( !NamespaceString::validDBName( db ) ) - return Status( ErrorCodes::BadValue, "invalid db name" ); - - if ( coll.size() == 0 ) - return Status( ErrorCodes::BadValue, "collection cannot be blank" ); - - if ( !NamespaceString::validCollectionName( coll ) ) - return Status( ErrorCodes::BadValue, "invalid collection name" ); - - if ( db.size() + 1 /* dot */ + coll.size() > NamespaceString::MaxNsCollectionLen ) - return Status( ErrorCodes::BadValue, - str::stream() - << "fully qualified namespace " << db << '.' << coll << " is too long " - << "(max is " << NamespaceString::MaxNsCollectionLen << " bytes)" ); + while (i.more()) { + BSONElement e = i.next(); + if (hadId && e.fieldNameStringData() == "_id") { + // no-op + } else if (e.type() == bsonTimestamp && e.timestampValue() == 0) { + b.append(e.fieldName(), getNextGlobalTimestamp()); + } else { + b.append(e); + } + } + return StatusWith<BSONObj>(b.obj()); +} - // check spceial areas +Status userAllowedWriteNS(StringData ns) { + return userAllowedWriteNS(nsToDatabaseSubstring(ns), nsToCollectionSubstring(ns)); +} - if ( db == "system" ) - return Status( ErrorCodes::BadValue, "cannot use 'system' database" ); +Status userAllowedWriteNS(const NamespaceString& ns) { + return userAllowedWriteNS(ns.db(), ns.coll()); +} +Status userAllowedWriteNS(StringData db, StringData coll) { + if (coll == "system.profile") { + return Status(ErrorCodes::BadValue, + str::stream() << "cannot write to '" << db << ".system.profile'"); + } + return userAllowedCreateNS(db, coll); +} - if ( coll.startsWith( "system." ) ) { - if ( coll == "system.indexes" ) return Status::OK(); - if ( coll == "system.js" ) return Status::OK(); - if ( coll == "system.profile" ) return Status::OK(); - if ( coll == "system.users" ) return Status::OK(); - if ( db == "admin" ) { - if ( coll == "system.version" ) return Status::OK(); - if ( coll == "system.roles" ) return Status::OK(); - if ( coll == "system.new_users" ) return Status::OK(); - if ( coll == "system.backup_users" ) return Status::OK(); - } - if ( db == "local" ) { - if ( coll == "system.replset" ) return Status::OK(); - } - return Status( ErrorCodes::BadValue, - str::stream() << "cannot write to '" << db << "." << coll << "'" ); +Status userAllowedCreateNS(StringData db, StringData coll) { + // validity checking + + if (db.size() == 0) + return Status(ErrorCodes::BadValue, "db cannot be blank"); + + if (!NamespaceString::validDBName(db)) + return Status(ErrorCodes::BadValue, "invalid db name"); + + if (coll.size() == 0) + return Status(ErrorCodes::BadValue, "collection cannot be blank"); + + if (!NamespaceString::validCollectionName(coll)) + return Status(ErrorCodes::BadValue, "invalid collection name"); + + if (db.size() + 1 /* dot */ + coll.size() > NamespaceString::MaxNsCollectionLen) + return Status(ErrorCodes::BadValue, + str::stream() + << "fully qualified namespace " << db << '.' << coll << " is too long " + << "(max is " << NamespaceString::MaxNsCollectionLen << " bytes)"); + + // check spceial areas + + if (db == "system") + return Status(ErrorCodes::BadValue, "cannot use 'system' database"); + + + if (coll.startsWith("system.")) { + if (coll == "system.indexes") + return Status::OK(); + if (coll == "system.js") + return Status::OK(); + if (coll == "system.profile") + return Status::OK(); + if (coll == "system.users") + return Status::OK(); + if (db == "admin") { + if (coll == "system.version") + return Status::OK(); + if (coll == "system.roles") + return Status::OK(); + if (coll == "system.new_users") + return Status::OK(); + if (coll == "system.backup_users") + return Status::OK(); } - - // some special rules - - if ( coll.find( ".system." ) != string::npos ) { - // this matches old (2.4 and older) behavior, but I'm not sure its a good idea - return Status( ErrorCodes::BadValue, - str::stream() << "cannot write to '" << db << "." << coll << "'" ); + if (db == "local") { + if (coll == "system.replset") + return Status::OK(); } + return Status(ErrorCodes::BadValue, + str::stream() << "cannot write to '" << db << "." << coll << "'"); + } - return Status::OK(); + // some special rules + + if (coll.find(".system.") != string::npos) { + // this matches old (2.4 and older) behavior, but I'm not sure its a good idea + return Status(ErrorCodes::BadValue, + str::stream() << "cannot write to '" << db << "." << coll << "'"); } + return Status::OK(); +} } diff --git a/src/mongo/db/ops/insert.h b/src/mongo/db/ops/insert.h index f55f8e80ea6..e0204c290c7 100644 --- a/src/mongo/db/ops/insert.h +++ b/src/mongo/db/ops/insert.h @@ -33,25 +33,24 @@ namespace mongo { - /** - * if doc is ok, then return is BSONObj() - * otherwise, BSONObj is what should be inserted instead - */ - StatusWith<BSONObj> fixDocumentForInsert( const BSONObj& doc ); - +/** + * if doc is ok, then return is BSONObj() + * otherwise, BSONObj is what should be inserted instead + */ +StatusWith<BSONObj> fixDocumentForInsert(const BSONObj& doc); - /** - * Returns Status::OK() if this namespace is valid for user write operations. If not, returns - * an error Status. - */ - Status userAllowedWriteNS( StringData db, StringData coll ); - Status userAllowedWriteNS( StringData ns ); - Status userAllowedWriteNS( const NamespaceString& ns ); - /** - * Returns Status::OK() if the namespace described by (db, coll) is valid for user create - * operations. If not, returns an error Status. - */ - Status userAllowedCreateNS( StringData db, StringData coll ); +/** + * Returns Status::OK() if this namespace is valid for user write operations. If not, returns + * an error Status. + */ +Status userAllowedWriteNS(StringData db, StringData coll); +Status userAllowedWriteNS(StringData ns); +Status userAllowedWriteNS(const NamespaceString& ns); +/** + * Returns Status::OK() if the namespace described by (db, coll) is valid for user create + * operations. If not, returns an error Status. + */ +Status userAllowedCreateNS(StringData db, StringData coll); } diff --git a/src/mongo/db/ops/log_builder.cpp b/src/mongo/db/ops/log_builder.cpp index 437c9056e5e..21baffe246c 100644 --- a/src/mongo/db/ops/log_builder.cpp +++ b/src/mongo/db/ops/log_builder.cpp @@ -31,143 +31,128 @@ namespace mongo { - using mutablebson::Document; - using mutablebson::Element; - namespace str = mongoutils::str; - - namespace { - const char kSet[] = "$set"; - const char kUnset[] = "$unset"; - } // namespace - - inline Status LogBuilder::addToSection(Element newElt, - Element* section, - const char* sectionName) { - - // If we don't already have this section, try to create it now. - if (!section->ok()) { - - // If we already have object replacement data, we can't also have section entries. - if (hasObjectReplacement()) - return Status( - ErrorCodes::IllegalOperation, - "LogBuilder: Invalid attempt to add a $set/$unset entry" - "to a log with an existing object replacement"); - - Document& doc = _logRoot.getDocument(); - - // We should not already have an element with the section name under the root. - dassert(_logRoot[sectionName] == doc.end()); - - // Construct a new object element to represent this section in the log. - const Element newElement = doc.makeElementObject(sectionName); - if (!newElement.ok()) - return Status(ErrorCodes::InternalError, - "LogBuilder: failed to construct Object Element for $set/$unset"); - - // Enqueue the new section under the root, and record it as our out parameter. - Status result = _logRoot.pushBack(newElement); - if (!result.isOK()) - return result; - *section = newElement; - - // Invalidate attempts to add an object replacement, now that we have a named - // section under the root. - _objectReplacementAccumulator = doc.end(); - } - - // Whatever transpired, we should now have an ok accumulator for the section, and not - // have a replacement accumulator. - dassert(section->ok()); - dassert(!_objectReplacementAccumulator.ok()); - - // Enqueue the provided element to the section and propagate the result. - return section->pushBack(newElt); - } - - Status LogBuilder::addToSets(Element elt) { - return addToSection(elt, &_setAccumulator, kSet); - } +using mutablebson::Document; +using mutablebson::Element; +namespace str = mongoutils::str; + +namespace { +const char kSet[] = "$set"; +const char kUnset[] = "$unset"; +} // namespace + +inline Status LogBuilder::addToSection(Element newElt, Element* section, const char* sectionName) { + // If we don't already have this section, try to create it now. + if (!section->ok()) { + // If we already have object replacement data, we can't also have section entries. + if (hasObjectReplacement()) + return Status(ErrorCodes::IllegalOperation, + "LogBuilder: Invalid attempt to add a $set/$unset entry" + "to a log with an existing object replacement"); - Status LogBuilder::addToSetsWithNewFieldName(StringData name, - const mutablebson::Element val) { - mutablebson::Element elemToSet = - _logRoot.getDocument().makeElementWithNewFieldName(name, val); - if (!elemToSet.ok()) - return Status(ErrorCodes::InternalError, - str::stream() << "Could not create new '" - << name << "' element from existing element '" - << val.getFieldName() << "' of type " - << typeName(val.getType())); + Document& doc = _logRoot.getDocument(); - return addToSets(elemToSet); - } + // We should not already have an element with the section name under the root. + dassert(_logRoot[sectionName] == doc.end()); - Status LogBuilder::addToSetsWithNewFieldName(StringData name, - const BSONElement& val){ - mutablebson::Element elemToSet = - _logRoot.getDocument().makeElementWithNewFieldName(name, val); - if (!elemToSet.ok()) + // Construct a new object element to represent this section in the log. + const Element newElement = doc.makeElementObject(sectionName); + if (!newElement.ok()) return Status(ErrorCodes::InternalError, - str::stream() << "Could not create new '" - << name << "' element from existing element '" - << val.fieldName() << "' of type " - << typeName(val.type())); + "LogBuilder: failed to construct Object Element for $set/$unset"); - return addToSets(elemToSet); - } + // Enqueue the new section under the root, and record it as our out parameter. + Status result = _logRoot.pushBack(newElement); + if (!result.isOK()) + return result; + *section = newElement; - Status LogBuilder::addToSets(StringData name, const SafeNum& val){ - mutablebson::Element elemToSet = _logRoot.getDocument().makeElementSafeNum(name, val); - if (!elemToSet.ok()) - return Status(ErrorCodes::InternalError, - str::stream() << "Could not create new '" - << name << "' SafeNum from " - << val.debugString()); - - return addToSets(elemToSet); + // Invalidate attempts to add an object replacement, now that we have a named + // section under the root. + _objectReplacementAccumulator = doc.end(); } - Status LogBuilder::addToUnsets(StringData path) { - mutablebson::Element logElement = _logRoot.getDocument().makeElementBool(path, true); - if (!logElement.ok()) - return Status(ErrorCodes::InternalError, - str::stream() << "Cannot create $unset oplog entry for path" << path); - - return addToSection(logElement, &_unsetAccumulator, kUnset); + // Whatever transpired, we should now have an ok accumulator for the section, and not + // have a replacement accumulator. + dassert(section->ok()); + dassert(!_objectReplacementAccumulator.ok()); + + // Enqueue the provided element to the section and propagate the result. + return section->pushBack(newElt); +} + +Status LogBuilder::addToSets(Element elt) { + return addToSection(elt, &_setAccumulator, kSet); +} + +Status LogBuilder::addToSetsWithNewFieldName(StringData name, const mutablebson::Element val) { + mutablebson::Element elemToSet = _logRoot.getDocument().makeElementWithNewFieldName(name, val); + if (!elemToSet.ok()) + return Status(ErrorCodes::InternalError, + str::stream() << "Could not create new '" << name + << "' element from existing element '" << val.getFieldName() + << "' of type " << typeName(val.getType())); + + return addToSets(elemToSet); +} + +Status LogBuilder::addToSetsWithNewFieldName(StringData name, const BSONElement& val) { + mutablebson::Element elemToSet = _logRoot.getDocument().makeElementWithNewFieldName(name, val); + if (!elemToSet.ok()) + return Status(ErrorCodes::InternalError, + str::stream() << "Could not create new '" << name + << "' element from existing element '" << val.fieldName() + << "' of type " << typeName(val.type())); + + return addToSets(elemToSet); +} + +Status LogBuilder::addToSets(StringData name, const SafeNum& val) { + mutablebson::Element elemToSet = _logRoot.getDocument().makeElementSafeNum(name, val); + if (!elemToSet.ok()) + return Status(ErrorCodes::InternalError, + str::stream() << "Could not create new '" << name << "' SafeNum from " + << val.debugString()); + + return addToSets(elemToSet); +} + +Status LogBuilder::addToUnsets(StringData path) { + mutablebson::Element logElement = _logRoot.getDocument().makeElementBool(path, true); + if (!logElement.ok()) + return Status(ErrorCodes::InternalError, + str::stream() << "Cannot create $unset oplog entry for path" << path); + + return addToSection(logElement, &_unsetAccumulator, kUnset); +} + +Status LogBuilder::getReplacementObject(Element* outElt) { + // If the replacement accumulator is not ok, we must have started a $set or $unset + // already, so an object replacement is not permitted. + if (!_objectReplacementAccumulator.ok()) { + dassert(_setAccumulator.ok() || _unsetAccumulator.ok()); + return Status(ErrorCodes::IllegalOperation, + "LogBuilder: Invalid attempt to obtain the object replacement slot " + "for a log containing $set or $unset entries"); } - Status LogBuilder::getReplacementObject(Element* outElt) { + if (hasObjectReplacement()) + return Status(ErrorCodes::IllegalOperation, + "LogBuilder: Invalid attempt to acquire the replacement object " + "in a log with existing object replacement data"); - // If the replacement accumulator is not ok, we must have started a $set or $unset - // already, so an object replacement is not permitted. - if (!_objectReplacementAccumulator.ok()) { - dassert(_setAccumulator.ok() || _unsetAccumulator.ok()); - return Status( - ErrorCodes::IllegalOperation, - "LogBuilder: Invalid attempt to obtain the object replacement slot " - "for a log containing $set or $unset entries"); - } + // OK to enqueue object replacement items. + *outElt = _objectReplacementAccumulator; + return Status::OK(); +} - if (hasObjectReplacement()) - return Status( - ErrorCodes::IllegalOperation, - "LogBuilder: Invalid attempt to acquire the replacement object " - "in a log with existing object replacement data"); - - // OK to enqueue object replacement items. - *outElt = _objectReplacementAccumulator; - return Status::OK(); - } - - inline bool LogBuilder::hasObjectReplacement() const { - if (!_objectReplacementAccumulator.ok()) - return false; +inline bool LogBuilder::hasObjectReplacement() const { + if (!_objectReplacementAccumulator.ok()) + return false; - dassert(!_setAccumulator.ok()); - dassert(!_unsetAccumulator.ok()); + dassert(!_setAccumulator.ok()); + dassert(!_unsetAccumulator.ok()); - return _objectReplacementAccumulator.hasChildren(); - } + return _objectReplacementAccumulator.hasChildren(); +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/log_builder.h b/src/mongo/db/ops/log_builder.h index d35f48846f4..945ba18cf8f 100644 --- a/src/mongo/db/ops/log_builder.h +++ b/src/mongo/db/ops/log_builder.h @@ -33,90 +33,89 @@ namespace mongo { - /** LogBuilder abstracts away some of the details of producing a properly constructed oplog - * update entry. It manages separate regions into which it accumulates $set and $unset - * operations, and distinguishes object replacement style oplog generation from - * $set/$unset style generation and prevents admixture. +/** LogBuilder abstracts away some of the details of producing a properly constructed oplog + * update entry. It manages separate regions into which it accumulates $set and $unset + * operations, and distinguishes object replacement style oplog generation from + * $set/$unset style generation and prevents admixture. + */ +class LogBuilder { +public: + /** Construct a new LogBuilder. Log entries will be recorded as new children under the + * 'logRoot' Element, which must be of type mongo::Object and have no children. */ - class LogBuilder { - public: - /** Construct a new LogBuilder. Log entries will be recorded as new children under the - * 'logRoot' Element, which must be of type mongo::Object and have no children. - */ - inline LogBuilder(mutablebson::Element logRoot) - : _logRoot(logRoot) - , _objectReplacementAccumulator(_logRoot) - , _setAccumulator(_logRoot.getDocument().end()) - , _unsetAccumulator(_setAccumulator) { - dassert(logRoot.isType(mongo::Object)); - dassert(!logRoot.hasChildren()); - } + inline LogBuilder(mutablebson::Element logRoot) + : _logRoot(logRoot), + _objectReplacementAccumulator(_logRoot), + _setAccumulator(_logRoot.getDocument().end()), + _unsetAccumulator(_setAccumulator) { + dassert(logRoot.isType(mongo::Object)); + dassert(!logRoot.hasChildren()); + } - /** Return the Document to which the logging root belongs. */ - inline mutablebson::Document& getDocument() { - return _logRoot.getDocument(); - } + /** Return the Document to which the logging root belongs. */ + inline mutablebson::Document& getDocument() { + return _logRoot.getDocument(); + } - /** Add the given Element as a new entry in the '$set' section of the log. If a $set - * section does not yet exist, it will be created. If this LogBuilder is currently - * configured to contain an object replacement, the request to add to the $set section - * will return an Error. - */ - Status addToSets(mutablebson::Element elt); + /** Add the given Element as a new entry in the '$set' section of the log. If a $set + * section does not yet exist, it will be created. If this LogBuilder is currently + * configured to contain an object replacement, the request to add to the $set section + * will return an Error. + */ + Status addToSets(mutablebson::Element elt); - /** - * Convenience method which calls addToSets after - * creating a new Element to wrap the SafeNum value. - * - * If any problem occurs then the operation will stop and return that error Status. - */ - Status addToSets(StringData name, const SafeNum& val); + /** + * Convenience method which calls addToSets after + * creating a new Element to wrap the SafeNum value. + * + * If any problem occurs then the operation will stop and return that error Status. + */ + Status addToSets(StringData name, const SafeNum& val); - /** - * Convenience method which calls addToSets after - * creating a new Element to wrap the old one. - * - * If any problem occurs then the operation will stop and return that error Status. - */ - Status addToSetsWithNewFieldName(StringData name, const mutablebson::Element val); + /** + * Convenience method which calls addToSets after + * creating a new Element to wrap the old one. + * + * If any problem occurs then the operation will stop and return that error Status. + */ + Status addToSetsWithNewFieldName(StringData name, const mutablebson::Element val); - /** - * Convenience method which calls addToSets after - * creating a new Element to wrap the old one. - * - * If any problem occurs then the operation will stop and return that error Status. - */ - Status addToSetsWithNewFieldName(StringData name, const BSONElement& val); + /** + * Convenience method which calls addToSets after + * creating a new Element to wrap the old one. + * + * If any problem occurs then the operation will stop and return that error Status. + */ + Status addToSetsWithNewFieldName(StringData name, const BSONElement& val); - /** Add the given path as a new entry in the '$unset' section of the log. If an - * '$unset' section does not yet exist, it will be created. If this LogBuilder is - * currently configured to contain an object replacement, the request to add to the - * $unset section will return an Error. - */ - Status addToUnsets(StringData path); + /** Add the given path as a new entry in the '$unset' section of the log. If an + * '$unset' section does not yet exist, it will be created. If this LogBuilder is + * currently configured to contain an object replacement, the request to add to the + * $unset section will return an Error. + */ + Status addToUnsets(StringData path); - /** Obtain, via the out parameter 'outElt', a pointer to the mongo::Object type Element - * to which the components of an object replacement should be recorded. It is an error - * to call this if any Elements have been added by calling either addToSets or - * addToUnsets, and attempts to do so will return a non-OK Status. Similarly, if there - * is already object replacement data recorded for this log, the call will fail. - */ - Status getReplacementObject(mutablebson::Element* outElt); + /** Obtain, via the out parameter 'outElt', a pointer to the mongo::Object type Element + * to which the components of an object replacement should be recorded. It is an error + * to call this if any Elements have been added by calling either addToSets or + * addToUnsets, and attempts to do so will return a non-OK Status. Similarly, if there + * is already object replacement data recorded for this log, the call will fail. + */ + Status getReplacementObject(mutablebson::Element* outElt); - private: - // Returns true if the object replacement accumulator is valid and has children, false - // otherwise. - inline bool hasObjectReplacement() const; +private: + // Returns true if the object replacement accumulator is valid and has children, false + // otherwise. + inline bool hasObjectReplacement() const; - inline Status addToSection( - mutablebson::Element newElt, - mutablebson::Element* section, - const char* sectionName); + inline Status addToSection(mutablebson::Element newElt, + mutablebson::Element* section, + const char* sectionName); - mutablebson::Element _logRoot; - mutablebson::Element _objectReplacementAccumulator; - mutablebson::Element _setAccumulator; - mutablebson::Element _unsetAccumulator; - }; + mutablebson::Element _logRoot; + mutablebson::Element _objectReplacementAccumulator; + mutablebson::Element _setAccumulator; + mutablebson::Element _unsetAccumulator; +}; -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/log_builder_test.cpp b/src/mongo/db/ops/log_builder_test.cpp index 253dd70e5a1..f2a3d20aa78 100644 --- a/src/mongo/db/ops/log_builder_test.cpp +++ b/src/mongo/db/ops/log_builder_test.cpp @@ -36,243 +36,240 @@ namespace { - namespace mmb = mongo::mutablebson; - using mongo::LogBuilder; +namespace mmb = mongo::mutablebson; +using mongo::LogBuilder; - TEST(LogBuilder, Initialization) { - mmb::Document doc; - LogBuilder lb(doc.root()); - ASSERT_EQUALS(&doc, &lb.getDocument()); - } +TEST(LogBuilder, Initialization) { + mmb::Document doc; + LogBuilder lb(doc.root()); + ASSERT_EQUALS(&doc, &lb.getDocument()); +} - TEST(LogBuilder, AddOneToSet) { - mmb::Document doc; - LogBuilder lb(doc.root()); +TEST(LogBuilder, AddOneToSet) { + mmb::Document doc; + LogBuilder lb(doc.root()); - const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); - ASSERT_TRUE(elt_ab.ok()); - ASSERT_OK(lb.addToSets(elt_ab)); + const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); + ASSERT_TRUE(elt_ab.ok()); + ASSERT_OK(lb.addToSets(elt_ab)); - ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); - } + ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); +} - TEST(LogBuilder, AddElementToSet) { - mmb::Document doc; - LogBuilder lb(doc.root()); +TEST(LogBuilder, AddElementToSet) { + mmb::Document doc; + LogBuilder lb(doc.root()); - const mmb::Element elt_ab = doc.makeElementInt("", 1); - ASSERT_TRUE(elt_ab.ok()); - ASSERT_OK(lb.addToSetsWithNewFieldName("a.b", elt_ab)); + const mmb::Element elt_ab = doc.makeElementInt("", 1); + ASSERT_TRUE(elt_ab.ok()); + ASSERT_OK(lb.addToSetsWithNewFieldName("a.b", elt_ab)); - ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); - } + ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); +} - TEST(LogBuilder, AddBSONElementToSet) { - mmb::Document doc; - LogBuilder lb(doc.root()); +TEST(LogBuilder, AddBSONElementToSet) { + mmb::Document doc; + LogBuilder lb(doc.root()); - mongo::BSONObj obj = mongo::fromjson("{'':1}"); - - ASSERT_OK(lb.addToSetsWithNewFieldName("a.b", obj.firstElement())); + mongo::BSONObj obj = mongo::fromjson("{'':1}"); - ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); - } + ASSERT_OK(lb.addToSetsWithNewFieldName("a.b", obj.firstElement())); - TEST(LogBuilder, AddSafeNumToSet) { - mmb::Document doc; - LogBuilder lb(doc.root()); + ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); +} - mongo::BSONObj obj = mongo::fromjson("{'':1}"); - - ASSERT_OK(lb.addToSets("a.b", mongo::SafeNum(1))); - - ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); - } - - TEST(LogBuilder, AddOneToUnset) { - mmb::Document doc; - LogBuilder lb(doc.root()); - ASSERT_OK(lb.addToUnsets("x.y")); - ASSERT_EQUALS(mongo::fromjson("{ $unset : { 'x.y' : true } }"), doc); - } - - TEST(LogBuilder, AddOneToEach) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); - ASSERT_TRUE(elt_ab.ok()); - ASSERT_OK(lb.addToSets(elt_ab)); - - ASSERT_OK(lb.addToUnsets("x.y")); - - ASSERT_EQUALS( - mongo::fromjson( - "{ " - " $set : { 'a.b' : 1 }, " - " $unset : { 'x.y' : true } " - "}" - ), doc); - } - - TEST(LogBuilder, AddOneObjectReplacementEntry) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - ASSERT_TRUE(replacement.isType(mongo::Object)); - - const mmb::Element elt_a = doc.makeElementInt("a", 1); - ASSERT_TRUE(elt_a.ok()); - ASSERT_OK(replacement.pushBack(elt_a)); - - ASSERT_EQUALS(mongo::fromjson("{ a : 1 }"), doc); - } - - TEST(LogBuilder, AddTwoObjectReplacementEntry) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - ASSERT_TRUE(replacement.isType(mongo::Object)); - - const mmb::Element elt_a = doc.makeElementInt("a", 1); - ASSERT_TRUE(elt_a.ok()); - ASSERT_OK(replacement.pushBack(elt_a)); - - const mmb::Element elt_b = doc.makeElementInt("b", 2); - ASSERT_TRUE(elt_b.ok()); - ASSERT_OK(replacement.pushBack(elt_b)); - - ASSERT_EQUALS(mongo::fromjson("{ a : 1, b: 2 }"), doc); - } - - TEST(LogBuilder, VerifySetsAreGrouped) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); - ASSERT_TRUE(elt_ab.ok()); - ASSERT_OK(lb.addToSets(elt_ab)); - - const mmb::Element elt_xy = doc.makeElementInt("x.y", 1); - ASSERT_TRUE(elt_xy.ok()); - ASSERT_OK(lb.addToSets(elt_xy)); - - ASSERT_EQUALS( - mongo::fromjson( - "{ $set : {" - " 'a.b' : 1, " - " 'x.y' : 1 " - "} }" - ), doc); - } - - TEST(LogBuilder, VerifyUnsetsAreGrouped) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - ASSERT_OK(lb.addToUnsets("a.b")); - ASSERT_OK(lb.addToUnsets("x.y")); - - ASSERT_EQUALS( - mongo::fromjson( - "{ $unset : {" - " 'a.b' : true, " - " 'x.y' : true " - "} }" - ), doc); - } - - TEST(LogBuilder, PresenceOfSetPreventsObjectReplacement) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - - const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); - ASSERT_TRUE(elt_ab.ok()); - ASSERT_OK(lb.addToSets(elt_ab)); - - replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_NOT_OK(lb.getReplacementObject(&replacement)); - ASSERT_FALSE(replacement.ok()); - } - - TEST(LogBuilder, PresenceOfUnsetPreventsObjectReplacement) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - - const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); - ASSERT_TRUE(elt_ab.ok()); - ASSERT_OK(lb.addToSets(elt_ab)); - - replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_NOT_OK(lb.getReplacementObject(&replacement)); - ASSERT_FALSE(replacement.ok()); - } - - TEST(LogBuilder, CantAddSetWithObjectReplacementDataPresent) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - ASSERT_OK(replacement.appendInt("a", 1)); - - mmb::Element setCandidate = doc.makeElementInt("x", 0); - ASSERT_NOT_OK(lb.addToSets(setCandidate)); - } - - TEST(LogBuilder, CantAddUnsetWithObjectReplacementDataPresent) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - ASSERT_OK(replacement.appendInt("a", 1)); - - ASSERT_NOT_OK(lb.addToUnsets("x")); - } - - // Ensure that once you have obtained the object replacement slot and mutated it, that the - // object replacement slot becomes in accessible. This is a bit paranoid, since in practice - // the modifier conflict detection logic should prevent that outcome at a higher level, but - // preventing it here costs us nothing and add an extra safety check. - TEST(LogBuilder, CantReacquireObjectReplacementData) { - mmb::Document doc; - LogBuilder lb(doc.root()); - - mmb::Element replacement = doc.end(); - ASSERT_FALSE(replacement.ok()); - ASSERT_OK(lb.getReplacementObject(&replacement)); - ASSERT_TRUE(replacement.ok()); - ASSERT_OK(replacement.appendInt("a", 1)); - - mmb::Element again = doc.end(); - ASSERT_FALSE(again.ok()); - ASSERT_NOT_OK(lb.getReplacementObject(&again)); - ASSERT_FALSE(again.ok()); - } - -} // namespace +TEST(LogBuilder, AddSafeNumToSet) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mongo::BSONObj obj = mongo::fromjson("{'':1}"); + + ASSERT_OK(lb.addToSets("a.b", mongo::SafeNum(1))); + + ASSERT_EQUALS(mongo::fromjson("{ $set : { 'a.b' : 1 } }"), doc); +} + +TEST(LogBuilder, AddOneToUnset) { + mmb::Document doc; + LogBuilder lb(doc.root()); + ASSERT_OK(lb.addToUnsets("x.y")); + ASSERT_EQUALS(mongo::fromjson("{ $unset : { 'x.y' : true } }"), doc); +} + +TEST(LogBuilder, AddOneToEach) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); + ASSERT_TRUE(elt_ab.ok()); + ASSERT_OK(lb.addToSets(elt_ab)); + + ASSERT_OK(lb.addToUnsets("x.y")); + + ASSERT_EQUALS(mongo::fromjson( + "{ " + " $set : { 'a.b' : 1 }, " + " $unset : { 'x.y' : true } " + "}"), + doc); +} + +TEST(LogBuilder, AddOneObjectReplacementEntry) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + ASSERT_TRUE(replacement.isType(mongo::Object)); + + const mmb::Element elt_a = doc.makeElementInt("a", 1); + ASSERT_TRUE(elt_a.ok()); + ASSERT_OK(replacement.pushBack(elt_a)); + + ASSERT_EQUALS(mongo::fromjson("{ a : 1 }"), doc); +} + +TEST(LogBuilder, AddTwoObjectReplacementEntry) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + ASSERT_TRUE(replacement.isType(mongo::Object)); + + const mmb::Element elt_a = doc.makeElementInt("a", 1); + ASSERT_TRUE(elt_a.ok()); + ASSERT_OK(replacement.pushBack(elt_a)); + + const mmb::Element elt_b = doc.makeElementInt("b", 2); + ASSERT_TRUE(elt_b.ok()); + ASSERT_OK(replacement.pushBack(elt_b)); + + ASSERT_EQUALS(mongo::fromjson("{ a : 1, b: 2 }"), doc); +} + +TEST(LogBuilder, VerifySetsAreGrouped) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); + ASSERT_TRUE(elt_ab.ok()); + ASSERT_OK(lb.addToSets(elt_ab)); + + const mmb::Element elt_xy = doc.makeElementInt("x.y", 1); + ASSERT_TRUE(elt_xy.ok()); + ASSERT_OK(lb.addToSets(elt_xy)); + + ASSERT_EQUALS(mongo::fromjson( + "{ $set : {" + " 'a.b' : 1, " + " 'x.y' : 1 " + "} }"), + doc); +} + +TEST(LogBuilder, VerifyUnsetsAreGrouped) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + ASSERT_OK(lb.addToUnsets("a.b")); + ASSERT_OK(lb.addToUnsets("x.y")); + + ASSERT_EQUALS(mongo::fromjson( + "{ $unset : {" + " 'a.b' : true, " + " 'x.y' : true " + "} }"), + doc); +} + +TEST(LogBuilder, PresenceOfSetPreventsObjectReplacement) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + + const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); + ASSERT_TRUE(elt_ab.ok()); + ASSERT_OK(lb.addToSets(elt_ab)); + + replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_NOT_OK(lb.getReplacementObject(&replacement)); + ASSERT_FALSE(replacement.ok()); +} + +TEST(LogBuilder, PresenceOfUnsetPreventsObjectReplacement) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + + const mmb::Element elt_ab = doc.makeElementInt("a.b", 1); + ASSERT_TRUE(elt_ab.ok()); + ASSERT_OK(lb.addToSets(elt_ab)); + + replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_NOT_OK(lb.getReplacementObject(&replacement)); + ASSERT_FALSE(replacement.ok()); +} + +TEST(LogBuilder, CantAddSetWithObjectReplacementDataPresent) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + ASSERT_OK(replacement.appendInt("a", 1)); + + mmb::Element setCandidate = doc.makeElementInt("x", 0); + ASSERT_NOT_OK(lb.addToSets(setCandidate)); +} + +TEST(LogBuilder, CantAddUnsetWithObjectReplacementDataPresent) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + ASSERT_OK(replacement.appendInt("a", 1)); + + ASSERT_NOT_OK(lb.addToUnsets("x")); +} + +// Ensure that once you have obtained the object replacement slot and mutated it, that the +// object replacement slot becomes in accessible. This is a bit paranoid, since in practice +// the modifier conflict detection logic should prevent that outcome at a higher level, but +// preventing it here costs us nothing and add an extra safety check. +TEST(LogBuilder, CantReacquireObjectReplacementData) { + mmb::Document doc; + LogBuilder lb(doc.root()); + + mmb::Element replacement = doc.end(); + ASSERT_FALSE(replacement.ok()); + ASSERT_OK(lb.getReplacementObject(&replacement)); + ASSERT_TRUE(replacement.ok()); + ASSERT_OK(replacement.appendInt("a", 1)); + + mmb::Element again = doc.end(); + ASSERT_FALSE(again.ok()); + ASSERT_NOT_OK(lb.getReplacementObject(&again)); + ASSERT_FALSE(again.ok()); +} + +} // namespace diff --git a/src/mongo/db/ops/modifier_add_to_set.cpp b/src/mongo/db/ops/modifier_add_to_set.cpp index 381d06632e3..383991c2b1f 100644 --- a/src/mongo/db/ops/modifier_add_to_set.cpp +++ b/src/mongo/db/ops/modifier_add_to_set.cpp @@ -37,155 +37,145 @@ namespace mongo { - namespace mb = mutablebson; - namespace str = mongoutils::str; - - namespace { - - template<typename Ordering, typename Equality> - void deduplicate(mb::Element parent, Ordering comp, Equality equal) { - - // First, build a vector of the children. - std::vector<mb::Element> children; - mb::Element current = parent.leftChild(); - while (current.ok()) { - children.push_back(current); - current = current.rightSibling(); - } +namespace mb = mutablebson; +namespace str = mongoutils::str; + +namespace { + +template <typename Ordering, typename Equality> +void deduplicate(mb::Element parent, Ordering comp, Equality equal) { + // First, build a vector of the children. + std::vector<mb::Element> children; + mb::Element current = parent.leftChild(); + while (current.ok()) { + children.push_back(current); + current = current.rightSibling(); + } - // Then, sort the child vector with our comparator. - std::sort(children.begin(), children.end(), comp); + // Then, sort the child vector with our comparator. + std::sort(children.begin(), children.end(), comp); - // Next, remove duplicates by walking the vector. - std::vector<mb::Element>::iterator where = children.begin(); - const std::vector<mb::Element>::iterator end = children.end(); + // Next, remove duplicates by walking the vector. + std::vector<mb::Element>::iterator where = children.begin(); + const std::vector<mb::Element>::iterator end = children.end(); - while( where != end ) { - std::vector<mb::Element>::iterator next = where; ++next; - while (next != end && equal(*where, *next)) { - next->remove(); - ++next; - } - where = next; - } + while (where != end) { + std::vector<mb::Element>::iterator next = where; + ++next; + while (next != end && equal(*where, *next)) { + next->remove(); + ++next; } + where = next; + } +} - } // namespace +} // namespace - struct ModifierAddToSet::PreparedState { +struct ModifierAddToSet::PreparedState { + PreparedState(mb::Document& doc) + : doc(doc), + idxFound(0), + elemFound(doc.end()), + addAll(false), + elementsToAdd(), + noOp(false) {} - PreparedState(mb::Document& doc) - : doc(doc) - , idxFound(0) - , elemFound(doc.end()) - , addAll(false) - , elementsToAdd() - , noOp(false) { - } + // Document that is going to be changed. + mb::Document& doc; - // Document that is going to be changed. - mb::Document& doc; + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + // Element corresponding to _fieldRef[0.._idxFound]. + mb::Element elemFound; - // Element corresponding to _fieldRef[0.._idxFound]. - mb::Element elemFound; + // Are we adding all of the $each elements, or just a subset? + bool addAll; - // Are we adding all of the $each elements, or just a subset? - bool addAll; + // Values to be applied. + std::vector<mb::Element> elementsToAdd; - // Values to be applied. - std::vector<mb::Element> elementsToAdd; + // True if this update is a no-op + bool noOp; +}; - // True if this update is a no-op - bool noOp; - }; +ModifierAddToSet::ModifierAddToSet() + : ModifierInterface(), _fieldRef(), _posDollar(0), _valDoc(), _val(_valDoc.end()) {} - ModifierAddToSet::ModifierAddToSet() - : ModifierInterface () - , _fieldRef() - , _posDollar(0) - , _valDoc() - , _val(_valDoc.end()) { - } +ModifierAddToSet::~ModifierAddToSet() {} - ModifierAddToSet::~ModifierAddToSet() { +Status ModifierAddToSet::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; } - Status ModifierAddToSet::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { + // 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); - // Perform standard field name and updateable checks. - _fieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fieldRef); - if (! status.isOK()) { - return status; - } + if (positional) + *positional = foundDollar; - // 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 (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); + } - if (positional) - *positional = foundDollar; + // TODO: The driver could potentially do this re-writing. - if (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); - } - - // TODO: The driver could potentially do this re-writing. - - // If the type of the value is 'Object', we might be dealing with a $each. See if that - // is the case. - if (modExpr.type() == mongo::Object) { - BSONElement modExprObjPayload = modExpr.embeddedObject().firstElement(); - if (!modExprObjPayload.eoo() && StringData(modExprObjPayload.fieldName()) == "$each") { - // It is a $each. Verify that the payload is an array as is required for $each, - // set our flag, and store the array as our value. - if (modExprObjPayload.type() != mongo::Array) { - return Status(ErrorCodes::BadValue, - str::stream() << "The argument to $each in $addToSet must " - "be an array but it was of type " - << typeName(modExprObjPayload.type())); - } - - status = _valDoc.root().appendElement(modExprObjPayload); - if (!status.isOK()) - return status; - - _val = _valDoc.root().leftChild(); - - deduplicate(_val, mb::woLess(false), mb::woEqual(false)); + // If the type of the value is 'Object', we might be dealing with a $each. See if that + // is the case. + if (modExpr.type() == mongo::Object) { + BSONElement modExprObjPayload = modExpr.embeddedObject().firstElement(); + if (!modExprObjPayload.eoo() && StringData(modExprObjPayload.fieldName()) == "$each") { + // It is a $each. Verify that the payload is an array as is required for $each, + // set our flag, and store the array as our value. + if (modExprObjPayload.type() != mongo::Array) { + return Status(ErrorCodes::BadValue, + str::stream() << "The argument to $each in $addToSet must " + "be an array but it was of type " + << typeName(modExprObjPayload.type())); } - } - - // If this wasn't an 'each', turn it into one. No need to sort or de-dup since we only - // have one element. - if (_val == _valDoc.end()) { - mb::Element each = _valDoc.makeElementArray("$each"); - status = each.appendElement(modExpr); + status = _valDoc.root().appendElement(modExprObjPayload); if (!status.isOK()) return status; - status = _valDoc.root().pushBack(each); - if (!status.isOK()) - return status; + _val = _valDoc.root().leftChild(); - _val = each; + deduplicate(_val, mb::woLess(false), mb::woEqual(false)); } + } - // Check if no invalid data (such as fields with '$'s) are being used in the $each - // clause. - mb::ConstElement valCursor = _val.leftChild(); - while (valCursor.ok()) { - const BSONType type = valCursor.getType(); - dassert(valCursor.hasValue()); - switch(type) { + // If this wasn't an 'each', turn it into one. No need to sort or de-dup since we only + // have one element. + if (_val == _valDoc.end()) { + mb::Element each = _valDoc.makeElementArray("$each"); + + status = each.appendElement(modExpr); + if (!status.isOK()) + return status; + + status = _valDoc.root().pushBack(each); + if (!status.isOK()) + return status; + + _val = each; + } + + // Check if no invalid data (such as fields with '$'s) are being used in the $each + // clause. + mb::ConstElement valCursor = _val.leftChild(); + while (valCursor.ok()) { + const BSONType type = valCursor.getType(); + dassert(valCursor.hasValue()); + switch (type) { case mongo::Object: { Status s = valCursor.getValueObject().storageValidEmbedded(); if (!s.isOK()) @@ -202,231 +192,207 @@ namespace mongo { } default: break; - } - - valCursor = valCursor.rightSibling(); } - return Status::OK(); + valCursor = valCursor.rightSibling(); } - Status ModifierAddToSet::prepare(mb::Element root, - StringData matchedField, - ExecInfo* execInfo) { + return Status::OK(); +} - _preparedState.reset(new PreparedState(root.getDocument())); +Status ModifierAddToSet::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; + // 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); + } - // 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; - - // - // 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 < (_fieldRef.numParts() - 1)) { - // If no target element exists, we will simply be creating a new array. - _preparedState->addAll = true; - return Status::OK(); - } + // 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; + } - // This operation only applies to arrays - if (_preparedState->elemFound.getType() != mongo::Array) { - mb::Element idElem = mb::findElementNamed(root.leftChild(), "_id"); - return Status( - ErrorCodes::BadValue, - str::stream() << "Cannot apply $addToSet to a non-array field. Field named '" - << _preparedState->elemFound.getFieldName() - << "' has a non-array type " - << typeName(_preparedState->elemFound.getType()) - << " in the document " - << idElem.toString()); - } + // 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 the array is empty, then we don't need to check anything: all of the values are - // going to be added. - if (!_preparedState->elemFound.hasChildren()) { - _preparedState->addAll = true; - return Status::OK(); - } + // + // in-place and no-op logic + // - // For each value in the $each clause, compare it against the values in the array. If - // the element is not present, record it as one to add. - mb::Element eachIter = _val.leftChild(); - while (eachIter.ok()) { - mb::Element where = mb::findElement( - _preparedState->elemFound.leftChild(), - mb::woEqualTo(eachIter, false)); - if (!where.ok()) { - // The element was not found. Record the element from $each as one to be added. - _preparedState->elementsToAdd.push_back(eachIter); - } - eachIter = eachIter.rightSibling(); - } + // 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 < (_fieldRef.numParts() - 1)) { + // If no target element exists, we will simply be creating a new array. + _preparedState->addAll = true; + return Status::OK(); + } - // If we didn't find any elements to add, then this is a no-op. - if (_preparedState->elementsToAdd.empty()) { - _preparedState->noOp = execInfo->noOp = true; - } + // This operation only applies to arrays + if (_preparedState->elemFound.getType() != mongo::Array) { + mb::Element idElem = mb::findElementNamed(root.leftChild(), "_id"); + return Status(ErrorCodes::BadValue, + str::stream() + << "Cannot apply $addToSet to a non-array field. Field named '" + << _preparedState->elemFound.getFieldName() << "' has a non-array type " + << typeName(_preparedState->elemFound.getType()) << " in the document " + << idElem.toString()); + } + // If the array is empty, then we don't need to check anything: all of the values are + // going to be added. + if (!_preparedState->elemFound.hasChildren()) { + _preparedState->addAll = true; return Status::OK(); } - Status ModifierAddToSet::apply() const { - dassert(_preparedState->noOp == false); + // For each value in the $each clause, compare it against the values in the array. If + // the element is not present, record it as one to add. + mb::Element eachIter = _val.leftChild(); + while (eachIter.ok()) { + mb::Element where = + mb::findElement(_preparedState->elemFound.leftChild(), mb::woEqualTo(eachIter, false)); + if (!where.ok()) { + // The element was not found. Record the element from $each as one to be added. + _preparedState->elementsToAdd.push_back(eachIter); + } + eachIter = eachIter.rightSibling(); + } - // TODO: The contents of this block are lifted directly from $push. + // If we didn't find any elements to add, then this is a no-op. + if (_preparedState->elementsToAdd.empty()) { + _preparedState->noOp = execInfo->noOp = true; + } - // If the array field is not there, create it as an array and attach it to the - // document. - if (!_preparedState->elemFound.ok() || - _preparedState->idxFound < (_fieldRef.numParts() - 1)) { - - // Creates the array element - mb::Document& doc = _preparedState->doc; - StringData lastPart = _fieldRef.getPart(_fieldRef.numParts() - 1); - mb::Element baseArray = doc.makeElementArray(lastPart); - if (!baseArray.ok()) { - return Status(ErrorCodes::InternalError, "can't create new base array"); - } + return Status::OK(); +} - // 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++; - } +Status ModifierAddToSet::apply() const { + dassert(_preparedState->noOp == false); - // createPathAt() will complete the path and attach 'elemToSet' at the end of it. - Status status = pathsupport::createPathAt(_fieldRef, - _preparedState->idxFound, - _preparedState->elemFound, - baseArray); - if (!status.isOK()) { - return status; - } + // TODO: The contents of this block are lifted directly from $push. - // Point to the base array just created. The subsequent code expects it to exist - // already. - _preparedState->elemFound = baseArray; + // If the array field is not there, create it as an array and attach it to the + // document. + if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { + // Creates the array element + mb::Document& doc = _preparedState->doc; + StringData lastPart = _fieldRef.getPart(_fieldRef.numParts() - 1); + mb::Element baseArray = doc.makeElementArray(lastPart); + if (!baseArray.ok()) { + return Status(ErrorCodes::InternalError, "can't create new base array"); } - if (_preparedState->addAll) { - - // If we are adding all the values, we can just walk over _val; + // 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++; + } - mb::Element where = _val.leftChild(); - while (where.ok()) { + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + Status status = pathsupport::createPathAt( + _fieldRef, _preparedState->idxFound, _preparedState->elemFound, baseArray); + if (!status.isOK()) { + return status; + } - dassert(where.hasValue()); + // Point to the base array just created. The subsequent code expects it to exist + // already. + _preparedState->elemFound = baseArray; + } - mb::Element toAdd = _preparedState->doc.makeElement(where.getValue()); - Status status = _preparedState->elemFound.pushBack(toAdd); - if (!status.isOK()) - return status; + if (_preparedState->addAll) { + // If we are adding all the values, we can just walk over _val; - where = where.rightSibling(); - } + mb::Element where = _val.leftChild(); + while (where.ok()) { + dassert(where.hasValue()); - } else { + mb::Element toAdd = _preparedState->doc.makeElement(where.getValue()); + Status status = _preparedState->elemFound.pushBack(toAdd); + if (!status.isOK()) + return status; - // Otherwise, we aren't adding all the values, and we need to add exactly those - // elements that were found to be missing during our scan in prepare. - std::vector<mb::Element>::const_iterator where = - _preparedState->elementsToAdd.begin(); + where = where.rightSibling(); + } - const std::vector<mb::Element>::const_iterator end = - _preparedState->elementsToAdd.end(); + } else { + // Otherwise, we aren't adding all the values, and we need to add exactly those + // elements that were found to be missing during our scan in prepare. + std::vector<mb::Element>::const_iterator where = _preparedState->elementsToAdd.begin(); - for ( ; where != end; ++where) { + const std::vector<mb::Element>::const_iterator end = _preparedState->elementsToAdd.end(); - dassert(where->hasValue()); + for (; where != end; ++where) { + dassert(where->hasValue()); - mb::Element toAdd = _preparedState->doc.makeElement(where->getValue()); - Status status = _preparedState->elemFound.pushBack(toAdd); - if (!status.isOK()) - return status; - } + mb::Element toAdd = _preparedState->doc.makeElement(where->getValue()); + Status status = _preparedState->elemFound.pushBack(toAdd); + if (!status.isOK()) + return status; } - - return Status::OK(); } - Status ModifierAddToSet::log(LogBuilder* logBuilder) const { + return Status::OK(); +} - // 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. +Status ModifierAddToSet::log(LogBuilder* logBuilder) const { + // 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 set in several cases. For now, let's just log the - // full resulting array. + // TODO We can log just a positional set in several cases. For now, let's just log the + // full resulting array. - // We'd like to create an entry such as {$set: {<fieldname>: [<resulting aray>]}} under - // 'logRoot'. We start by creating the {$set: ...} Element. - mb::Document& doc = logBuilder->getDocument(); + // We'd like to create an entry such as {$set: {<fieldname>: [<resulting aray>]}} under + // 'logRoot'. We start by creating the {$set: ...} Element. + mb::Document& doc = logBuilder->getDocument(); - // Then we create the {<fieldname>:[]} Element, that is, an empty array. - mb::Element logElement = doc.makeElementArray(_fieldRef.dottedField()); - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, "cannot create details for $addToSet mod"); - } - - // Fill up the empty array. - mb::Element curr = _preparedState->elemFound.leftChild(); - while (curr.ok()) { + // Then we create the {<fieldname>:[]} Element, that is, an empty array. + mb::Element logElement = doc.makeElementArray(_fieldRef.dottedField()); + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, "cannot create details for $addToSet mod"); + } - dassert(curr.hasValue()); + // Fill up the empty array. + 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, - str::stream() << "Could not append entry for $addToSet oplog entry." - << "Underlying cause: " << status.toString()); - } - curr = curr.rightSibling(); + // 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"); } - - return logBuilder->addToSets(logElement); + Status status = logElement.pushBack(currCopy); + if (!status.isOK()) { + return Status(ErrorCodes::BadValue, + str::stream() << "Could not append entry for $addToSet oplog entry." + << "Underlying cause: " << status.toString()); + } + curr = curr.rightSibling(); } -} // namespace mongo + return logBuilder->addToSets(logElement); +} + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_add_to_set.h b/src/mongo/db/ops/modifier_add_to_set.h index bbbc37ece1e..66bd372834c 100644 --- a/src/mongo/db/ops/modifier_add_to_set.h +++ b/src/mongo/db/ops/modifier_add_to_set.h @@ -36,50 +36,46 @@ namespace mongo { - class LogBuilder; +class LogBuilder; - class ModifierAddToSet : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierAddToSet); +class ModifierAddToSet : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierAddToSet); - public: +public: + ModifierAddToSet(); + virtual ~ModifierAddToSet(); - ModifierAddToSet(); - virtual ~ModifierAddToSet(); + /** Goes over the array item(s) that are going to be set- unioned and converts them + * internally to a mutable bson. Both single and $each forms are supported. Returns OK + * if the item(s) are valid otherwise returns a status describing the error. + */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); - /** Goes over the array item(s) that are going to be set- unioned and converts them - * internally to a mutable bson. Both single and $each forms are supported. Returns OK - * if the item(s) are valid otherwise returns a status describing the error. - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); + /** Decides which portion of the array items that are going to be set-unioned to root's + * document and fills in 'execInfo' accordingly. Returns OK if the document has a + * valid array to set-union to, othwise returns a status describing the error. + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - /** Decides which portion of the array items that are going to be set-unioned to root's - * document and fills in 'execInfo' accordingly. Returns OK if the document has a - * valid array to set-union to, othwise returns a status describing the error. - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); + /** Updates the Element used in prepare with the effects of the $addToSet operation. */ + virtual Status apply() const; - /** Updates the Element used in prepare with the effects of the $addToSet operation. */ - virtual Status apply() const; + /** Converts the effects of this $addToSet into one or more equivalent $set operations. */ + virtual Status log(LogBuilder* logBuilder) const; - /** Converts the effects of this $addToSet into one or more equivalent $set operations. */ - virtual Status log(LogBuilder* logBuilder) const; +private: + // Access to each component of fieldName that's the target of this mod. + FieldRef _fieldRef; - 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; - // 0 or index for $-positional in _fieldRef. - size_t _posDollar; + // Array of values to be set-union'ed onto target. + mutablebson::Document _valDoc; + mutablebson::Element _val; - // Array of values to be set-union'ed onto target. - mutablebson::Document _valDoc; - mutablebson::Element _val; + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; - -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_add_to_set_test.cpp b/src/mongo/db/ops/modifier_add_to_set_test.cpp index 45741b4c000..43335cb4847 100644 --- a/src/mongo/db/ops/modifier_add_to_set_test.cpp +++ b/src/mongo/db/ops/modifier_add_to_set_test.cpp @@ -40,354 +40,350 @@ namespace { - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::ModifierAddToSet; - using mongo::ModifierInterface; - using mongo::Status; - using mongo::StringData; - using mongo::fromjson; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate a $addToSet mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) - : _modObj(modObj) - , _mod() { - ASSERT_OK(_mod.init(_modObj["$addToSet"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierAddToSet& mod() { - return _mod; - } - - private: - BSONObj _modObj; - ModifierAddToSet _mod; - }; - - TEST(Init, FailToInitWithInvalidValue) { - BSONObj modObj; - ModifierAddToSet mod; - - modObj = fromjson("{ $addToSet : { a : { 'x.$.y' : 'bad' } } }"); - ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - modObj = fromjson("{ $addToSet : { a : { $each : [ { 'x.$.y' : 'bad' } ] } } }"); - ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An int is not valid after $each - modObj = fromjson("{ $addToSet : { a : { $each : 0 } } }"); - ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object is not valid after $each - modObj = fromjson("{ $addToSet : { a : { $each : { a : 1 } } } }"); - ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::ModifierAddToSet; +using mongo::ModifierInterface; +using mongo::Status; +using mongo::StringData; +using mongo::fromjson; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** Helper to build and manipulate a $addToSet mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) : _modObj(modObj), _mod() { + ASSERT_OK(_mod.init(_modObj["$addToSet"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(Init, ParsesSimple) { - Mod(fromjson("{ $addToSet : { a : 1 } }")); - Mod(fromjson("{ $addToSet : { a : 'foo' } }")); - Mod(fromjson("{ $addToSet : { a : {} } }")); - Mod(fromjson("{ $addToSet : { a : { x : 1 } } }")); - Mod(fromjson("{ $addToSet : { a : [] } }")); - Mod(fromjson("{ $addToSet : { a : [1, 2] } } }")); - Mod(fromjson("{ $addToSet : { 'a.b' : 1 } }")); - Mod(fromjson("{ $addToSet : { 'a.b' : 'foo' } }")); - Mod(fromjson("{ $addToSet : { 'a.b' : {} } }")); - Mod(fromjson("{ $addToSet : { 'a.b' : { x : 1} } }")); - Mod(fromjson("{ $addToSet : { 'a.b' : [] } }")); - Mod(fromjson("{ $addToSet : { 'a.b' : [1, 2] } } }")); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(Init, ParsesEach) { - Mod(fromjson("{ $addToSet : { a : { $each : [] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ 1 ] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ 1, 2 ] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ 1, 2, 1 ] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ {} ] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ { x : 1 } ] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ { x : 1 }, { y : 2 } ] } } }")); - Mod(fromjson("{ $addToSet : { a : { $each : [ { x : 1 }, { y : 2 }, { x : 1 } ] } } }")); + Status apply() const { + return _mod.apply(); } - TEST(SimpleMod, PrepareOKTargetNotFound) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - } - - TEST(SimpleMod, PrepareOKTargetFound) { - Document doc(fromjson("{ a : [ 1 ] }")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1 ] } }"), logDoc); - } - - TEST(SimpleMod, PrepareInvalidTargetNumber) { - Document doc(fromjson("{ a : 1 }")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } - - TEST(SimpleMod, PrepareInvalidTarget) { - Document doc(fromjson("{ a : {} }")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } - - TEST(SimpleMod, ApplyAndLogEmptyDocument) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1 ] } }"), logDoc); - } - - TEST(SimpleMod, ApplyAndLogEmptyArray) { - Document doc(fromjson("{ a : [] }")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1 ] } }"), logDoc); - } - - TEST(SimpleEachMod, ApplyAndLogEmptyDocument) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); - } - - TEST(SimpleEachMod, ApplyAndLogEmptyArray) { - Document doc(fromjson("{ a : [] }")); - Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(SimpleMod, ApplyAndLogPopulatedArray) { - Document doc(fromjson("{ a : [ 'x' ] }")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 'x', 1 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 'x', 1 ] } }"), logDoc); - } - - TEST(SimpleEachMod, ApplyAndLogPopulatedArray) { - Document doc(fromjson("{ a : [ 'x' ] }")); - Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 'x', 1, 2, 3 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 'x', 1, 2, 3 ] } }"), logDoc); - } - - TEST(NoOp, AddOneExistingIsNoOp) { - Document doc(fromjson("{ a : [ 1, 2, 3 ] }")); - Mod mod(fromjson("{ $addToSet : { a : 1 } }")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); - } - - TEST(NoOp, AddSeveralExistingIsNoOp) { - Document doc(fromjson("{ a : [ 1, 2, 3 ] }")); - Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2] } } }")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); - } - - TEST(NoOp, AddAllExistingIsNoOp) { - Document doc(fromjson("{ a : [ 1, 2, 3 ] }")); - Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); - } - - TEST(Deduplication, ExistingDuplicatesArePreserved) { - Document doc(fromjson("{ a : [ 1, 1, 2, 1, 2, 2 ] }")); - Mod mod(fromjson("{ $addToSet : { a : 3 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1, 1, 2, 1, 2, 2, 3] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 1, 2, 1, 2, 2, 3] } }"), logDoc); - } - - TEST(Deduplication, NewDuplicatesAreElided) { - Document doc(fromjson("{ a : [ 1, 1, 2, 1, 2, 2 ] }")); - Mod mod(fromjson("{ $addToSet : { a : { $each : [ 4, 1, 3, 2, 3, 1, 3, 3, 2, 4] } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1, 1, 2, 1, 2, 2, 4, 3] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 1, 2, 1, 2, 2, 4, 3] } }"), logDoc); - } - - TEST(Regressions, SERVER_12848) { - // Proof that the mod works ok (the real issue was in validate). - - Document doc(fromjson("{ _id : 1, a : [ 1, [ ] ] }")); - Mod mod(fromjson("{ $addToSet : { 'a.1' : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ _id : 1, a : [ 1, [ 1 ] ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { 'a.1' : [ 1 ] } }"), logDoc); + ModifierAddToSet& mod() { + return _mod; } -} // namespace +private: + BSONObj _modObj; + ModifierAddToSet _mod; +}; + +TEST(Init, FailToInitWithInvalidValue) { + BSONObj modObj; + ModifierAddToSet mod; + + modObj = fromjson("{ $addToSet : { a : { 'x.$.y' : 'bad' } } }"); + ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + modObj = fromjson("{ $addToSet : { a : { $each : [ { 'x.$.y' : 'bad' } ] } } }"); + ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + // An int is not valid after $each + modObj = fromjson("{ $addToSet : { a : { $each : 0 } } }"); + ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + // An object is not valid after $each + modObj = fromjson("{ $addToSet : { a : { $each : { a : 1 } } } }"); + ASSERT_NOT_OK(mod.init(modObj["$addToSet"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, ParsesSimple) { + Mod(fromjson("{ $addToSet : { a : 1 } }")); + Mod(fromjson("{ $addToSet : { a : 'foo' } }")); + Mod(fromjson("{ $addToSet : { a : {} } }")); + Mod(fromjson("{ $addToSet : { a : { x : 1 } } }")); + Mod(fromjson("{ $addToSet : { a : [] } }")); + Mod(fromjson("{ $addToSet : { a : [1, 2] } } }")); + Mod(fromjson("{ $addToSet : { 'a.b' : 1 } }")); + Mod(fromjson("{ $addToSet : { 'a.b' : 'foo' } }")); + Mod(fromjson("{ $addToSet : { 'a.b' : {} } }")); + Mod(fromjson("{ $addToSet : { 'a.b' : { x : 1} } }")); + Mod(fromjson("{ $addToSet : { 'a.b' : [] } }")); + Mod(fromjson("{ $addToSet : { 'a.b' : [1, 2] } } }")); +} + +TEST(Init, ParsesEach) { + Mod(fromjson("{ $addToSet : { a : { $each : [] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ 1 ] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ 1, 2 ] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ 1, 2, 1 ] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ {} ] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ { x : 1 } ] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ { x : 1 }, { y : 2 } ] } } }")); + Mod(fromjson("{ $addToSet : { a : { $each : [ { x : 1 }, { y : 2 }, { x : 1 } ] } } }")); +} + +TEST(SimpleMod, PrepareOKTargetNotFound) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(SimpleMod, PrepareOKTargetFound) { + Document doc(fromjson("{ a : [ 1 ] }")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1 ] } }"), logDoc); +} + +TEST(SimpleMod, PrepareInvalidTargetNumber) { + Document doc(fromjson("{ a : 1 }")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, PrepareInvalidTarget) { + Document doc(fromjson("{ a : {} }")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, ApplyAndLogEmptyDocument) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1 ] } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogEmptyArray) { + Document doc(fromjson("{ a : [] }")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1 ] } }"), logDoc); +} + +TEST(SimpleEachMod, ApplyAndLogEmptyDocument) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); +} + +TEST(SimpleEachMod, ApplyAndLogEmptyArray) { + Document doc(fromjson("{ a : [] }")); + Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogPopulatedArray) { + Document doc(fromjson("{ a : [ 'x' ] }")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 'x', 1 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 'x', 1 ] } }"), logDoc); +} + +TEST(SimpleEachMod, ApplyAndLogPopulatedArray) { + Document doc(fromjson("{ a : [ 'x' ] }")); + Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 'x', 1, 2, 3 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 'x', 1, 2, 3 ] } }"), logDoc); +} + +TEST(NoOp, AddOneExistingIsNoOp) { + Document doc(fromjson("{ a : [ 1, 2, 3 ] }")); + Mod mod(fromjson("{ $addToSet : { a : 1 } }")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); +} + +TEST(NoOp, AddSeveralExistingIsNoOp) { + Document doc(fromjson("{ a : [ 1, 2, 3 ] }")); + Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2] } } }")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); +} + +TEST(NoOp, AddAllExistingIsNoOp) { + Document doc(fromjson("{ a : [ 1, 2, 3 ] }")); + Mod mod(fromjson("{ $addToSet : { a : { $each : [1, 2, 3] } } }")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 2, 3 ] } }"), logDoc); +} + +TEST(Deduplication, ExistingDuplicatesArePreserved) { + Document doc(fromjson("{ a : [ 1, 1, 2, 1, 2, 2 ] }")); + Mod mod(fromjson("{ $addToSet : { a : 3 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1, 1, 2, 1, 2, 2, 3] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 1, 2, 1, 2, 2, 3] } }"), logDoc); +} + +TEST(Deduplication, NewDuplicatesAreElided) { + Document doc(fromjson("{ a : [ 1, 1, 2, 1, 2, 2 ] }")); + Mod mod(fromjson("{ $addToSet : { a : { $each : [ 4, 1, 3, 2, 3, 1, 3, 3, 2, 4] } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1, 1, 2, 1, 2, 2, 4, 3] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [ 1, 1, 2, 1, 2, 2, 4, 3] } }"), logDoc); +} + +TEST(Regressions, SERVER_12848) { + // Proof that the mod works ok (the real issue was in validate). + + Document doc(fromjson("{ _id : 1, a : [ 1, [ ] ] }")); + Mod mod(fromjson("{ $addToSet : { 'a.1' : 1 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ _id : 1, a : [ 1, [ 1 ] ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { 'a.1' : [ 1 ] } }"), logDoc); +} + +} // namespace diff --git a/src/mongo/db/ops/modifier_bit.cpp b/src/mongo/db/ops/modifier_bit.cpp index c6d221875f9..bdddb9415f2 100644 --- a/src/mongo/db/ops/modifier_bit.cpp +++ b/src/mongo/db/ops/modifier_bit.cpp @@ -38,263 +38,233 @@ namespace mongo { - namespace mb = mutablebson; - namespace str = mongoutils::str; +namespace mb = mutablebson; +namespace str = mongoutils::str; - struct ModifierBit::PreparedState { +struct ModifierBit::PreparedState { + PreparedState(mutablebson::Document& doc) + : doc(doc), idxFound(0), elemFound(doc.end()), noOp(false) {} - PreparedState(mutablebson::Document& doc) - : doc(doc) - , idxFound(0) - , elemFound(doc.end()) - , noOp(false) { - } + // Document that is going to be changed. + mutablebson::Document& doc; + + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; - // Document that is going to be changed. - mutablebson::Document& doc; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + // Value to be applied. + SafeNum newValue; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; + // True if this update is a no-op + bool noOp; +}; - // Value to be applied. - SafeNum newValue; +ModifierBit::ModifierBit() : ModifierInterface(), _fieldRef(), _posDollar(0), _ops() {} - // True if this update is a no-op - bool noOp; - }; +ModifierBit::~ModifierBit() {} - ModifierBit::ModifierBit() - : ModifierInterface () - , _fieldRef() - , _posDollar(0) - , _ops() { +Status ModifierBit::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; } - ModifierBit::~ModifierBit() { + // 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() << "'"); } - Status ModifierBit::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { + if (modExpr.type() != mongo::Object) + return Status(ErrorCodes::BadValue, + str::stream() << "The $bit modifier is not compatible with a " + << typeName(modExpr.type()) + << ". You must pass in an embedded document: " + "{$bit: {field: {and/or/xor: #}}"); - // Perform standard field name and updateable checks. - _fieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fieldRef); - if (! status.isOK()) { - return status; - } + BSONObjIterator opsIterator(modExpr.embeddedObject()); - // 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); + while (opsIterator.more()) { + BSONElement curOp = opsIterator.next(); - if (positional) - *positional = foundDollar; + const StringData payloadFieldName = curOp.fieldName(); - if (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); - } + SafeNumOp op = NULL; - if (modExpr.type() != mongo::Object) + if (payloadFieldName == "and") { + op = &SafeNum::bitAnd; + } else if (payloadFieldName == "or") { + op = &SafeNum::bitOr; + } else if (payloadFieldName == "xor") { + op = &SafeNum::bitXor; + } else { return Status(ErrorCodes::BadValue, - str::stream() << "The $bit modifier is not compatible with a " - << typeName(modExpr.type()) - << ". You must pass in an embedded document: " - "{$bit: {field: {and/or/xor: #}}"); - - BSONObjIterator opsIterator(modExpr.embeddedObject()); - - while (opsIterator.more()) { - BSONElement curOp = opsIterator.next(); - - const StringData payloadFieldName = curOp.fieldName(); - - SafeNumOp op = NULL; - - if (payloadFieldName == "and") { - op = &SafeNum::bitAnd; - } - else if (payloadFieldName == "or") { - op = &SafeNum::bitOr; - } - else if (payloadFieldName == "xor") { - op = &SafeNum::bitXor; - } - else { - return Status( - ErrorCodes::BadValue, - str::stream() << "The $bit modifier only supports 'and', 'or', and 'xor', not '" - << payloadFieldName - << "' which is an unknown operator: {" << curOp << "}"); - } - - if ((curOp.type() != mongo::NumberInt) && - (curOp.type() != mongo::NumberLong)) - return Status( - ErrorCodes::BadValue, - str::stream() << "The $bit modifier field must be an Integer(32/64 bit); a '" - << typeName(curOp.type()) - << "' is not supported here: {" << curOp << "}"); - - const OpEntry entry = {SafeNum(curOp), op}; - _ops.push_back(entry); + str::stream() + << "The $bit modifier only supports 'and', 'or', and 'xor', not '" + << payloadFieldName << "' which is an unknown operator: {" << curOp + << "}"); } - dassert(!_ops.empty()); + if ((curOp.type() != mongo::NumberInt) && (curOp.type() != mongo::NumberLong)) + return Status(ErrorCodes::BadValue, + str::stream() + << "The $bit modifier field must be an Integer(32/64 bit); a '" + << typeName(curOp.type()) << "' is not supported here: {" << curOp + << "}"); - return Status::OK(); + const OpEntry entry = {SafeNum(curOp), op}; + _ops.push_back(entry); } - Status ModifierBit::prepare(mutablebson::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); - } + dassert(!_ops.empty()); - // Locate the field name in 'root'. - Status status = pathsupport::findLongestPrefix(_fieldRef, - root, - &_preparedState->idxFound, - &_preparedState->elemFound); + return Status::OK(); +} +Status ModifierBit::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(root.getDocument())); - // 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; + // 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); + } - // 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; - - // - // 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 < (_fieldRef.numParts() - 1)) { - // If no target element exists, the value we will write is the result of applying - // the operation to a zero-initialized integer element. - _preparedState->newValue = apply(SafeNum(static_cast<int>(0))); - return Status::OK(); - } + // Locate the field name in 'root'. + Status status = pathsupport::findLongestPrefix( + _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); - if (!_preparedState->elemFound.isIntegral()) { - mb::Element idElem = mb::findElementNamed(root.leftChild(), "_id"); - return Status( - ErrorCodes::BadValue, - str::stream() << "Cannot apply $bit to a value of non-integral type." - << idElem.toString() - << " has the field " << _preparedState->elemFound.getFieldName() - << " of non-integer type " - << typeName(_preparedState->elemFound.getType())); - } - const SafeNum currentValue = _preparedState->elemFound.getValueSafeNum(); + // 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; + } - // Apply the op over the existing value and the mod value, and capture the result. - _preparedState->newValue = apply(currentValue); + // 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->newValue.isValid()) { - // TODO: Include list of ops, if that is easy, at some future point. - return Status(ErrorCodes::BadValue, - str::stream() << "Failed to apply $bit operations to current value: " - << currentValue.debugString()); - } - // If the values are identical (same type, same value), then this is a no-op. - if (_preparedState->newValue.isIdentical(currentValue)) { - _preparedState->noOp = execInfo->noOp = true; - return Status::OK(); - } + // + // 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 < (_fieldRef.numParts() - 1)) { + // If no target element exists, the value we will write is the result of applying + // the operation to a zero-initialized integer element. + _preparedState->newValue = apply(SafeNum(static_cast<int>(0))); return Status::OK(); } - Status ModifierBit::apply() const { - dassert(_preparedState->noOp == false); - - // If there's no need to create any further field part, the $bit is simply a value - // assignment. - if (_preparedState->elemFound.ok() && - _preparedState->idxFound == (_fieldRef.numParts() - 1)) { - return _preparedState->elemFound.setValueSafeNum(_preparedState->newValue); - } - - // - // Complete document path logic - // + if (!_preparedState->elemFound.isIntegral()) { + mb::Element idElem = mb::findElementNamed(root.leftChild(), "_id"); + return Status(ErrorCodes::BadValue, + str::stream() << "Cannot apply $bit to a value of non-integral type." + << idElem.toString() << " has the field " + << _preparedState->elemFound.getFieldName() + << " of non-integer type " + << typeName(_preparedState->elemFound.getType())); + } - // 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"); - } + const SafeNum currentValue = _preparedState->elemFound.getValueSafeNum(); - // 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++; - } + // Apply the op over the existing value and the mod value, and capture the result. + _preparedState->newValue = apply(currentValue); - // createPathAt() will complete the path and attach 'elemToSet' at the end of it. - return pathsupport::createPathAt(_fieldRef, - _preparedState->idxFound, - _preparedState->elemFound, - elemToSet); + if (!_preparedState->newValue.isValid()) { + // TODO: Include list of ops, if that is easy, at some future point. + return Status(ErrorCodes::BadValue, + str::stream() << "Failed to apply $bit operations to current value: " + << currentValue.debugString()); + } + // If the values are identical (same type, same value), then this is a no-op. + if (_preparedState->newValue.isIdentical(currentValue)) { + _preparedState->noOp = execInfo->noOp = true; + return Status::OK(); } - Status ModifierBit::log(LogBuilder* logBuilder) const { + return Status::OK(); +} - mutablebson::Element logElement = logBuilder->getDocument().makeElementSafeNum( - _fieldRef.dottedField(), - _preparedState->newValue); +Status ModifierBit::apply() const { + dassert(_preparedState->noOp == false); - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, - str::stream() << "Could not append entry to $bit oplog entry: " - << "set '" << _fieldRef.dottedField() << "' -> " - << _preparedState->newValue.debugString() ); - } - return logBuilder->addToSets(logElement); + // If there's no need to create any further field part, the $bit is simply a value + // assignment. + if (_preparedState->elemFound.ok() && _preparedState->idxFound == (_fieldRef.numParts() - 1)) { + return _preparedState->elemFound.setValueSafeNum(_preparedState->newValue); + } + // + // 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"); } - SafeNum ModifierBit::apply(SafeNum value) const { - OpEntries::const_iterator where = _ops.begin(); - const OpEntries::const_iterator end = _ops.end(); - for (; where != end; ++where) - value = (value.*(where->op))(where->val); - return value; + // 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++; } -} // namespace mongo + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + return pathsupport::createPathAt( + _fieldRef, _preparedState->idxFound, _preparedState->elemFound, elemToSet); +} + +Status ModifierBit::log(LogBuilder* logBuilder) const { + mutablebson::Element logElement = logBuilder->getDocument().makeElementSafeNum( + _fieldRef.dottedField(), _preparedState->newValue); + + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, + str::stream() << "Could not append entry to $bit oplog entry: " + << "set '" << _fieldRef.dottedField() << "' -> " + << _preparedState->newValue.debugString()); + } + return logBuilder->addToSets(logElement); +} + +SafeNum ModifierBit::apply(SafeNum value) const { + OpEntries::const_iterator where = _ops.begin(); + const OpEntries::const_iterator end = _ops.end(); + for (; where != end; ++where) + value = (value.*(where->op))(where->val); + return value; +} + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_bit.h b/src/mongo/db/ops/modifier_bit.h index 812ac8f41e1..7ecc416a42d 100644 --- a/src/mongo/db/ops/modifier_bit.h +++ b/src/mongo/db/ops/modifier_bit.h @@ -39,61 +39,57 @@ namespace mongo { - class LogBuilder; +class LogBuilder; - class ModifierBit : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierBit); +class ModifierBit : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierBit); - public: +public: + ModifierBit(); + virtual ~ModifierBit(); - ModifierBit(); - virtual ~ModifierBit(); + /** + * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming from a $bit mod such as + * {$bit: {<field: { [and|or] : <value>}}. init() extracts the field name, the + * operation subtype, 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, bool* positional = NULL); - /** - * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming from a $bit mod such as - * {$bit: {<field: { [and|or] : <value>}}. init() extracts the field name, the - * operation subtype, 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, - bool* positional = NULL); + /** Validates the potential application of the init'ed mod to the given Element and + * configures the internal state of the mod as necessary. + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - /** Validates the potential application of the init'ed mod to the given Element and - * configures the internal state of the mod as necessary. - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); + /** Updates the Element used in prepare with the effects of the $bit operation */ + virtual Status apply() const; - /** Updates the Element used in prepare with the effects of the $bit operation */ - virtual Status apply() const; + /** Converts the effects of this $bit into an equivalent $set */ + virtual Status log(LogBuilder* logBuilder) const; - /** Converts the effects of this $bit into an equivalent $set */ - virtual Status log(LogBuilder* logBuilder) const; +private: + SafeNum apply(SafeNum value) const; - private: - SafeNum apply(SafeNum value) const; + // Access to each component of fieldName that's the target of this mod. + FieldRef _fieldRef; - // 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; - // 0 or index for $-positional in _fieldRef. - size_t _posDollar; + // The operator on SafeNum that we will invoke. + typedef SafeNum (SafeNum::*SafeNumOp)(const SafeNum&) const; - // The operator on SafeNum that we will invoke. - typedef SafeNum (SafeNum::* SafeNumOp)(const SafeNum&) const; - - struct OpEntry { - SafeNum val; - SafeNumOp op; - }; + struct OpEntry { + SafeNum val; + SafeNumOp op; + }; - typedef std::vector<OpEntry> OpEntries; + typedef std::vector<OpEntry> OpEntries; - OpEntries _ops; + OpEntries _ops; - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_bit_test.cpp b/src/mongo/db/ops/modifier_bit_test.cpp index 3d19808757e..59e13f89e24 100644 --- a/src/mongo/db/ops/modifier_bit_test.cpp +++ b/src/mongo/db/ops/modifier_bit_test.cpp @@ -40,697 +40,693 @@ namespace { - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::ModifierBit; - using mongo::ModifierInterface; - using mongo::Status; - using mongo::StringData; - using mongo::fromjson; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate a $bit mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) - : _modObj(modObj) - , _mod() { - ASSERT_OK(_mod.init(_modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - } - - Status prepare(Element root, - 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); - } - - ModifierBit& mod() { return _mod; } - - private: - BSONObj _modObj; - ModifierBit _mod; - }; - - - TEST(Init, FailToInitWithInvalidValue) { - BSONObj modObj; - ModifierBit mod; - - // String is an invalid $bit argument - modObj = fromjson("{ $bit : { a : '' } }"); - ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // Array is an invalid $bit argument - modObj = fromjson("{ $bit : { a : [] } }"); - ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object with value not in ('and', 'or') is an invalid $bit argument - modObj = fromjson("{ $bit : { a : { foo : 4 } } }"); - ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // The argument to the sub-operator must be numeric - modObj = fromjson("{ $bit : { a : { or : [] } } }"); - ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - modObj = fromjson("{ $bit : { a : { or : 'foo' } } }"); - ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // The argument to the sub-operator must be integral - modObj = fromjson("{ $bit : { a : { or : 1.0 } } }"); - ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::ModifierBit; +using mongo::ModifierInterface; +using mongo::Status; +using mongo::StringData; +using mongo::fromjson; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; - TEST(Init, ParsesAndInt) { - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(1))))); - } +/** Helper to build and manipulate a $bit mod. */ +class Mod { +public: + Mod() : _mod() {} - TEST(Init, ParsesOrInt) { - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(1))))); + explicit Mod(BSONObj modObj) : _modObj(modObj), _mod() { + ASSERT_OK(_mod.init(_modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(Init, ParsesXorInt) { - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(1))))); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(Init, ParsesAndLong) { - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); + Status apply() const { + return _mod.apply(); } - TEST(Init, ParsesOrLong) { - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(Init, ParsesXorLong) { - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(1))))); + ModifierBit& mod() { + return _mod; } - TEST(SimpleMod, PrepareOKTargetNotFound) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); +private: + BSONObj _modObj; + ModifierBit _mod; +}; - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - } +TEST(Init, FailToInitWithInvalidValue) { + BSONObj modObj; + ModifierBit mod; - TEST(SimpleMod, PrepareOKTargetFound) { - Document doc(fromjson("{ a : 1 }")); - Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); + // String is an invalid $bit argument + modObj = fromjson("{ $bit : { a : '' } }"); + ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - ModifierInterface::ExecInfo execInfo; + // Array is an invalid $bit argument + modObj = fromjson("{ $bit : { a : [] } }"); + ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); + // An object with value not in ('and', 'or') is an invalid $bit argument + modObj = fromjson("{ $bit : { a : { foo : 4 } } }"); + ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); - } + // The argument to the sub-operator must be numeric + modObj = fromjson("{ $bit : { a : { or : [] } } }"); + ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(SimpleMod, PrepareSimpleNonNumericObject) { - Document doc(fromjson("{ a : {} }")); - Mod mod(fromjson("{ $bit : { a : { or : 1 } } }")); + modObj = fromjson("{ $bit : { a : { or : 'foo' } } }"); + ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } + // The argument to the sub-operator must be integral + modObj = fromjson("{ $bit : { a : { or : 1.0 } } }"); + ASSERT_NOT_OK(mod.init(modObj["$bit"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(SimpleMod, PrepareSimpleNonNumericArray) { +TEST(Init, ParsesAndInt) { + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(1))))); +} - Document doc(fromjson("{ a : [] }")); - Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); +TEST(Init, ParsesOrInt) { + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(1))))); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } +TEST(Init, ParsesXorInt) { + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(1))))); +} - TEST(SimpleMod, PrepareSimpleNonNumericString) { - Document doc(fromjson("{ a : '' }")); - Mod mod(fromjson("{ $bit : { a : { or : 1 } } }")); +TEST(Init, ParsesAndLong) { + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } +TEST(Init, ParsesOrLong) { + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); +} - TEST(SimpleMod, ApplyAndLogEmptyDocumentAnd) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); +TEST(Init, ParsesXorLong) { + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(1))))); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareOKTargetNotFound) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 0 }"), doc); + ModifierInterface::ExecInfo execInfo; - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 0 } }"), logDoc); - } + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); +} - TEST(SimpleMod, ApplyAndLogEmptyDocumentOr) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $bit : { a : { or : 1 } } }")); +TEST(SimpleMod, PrepareOKTargetFound) { + Document doc(fromjson("{ a : 1 }")); + Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); - } - - TEST(SimpleMod, ApplyAndLogEmptyDocumentXor) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $bit : { a : { xor : 1 } } }")); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareSimpleNonNumericObject) { + Document doc(fromjson("{ a : {} }")); + Mod mod(fromjson("{ $bit : { a : { or : 1 } } }")); + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, PrepareSimpleNonNumericArray) { + Document doc(fromjson("{ a : [] }")); + Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, PrepareSimpleNonNumericString) { + Document doc(fromjson("{ a : '' }")); + Mod mod(fromjson("{ $bit : { a : { or : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, ApplyAndLogEmptyDocumentAnd) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $bit : { a : { and : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 0 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 0 } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogEmptyDocumentOr) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $bit : { a : { or : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogEmptyDocumentXor) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $bit : { a : { xor : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogSimpleDocumentAnd) { + Document doc(fromjson("{ a : 5 }")); + Mod mod(fromjson("{ $bit : { a : { and : 6 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 4 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 4 } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogSimpleDocumentOr) { + Document doc(fromjson("{ a : 5 }")); + Mod mod(fromjson("{ $bit : { a : { or : 6 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 7 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 7 } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogSimpleDocumentXor) { + Document doc(fromjson("{ a : 5 }")); + Mod mod(fromjson("{ $bit : { a : { xor : 6 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 3 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 3 } }"), logDoc); +} + +TEST(InPlace, IntToIntAndIsInPlace) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(1))), logDoc); +} + +TEST(InPlace, IntToIntOrIsInPlace) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(1))), logDoc); +} + +TEST(InPlace, IntToIntXorIsInPlace) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0))), logDoc); +} + +TEST(InPlace, LongToLongAndIsInPlace) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(1))), logDoc); +} + +TEST(InPlace, LongToLongOrIsInPlace) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(1))), logDoc); +} + +TEST(InPlace, LongToLongXorIsInPlace) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(0))), logDoc); +} + +TEST(InPlace, IntToLongAndIsNotInPlace) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(InPlace, IntToLongOrIsNotInPlace) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(InPlace, IntToLongXorIsNotInPlace) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(NoOp, IntAnd) { + Document doc(BSON("a" << static_cast<int>(0xABCD1234U))); + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(0xFFFFFFFFU))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0xABCD1234U))), logDoc); +} + +TEST(NoOp, IntOr) { + Document doc(BSON("a" << static_cast<int>(0xABCD1234U))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(0x0U))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0xABCD1234U))), logDoc); +} + +TEST(NoOp, IntXor) { + Document doc(BSON("a" << static_cast<int>(0xABCD1234U))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(0x0U))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0xABCD1234U))), logDoc); +} + +TEST(NoOp, LongAnd) { + Document doc(BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))); + Mod mod( + BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(0xFFFFFFFFFFFFFFFFULL))))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))), + logDoc); +} + +TEST(NoOp, LongOr) { + Document doc(BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(0x0ULL))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))), + logDoc); +} + +TEST(NoOp, LongXor) { + Document doc(BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(0x0ULL))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))), + logDoc); +} + +TEST(Upcasting, UpcastIntToLongAnd) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); +} + +TEST(Upcasting, UpcastIntToLongOr) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); +} + +TEST(Upcasting, UpcastIntToLongXor) { + Document doc(BSON("a" << static_cast<int>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(0))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); +} + +TEST(Upcasting, LongsStayLongsAnd) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(2))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 0 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); +} + +TEST(Upcasting, LongsStayLongsOr) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(2))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 3 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); +} + +TEST(Upcasting, LongsStayLongsXor) { + Document doc(BSON("a" << static_cast<long long>(1))); + Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 0 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); +} + +// The following tests are re-created from the previous $bit tests in updatetests.cpp. They +// are probably redundant with the tests above in various ways. + +TEST(DbUpdateTests, BitRewriteExistingField) { + Document doc(BSON("a" << static_cast<int>(0))); + Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << static_cast<int>(1)), doc); + ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(1))), logDoc); +} + +TEST(DbUpdateTests, BitRewriteNonExistingField) { + Document doc(BSON("a" << static_cast<int>(0))); + Mod mod(BSON("$bit" << BSON("b" << BSON("or" << static_cast<int>(1))))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << static_cast<int>(0) << "b" << static_cast<int>(1)), doc); + ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("b" << static_cast<int>(1))), logDoc); +} + +TEST(DbUpdateTests, Bit1_1) { + Document doc(BSON("_id" << 1 << "x" << 3)); + Mod mod(BSON("$bit" << BSON("x" << BSON("and" << 2)))); + const BSONObj result(BSON("_id" << 1 << "x" << (3 & 2))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + if (!execInfo.noOp) ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); - } + ASSERT_EQUALS(result, doc); - TEST(SimpleMod, ApplyAndLogSimpleDocumentAnd) { - Document doc(fromjson("{ a : 5 }")); - Mod mod(fromjson("{ $bit : { a : { and : 6 } } }")); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("x" << (3 & 2))), logDoc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); +TEST(DbUpdateTests, Bit1_2) { + Document doc(BSON("_id" << 1 << "x" << 1)); + Mod mod(BSON("$bit" << BSON("x" << BSON("or" << 4)))); + const BSONObj result(BSON("_id" << 1 << "x" << (1 | 4))); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + if (!execInfo.noOp) ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 4 }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 4 } }"), logDoc); - } - - TEST(SimpleMod, ApplyAndLogSimpleDocumentOr) { - Document doc(fromjson("{ a : 5 }")); - Mod mod(fromjson("{ $bit : { a : { or : 6 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(result, doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("x" << (1 | 4))), logDoc); +} + +TEST(DbUpdateTests, Bit1_3) { + Document doc(BSON("_id" << 1 << "x" << 3)); + Mod mod1(BSON("$bit" << BSON("x" << BSON("and" << 2)))); + Mod mod2(BSON("$bit" << BSON("x" << BSON("or" << 8)))); + const BSONObj result(BSON("_id" << 1 << "x" << ((3 & 2) | 8))); + + ModifierInterface::ExecInfo execInfo1; + ASSERT_OK(mod1.prepare(doc.root(), "", &execInfo1)); + if (!execInfo1.noOp) + ASSERT_OK(mod1.apply()); + + ModifierInterface::ExecInfo execInfo2; + ASSERT_OK(mod2.prepare(doc.root(), "", &execInfo2)); + if (!execInfo2.noOp) + ASSERT_OK(mod2.apply()); + + ASSERT_EQUALS(result, doc); +} + +TEST(DbUpdateTests, Bit1_3_Combined) { + Document doc(BSON("_id" << 1 << "x" << 3)); + Mod mod(BSON("$bit" << BSON("x" << BSON("and" << 2 << "or" << 8)))); + const BSONObj result(BSON("_id" << 1 << "x" << ((3 & 2) | 8))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + if (!execInfo.noOp) ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 7 }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 7 } }"), logDoc); - } - - TEST(SimpleMod, ApplyAndLogSimpleDocumentXor) { - Document doc(fromjson("{ a : 5 }")); - Mod mod(fromjson("{ $bit : { a : { xor : 6 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(result, doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("x" << ((3 & 2) | 8))), logDoc); +} + +TEST(DbUpdateTests, Bit1_4) { + Document doc(BSON("_id" << 1 << "x" << 3)); + Mod mod1(BSON("$bit" << BSON("x" << BSON("or" << 2)))); + Mod mod2(BSON("$bit" << BSON("x" << BSON("and" << 8)))); + const BSONObj result(BSON("_id" << 1 << "x" << ((3 | 2) & 8))); + + ModifierInterface::ExecInfo execInfo1; + ASSERT_OK(mod1.prepare(doc.root(), "", &execInfo1)); + if (!execInfo1.noOp) + ASSERT_OK(mod1.apply()); + + ModifierInterface::ExecInfo execInfo2; + ASSERT_OK(mod2.prepare(doc.root(), "", &execInfo2)); + if (!execInfo2.noOp) + ASSERT_OK(mod2.apply()); + + ASSERT_EQUALS(result, doc); +} + +TEST(DbUpdateTests, Bit1_4_Combined) { + Document doc(BSON("_id" << 1 << "x" << 3)); + Mod mod(BSON("$bit" << BSON("x" << BSON("or" << 2 << "and" << 8)))); + const BSONObj result(BSON("_id" << 1 << "x" << ((3 | 2) & 8))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + if (!execInfo.noOp) ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 3 }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 3 } }"), logDoc); - } - - TEST(InPlace, IntToIntAndIsInPlace) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(1))), logDoc); - } - - TEST(InPlace, IntToIntOrIsInPlace) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(1))), logDoc); - } - - TEST(InPlace, IntToIntXorIsInPlace) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0))), logDoc); - } - - TEST(InPlace, LongToLongAndIsInPlace) { - Document doc(BSON("a" << static_cast<long long>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(1))), logDoc); - } - - TEST(InPlace, LongToLongOrIsInPlace) { - Document doc(BSON("a" << static_cast<long long>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(1))), logDoc); - } - - TEST(InPlace, LongToLongXorIsInPlace) { - Document doc(BSON("a" << static_cast<long long>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<long long>(0))), logDoc); - } - - TEST(InPlace, IntToLongAndIsNotInPlace) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - } - - TEST(InPlace, IntToLongOrIsNotInPlace) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - } - - TEST(InPlace, IntToLongXorIsNotInPlace) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - } - - TEST(NoOp, IntAnd) { - Document doc(BSON("a" << static_cast<int>(0xABCD1234U))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(0xFFFFFFFFU))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0xABCD1234U))), logDoc); - } - - TEST(NoOp, IntOr) { - Document doc(BSON("a" << static_cast<int>(0xABCD1234U))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(0x0U))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0xABCD1234U))), logDoc); - } - - TEST(NoOp, IntXor) { - Document doc(BSON("a" << static_cast<int>(0xABCD1234U))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(0x0U))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(0xABCD1234U))), logDoc); - } - - TEST(NoOp, LongAnd) { - Document doc(BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << - static_cast<long long>(0xFFFFFFFFFFFFFFFFULL))))); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << - static_cast<long long>(0xABCD1234EF981234ULL))), logDoc); - } - - TEST(NoOp, LongOr) { - Document doc(BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(0x0ULL))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << - static_cast<long long>(0xABCD1234EF981234ULL))), logDoc); - } - TEST(NoOp, LongXor) { - Document doc(BSON("a" << static_cast<long long>(0xABCD1234EF981234ULL))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(0x0ULL))))); + ASSERT_EQUALS(result, doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << - static_cast<long long>(0xABCD1234EF981234ULL))), logDoc); - } - - TEST(Upcasting, UpcastIntToLongAnd) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - } - - TEST(Upcasting, UpcastIntToLongOr) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<long long>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - } - - TEST(Upcasting, UpcastIntToLongXor) { - Document doc(BSON("a" << static_cast<int>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<long long>(0))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - } - - TEST(Upcasting, LongsStayLongsAnd) { - Document doc(BSON("a" << static_cast<long long>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("and" << static_cast<int>(2))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 0 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - } - - TEST(Upcasting, LongsStayLongsOr) { - Document doc(BSON("a" << static_cast<long long>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(2))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 3 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - } - - TEST(Upcasting, LongsStayLongsXor) { - Document doc(BSON("a" << static_cast<long long>(1))); - Mod mod(BSON("$bit" << BSON("a" << BSON("xor" << static_cast<int>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 0 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - } - - // The following tests are re-created from the previous $bit tests in updatetests.cpp. They - // are probably redundant with the tests above in various ways. - - TEST(DbUpdateTests, BitRewriteExistingField) { - Document doc(BSON("a" << static_cast<int>(0))); - Mod mod(BSON("$bit" << BSON("a" << BSON("or" << static_cast<int>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << static_cast<int>(1)), doc); - ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("a" << static_cast<int>(1))), logDoc); - } - - TEST(DbUpdateTests, BitRewriteNonExistingField) { - Document doc(BSON("a" << static_cast<int>(0))); - Mod mod(BSON("$bit" << BSON("b" << BSON("or" << static_cast<int>(1))))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << static_cast<int>(0) << "b" << static_cast<int>(1)), doc); - ASSERT_EQUALS(mongo::NumberInt, doc.root()["a"].getType()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("b" << static_cast<int>(1))), logDoc); - } - - TEST(DbUpdateTests, Bit1_1) { - Document doc(BSON("_id" << 1 << "x" << 3)); - Mod mod(BSON("$bit" << BSON("x" << BSON("and" << 2)))); - const BSONObj result(BSON("_id" << 1 << "x" << (3 & 2))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - if (!execInfo.noOp) - ASSERT_OK(mod.apply()); - - ASSERT_EQUALS(result, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("x" << (3 & 2))), logDoc); - } - - TEST(DbUpdateTests, Bit1_2) { - Document doc(BSON("_id" << 1 << "x" << 1)); - Mod mod(BSON("$bit" << BSON("x" << BSON("or" << 4)))); - const BSONObj result(BSON("_id" << 1 << "x" << (1 | 4))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - if (!execInfo.noOp) - ASSERT_OK(mod.apply()); - - ASSERT_EQUALS(result, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("x" << (1 | 4))), logDoc); - } - - TEST(DbUpdateTests, Bit1_3) { - Document doc(BSON("_id" << 1 << "x" << 3)); - Mod mod1(BSON("$bit" << BSON("x" << BSON("and" << 2)))); - Mod mod2(BSON("$bit" << BSON("x" << BSON("or" << 8)))); - const BSONObj result(BSON("_id" << 1 << "x" << ((3 & 2) | 8))); - - ModifierInterface::ExecInfo execInfo1; - ASSERT_OK(mod1.prepare(doc.root(), "", &execInfo1)); - if (!execInfo1.noOp) - ASSERT_OK(mod1.apply()); - - ModifierInterface::ExecInfo execInfo2; - ASSERT_OK(mod2.prepare(doc.root(), "", &execInfo2)); - if (!execInfo2.noOp) - ASSERT_OK(mod2.apply()); - - ASSERT_EQUALS(result, doc); - } - - TEST(DbUpdateTests, Bit1_3_Combined) { - Document doc(BSON("_id" << 1 << "x" << 3)); - Mod mod(BSON("$bit" << BSON("x" << BSON("and" << 2 << "or" << 8)))); - const BSONObj result(BSON("_id" << 1 << "x" << ((3 & 2) | 8))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - if (!execInfo.noOp) - ASSERT_OK(mod.apply()); - - ASSERT_EQUALS(result, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("x" << ((3 & 2) | 8))), logDoc); - } - - TEST(DbUpdateTests, Bit1_4) { - Document doc(BSON("_id" << 1 << "x" << 3)); - Mod mod1(BSON("$bit" << BSON("x" << BSON("or" << 2)))); - Mod mod2(BSON("$bit" << BSON("x" << BSON("and" << 8)))); - const BSONObj result(BSON("_id" << 1 << "x" << ((3 | 2) & 8))); - - ModifierInterface::ExecInfo execInfo1; - ASSERT_OK(mod1.prepare(doc.root(), "", &execInfo1)); - if (!execInfo1.noOp) - ASSERT_OK(mod1.apply()); - - ModifierInterface::ExecInfo execInfo2; - ASSERT_OK(mod2.prepare(doc.root(), "", &execInfo2)); - if (!execInfo2.noOp) - ASSERT_OK(mod2.apply()); - - ASSERT_EQUALS(result, doc); - } - - TEST(DbUpdateTests, Bit1_4_Combined) { - Document doc(BSON("_id" << 1 << "x" << 3)); - Mod mod(BSON("$bit" << BSON("x" << BSON("or" << 2 << "and" << 8)))); - const BSONObj result(BSON("_id" << 1 << "x" << ((3 | 2) & 8))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - if (!execInfo.noOp) - ASSERT_OK(mod.apply()); - - ASSERT_EQUALS(result, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(BSON("$set" << BSON("x" << ((3 | 2) & 8))), logDoc); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(BSON("$set" << BSON("x" << ((3 | 2) & 8))), logDoc); +} -} // namespace +} // namespace diff --git a/src/mongo/db/ops/modifier_compare.cpp b/src/mongo/db/ops/modifier_compare.cpp index 6d37e4b2797..36f800202e4 100644 --- a/src/mongo/db/ops/modifier_compare.cpp +++ b/src/mongo/db/ops/modifier_compare.cpp @@ -37,156 +37,137 @@ namespace mongo { - namespace str = mongoutils::str; +namespace str = mongoutils::str; - struct ModifierCompare::PreparedState { +struct ModifierCompare::PreparedState { + PreparedState(mutablebson::Document& targetDoc) + : doc(targetDoc), idxFound(0), elemFound(doc.end()) {} - PreparedState(mutablebson::Document& targetDoc) - : doc(targetDoc) - , idxFound(0) - , elemFound(doc.end()) { - } + // Document that is going to be changed. + mutablebson::Document& doc; - // Document that is going to be changed. - mutablebson::Document& doc; + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; +}; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; - }; +ModifierCompare::ModifierCompare(ModifierCompare::ModifierCompareMode mode) + : _mode(mode), _pathReplacementPosition(0) {} - ModifierCompare::ModifierCompare(ModifierCompare::ModifierCompareMode mode) - : _mode(mode) - , _pathReplacementPosition(0) { - } +ModifierCompare::~ModifierCompare() {} - ModifierCompare::~ModifierCompare() { +Status ModifierCompare::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + _updatePath.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_updatePath); + if (!status.isOK()) { + return status; } - Status ModifierCompare::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - - _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; + bool foundDollar = + fieldchecker::isPositional(_updatePath, &_pathReplacementPosition, &foundCount); - // 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( - _updatePath, &_pathReplacementPosition, &foundCount); - - if (positional) - *positional = foundDollar; - - if (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _updatePath.dottedField() << "'"); - } + if (positional) + *positional = foundDollar; - // Store value for later. - _val = modExpr; - return Status::OK(); + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _updatePath.dottedField() << "'"); } - Status ModifierCompare::prepare(mutablebson::Element root, + // Store value for later. + _val = modExpr; + return Status::OK(); +} + +Status ModifierCompare::prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(root.getDocument())); - _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, - str::stream() << "The positional operator did not find the match " - "needed from the query. Unexpanded update: " - << _updatePath.dottedField()); - } - _updatePath.setPart(_pathReplacementPosition, matchedField); - } - - // 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)); + // 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, + str::stream() << "The positional operator did not find the match " + "needed from the query. Unexpanded update: " + << _updatePath.dottedField()); } + _updatePath.setPart(_pathReplacementPosition, matchedField); + } - return Status::OK(); + // 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; } - Status ModifierCompare::apply() const { + // 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)); + } - 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); - } + return Status::OK(); +} - 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++; - } +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); + } - // createPathAt() will complete the path and attach 'elemToSet' at the end of it. - return pathsupport::createPathAt(_updatePath, - _preparedState->idxFound, - _preparedState->elemFound, - elemToSet); + 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"); } - Status ModifierCompare::log(LogBuilder* logBuilder) const { - return logBuilder->addToSetsWithNewFieldName(_updatePath.dottedField(), _val); + // 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++; } -} // namespace mongo + // 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 index 8285f3e10ad..f0be887d136 100644 --- a/src/mongo/db/ops/modifier_compare.h +++ b/src/mongo/db/ops/modifier_compare.h @@ -38,73 +38,68 @@ 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, - bool* positional = NULL); - - /** - * 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, - 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; - std::unique_ptr<PreparedState> _preparedState; - }; - -} // 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, bool* positional = NULL); + + /** + * 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, 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; + std::unique_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 index 9f19dd1d718..fb6bcf8d84f 100644 --- a/src/mongo/db/ops/modifier_compare_test.cpp +++ b/src/mongo/db/ops/modifier_compare_test.cpp @@ -39,257 +39,256 @@ 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) { - StringData modName = modObj.firstElement().fieldName(); - ASSERT_OK(_mod.init(modObj[modName].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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())); +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) { + StringData modName = modObj.firstElement().fieldName(); + ASSERT_OK(_mod.init(modObj[modName].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); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - 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); + Status apply() const { + return _mod.apply(); } - 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); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - 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); - - 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()); + ModifierCompare& mod() { + return _mod; } -} // namespace +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); + + 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 diff --git a/src/mongo/db/ops/modifier_current_date.cpp b/src/mongo/db/ops/modifier_current_date.cpp index f38cc4e4441..75d0be014e3 100644 --- a/src/mongo/db/ops/modifier_current_date.cpp +++ b/src/mongo/db/ops/modifier_current_date.cpp @@ -38,239 +38,215 @@ namespace mongo { - namespace str = mongoutils::str; +namespace str = mongoutils::str; - namespace { - const char kType[] = "$type"; - const char kDate[] = "date"; - const char kTimestamp[] = "timestamp"; - } +namespace { +const char kType[] = "$type"; +const char kDate[] = "date"; +const char kTimestamp[] = "timestamp"; +} - struct ModifierCurrentDate::PreparedState { +struct ModifierCurrentDate::PreparedState { + PreparedState(mutablebson::Document& doc) : doc(doc), elemFound(doc.end()), idxFound(0) {} - PreparedState(mutablebson::Document& doc) - : doc(doc) - , elemFound(doc.end()) - , idxFound(0) { - } + // Document that is going to be changed. + mutablebson::Document& doc; - // Document that is going to be changed. - mutablebson::Document& doc; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; +}; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; - }; +ModifierCurrentDate::ModifierCurrentDate() : _pathReplacementPosition(0), _typeIsDate(true) {} - ModifierCurrentDate::ModifierCurrentDate() - : _pathReplacementPosition(0) - , _typeIsDate(true) { - } +ModifierCurrentDate::~ModifierCurrentDate() {} - ModifierCurrentDate::~ModifierCurrentDate() { +Status ModifierCurrentDate::init(const BSONElement& modExpr, + const Options& opts, + bool* positional) { + _updatePath.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_updatePath); + if (!status.isOK()) { + return status; } - Status ModifierCurrentDate::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - - _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; - bool foundDollar = fieldchecker::isPositional(_updatePath, - &_pathReplacementPosition, - &foundCount); + // 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(_updatePath, &_pathReplacementPosition, &foundCount); - if (positional) - *positional = foundDollar; + if (positional) + *positional = foundDollar; - if (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _updatePath.dottedField() << "'"); - } + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _updatePath.dottedField() << "'"); + } - // Validate and store the type to produce - switch (modExpr.type()) { - case Bool: - _typeIsDate = true; - break; - case Object: { - const BSONObj argObj = modExpr.embeddedObject(); - const BSONElement typeElem = argObj.getField(kType); - bool badInput = typeElem.eoo() || !(typeElem.type() == String); + // Validate and store the type to produce + switch (modExpr.type()) { + case Bool: + _typeIsDate = true; + break; + case Object: { + const BSONObj argObj = modExpr.embeddedObject(); + const BSONElement typeElem = argObj.getField(kType); + bool badInput = typeElem.eoo() || !(typeElem.type() == String); + + if (!badInput) { + std::string typeVal = typeElem.String(); + badInput = !(typeElem.String() == kDate || typeElem.String() == kTimestamp); + if (!badInput) + _typeIsDate = (typeVal == kDate); if (!badInput) { - std::string typeVal = typeElem.String(); - badInput = !(typeElem.String() == kDate || typeElem.String() == kTimestamp); - if (!badInput) - _typeIsDate = (typeVal == kDate); - - if (!badInput) { - // Check to make sure only the $type field was given as an arg - BSONObjIterator i( argObj ); - const bool onlyHasTypeField = ((i.next().fieldNameStringData() == kType) - && i.next().eoo()); - if (!onlyHasTypeField) { - return Status(ErrorCodes::BadValue, - str::stream() << - "The only valid field of the option is '$type': " - "{$currentDate: {field : {$type: 'date/timestamp'}}}; " + // Check to make sure only the $type field was given as an arg + BSONObjIterator i(argObj); + const bool onlyHasTypeField = + ((i.next().fieldNameStringData() == kType) && i.next().eoo()); + if (!onlyHasTypeField) { + return Status(ErrorCodes::BadValue, + str::stream() + << "The only valid field of the option is '$type': " + "{$currentDate: {field : {$type: 'date/timestamp'}}}; " << "arg: " << argObj); - } - } - - } - - if (badInput) { - return Status(ErrorCodes::BadValue, - "The '$type' string field is required " - "to be 'date' or 'timestamp': " - "{$currentDate: {field : {$type: 'date'}}}"); } - break; } - default: - return Status(ErrorCodes::BadValue, - str::stream() << typeName(modExpr.type()) - << " is not valid type for $currentDate." - " Please use a boolean ('true')" - " or a $type expression ({$type: 'timestamp/date'})."); - } - - return Status::OK(); - } - - Status ModifierCurrentDate::prepare(mutablebson::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 (_pathReplacementPosition) { - if (matchedField.empty()) { + if (badInput) { return Status(ErrorCodes::BadValue, - str::stream() << "The positional operator did not find the match " - "needed from the query. Unexpanded update: " - << _updatePath.dottedField()); + "The '$type' string field is required " + "to be 'date' or 'timestamp': " + "{$currentDate: {field : {$type: 'date'}}}"); } - _updatePath.setPart(_pathReplacementPosition, matchedField); + break; } + default: + return Status(ErrorCodes::BadValue, + str::stream() << typeName(modExpr.type()) + << " is not valid type for $currentDate." + " Please use a boolean ('true')" + " or a $type expression ({$type: 'timestamp/date'})."); + } - // 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; - } + return Status::OK(); +} - // 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; +Status ModifierCurrentDate::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(root.getDocument())); - return Status::OK(); + // 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, + str::stream() << "The positional operator did not find the match " + "needed from the query. Unexpanded update: " + << _updatePath.dottedField()); + } + _updatePath.setPart(_pathReplacementPosition, matchedField); } - Status ModifierCurrentDate::apply() const { + // 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; + } - const bool destExists = (_preparedState->elemFound.ok() && - _preparedState->idxFound == (_updatePath.numParts() - 1)); + // 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; - 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 = destExists ? - _preparedState->elemFound: - doc.end() ; + return Status::OK(); +} - if (!destExists) { - // Creates the final element that's going to be $set in 'doc'. - // fills in the value with place-holder/empty +Status ModifierCurrentDate::apply() const { + const bool destExists = (_preparedState->elemFound.ok() && + _preparedState->idxFound == (_updatePath.numParts() - 1)); - elemToSet = _typeIsDate ? - doc.makeElementDate(lastPart, Date_t()) : - doc.makeElementTimestamp(lastPart, Timestamp()); + 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 = destExists ? _preparedState->elemFound : doc.end(); - if (!elemToSet.ok()) { - return Status(ErrorCodes::InternalError, "can't create new element"); - } + if (!destExists) { + // Creates the final element that's going to be $set in 'doc'. + // fills in the value with place-holder/empty - // 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++; - } + elemToSet = _typeIsDate ? doc.makeElementDate(lastPart, Date_t()) + : doc.makeElementTimestamp(lastPart, Timestamp()); - // createPathAt() will complete the path and attach 'elemToSet' at the end of it. - Status s = pathsupport::createPathAt(_updatePath, - _preparedState->idxFound, - _preparedState->elemFound, - elemToSet); - if (!s.isOK()) - return s; + if (!elemToSet.ok()) { + return Status(ErrorCodes::InternalError, "can't create new element"); } - dassert(elemToSet.ok()); - - // By the time we are here the element is in place and we just need to update the value - if (_typeIsDate) { - const mongo::Date_t now = mongo::jsTime(); - Status s = elemToSet.setValueDate(now); - if (!s.isOK()) - return s; - } - else { - Status s = elemToSet.setValueTimestamp(getNextGlobalTimestamp()); - if (!s.isOK()) - return s; + // 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++; } - // Set the elemFound, idxFound to the changed element for oplog logging. - _preparedState->elemFound = elemToSet; - _preparedState->idxFound = (_updatePath.numParts() - 1); + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + Status s = pathsupport::createPathAt( + _updatePath, _preparedState->idxFound, _preparedState->elemFound, elemToSet); + if (!s.isOK()) + return s; + } - return Status::OK(); + dassert(elemToSet.ok()); + + // By the time we are here the element is in place and we just need to update the value + if (_typeIsDate) { + const mongo::Date_t now = mongo::jsTime(); + Status s = elemToSet.setValueDate(now); + if (!s.isOK()) + return s; + } else { + Status s = elemToSet.setValueTimestamp(getNextGlobalTimestamp()); + if (!s.isOK()) + return s; } - Status ModifierCurrentDate::log(LogBuilder* logBuilder) const { - // TODO: None of this checks should be needed unless someone calls if we are a noOp - // When we cleanup we should build in testing that no-one calls in the noOp case - const bool destExists = (_preparedState->elemFound.ok() && - _preparedState->idxFound == (_updatePath.numParts() - 1)); + // Set the elemFound, idxFound to the changed element for oplog logging. + _preparedState->elemFound = elemToSet; + _preparedState->idxFound = (_updatePath.numParts() - 1); - // If the destination doesn't exist then we have nothing to log. - // This would only happen if apply isn't called or fails when it had to create an element - if (!destExists) - return Status::OK(); + return Status::OK(); +} - return logBuilder->addToSetsWithNewFieldName(_updatePath.dottedField(), - _preparedState->elemFound); - } +Status ModifierCurrentDate::log(LogBuilder* logBuilder) const { + // TODO: None of this checks should be needed unless someone calls if we are a noOp + // When we cleanup we should build in testing that no-one calls in the noOp case + const bool destExists = (_preparedState->elemFound.ok() && + _preparedState->idxFound == (_updatePath.numParts() - 1)); + + // If the destination doesn't exist then we have nothing to log. + // This would only happen if apply isn't called or fails when it had to create an element + if (!destExists) + return Status::OK(); + + return logBuilder->addToSetsWithNewFieldName(_updatePath.dottedField(), + _preparedState->elemFound); +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_current_date.h b/src/mongo/db/ops/modifier_current_date.h index 291c05b377a..d7e1bd5e875 100644 --- a/src/mongo/db/ops/modifier_current_date.h +++ b/src/mongo/db/ops/modifier_current_date.h @@ -37,53 +37,48 @@ namespace mongo { - class LogBuilder; +class LogBuilder; - class ModifierCurrentDate : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierCurrentDate); +class ModifierCurrentDate : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierCurrentDate); - public: +public: + ModifierCurrentDate(); + virtual ~ModifierCurrentDate(); - ModifierCurrentDate(); - virtual ~ModifierCurrentDate(); + /** + * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming + * from a $currentDate mod such as + * {$currentDate: {<fieldname: true/{$type: "date/timestamp"}}. + * 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, bool* positional = NULL); - /** - * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming - * from a $currentDate mod such as - * {$currentDate: {<fieldname: true/{$type: "date/timestamp"}}. - * 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, - bool* positional = NULL); + /** Evaluates the validity of applying $currentDate. + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - /** Evaluates the validity of applying $currentDate. - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); + /** Updates the node passed in prepare with the results from prepare */ + virtual Status apply() const; - /** Updates the node passed in prepare with the results from prepare */ - virtual Status apply() const; + /** Converts the result into a $set */ + virtual Status log(LogBuilder* logBuilder) const; - /** Converts the result into a $set */ - virtual Status log(LogBuilder* logBuilder) const; +private: + // Access to each component of fieldName that's the target of this mod. + FieldRef _updatePath; - private: + // 0 or index for $-positional in _updatePath. + size_t _pathReplacementPosition; - // Access to each component of fieldName that's the target of this mod. - FieldRef _updatePath; + // Is the final type being added a Date or Timestamp? + bool _typeIsDate; - // 0 or index for $-positional in _updatePath. - size_t _pathReplacementPosition; + // State which changes with each call of the mod. + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; - // Is the final type being added a Date or Timestamp? - bool _typeIsDate; - - // State which changes with each call of the mod. - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; - -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_current_date_test.cpp b/src/mongo/db/ops/modifier_current_date_test.cpp index 51eb4a79659..ecb4b86bac8 100644 --- a/src/mongo/db/ops/modifier_current_date_test.cpp +++ b/src/mongo/db/ops/modifier_current_date_test.cpp @@ -39,327 +39,323 @@ namespace { - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::ModifierCurrentDate; - using mongo::ModifierInterface; - using mongo::Timestamp; - using mongo::Status; - using mongo::StringData; - using mongo::fromjson; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** - * Helper to validate oplog entries in the tests below. - */ - void validateOplogEntry(BSONObj& oplogFormat, Document& doc){ - // Ensure that the field is the same - ASSERT_EQUALS(oplogFormat.firstElement().fieldName(), - doc.root().leftChild().getFieldName()); - - // Ensure the field names are the same - ASSERT_EQUALS(oplogFormat.firstElement().embeddedObject().firstElement().fieldName(), - doc.root().leftChild().leftChild().getFieldName()); - - // Ensure the type is the same in the document as the oplog - ASSERT_EQUALS(oplogFormat.firstElement().embeddedObject().firstElement().type(), - doc.root().leftChild().leftChild().getType()); +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::ModifierCurrentDate; +using mongo::ModifierInterface; +using mongo::Timestamp; +using mongo::Status; +using mongo::StringData; +using mongo::fromjson; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** + * Helper to validate oplog entries in the tests below. + */ +void validateOplogEntry(BSONObj& oplogFormat, Document& doc) { + // Ensure that the field is the same + ASSERT_EQUALS(oplogFormat.firstElement().fieldName(), doc.root().leftChild().getFieldName()); + + // Ensure the field names are the same + ASSERT_EQUALS(oplogFormat.firstElement().embeddedObject().firstElement().fieldName(), + doc.root().leftChild().leftChild().getFieldName()); + + // Ensure the type is the same in the document as the oplog + ASSERT_EQUALS(oplogFormat.firstElement().embeddedObject().firstElement().type(), + doc.root().leftChild().leftChild().getType()); +} + +/** Helper to build and manipulate a $currentDate mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) : _modObj(modObj), _mod() { + ASSERT_OK(_mod.init(_modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - /** Helper to build and manipulate a $currentDate mod. */ - class Mod { - public: - Mod() : _mod() {} + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); + } - explicit Mod(BSONObj modObj) - : _modObj(modObj) - , _mod() { - ASSERT_OK(_mod.init(_modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + Status apply() const { + return _mod.apply(); + } - } + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); + } - Status prepare(Element root, - StringData matchedField, - ModifierInterface::ExecInfo* execInfo) { - return _mod.prepare(root, matchedField, execInfo); - } + ModifierCurrentDate& mod() { + return _mod; + } - Status apply() const { - return _mod.apply(); - } +private: + BSONObj _modObj; + ModifierCurrentDate _mod; +}; - Status log(LogBuilder* logBuilder) const { - return _mod.log(logBuilder); - } +TEST(Init, ValidValues) { + BSONObj modObj; + ModifierCurrentDate mod; - ModifierCurrentDate& mod() { return _mod; } + modObj = fromjson("{ $currentDate : { a : true } }"); + ASSERT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - private: - BSONObj _modObj; - ModifierCurrentDate _mod; - }; + modObj = fromjson("{ $currentDate : { a : {$type : 'timestamp' } } }"); + ASSERT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(Init, ValidValues) { - BSONObj modObj; - ModifierCurrentDate mod; + modObj = fromjson("{ $currentDate : { a : {$type : 'date' } } }"); + ASSERT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - modObj = fromjson("{ $currentDate : { a : true } }"); - ASSERT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); +TEST(Init, FailToInitWithInvalidValue) { + BSONObj modObj; + ModifierCurrentDate mod; - modObj = fromjson("{ $currentDate : { a : {$type : 'timestamp' } } }"); - ASSERT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + // String is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : 'Oct 11, 2001' } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - modObj = fromjson("{ $currentDate : { a : {$type : 'date' } } }"); - ASSERT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + // Array is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : [] } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } - - TEST(Init, FailToInitWithInvalidValue) { - BSONObj modObj; - ModifierCurrentDate mod; - - // String is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : 'Oct 11, 2001' } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // Array is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : [] } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // Number is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : 1 } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // Regex is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : /1/ } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object with missing $type field is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : { foo : 4 } } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object with extra fields, including the $type field is bad - modObj = fromjson("{ $currentDate : { a : { $type: 'date', foo : 4 } } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object with extra fields, including the $type field is bad - modObj = fromjson("{ $currentDate : { a : { foo: 4, $type : 'date' } } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object with non-date/timestamp $type field is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : { $type : 4 } } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // An object with non-date/timestamp $type field is an invalid $currentDate argument - modObj = fromjson("{ $currentDate : { a : { $type : 'foo' } } }"); - ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(BoolInput, EmptyStartDoc) { - Document doc(fromjson("{ }")); - Mod mod(fromjson("{ $currentDate : { a : true } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); - ASSERT_OK(mod.apply()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); - validateOplogEntry(oplogFormat, logDoc); - } - - TEST(DateInput, EmptyStartDoc) { - Document doc(fromjson("{ }")); - Mod mod(fromjson("{ $currentDate : { a : {$type: 'date' } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); - ASSERT_OK(mod.apply()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); - validateOplogEntry(oplogFormat, logDoc); - } - TEST(TimestampInput, EmptyStartDoc) { - Document doc(fromjson("{ }")); - Mod mod(fromjson("{ $currentDate : { a : {$type : 'timestamp' } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - mongo::Timestamp ts; - BSONObj olderDateObj = BSON("a" << ts); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $timestamp : {t:0, i:0} } } }"); - validateOplogEntry(oplogFormat, logDoc); - } - - TEST(BoolInput, ExistingStringDoc) { - Document doc(fromjson("{ a: 'a' }")); - Mod mod(fromjson("{ $currentDate : { a : true } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); - ASSERT_OK(mod.apply()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); - validateOplogEntry(oplogFormat, logDoc); - } + // Number is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : 1 } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(BoolInput, ExistingDateDoc) { - Document doc(fromjson("{ a: {$date: 0 } }")); - Mod mod(fromjson("{ $currentDate : { a : true } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); - ASSERT_OK(mod.apply()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); - validateOplogEntry(oplogFormat, logDoc); - } + // Regex is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : /1/ } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(DateInput, ExistingDateDoc) { - Document doc(fromjson("{ a: {$date: 0 } }")); - Mod mod(fromjson("{ $currentDate : { a : {$type: 'date' } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); - ASSERT_OK(mod.apply()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); - validateOplogEntry(oplogFormat, logDoc); - } + // An object with missing $type field is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : { foo : 4 } } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(TimestampInput, ExistingDateDoc) { - Document doc(fromjson("{ a: {$date: 0 } }")); - Mod mod(fromjson("{ $currentDate : { a : {$type : 'timestamp' } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); - - mongo::Timestamp ts; - BSONObj olderDateObj = BSON("a" << ts); - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); //Same Size as Date - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { a : { $timestamp : {t:0, i:0} } } }"); - validateOplogEntry(oplogFormat, logDoc); - } + // An object with extra fields, including the $type field is bad + modObj = fromjson("{ $currentDate : { a : { $type: 'date', foo : 4 } } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(TimestampInput, ExistingEmbeddedDateDoc) { - Document doc(fromjson("{ a: {b: {$date: 0 } } }")); - Mod mod(fromjson("{ $currentDate : { 'a.b' : {$type : 'timestamp' } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a.b", execInfo.fieldRef[0]->dottedField()); - - mongo::Timestamp ts; - BSONObj olderDateObj = BSON("a" << BSON( "b" << ts)); - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); //Same Size as Date - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { 'a.b' : { $timestamp : {t:0, i:0} } } }"); - validateOplogEntry(oplogFormat, logDoc); - } + // An object with extra fields, including the $type field is bad + modObj = fromjson("{ $currentDate : { a : { foo: 4, $type : 'date' } } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); - TEST(DottedTimestampInput, EmptyStartDoc) { - Document doc(fromjson("{ }")); - Mod mod(fromjson("{ $currentDate : { 'a.b' : {$type : 'timestamp' } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS("a.b", execInfo.fieldRef[0]->dottedField()); - - mongo::Timestamp ts; - BSONObj olderDateObj = BSON("a" << BSON( "b" << ts)); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_LESS_THAN(olderDateObj, doc.getObject()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - BSONObj oplogFormat = fromjson("{ $set : { 'a.b' : { $timestamp : {t:0, i:0} } } }"); - validateOplogEntry(oplogFormat, logDoc); - } + // An object with non-date/timestamp $type field is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : { $type : 4 } } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); -} // namespace + // An object with non-date/timestamp $type field is an invalid $currentDate argument + modObj = fromjson("{ $currentDate : { a : { $type : 'foo' } } }"); + ASSERT_NOT_OK(mod.init(modObj["$currentDate"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(BoolInput, EmptyStartDoc) { + Document doc(fromjson("{ }")); + Mod mod(fromjson("{ $currentDate : { a : true } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); + ASSERT_OK(mod.apply()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(DateInput, EmptyStartDoc) { + Document doc(fromjson("{ }")); + Mod mod(fromjson("{ $currentDate : { a : {$type: 'date' } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); + ASSERT_OK(mod.apply()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(TimestampInput, EmptyStartDoc) { + Document doc(fromjson("{ }")); + Mod mod(fromjson("{ $currentDate : { a : {$type : 'timestamp' } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + mongo::Timestamp ts; + BSONObj olderDateObj = BSON("a" << ts); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $timestamp : {t:0, i:0} } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(BoolInput, ExistingStringDoc) { + Document doc(fromjson("{ a: 'a' }")); + Mod mod(fromjson("{ $currentDate : { a : true } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); + ASSERT_OK(mod.apply()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(BoolInput, ExistingDateDoc) { + Document doc(fromjson("{ a: {$date: 0 } }")); + Mod mod(fromjson("{ $currentDate : { a : true } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); + ASSERT_OK(mod.apply()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(DateInput, ExistingDateDoc) { + Document doc(fromjson("{ a: {$date: 0 } }")); + Mod mod(fromjson("{ $currentDate : { a : {$type: 'date' } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + BSONObj olderDateObj = fromjson("{ a : { $date : 0 } }"); + ASSERT_OK(mod.apply()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $date : 0 } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(TimestampInput, ExistingDateDoc) { + Document doc(fromjson("{ a: {$date: 0 } }")); + Mod mod(fromjson("{ $currentDate : { a : {$type : 'timestamp' } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a", execInfo.fieldRef[0]->dottedField()); + + mongo::Timestamp ts; + BSONObj olderDateObj = BSON("a" << ts); + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); // Same Size as Date + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { a : { $timestamp : {t:0, i:0} } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(TimestampInput, ExistingEmbeddedDateDoc) { + Document doc(fromjson("{ a: {b: {$date: 0 } } }")); + Mod mod(fromjson("{ $currentDate : { 'a.b' : {$type : 'timestamp' } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a.b", execInfo.fieldRef[0]->dottedField()); + + mongo::Timestamp ts; + BSONObj olderDateObj = BSON("a" << BSON("b" << ts)); + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); // Same Size as Date + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { 'a.b' : { $timestamp : {t:0, i:0} } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +TEST(DottedTimestampInput, EmptyStartDoc) { + Document doc(fromjson("{ }")); + Mod mod(fromjson("{ $currentDate : { 'a.b' : {$type : 'timestamp' } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS("a.b", execInfo.fieldRef[0]->dottedField()); + + mongo::Timestamp ts; + BSONObj olderDateObj = BSON("a" << BSON("b" << ts)); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_LESS_THAN(olderDateObj, doc.getObject()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + BSONObj oplogFormat = fromjson("{ $set : { 'a.b' : { $timestamp : {t:0, i:0} } } }"); + validateOplogEntry(oplogFormat, logDoc); +} + +} // namespace diff --git a/src/mongo/db/ops/modifier_inc.cpp b/src/mongo/db/ops/modifier_inc.cpp index a967e0a734a..b683a1f9561 100644 --- a/src/mongo/db/ops/modifier_inc.cpp +++ b/src/mongo/db/ops/modifier_inc.cpp @@ -38,260 +38,229 @@ namespace mongo { - namespace mb = mutablebson; - namespace str = mongoutils::str; +namespace mb = mutablebson; +namespace str = mongoutils::str; - struct ModifierInc::PreparedState { +struct ModifierInc::PreparedState { + PreparedState(mutablebson::Document& doc) + : doc(doc), idxFound(0), elemFound(doc.end()), newValue(), noOp(false) {} - PreparedState(mutablebson::Document& doc) - : doc(doc) - , idxFound(0) - , elemFound(doc.end()) - , newValue() - , noOp(false) { - } + // 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; - // Document that is going to be changed. - mutablebson::Document& doc; + // Value to be applied + SafeNum newValue; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + // This $inc is a no-op? + bool noOp; +}; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; +ModifierInc::ModifierInc(ModifierIncMode mode) + : ModifierInterface(), _mode(mode), _fieldRef(), _posDollar(0), _val() {} - // Value to be applied - SafeNum newValue; +ModifierInc::~ModifierInc() {} - // This $inc is a no-op? - bool noOp; - }; +Status ModifierInc::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + // + // field name analysis + // - ModifierInc::ModifierInc(ModifierIncMode mode) - : ModifierInterface () - , _mode(mode) - , _fieldRef() - , _posDollar(0) - , _val() { + // Perform standard field name and updateable checks. + _fieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fieldRef); + if (!status.isOK()) { + return status; } - ModifierInc::~ModifierInc() { + // 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() << "'"); } - Status ModifierInc::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { + // + // value analysis + // - // - // field name analysis - // + if (!modExpr.isNumber()) { + // TODO: Context for mod error messages would be helpful + // include mod code, etc. + return Status(ErrorCodes::TypeMismatch, + str::stream() << "Cannot " << (_mode == MODE_INC ? "increment" : "multiply") + << " with non-numeric argument: {" << modExpr << "}"); + } - // Perform standard field name and updateable checks. - _fieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fieldRef); - if (! status.isOK()) { - return status; - } + _val = modExpr; + dassert(_val.isValid()); - // 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); + return Status::OK(); +} - if (positional) - *positional = foundDollar; +Status ModifierInc::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(root.getDocument())); - if (foundDollar && foundCount > 1) { + // 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() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); + str::stream() << "The positional operator did not find the match " + "needed from the query. Unexpanded update: " + << _fieldRef.dottedField()); } + _fieldRef.setPart(_posDollar, matchedField); + } - // - // value analysis - // - - if (!modExpr.isNumber()) { - // TODO: Context for mod error messages would be helpful - // include mod code, etc. - return Status(ErrorCodes::TypeMismatch, - str::stream() << "Cannot " - << (_mode == MODE_INC ? "increment" : "multiply") - << " with non-numeric argument: {" - << modExpr << "}"); - } + // 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; + } - _val = modExpr; - dassert(_val.isValid()); + // 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 < (_fieldRef.numParts() - 1)) { + // For multiplication, we treat ops against missing as yielding zero. We take + // advantage here of the promotion rules for SafeNum; the expression below will + // always yield a zero of the same type of operand that the user provided + // (e.g. double). + if (_mode == MODE_MUL) + _preparedState->newValue *= SafeNum(static_cast<int>(0)); return Status::OK(); } - Status ModifierInc::prepare(mutablebson::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'. 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 < (_fieldRef.numParts() - 1)) { - - // For multiplication, we treat ops against missing as yielding zero. We take - // advantage here of the promotion rules for SafeNum; the expression below will - // always yield a zero of the same type of operand that the user provided - // (e.g. double). - if (_mode == MODE_MUL) - _preparedState->newValue *= SafeNum(static_cast<int>(0)); - - 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()) { - mb::Element idElem = mb::findFirstChildNamed(root, "_id"); - return Status( - ErrorCodes::TypeMismatch, - str::stream() << "Cannot apply " - << (_mode == MODE_INC ? "$inc" : "$mul") - << " to a value of non-numeric type. {" - << idElem.toString() - << "} has the field '" << _preparedState->elemFound.getFieldName() - << "' of non-numeric type " - << typeName(_preparedState->elemFound.getType())); - } - const SafeNum currentValue = _preparedState->elemFound.getValueSafeNum(); - - // Update newValue w.r.t to the current value of the found element. - if (_mode == MODE_INC) - _preparedState->newValue += currentValue; - else - _preparedState->newValue *= currentValue; - - // If the result of the addition is invalid, we must return an error. - if (!_preparedState->newValue.isValid()) { - mb::Element idElem = mb::findFirstChildNamed(root, "_id"); - return Status(ErrorCodes::BadValue, - str::stream() << "Failed to apply $inc operations to current value (" - << currentValue.debugString() << ") for document {" - << idElem.toString() << "}"); - } - - // If the values are identical (same type, same value), then this is a no-op. - if (_preparedState->newValue.isIdentical(currentValue)) { - _preparedState->noOp = execInfo->noOp = true; - 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()) { + mb::Element idElem = mb::findFirstChildNamed(root, "_id"); + return Status(ErrorCodes::TypeMismatch, + str::stream() << "Cannot apply " << (_mode == MODE_INC ? "$inc" : "$mul") + << " to a value of non-numeric type. {" << idElem.toString() + << "} has the field '" + << _preparedState->elemFound.getFieldName() + << "' of non-numeric type " + << typeName(_preparedState->elemFound.getType())); + } + const SafeNum currentValue = _preparedState->elemFound.getValueSafeNum(); + + // Update newValue w.r.t to the current value of the found element. + if (_mode == MODE_INC) + _preparedState->newValue += currentValue; + else + _preparedState->newValue *= currentValue; + + // If the result of the addition is invalid, we must return an error. + if (!_preparedState->newValue.isValid()) { + mb::Element idElem = mb::findFirstChildNamed(root, "_id"); + return Status(ErrorCodes::BadValue, + str::stream() << "Failed to apply $inc operations to current value (" + << currentValue.debugString() << ") for document {" + << idElem.toString() << "}"); + } + // If the values are identical (same type, same value), then this is a no-op. + if (_preparedState->newValue.isIdentical(currentValue)) { + _preparedState->noOp = execInfo->noOp = true; return Status::OK(); } - Status ModifierInc::apply() const { - dassert(_preparedState->noOp == false); + return Status::OK(); +} - // If there's no need to create any further field part, the $inc is simply a value - // assignment. - if (_preparedState->elemFound.ok() && - _preparedState->idxFound == (_fieldRef.numParts() - 1)) { - return _preparedState->elemFound.setValueSafeNum(_preparedState->newValue); - } +Status ModifierInc::apply() const { + dassert(_preparedState->noOp == 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"); - } + // If there's no need to create any further field part, the $inc is simply a value + // assignment. + if (_preparedState->elemFound.ok() && _preparedState->idxFound == (_fieldRef.numParts() - 1)) { + return _preparedState->elemFound.setValueSafeNum(_preparedState->newValue); + } - // 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++; - } + // + // Complete document path logic + // - // createPathAt() will complete the path and attach 'elemToSet' at the end of it. - return pathsupport::createPathAt(_fieldRef, - _preparedState->idxFound, - _preparedState->elemFound, - elemToSet); + // 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"); } - Status ModifierInc::log(LogBuilder* logBuilder) const { + // 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++; + } - dassert(_preparedState->newValue.isValid()); + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + return pathsupport::createPathAt( + _fieldRef, _preparedState->idxFound, _preparedState->elemFound, elemToSet); +} - // We'd like to create an entry such as {$set: {<fieldname>: <value>}} under 'logRoot'. - // We start by creating the {$set: ...} Element. - mutablebson::Document& doc = logBuilder->getDocument(); +Status ModifierInc::log(LogBuilder* logBuilder) const { + dassert(_preparedState->newValue.isValid()); - // Then we create the {<fieldname>: <value>} Element. - mutablebson::Element logElement = doc.makeElementSafeNum( - _fieldRef.dottedField(), - _preparedState->newValue); + // We'd like to create an entry such as {$set: {<fieldname>: <value>}} under 'logRoot'. + // We start by creating the {$set: ...} Element. + mutablebson::Document& doc = logBuilder->getDocument(); - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, - str::stream() << "Could not append entry to " - << (_mode == MODE_INC ? "$inc" : "$mul") - << " oplog entry: " - << "set '" << _fieldRef.dottedField() << "' -> " - << _preparedState->newValue.debugString() ); - } + // Then we create the {<fieldname>: <value>} Element. + mutablebson::Element logElement = + doc.makeElementSafeNum(_fieldRef.dottedField(), _preparedState->newValue); - // Now, we attach the {<fieldname>: <value>} Element under the {$set: ...} segment. - return logBuilder->addToSets(logElement); + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, + str::stream() << "Could not append entry to " + << (_mode == MODE_INC ? "$inc" : "$mul") << " oplog entry: " + << "set '" << _fieldRef.dottedField() << "' -> " + << _preparedState->newValue.debugString()); } -} // namespace mongo + // Now, we attach the {<fieldname>: <value>} Element under the {$set: ...} segment. + return logBuilder->addToSets(logElement); +} + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_inc.h b/src/mongo/db/ops/modifier_inc.h index cfb7bbdf25d..17c76acf88f 100644 --- a/src/mongo/db/ops/modifier_inc.h +++ b/src/mongo/db/ops/modifier_inc.h @@ -37,64 +37,57 @@ namespace mongo { - class LogBuilder; - - class ModifierInc : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierInc); - - public: - - // TODO: This is a shortcut to implementing $mul by hijacking $inc. In the near future, - // we should consider either pulling $mul into its own operator, or creating a general - // purpose "numeric binary op" operator. Potentially, that operator could also subsume - // $bit (thought there are some subtleties, like that $bit can have multiple - // operations, and doing so with arbirary math operations introduces potential - // associativity difficulties). At the very least, if this mechanism is retained, then - // this class should be renamed at some point away from ModifierInc. - enum ModifierIncMode { - MODE_INC, - MODE_MUL - }; - - ModifierInc(ModifierIncMode mode = MODE_INC); - 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, const Options& opts, - bool* positional = NULL); - - /** 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, - 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(LogBuilder* logBuilder) const; - - private: - const ModifierIncMode _mode; - - // 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; - std::unique_ptr<PreparedState> _preparedState; - }; - -} // namespace mongo +class LogBuilder; + +class ModifierInc : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierInc); + +public: + // TODO: This is a shortcut to implementing $mul by hijacking $inc. In the near future, + // we should consider either pulling $mul into its own operator, or creating a general + // purpose "numeric binary op" operator. Potentially, that operator could also subsume + // $bit (thought there are some subtleties, like that $bit can have multiple + // operations, and doing so with arbirary math operations introduces potential + // associativity difficulties). At the very least, if this mechanism is retained, then + // this class should be renamed at some point away from ModifierInc. + enum ModifierIncMode { MODE_INC, MODE_MUL }; + + ModifierInc(ModifierIncMode mode = MODE_INC); + 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, const Options& opts, bool* positional = NULL); + + /** 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, 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(LogBuilder* logBuilder) const; + +private: + const ModifierIncMode _mode; + + // 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; + std::unique_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 index 8a97bc490e7..52ab8d0e41c 100644 --- a/src/mongo/db/ops/modifier_inc_test.cpp +++ b/src/mongo/db/ops/modifier_inc_test.cpp @@ -42,483 +42,482 @@ namespace { - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::ModifierInc; - using mongo::ModifierInterface; - using mongo::NumberInt; - using mongo::Status; - using mongo::StringData; - using mongo::fromjson; - 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: - - explicit Mod(BSONObj modObj) - : _modObj(modObj) - , _mod(mongoutils::str::equals(modObj.firstElement().fieldName(), "$mul") ? - ModifierInc::MODE_MUL : ModifierInc::MODE_INC) { - StringData modName = modObj.firstElement().fieldName(); - ASSERT_OK(_mod.init(_modObj[modName].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - 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(), - ModifierInterface::Options::normal())); - - // Object is an invalid increment argument - modObj = fromjson("{ $inc : { a : {} } }"); - ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - // Array is an invalid increment argument - modObj = fromjson("{ $inc : { a : [] } }"); - ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::ModifierInc; +using mongo::ModifierInterface; +using mongo::NumberInt; +using mongo::Status; +using mongo::StringData; +using mongo::fromjson; +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: + explicit Mod(BSONObj modObj) + : _modObj(modObj), + _mod(mongoutils::str::equals(modObj.firstElement().fieldName(), "$mul") + ? ModifierInc::MODE_MUL + : ModifierInc::MODE_INC) { + StringData modName = modObj.firstElement().fieldName(); + ASSERT_OK(_mod.init(_modObj[modName].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(Init, InitParsesNumberInt) { - Mod incMod(BSON("$inc" << BSON("a" << static_cast<int>(1)))); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(Init, InitParsesNumberLong) { - Mod incMod(BSON("$inc" << BSON("a" << static_cast<long long>(1)))); + Status apply() const { + return _mod.apply(); } - TEST(Init, InitParsesNumberDouble) { - Mod incMod(BSON("$inc" << BSON("a" << 1.0))); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - 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(doc.isInPlaceModeEnabled()); - 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.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); - } - - TEST(SimpleMod, LogWithoutApplyEmptyDocument) { - Document doc(fromjson("{}")); - Mod incMod(fromjson("{ $inc: { a : 1 }}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); - } - - 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_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 3 }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 3 } }"), logDoc); + ModifierInc& mod() { + return _mod; } - TEST(DottedMod, ApplyAndLogSimpleDocument) { - Document doc(fromjson("{ a : { b : 2 } }")); - Mod incMod(fromjson("{ $inc: { 'a.b' : 1 } }")); +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(), + ModifierInterface::Options::normal())); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : { b : 3 } }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { 'a.b' : 3 } }"), logDoc); - } - - 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); - } - - 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); - } - - 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); - } - - 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); - } - - 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); - } - - 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); - } - - 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_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); - 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_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1.0 }"), doc); - ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1.0 } }"), logDoc); - ASSERT_EQUALS(mongo::NumberDouble, logDoc.root()["$set"]["a"].getType()); - } + // Object is an invalid increment argument + modObj = fromjson("{ $inc : { a : {} } }"); + ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + // Array is an invalid increment argument + modObj = fromjson("{ $inc : { a : [] } }"); + ASSERT_NOT_OK(mod.init(modObj["$inc"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +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(doc.isInPlaceModeEnabled()); + 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.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); +} + +TEST(SimpleMod, LogWithoutApplyEmptyDocument) { + Document doc(fromjson("{}")); + Mod incMod(fromjson("{ $inc: { a : 1 }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); +} + +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_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 3 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 3 } }"), logDoc); +} + +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_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : { b : 3 } }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { 'a.b' : 3 } }"), logDoc); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1 }"), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root()["a"].getType()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1 } }"), logDoc); + 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_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1.0 }"), doc); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1.0 } }"), logDoc); + 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_OK(incMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 1.0 }"), doc); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 1.0 } }"), logDoc); + 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_OK(incMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 2.0 }"), doc); + ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : 2.0 } }"), logDoc); + 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); + + const long long target_value = static_cast<long long>(initial_value) + 1; + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << target_value), doc); +} + +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); + + const long long target_value = static_cast<long long>(initial_value) - 1; + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << target_value), doc); +} + +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_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(doc1.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 2 }"), doc1); + + ASSERT_OK(incMod.prepare(doc2.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(doc2.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 2 }"), doc2); +} + +// Given the current implementation of $mul, we really only need one test for +// $mul. However, in the future, we should probably write additional ones, or, perhaps find +// a way to run all the obove tests in both modes. +TEST(Multiplication, ApplyAndLogSimpleDocument) { + Document doc(fromjson("{ a : { b : 2 } }")); + Mod incMod(fromjson("{ $mul: { 'a.b' : 3 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : { b : 6 } }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { 'a.b' : 6 } }"), logDoc); +} - 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_OK(incMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 1.0 }"), doc); - ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 1.0 } }"), logDoc); - 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_OK(incMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 2.0 }"), doc); - ASSERT_EQUALS(mongo::NumberDouble, doc.root()["a"].getType()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : 2.0 } }"), logDoc); - 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); - - const long long target_value = static_cast<long long>(initial_value) + 1; - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << target_value), doc); - } - - 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); - - const long long target_value = static_cast<long long>(initial_value) - 1; - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << target_value), doc); - } - - 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_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_TRUE(doc1.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 2 }"), doc1); - - ASSERT_OK(incMod.prepare(doc2.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_TRUE(doc2.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 2 }"), doc2); - } - - // Given the current implementation of $mul, we really only need one test for - // $mul. However, in the future, we should probably write additional ones, or, perhaps find - // a way to run all the obove tests in both modes. - TEST(Multiplication, ApplyAndLogSimpleDocument) { - Document doc(fromjson("{ a : { b : 2 } }")); - Mod incMod(fromjson("{ $mul: { 'a.b' : 3 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : { b : 6 } }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { 'a.b' : 6 } }"), logDoc); - } - - TEST(Multiplication, ApplyAndLogMissingElement) { - Document doc(fromjson("{ a : 0 }")); - Mod incMod(fromjson("{ $mul : { b : 3 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : 0, b : 0 }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(incMod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { b : 0 } }"), logDoc); - } - - TEST(Multiplication, ApplyMissingElementInt) { - const int int_zero = 0; - const int int_three = 3; - - Document doc(BSON("a" << int_zero)); - Mod incMod(BSON("$mul" << BSON("b" << int_three))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << int_zero << "b" << int_zero), doc); - ASSERT_EQUALS(mongo::NumberInt, doc.root().rightChild().getType()); - } - - TEST(Multiplication, ApplyMissingElementLongLong) { - const long long ll_zero = 0; - const long long ll_three = 3; - - Document doc(BSON("a" << ll_zero)); - Mod incMod(BSON("$mul" << BSON("b" << ll_three))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << ll_zero << "b" << ll_zero), doc); - ASSERT_EQUALS(mongo::NumberLong, doc.root().rightChild().getType()); - } - - TEST(Multiplication, ApplyMissingElementDouble) { - const double double_zero = 0; - const double double_three = 3; - - Document doc(BSON("a" << double_zero)); - Mod incMod(BSON("$mul" << BSON("b" << double_three))); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(incMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(BSON("a" << double_zero << "b" << 0), doc); - ASSERT_EQUALS(mongo::NumberDouble, doc.root().rightChild().getType()); - } +TEST(Multiplication, ApplyAndLogMissingElement) { + Document doc(fromjson("{ a : 0 }")); + Mod incMod(fromjson("{ $mul : { b : 3 } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : 0, b : 0 }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(incMod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { b : 0 } }"), logDoc); +} -} // namespace +TEST(Multiplication, ApplyMissingElementInt) { + const int int_zero = 0; + const int int_three = 3; + + Document doc(BSON("a" << int_zero)); + Mod incMod(BSON("$mul" << BSON("b" << int_three))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << int_zero << "b" << int_zero), doc); + ASSERT_EQUALS(mongo::NumberInt, doc.root().rightChild().getType()); +} + +TEST(Multiplication, ApplyMissingElementLongLong) { + const long long ll_zero = 0; + const long long ll_three = 3; + + Document doc(BSON("a" << ll_zero)); + Mod incMod(BSON("$mul" << BSON("b" << ll_three))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << ll_zero << "b" << ll_zero), doc); + ASSERT_EQUALS(mongo::NumberLong, doc.root().rightChild().getType()); +} + +TEST(Multiplication, ApplyMissingElementDouble) { + const double double_zero = 0; + const double double_three = 3; + + Document doc(BSON("a" << double_zero)); + Mod incMod(BSON("$mul" << BSON("b" << double_three))); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(incMod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(incMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(BSON("a" << double_zero << "b" << 0), doc); + ASSERT_EQUALS(mongo::NumberDouble, doc.root().rightChild().getType()); +} + +} // namespace diff --git a/src/mongo/db/ops/modifier_interface.h b/src/mongo/db/ops/modifier_interface.h index 3574615e6f4..eba8b6c324d 100644 --- a/src/mongo/db/ops/modifier_interface.h +++ b/src/mongo/db/ops/modifier_interface.h @@ -36,160 +36,167 @@ namespace mongo { - class LogBuilder; +class LogBuilder; +/** + * Abstract base class for update "modifiers" (a.k.a "$ operators"). To create a new + * operator, implement a new derived class. + * + * A typical call sequence for the class is: + * + * + init() with the mod arguments + * + * + For each document that is being touched on that update, the following methods are + * going to be called once for that document and in the order the calls appear here. + * + * + prepare() to check if mod is viable over the document + * + * + apply(), effectively computing the update + * + * + log() registering the change in the log for replication purposes + * + * Again, a modifier implementation may rely on these last three calls being made and in + * that particular order and therefore can keep and reuse state between these calls, when + * appropriate. + * + * TODO: + * For a reference implementation, see modifier_identity.{h,cpp} used in tests. + */ +class ModifierInterface { +public: + virtual ~ModifierInterface() {} + + struct Options; /** - * Abstract base class for update "modifiers" (a.k.a "$ operators"). To create a new - * operator, implement a new derived class. + * Returns OK and extracts the parameters for this given mod from 'modExpr'. For + * instance, for a $inc, extracts the increment value. The init() method would be + * called only once per operand, that is, if a { $inc: { a: 1, b: 1 } } is issued, + * there would be one instance of the operator working on 'a' and one on 'b'. In each + * case, init() would be called once with the respective bson element. * - * A typical call sequence for the class is: + * If 'modExpr' is invalid, returns an error status with a reason description. * - * + init() with the mod arguments + * The optional bool out parameter 'positional', if provided, will be set to 'true' if + * the mod requires matched field details to be provided when calling 'prepare'. The + * field is optional since this is a hint to the caller about what work is needed to + * correctly invoke 'prepare'. It is always legal to provide any match details + * unconditionally. The value set in 'positional' if any, is only meaningful if 'init' + * returns an OK status. * - * + For each document that is being touched on that update, the following methods are - * going to be called once for that document and in the order the calls appear here. + * Note: * - * + prepare() to check if mod is viable over the document + * + An operator may assume the modExpr passed here will be unchanged throughout all + * the mod object lifetime and also that the modExrp's lifetime exceeds the life + * time of this mod. Therefore, taking references to elements inside modExpr is + * valid. + */ + virtual Status init(const BSONElement& modExpr, + const Options& opts, + bool* positional = NULL) = 0; + + /** + * Returns OK if it would be correct to apply this mod over the document 'root' (e.g, if + * we're $inc-ing a field, is that field numeric in the current doc?). * - * + apply(), effectively computing the update + * If the field this mod is targeted to contains a $-positional parameter, that value + * can be bound with 'matchedField', passed by the caller. * - * + log() registering the change in the log for replication purposes + * In addition, the call also identifies which fields(s) of 'root' the mod is interested + * in changing (note that the modifier may want to add a field that's not present in + * the document). The call also determines whether it could modify the document in + * place and whether it is a no-op for the given document. All this information is in + * the passed 'execInfo', which is filled inside the call. * - * Again, a modifier implementation may rely on these last three calls being made and in - * that particular order and therefore can keep and reuse state between these calls, when - * appropriate. + * If the mod cannot be applied over 'root', returns an error status with a reason + * description. * - * TODO: - * For a reference implementation, see modifier_identity.{h,cpp} used in tests. + * Note that you must provide a meaningful 'matchedField' here, unless 'init' set + * 'positional' to 'false', in which case you may pass an empty StringData object. */ - class ModifierInterface { - public: - virtual ~ModifierInterface() { } - - struct Options; - /** - * Returns OK and extracts the parameters for this given mod from 'modExpr'. For - * instance, for a $inc, extracts the increment value. The init() method would be - * called only once per operand, that is, if a { $inc: { a: 1, b: 1 } } is issued, - * there would be one instance of the operator working on 'a' and one on 'b'. In each - * case, init() would be called once with the respective bson element. - * - * If 'modExpr' is invalid, returns an error status with a reason description. - * - * The optional bool out parameter 'positional', if provided, will be set to 'true' if - * the mod requires matched field details to be provided when calling 'prepare'. The - * field is optional since this is a hint to the caller about what work is needed to - * correctly invoke 'prepare'. It is always legal to provide any match details - * unconditionally. The value set in 'positional' if any, is only meaningful if 'init' - * returns an OK status. - * - * Note: - * - * + An operator may assume the modExpr passed here will be unchanged throughout all - * the mod object lifetime and also that the modExrp's lifetime exceeds the life - * time of this mod. Therefore, taking references to elements inside modExpr is - * valid. - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL) = 0; - - /** - * Returns OK if it would be correct to apply this mod over the document 'root' (e.g, if - * we're $inc-ing a field, is that field numeric in the current doc?). - * - * If the field this mod is targeted to contains a $-positional parameter, that value - * can be bound with 'matchedField', passed by the caller. - * - * In addition, the call also identifies which fields(s) of 'root' the mod is interested - * in changing (note that the modifier may want to add a field that's not present in - * the document). The call also determines whether it could modify the document in - * place and whether it is a no-op for the given document. All this information is in - * the passed 'execInfo', which is filled inside the call. - * - * If the mod cannot be applied over 'root', returns an error status with a reason - * description. - * - * Note that you must provide a meaningful 'matchedField' here, unless 'init' set - * 'positional' to 'false', in which case you may pass an empty StringData object. - */ - struct ExecInfo; - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - /* IN-OUT */ ExecInfo* execInfo) = 0; - - /** - * Returns OK and modifies (or adds) an element (or elements) from the 'root' passed on - * the prepareMod call. This may act on multiple fields but should only be called once - * per operator. - * - * For this call to be issued, the call to 'prepareElem' must have necessarily turned - * off 'ExecInfo.noOp', ie this mod over this document is not a no-op. - * - * If the mod could not be applied, returns an error status with a reason description. - */ - virtual Status apply() const = 0 ; - - /** - * Returns OK and records the result of this mod in the provided LogBuilder. The mod - * must have kept enough state to be able to produce the log record (see idempotency - * note below). This call may be issued even if apply() was not. - * - * If the mod could not be logged, returns an error status with a reason description. - * - * Idempotency Note: - * - * + The modifier must log a mod that is idempotent, ie, applying it more than once - * to a base collection would produce the same result as applying it only once. For - * example, a $inc can be switched to a $set for the resulting incremented value, - * for logging purposes. An array based operator may check the contents of the - * array before operating on it. - */ - virtual Status log(LogBuilder* logBuilder) const = 0; - }; + struct ExecInfo; + virtual Status prepare(mutablebson::Element root, + StringData matchedField, + /* IN-OUT */ ExecInfo* execInfo) = 0; /** - * Options used to control Modifier behavior + * Returns OK and modifies (or adds) an element (or elements) from the 'root' passed on + * the prepareMod call. This may act on multiple fields but should only be called once + * per operator. + * + * For this call to be issued, the call to 'prepareElem' must have necessarily turned + * off 'ExecInfo.noOp', ie this mod over this document is not a no-op. + * + * If the mod could not be applied, returns an error status with a reason description. */ - struct ModifierInterface::Options { - Options() : fromReplication(false), enforceOkForStorage(true) {} - Options(bool repl, bool ofs) : fromReplication(repl), enforceOkForStorage(ofs) {} + virtual Status apply() const = 0; - static Options normal() { return Options(false, true); } - static Options fromRepl() { return Options(true, false); } - static Options unchecked() { return Options(false, false); } - - bool fromReplication; - bool enforceOkForStorage; - }; + /** + * Returns OK and records the result of this mod in the provided LogBuilder. The mod + * must have kept enough state to be able to produce the log record (see idempotency + * note below). This call may be issued even if apply() was not. + * + * If the mod could not be logged, returns an error status with a reason description. + * + * Idempotency Note: + * + * + The modifier must log a mod that is idempotent, ie, applying it more than once + * to a base collection would produce the same result as applying it only once. For + * example, a $inc can be switched to a $set for the resulting incremented value, + * for logging purposes. An array based operator may check the contents of the + * array before operating on it. + */ + virtual Status log(LogBuilder* logBuilder) const = 0; +}; - struct ModifierInterface::ExecInfo { - static const int MAX_NUM_FIELDS = 2; +/** + * Options used to control Modifier behavior + */ +struct ModifierInterface::Options { + Options() : fromReplication(false), enforceOkForStorage(true) {} + Options(bool repl, bool ofs) : fromReplication(repl), enforceOkForStorage(ofs) {} + + static Options normal() { + return Options(false, true); + } + static Options fromRepl() { + return Options(true, false); + } + static Options unchecked() { + return Options(false, false); + } + + bool fromReplication; + bool enforceOkForStorage; +}; + +struct ModifierInterface::ExecInfo { + static const int MAX_NUM_FIELDS = 2; - /** - * An update mod may specify that it wishes to the applied only if the context - * of the update turns out a certain way. - */ - enum UpdateContext { - // This mod wants to be applied only if the update turns out to be an insert. - INSERT_CONTEXT, + /** + * An update mod may specify that it wishes to the applied only if the context + * of the update turns out a certain way. + */ + enum UpdateContext { + // This mod wants to be applied only if the update turns out to be an insert. + INSERT_CONTEXT, - // This mod wants to be applied only if the update is not an insert. - UPDATE_CONTEXT, + // This mod wants to be applied only if the update is not an insert. + UPDATE_CONTEXT, - // This mod doesn't care if the update will be an update or an upsert. - ANY_CONTEXT - }; + // This mod doesn't care if the update will be an update or an upsert. + ANY_CONTEXT + }; - ExecInfo() : noOp(false), context(ANY_CONTEXT) { - for (int i = 0; i < MAX_NUM_FIELDS; i++) { - fieldRef[i] = NULL; - } + ExecInfo() : noOp(false), context(ANY_CONTEXT) { + for (int i = 0; i < MAX_NUM_FIELDS; i++) { + fieldRef[i] = NULL; } + } - // The fields of concern to the driver: no other op may modify the fields listed here. - FieldRef* fieldRef[MAX_NUM_FIELDS]; // not owned here - bool noOp; - UpdateContext context; - }; + // The fields of concern to the driver: no other op may modify the fields listed here. + FieldRef* fieldRef[MAX_NUM_FIELDS]; // not owned here + bool noOp; + UpdateContext context; +}; -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_object_replace.cpp b/src/mongo/db/ops/modifier_object_replace.cpp index 5ebbb4bd342..700f111e5b3 100644 --- a/src/mongo/db/ops/modifier_object_replace.cpp +++ b/src/mongo/db/ops/modifier_object_replace.cpp @@ -36,161 +36,151 @@ namespace mongo { - namespace str = mongoutils::str; - - namespace { - const char idFieldName[] = "_id"; - - Status fixupTimestamps( const BSONObj& obj ) { - BSONObjIterator i(obj); - while (i.more()) { - BSONElement e = i.next(); - - // Skip _id field -- we do not replace it - if (e.type() == bsonTimestamp && e.fieldNameStringData() != idFieldName) { - // TODO(emilkie): This is not endian-safe. - unsigned long long ×tamp = - *(reinterpret_cast<unsigned long long*>( - const_cast<char *>(e.value()))); - if (timestamp == 0) { - // performance note, this locks a mutex: - Timestamp ts(getNextGlobalTimestamp()); - timestamp = ts.asULL(); - } - } +namespace str = mongoutils::str; + +namespace { +const char idFieldName[] = "_id"; + +Status fixupTimestamps(const BSONObj& obj) { + BSONObjIterator i(obj); + while (i.more()) { + BSONElement e = i.next(); + + // Skip _id field -- we do not replace it + if (e.type() == bsonTimestamp && e.fieldNameStringData() != idFieldName) { + // TODO(emilkie): This is not endian-safe. + unsigned long long& timestamp = + *(reinterpret_cast<unsigned long long*>(const_cast<char*>(e.value()))); + if (timestamp == 0) { + // performance note, this locks a mutex: + Timestamp ts(getNextGlobalTimestamp()); + timestamp = ts.asULL(); } - - return Status::OK(); } } - struct ModifierObjectReplace::PreparedState { + return Status::OK(); +} +} - PreparedState(mutablebson::Document* targetDoc) - : doc(*targetDoc) - , noOp(false) { - } +struct ModifierObjectReplace::PreparedState { + PreparedState(mutablebson::Document* targetDoc) : doc(*targetDoc), noOp(false) {} - // Document that is going to be changed - mutablebson::Document& doc; + // Document that is going to be changed + mutablebson::Document& doc; - // This is a no op - bool noOp; + // This is a no op + bool noOp; +}; - }; +ModifierObjectReplace::ModifierObjectReplace() : _val() {} - ModifierObjectReplace::ModifierObjectReplace() : _val() { - } +ModifierObjectReplace::~ModifierObjectReplace() {} - ModifierObjectReplace::~ModifierObjectReplace() { +Status ModifierObjectReplace::init(const BSONElement& modExpr, + const Options& opts, + bool* positional) { + if (modExpr.type() != Object) { + // Impossible, really since the caller check this already... + return Status(ErrorCodes::BadValue, + str::stream() << "Document replacement expects a complete document" + " but the type supplied was " << modExpr.type()); } - Status ModifierObjectReplace::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - - if (modExpr.type() != Object) { - // Impossible, really since the caller check this already... - return Status(ErrorCodes::BadValue, - str::stream() << "Document replacement expects a complete document" - " but the type supplied was " - << modExpr.type()); - } + // Object replacements never have positional operator. + if (positional) + *positional = false; + + // We make a copy of the object here because the update driver does not guarantees, in + // the case of object replacement, that the modExpr is going to outlive this mod. + _val = modExpr.embeddedObject().getOwned(); + return fixupTimestamps(_val); +} + +Status ModifierObjectReplace::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(&root.getDocument())); + + // objectSize checked by binaryEqual (optimization) + BSONObj objOld = root.getDocument().getObject(); + if (objOld.binaryEqual(_val)) { + _preparedState->noOp = true; + execInfo->noOp = true; + } - // Object replacements never have positional operator. - if (positional) - *positional = false; + return Status::OK(); +} - // We make a copy of the object here because the update driver does not guarantees, in - // the case of object replacement, that the modExpr is going to outlive this mod. - _val = modExpr.embeddedObject().getOwned(); - return fixupTimestamps(_val); - } +Status ModifierObjectReplace::apply() const { + dassert(!_preparedState->noOp); - Status ModifierObjectReplace::prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo) { - _preparedState.reset(new PreparedState(&root.getDocument())); + // Remove the contents of the provided doc. + mutablebson::Document& doc = _preparedState->doc; + mutablebson::Element current = doc.root().leftChild(); + mutablebson::Element srcIdElement = doc.end(); + while (current.ok()) { + mutablebson::Element toRemove = current; + current = current.rightSibling(); - // objectSize checked by binaryEqual (optimization) - BSONObj objOld = root.getDocument().getObject(); - if (objOld.binaryEqual(_val)) { - _preparedState->noOp = true; - execInfo->noOp = true; + // Skip _id field element -- it should not change + if (toRemove.getFieldName() == idFieldName) { + srcIdElement = toRemove; + continue; } - return Status::OK(); + Status status = toRemove.remove(); + if (!status.isOK()) { + return status; + } } - Status ModifierObjectReplace::apply() const { - dassert(!_preparedState->noOp); - - // Remove the contents of the provided doc. - mutablebson::Document& doc = _preparedState->doc; - mutablebson::Element current = doc.root().leftChild(); - mutablebson::Element srcIdElement = doc.end(); - while (current.ok()) { - mutablebson::Element toRemove = current; - current = current.rightSibling(); - - // Skip _id field element -- it should not change - if (toRemove.getFieldName() == idFieldName) { - srcIdElement = toRemove; + // Insert the provided contents instead. + BSONElement dstIdElement; + BSONObjIterator it(_val); + while (it.more()) { + BSONElement elem = it.next(); + if (elem.fieldNameStringData() == idFieldName) { + dstIdElement = elem; + + // Do not duplicate _id field + if (srcIdElement.ok()) { + if (srcIdElement.compareWithBSONElement(dstIdElement, true) != 0) { + return Status(ErrorCodes::ImmutableField, + str::stream() << "The _id field cannot be changed from {" + << srcIdElement.toString() << "} to {" + << dstIdElement.toString() << "}."); + } continue; } - - Status status = toRemove.remove(); - if (!status.isOK()) { - return status; - } } - // Insert the provided contents instead. - BSONElement dstIdElement; - BSONObjIterator it(_val); - while (it.more()) { - BSONElement elem = it.next(); - if (elem.fieldNameStringData() == idFieldName) { - dstIdElement = elem; - - // Do not duplicate _id field - if (srcIdElement.ok()) { - if (srcIdElement.compareWithBSONElement(dstIdElement, true) != 0) { - return Status(ErrorCodes::ImmutableField, - str::stream() << "The _id field cannot be changed from {" - << srcIdElement.toString() << "} to {" - << dstIdElement.toString() << "}."); - } - continue; - } - } - - Status status = doc.root().appendElement(elem); - if (!status.isOK()) { - return status; - } + Status status = doc.root().appendElement(elem); + if (!status.isOK()) { + return status; } - - return Status::OK(); } - Status ModifierObjectReplace::log(LogBuilder* logBuilder) const { + return Status::OK(); +} - mutablebson::Document& doc = logBuilder->getDocument(); +Status ModifierObjectReplace::log(LogBuilder* logBuilder) const { + mutablebson::Document& doc = logBuilder->getDocument(); - mutablebson::Element replacementObject = doc.end(); - Status status = logBuilder->getReplacementObject(&replacementObject); + mutablebson::Element replacementObject = doc.end(); + Status status = logBuilder->getReplacementObject(&replacementObject); - if (status.isOK()) { - mutablebson::Element current = _preparedState->doc.root().leftChild(); - while (current.ok()) { - status = replacementObject.appendElement(current.getValue()); - if (!status.isOK()) - return status; - current = current.rightSibling(); - } + if (status.isOK()) { + mutablebson::Element current = _preparedState->doc.root().leftChild(); + while (current.ok()) { + status = replacementObject.appendElement(current.getValue()); + if (!status.isOK()) + return status; + current = current.rightSibling(); } - - return status; } -} // namespace mongo + return status; +} + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_object_replace.h b/src/mongo/db/ops/modifier_object_replace.h index 4e923e4a7fd..add180d472e 100644 --- a/src/mongo/db/ops/modifier_object_replace.h +++ b/src/mongo/db/ops/modifier_object_replace.h @@ -37,62 +37,57 @@ namespace mongo { - class LogBuilder; - - class ModifierObjectReplace : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierObjectReplace); - - public: - - ModifierObjectReplace(); - - // - // Modifier interface implementation - // - - virtual ~ModifierObjectReplace(); - - - /** - * Returns true and takes the embedded object contained in 'modExpr' to be the object - * we're replacing for. The field name of 'modExpr' is ignored. If 'modExpr' is in an - * unexpected format or if it can't be parsed for some reason, returns an error status - * describing the error. - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); - - /** - * Registers the that 'root' is in the document that we want to fully replace. - * prepare() returns OK and always fills 'execInfo' with true for - * noOp. - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); - - /** - * Replaces the document passed in prepare() for the object passed in init(). Returns - * OK if successful or a status describing the error. - */ - virtual Status apply() const; - - /** - * Adds a log entry to logRoot corresponding to full object replacement. Returns OK if - * successful or a status describing the error. - */ - virtual Status log(LogBuilder* logBuilder) const; - - private: - - // Object to replace with. - BSONObj _val; - - // The document whose value needs to be replaced. This state is valid after a prepare() - // was issued and until a log() is issued. The document this mod is being prepared - // against must e live throughout all the calls. - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; - -} // namespace mongo +class LogBuilder; + +class ModifierObjectReplace : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierObjectReplace); + +public: + ModifierObjectReplace(); + + // + // Modifier interface implementation + // + + virtual ~ModifierObjectReplace(); + + + /** + * Returns true and takes the embedded object contained in 'modExpr' to be the object + * we're replacing for. The field name of 'modExpr' is ignored. If 'modExpr' is in an + * unexpected format or if it can't be parsed for some reason, returns an error status + * describing the error. + */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); + + /** + * Registers the that 'root' is in the document that we want to fully replace. + * prepare() returns OK and always fills 'execInfo' with true for + * noOp. + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); + + /** + * Replaces the document passed in prepare() for the object passed in init(). Returns + * OK if successful or a status describing the error. + */ + virtual Status apply() const; + + /** + * Adds a log entry to logRoot corresponding to full object replacement. Returns OK if + * successful or a status describing the error. + */ + virtual Status log(LogBuilder* logBuilder) const; + +private: + // Object to replace with. + BSONObj _val; + + // The document whose value needs to be replaced. This state is valid after a prepare() + // was issued and until a log() is issued. The document this mod is being prepared + // against must e live throughout all the calls. + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_object_replace_test.cpp b/src/mongo/db/ops/modifier_object_replace_test.cpp index 3a5de50fead..083981fb34f 100644 --- a/src/mongo/db/ops/modifier_object_replace_test.cpp +++ b/src/mongo/db/ops/modifier_object_replace_test.cpp @@ -43,267 +43,267 @@ namespace { - using mongo::BSONObj; - using mongo::fromjson; - using mongo::LogBuilder; - using mongo::ModifierInterface; - using mongo::ModifierObjectReplace; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::countChildren; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - using mongo::mutablebson::findFirstChildNamed; - using mongo::NumberInt; - using mongo::Status; - using mongo::StringData; - - /** Helper to build and manipulate a $set mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) - : _mod() { - _modObj = modObj; - ASSERT_OK(_mod.init(BSON("" << modObj).firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierObjectReplace& mod() { return _mod; } - BSONObj& obj() { return _modObj; } - - private: - ModifierObjectReplace _mod; - BSONObj _modObj; - }; - - // Normal replacements below - TEST(Normal, SingleFieldDoc){ - Document doc(fromjson("{_id:1, a:1}")); - Mod mod(fromjson("{_id:1, b:12}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, fromjson("{_id:1, b:12}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); +using mongo::BSONObj; +using mongo::fromjson; +using mongo::LogBuilder; +using mongo::ModifierInterface; +using mongo::ModifierObjectReplace; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::countChildren; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; +using mongo::mutablebson::findFirstChildNamed; +using mongo::NumberInt; +using mongo::Status; +using mongo::StringData; + +/** Helper to build and manipulate a $set mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) : _mod() { + _modObj = modObj; + ASSERT_OK( + _mod.init(BSON("" << modObj).firstElement(), ModifierInterface::Options::normal())); } - TEST(Normal, ComplexDoc){ - Document doc(fromjson("{_id:1, a:1}")); - Mod mod(fromjson("{_id:1, b:[123], c: {r:true}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, fromjson("{_id:1, b:[123], c: {r:true}}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(Normal, OnlyIdField){ - Document doc(fromjson("{}")); - Mod mod(fromjson("{_id:1}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, mod.obj()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); + Status apply() const { + return _mod.apply(); } - // These updates have to do with updates without an _id field - // (the existing _id isn't removed) - TEST(IdLeft, EmptyDocReplacement){ - Document doc(fromjson("{_id:1}")); - Mod mod(fromjson("{}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, fromjson("{_id:1}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(IdLeft, EmptyDoc){ - Document doc(fromjson("{_id:1}")); - Mod mod(fromjson("{}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, fromjson("{_id:1}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); + ModifierObjectReplace& mod() { + return _mod; } - - TEST(IdLeft, SingleFieldAddition){ - Document doc(fromjson("{_id:1}")); - Mod mod(fromjson("{a:1}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, fromjson("{_id:1, a:1}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); - } - - TEST(IdLeft, SingleFieldReplaced){ - Document doc(fromjson("{a: []}")); - Mod mod(fromjson("{a:10}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, mod.obj()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); - } - - TEST(IdLeft, SwapFields){ - Document doc(fromjson("{_id:1, a:1}")); - Mod mod(fromjson("{b:1}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, fromjson("{_id:1, b:1}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(doc, logDoc); - } - - TEST(IdImmutable, ReplaceIdNumber){ - Document doc(fromjson("{_id:1, a:1}")); - Mod mod(fromjson("{_id:2}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_NOT_OK(mod.apply()); - } - - TEST(IdImmutable, ReplaceIdNumberSameVal){ - Document doc(fromjson("{_id:1, a:1}")); - Mod mod(fromjson("{_id:2}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_NOT_OK(mod.apply()); - } - - TEST(IdImmutable, ReplaceEmbeddedId){ - Document doc(fromjson("{_id:{a:1, b:2}, a:1}")); - Mod mod(fromjson("{_id:{b:2, a:1}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - ASSERT_NOT_OK(mod.apply()); - } - - TEST(Timestamp, IdNotReplaced){ - Document doc(fromjson("{}")); - Mod mod(fromjson("{_id:Timestamp(0,0), a:1}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_EQUALS(doc, mod.obj()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(mod.obj(), logDoc); - - Element idElem = findFirstChildNamed(logDoc.root(), "_id"); - ASSERT(idElem.ok()); - ASSERT(idElem.getValueTimestamp().isNull()); - - } - - TEST(Timestamp, ReplaceAll){ - Document doc(fromjson("{}")); - Mod mod(fromjson("{a:Timestamp(0,0), r:1, x:1, b:Timestamp(0,0)}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - - Element elem = findFirstChildNamed(doc.root(), "a"); - ASSERT(elem.ok()); - ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getSecs()); - ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getInc()); - - elem = findFirstChildNamed(doc.root(), "b"); - ASSERT(elem.ok()); - ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getSecs()); - ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getInc()); + BSONObj& obj() { + return _modObj; } -} // unnamed namespace +private: + ModifierObjectReplace _mod; + BSONObj _modObj; +}; + +// Normal replacements below +TEST(Normal, SingleFieldDoc) { + Document doc(fromjson("{_id:1, a:1}")); + Mod mod(fromjson("{_id:1, b:12}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, fromjson("{_id:1, b:12}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(Normal, ComplexDoc) { + Document doc(fromjson("{_id:1, a:1}")); + Mod mod(fromjson("{_id:1, b:[123], c: {r:true}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, fromjson("{_id:1, b:[123], c: {r:true}}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(Normal, OnlyIdField) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{_id:1}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, mod.obj()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +// These updates have to do with updates without an _id field +// (the existing _id isn't removed) +TEST(IdLeft, EmptyDocReplacement) { + Document doc(fromjson("{_id:1}")); + Mod mod(fromjson("{}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, fromjson("{_id:1}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(IdLeft, EmptyDoc) { + Document doc(fromjson("{_id:1}")); + Mod mod(fromjson("{}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, fromjson("{_id:1}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(IdLeft, SingleFieldAddition) { + Document doc(fromjson("{_id:1}")); + Mod mod(fromjson("{a:1}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, fromjson("{_id:1, a:1}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(IdLeft, SingleFieldReplaced) { + Document doc(fromjson("{a: []}")); + Mod mod(fromjson("{a:10}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, mod.obj()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(IdLeft, SwapFields) { + Document doc(fromjson("{_id:1, a:1}")); + Mod mod(fromjson("{b:1}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, fromjson("{_id:1, b:1}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(doc, logDoc); +} + +TEST(IdImmutable, ReplaceIdNumber) { + Document doc(fromjson("{_id:1, a:1}")); + Mod mod(fromjson("{_id:2}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_NOT_OK(mod.apply()); +} + +TEST(IdImmutable, ReplaceIdNumberSameVal) { + Document doc(fromjson("{_id:1, a:1}")); + Mod mod(fromjson("{_id:2}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_NOT_OK(mod.apply()); +} + +TEST(IdImmutable, ReplaceEmbeddedId) { + Document doc(fromjson("{_id:{a:1, b:2}, a:1}")); + Mod mod(fromjson("{_id:{b:2, a:1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + ASSERT_NOT_OK(mod.apply()); +} + +TEST(Timestamp, IdNotReplaced) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{_id:Timestamp(0,0), a:1}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_EQUALS(doc, mod.obj()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(mod.obj(), logDoc); + + Element idElem = findFirstChildNamed(logDoc.root(), "_id"); + ASSERT(idElem.ok()); + ASSERT(idElem.getValueTimestamp().isNull()); +} + +TEST(Timestamp, ReplaceAll) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{a:Timestamp(0,0), r:1, x:1, b:Timestamp(0,0)}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + + Element elem = findFirstChildNamed(doc.root(), "a"); + ASSERT(elem.ok()); + ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getSecs()); + ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getInc()); + + elem = findFirstChildNamed(doc.root(), "b"); + ASSERT(elem.ok()); + ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getSecs()); + ASSERT_NOT_EQUALS(0U, elem.getValueTimestamp().getInc()); +} + +} // unnamed namespace diff --git a/src/mongo/db/ops/modifier_pop.cpp b/src/mongo/db/ops/modifier_pop.cpp index e5fd836dea7..c46fdd7a9bf 100644 --- a/src/mongo/db/ops/modifier_pop.cpp +++ b/src/mongo/db/ops/modifier_pop.cpp @@ -38,176 +38,161 @@ namespace mongo { - namespace mb = mutablebson; - namespace str = mongoutils::str; +namespace mb = mutablebson; +namespace str = mongoutils::str; - struct ModifierPop::PreparedState { +struct ModifierPop::PreparedState { + PreparedState(mutablebson::Document* targetDoc) + : doc(*targetDoc), + elementToRemove(doc.end()), + pathFoundIndex(0), + pathFoundElement(doc.end()) {} - PreparedState(mutablebson::Document* targetDoc) - : doc(*targetDoc) - , elementToRemove(doc.end()) - , pathFoundIndex(0) - , pathFoundElement(doc.end()) { - } + // Document that is going to be changed. + mutablebson::Document& doc; - // Document that is going to be changed. - mutablebson::Document& doc; + // Element to be removed + mutablebson::Element elementToRemove; - // Element to be removed - mutablebson::Element elementToRemove; + // Index in _fieldRef for which an Element exist in the document. + size_t pathFoundIndex; - // Index in _fieldRef for which an Element exist in the document. - size_t pathFoundIndex; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element pathFoundElement; +}; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element pathFoundElement; - }; +ModifierPop::ModifierPop() : _fieldRef(), _positionalPathIndex(0), _fromTop(false) {} - ModifierPop::ModifierPop() - : _fieldRef() - , _positionalPathIndex(0) - , _fromTop(false) { - } +ModifierPop::~ModifierPop() {} - ModifierPop::~ModifierPop() { - } +Status ModifierPop::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + // + // field name analysis + // - Status ModifierPop::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - // - // field name analysis - // - - // Break down the field name into its 'dotted' components (aka parts) and check that - // there are no empty parts. - _fieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fieldRef); - if (! status.isOK()) { - return status; - } + // Break down the field name into its 'dotted' components (aka parts) and check that + // there are no empty parts. + _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, - &_positionalPathIndex, - &foundCount); + // 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, &_positionalPathIndex, &foundCount); - if (positional) - *positional = foundDollar; + 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() << "'"); - } + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); + } - // - // value analysis - // + // + // value analysis + // - // TODO: tighten validation to numbers and just 1/-1 explicitly - //if (!modExpr.isNumber()) { - // return Status(ErrorCodes::BadValue, "Must be a number"); - //} + // TODO: tighten validation to numbers and just 1/-1 explicitly + // if (!modExpr.isNumber()) { + // return Status(ErrorCodes::BadValue, "Must be a number"); + //} - _fromTop = (modExpr.isNumber() && modExpr.number() < 0) ? true : false; + _fromTop = (modExpr.isNumber() && modExpr.number() < 0) ? true : false; - return Status::OK(); - } + return Status::OK(); +} - Status ModifierPop::prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo) { +Status ModifierPop::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(&root.getDocument())); - _preparedState.reset(new PreparedState(&root.getDocument())); + // If we have a $-positional field, it is time to bind it to an actual field part. + if (_positionalPathIndex) { + 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(_positionalPathIndex, matchedField); + } - // If we have a $-positional field, it is time to bind it to an actual field part. - if (_positionalPathIndex) { - if (matchedField.empty()) { + // Locate the field name in 'root'. Note that if we don't have the full path in the + // doc, there isn't anything to unset, really. + Status status = pathsupport::findLongestPrefix( + _fieldRef, root, &_preparedState->pathFoundIndex, &_preparedState->pathFoundElement); + // Check if we didn't find the full path + if (status.isOK()) { + const bool destExists = (_preparedState->pathFoundIndex == (_fieldRef.numParts() - 1)); + if (!destExists) { + execInfo->noOp = true; + } else { + // If the path exists, we require the target field to be already an + // array. + if (_preparedState->pathFoundElement.getType() != Array) { + mb::Element idElem = mb::findFirstChildNamed(root, "_id"); return Status(ErrorCodes::BadValue, - str::stream() << "The positional operator did not find the match " - "needed from the query. Unexpanded update: " - << _fieldRef.dottedField()); + str::stream() + << "Can only $pop from arrays. {" << idElem.toString() + << "} has the field '" + << _preparedState->pathFoundElement.getFieldName() + << "' of non-array type " + << typeName(_preparedState->pathFoundElement.getType())); } - _fieldRef.setPart(_positionalPathIndex, matchedField); - } - // Locate the field name in 'root'. Note that if we don't have the full path in the - // doc, there isn't anything to unset, really. - Status status = pathsupport::findLongestPrefix(_fieldRef, - root, - &_preparedState->pathFoundIndex, - &_preparedState->pathFoundElement); - // Check if we didn't find the full path - if (status.isOK()) { - const bool destExists = (_preparedState->pathFoundIndex == (_fieldRef.numParts()-1)); - if (!destExists) { + // No children, nothing to do -- not an error state + if (!_preparedState->pathFoundElement.hasChildren()) { execInfo->noOp = true; } else { - // If the path exists, we require the target field to be already an - // array. - if (_preparedState->pathFoundElement.getType() != Array) { - mb::Element idElem = mb::findFirstChildNamed(root, "_id"); - return Status( - ErrorCodes::BadValue, - str::stream() << "Can only $pop from arrays. {" - << idElem.toString() - << "} has the field '" - << _preparedState->pathFoundElement.getFieldName() - << "' of non-array type " - << typeName(_preparedState->pathFoundElement.getType())); - } - - // No children, nothing to do -- not an error state - if (!_preparedState->pathFoundElement.hasChildren()) { - execInfo->noOp = true; - } else { - _preparedState->elementToRemove = _fromTop ? - _preparedState->pathFoundElement.leftChild() : - _preparedState->pathFoundElement.rightChild(); - } + _preparedState->elementToRemove = _fromTop + ? _preparedState->pathFoundElement.leftChild() + : _preparedState->pathFoundElement.rightChild(); } - } else { - // Let the caller know we can't do anything given the mod, _fieldRef, and doc. - execInfo->noOp = true; - _preparedState->pathFoundElement = root.getDocument().end(); - - //okay if path not found - if (status.code() == ErrorCodes::NonExistentPath) - status = Status::OK(); } + } else { + // Let the caller know we can't do anything given the mod, _fieldRef, and doc. + execInfo->noOp = true; + _preparedState->pathFoundElement = root.getDocument().end(); + + // okay if path not found + if (status.code() == ErrorCodes::NonExistentPath) + status = Status::OK(); + } - // Let the caller know what field we care about - execInfo->fieldRef[0] = &_fieldRef; + // Let the caller know what field we care about + execInfo->fieldRef[0] = &_fieldRef; - return status; - } + return status; +} - Status ModifierPop::apply() const { - return _preparedState->elementToRemove.remove(); - } +Status ModifierPop::apply() const { + return _preparedState->elementToRemove.remove(); +} - Status ModifierPop::log(LogBuilder* logBuilder) const { - // log document - mutablebson::Document& doc = logBuilder->getDocument(); - const bool pathExists = _preparedState->pathFoundElement.ok() && - (_preparedState->pathFoundIndex == (_fieldRef.numParts() - 1)); - - if (!pathExists) - return logBuilder->addToUnsets(_fieldRef.dottedField()); - - // value for the logElement ("field.path.name": <value>) - mutablebson::Element logElement = doc.makeElementWithNewFieldName( - _fieldRef.dottedField(), - _preparedState->pathFoundElement); - - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, - str::stream() << "Could not append entry to $pop oplog entry: " - << "set '" << _fieldRef.dottedField() << "' -> " - << _preparedState->pathFoundElement.toString() ); - } - return logBuilder->addToSets(logElement); +Status ModifierPop::log(LogBuilder* logBuilder) const { + // log document + mutablebson::Document& doc = logBuilder->getDocument(); + const bool pathExists = _preparedState->pathFoundElement.ok() && + (_preparedState->pathFoundIndex == (_fieldRef.numParts() - 1)); + + if (!pathExists) + return logBuilder->addToUnsets(_fieldRef.dottedField()); + + // value for the logElement ("field.path.name": <value>) + mutablebson::Element logElement = + doc.makeElementWithNewFieldName(_fieldRef.dottedField(), _preparedState->pathFoundElement); + + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, + str::stream() << "Could not append entry to $pop oplog entry: " + << "set '" << _fieldRef.dottedField() << "' -> " + << _preparedState->pathFoundElement.toString()); } -} // namespace mongo + return logBuilder->addToSets(logElement); +} +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_pop.h b/src/mongo/db/ops/modifier_pop.h index 41501275619..b0de117a946 100644 --- a/src/mongo/db/ops/modifier_pop.h +++ b/src/mongo/db/ops/modifier_pop.h @@ -38,51 +38,46 @@ namespace mongo { - class LogBuilder; +class LogBuilder; - class ModifierPop : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierPop); +class ModifierPop : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierPop); - public: +public: + ModifierPop(); + virtual ~ModifierPop(); - ModifierPop(); - virtual ~ModifierPop(); + /** + * The format of this modifier ($pop) is {<fieldname>: <value>}. + * If the value is number and greater than -1 then an element is removed from the bottom, + * otherwise the top. Currently the value can be any anything but we document + * the use of the numbers "1, -1" only. + * + * Ex. $pop: {'a':1} will remove the last item from this array: [1,2,3] -> [1,2] + */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); - /** - * The format of this modifier ($pop) is {<fieldname>: <value>}. - * If the value is number and greater than -1 then an element is removed from the bottom, - * otherwise the top. Currently the value can be any anything but we document - * the use of the numbers "1, -1" only. - * - * Ex. $pop: {'a':1} will remove the last item from this array: [1,2,3] -> [1,2] - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); + virtual Status apply() const; - virtual Status apply() const; + virtual Status log(LogBuilder* logBuilder) const; - virtual Status log(LogBuilder* logBuilder) const; +private: + // Access to each component of fieldName that's the target of this mod. + FieldRef _fieldRef; - private: + // 0 or index for $-positional in _fieldRef. + size_t _positionalPathIndex; - // Access to each component of fieldName that's the target of this mod. - FieldRef _fieldRef; + // element position to remove from + bool _fromTop; - // 0 or index for $-positional in _fieldRef. - size_t _positionalPathIndex; + // The instance of the field in the provided doc. + // This data is valid after prepare, for use by log and apply + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; - // element position to remove from - bool _fromTop; - - // The instance of the field in the provided doc. - // This data is valid after prepare, for use by log and apply - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; - -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_pop_test.cpp b/src/mongo/db/ops/modifier_pop_test.cpp index 283b9bc5a81..373c61aeca6 100644 --- a/src/mongo/db/ops/modifier_pop_test.cpp +++ b/src/mongo/db/ops/modifier_pop_test.cpp @@ -42,275 +42,277 @@ namespace { - using mongo::Array; - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::fromjson; - using mongo::ModifierInterface; - using mongo::ModifierPop; - using mongo::Status; - using mongo::StringData; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate a $pop mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) { - _modObj = modObj; - ASSERT_OK(_mod.init(_modObj["$pop"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierPop& mod() { return _mod; } - - BSONObj modObj() { return _modObj; } - - private: - ModifierPop _mod; - BSONObj _modObj; - }; - - // - // Test init values which aren't numbers. - // These are going to cause a pop from the bottom. - // - TEST(Init, StringArg) { - BSONObj modObj = fromjson("{$pop: {a: 'hi'}}"); - ModifierPop mod; - ASSERT_OK(mod.init(modObj["$pop"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); +using mongo::Array; +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::fromjson; +using mongo::ModifierInterface; +using mongo::ModifierPop; +using mongo::Status; +using mongo::StringData; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** Helper to build and manipulate a $pop mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) { + _modObj = modObj; + ASSERT_OK(_mod.init(_modObj["$pop"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(Init, BoolTrueArg) { - BSONObj modObj = fromjson("{$pop: {a: true}}"); - ModifierPop mod; - ASSERT_OK(mod.init(modObj["$pop"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(Init, BoolFalseArg) { - BSONObj modObj = fromjson("{$pop: {a: false}}"); - ModifierPop mod; - ASSERT_OK(mod.init(modObj["$pop"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + Status apply() const { + return _mod.apply(); } - TEST(MissingField, AllButApply) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {s: 1}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "s"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$unset: {'s': true}}"), logDoc); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(SimpleMod, PrepareBottom) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {a: 1}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + ModifierPop& mod() { + return _mod; } - TEST(SimpleMod, PrepareApplyBottomO) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {a: 0}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a: [1]}")), doc); + BSONObj modObj() { + return _modObj; } - TEST(SimpleMod, PrepareTop) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {a: -1}}")); +private: + ModifierPop _mod; + BSONObj _modObj; +}; + +// +// Test init values which aren't numbers. +// These are going to cause a pop from the bottom. +// +TEST(Init, StringArg) { + BSONObj modObj = fromjson("{$pop: {a: 'hi'}}"); + ModifierPop mod; + ASSERT_OK(mod.init(modObj["$pop"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, BoolTrueArg) { + BSONObj modObj = fromjson("{$pop: {a: true}}"); + ModifierPop mod; + ASSERT_OK(mod.init(modObj["$pop"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, BoolFalseArg) { + BSONObj modObj = fromjson("{$pop: {a: false}}"); + ModifierPop mod; + ASSERT_OK(mod.init(modObj["$pop"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(MissingField, AllButApply) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {s: 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "s"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$unset: {'s': true}}"), logDoc); +} + +TEST(SimpleMod, PrepareBottom) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {a: 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(SimpleMod, PrepareApplyBottomO) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {a: 0}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a: [1]}")), doc); +} + +TEST(SimpleMod, PrepareTop) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {a: -1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(SimpleMod, ApplyTopPop) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {a: -1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a: [2]}")), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); +TEST(SimpleMod, ApplyBottomPop) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {a: 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a: [1]}")), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - } +TEST(SimpleMod, ApplyLogBottomPop) { + Document doc(fromjson("{a: [1,2]}")); + Mod mod(fromjson("{$pop: {a: 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - TEST(SimpleMod, ApplyTopPop) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {a: -1}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a:[1]}")), doc); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); +} - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a: [2]}")), doc); - } +TEST(EmptyArray, PrepareNoOp) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{$pop: {a: 1}}")); - TEST(SimpleMod, ApplyBottomPop) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {a: 1}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(SingleElemArray, ApplyLog) { + Document doc(fromjson("{a: [1]}")); + Mod mod(fromjson("{$pop: {a: 1}}")); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a: [1]}")), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - TEST(SimpleMod, ApplyLogBottomPop) { - Document doc(fromjson("{a: [1,2]}")); - Mod mod(fromjson("{$pop: {a: 1}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a:[]}")), doc); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$set: {a: []}}"), logDoc); +} - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a:[1]}")), doc); +TEST(ArrayOfArray, ApplyLogPop) { + Document doc(fromjson("{a: [[1,2], 1]}")); + Mod mod(fromjson("{$pop: {'a.0': 1}}")); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - TEST(EmptyArray, PrepareNoOp) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{$pop: {a: 1}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a:[[1], 1]}")), doc); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$set: { 'a.0': [1]}}"), logDoc); +} - TEST(SingleElemArray, ApplyLog) { - Document doc(fromjson("{a: [1]}")); - Mod mod(fromjson("{$pop: {a: 1}}")); +TEST(ArrayOfArray, ApplyLogPopOnlyElement) { + Document doc(fromjson("{a: [[1], 1]}")); + Mod mod(fromjson("{$pop: {'a.0': 1}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a:[]}")), doc); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(("{a:[[], 1]}")), doc); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$set: {a: []}}"), logDoc); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$set: { 'a.0': []}}"), logDoc); +} - TEST(ArrayOfArray, ApplyLogPop) { - Document doc(fromjson("{a: [[1,2], 1]}")); - Mod mod(fromjson("{$pop: {'a.0': 1}}")); +TEST(Prepare, MissingPath) { + Document doc(fromjson("{ a : [1, 2] }")); + Mod mod(fromjson("{ $pop : { b : 1 } }")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); +// from SERVER-12846 +TEST(Prepare, MissingArrayElementPath) { + Document doc(fromjson("{_id : 1, a : [1, 2]}")); + Mod mod(fromjson("{ $pop : { 'a.3' : 1 } }")); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a:[[1], 1]}")), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); +} - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$set: { 'a.0': [1]}}"), logDoc); - } +TEST(Prepare, FromArrayElementPath) { + Document doc(fromjson("{ a : [1, 2] }")); + Mod mod(fromjson("{ $pop : { 'a.0' : 1 } }")); - TEST(ArrayOfArray, ApplyLogPopOnlyElement) { - Document doc(fromjson("{a: [[1], 1]}")); - Mod mod(fromjson("{$pop: {'a.0': 1}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(("{a:[[], 1]}")), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$set: { 'a.0': []}}"), logDoc); - } - - TEST(Prepare, MissingPath) { - Document doc(fromjson("{ a : [1, 2] }")); - Mod mod(fromjson("{ $pop : { b : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - } - - // from SERVER-12846 - TEST(Prepare, MissingArrayElementPath) { - Document doc(fromjson("{_id : 1, a : [1, 2]}")); - Mod mod(fromjson("{ $pop : { 'a.3' : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - } - - TEST(Prepare, FromArrayElementPath) { - Document doc(fromjson("{ a : [1, 2] }")); - Mod mod(fromjson("{ $pop : { 'a.0' : 1 } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} -} // unnamed namespace +} // unnamed namespace diff --git a/src/mongo/db/ops/modifier_pull.cpp b/src/mongo/db/ops/modifier_pull.cpp index 5fe94e878ea..3b45064afc7 100644 --- a/src/mongo/db/ops/modifier_pull.cpp +++ b/src/mongo/db/ops/modifier_pull.cpp @@ -38,265 +38,238 @@ namespace mongo { - namespace mb = mutablebson; - namespace str = mongoutils::str; +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<mb::Element> 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; + } - struct ModifierPull::PreparedState { + // 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); - PreparedState(mb::Document& doc) - : doc(doc) - , idxFound(0) - , elemFound(doc.end()) - , elementsToRemove() - , noOp(false) { - } + if (positional) + *positional = foundDollar; - // Document that is going to be changed. - mb::Document& doc; + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); + } - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + _exprElt = modExpr; - // Element corresponding to _fieldRef[0.._idxFound]. - mb::Element elemFound; + // 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(); - // Values to be removed. - std::vector<mb::Element> elementsToRemove; + // If not is not a query operator, then it is a primitive. + _matcherOnPrimitive = (_exprObj.firstElement().getGtLtOp() != 0); - // True if this update is a no-op - bool noOp; - }; + // 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(""); + } - ModifierPull::ModifierPull() - : ModifierInterface() - , _fieldRef() - , _posDollar(0) - , _exprElt() - , _exprObj() - , _matchExpr() - , _matcherOnPrimitive(false) - , _preparedState() { - } + // 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(); - ModifierPull::~ModifierPull() { + _matchExpr.reset(parseResult.getValue()); } - 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); + return Status::OK(); +} - if (positional) - *positional = foundDollar; +Status ModifierPull::prepare(mb::Element root, StringData matchedField, ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(root.getDocument())); - if (foundDollar && foundCount > 1) { + // 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() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); + str::stream() << "The positional operator did not find the match " + "needed from the query. Unexpanded update: " + << _fieldRef.dottedField()); } + _fieldRef.setPart(_posDollar, matchedField); + } - _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(); + // 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; + } - _matchExpr.reset(parseResult.getValue()); - } + // 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(); } - 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(); - } + // This operation only applies to arrays + if (_preparedState->elemFound.getType() != mongo::Array) + return Status(ErrorCodes::BadValue, "Cannot apply $pull to a non-array value"); - // 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 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(); + } - // 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; - } + // 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(); + } - return Status::OK(); + // 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; } - Status ModifierPull::apply() const { - dassert(_preparedState->noOp == false); + return Status::OK(); +} - dassert(_preparedState->elemFound.ok() && - _preparedState->idxFound == (_fieldRef.numParts() - 1)); +Status ModifierPull::apply() const { + dassert(_preparedState->noOp == false); - std::vector<mb::Element>::const_iterator where = _preparedState->elementsToRemove.begin(); - const std::vector<mb::Element>::const_iterator end = _preparedState->elementsToRemove.end(); - for ( ; where != end; ++where) - const_cast<mb::Element&>(*where).remove(); + dassert(_preparedState->elemFound.ok() && + _preparedState->idxFound == (_fieldRef.numParts() - 1)); - return Status::OK(); - } + std::vector<mb::Element>::const_iterator where = _preparedState->elementsToRemove.begin(); + const std::vector<mb::Element>::const_iterator end = _preparedState->elementsToRemove.end(); + for (; where != end; ++where) + const_cast<mb::Element&>(*where).remove(); - Status ModifierPull::log(LogBuilder* logBuilder) const { + return Status::OK(); +} - mb::Document& doc = logBuilder->getDocument(); +Status ModifierPull::log(LogBuilder* logBuilder) const { + mb::Document& doc = logBuilder->getDocument(); - if (!_preparedState->elemFound.ok() || - _preparedState->idxFound < (_fieldRef.numParts() - 1)) { + 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()); - // 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. - } else { + // TODO We can log just a positional unset in several cases. For now, let's just log + // the full resulting array. - // 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. + // We'd like to create an entry such as {$set: {<fieldname>: [<resulting aray>]}} under + // 'logRoot'. We start by creating the {$set: ...} Element. - // TODO We can log just a positional unset in several cases. For now, let's just log - // the full resulting array. + // Then we create the {<fieldname>:[]} 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"); + } - // We'd like to create an entry such as {$set: {<fieldname>: [<resulting aray>]}} under - // 'logRoot'. We start by creating the {$set: ...} Element. + mb::Element curr = _preparedState->elemFound.leftChild(); + while (curr.ok()) { + dassert(curr.hasValue()); - // Then we create the {<fieldname>:[]} 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"); + // 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"); } - - 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(); + Status status = logElement.pushBack(currCopy); + if (!status.isOK()) { + return Status(ErrorCodes::BadValue, "could not append entry for $pull log"); } - - return logBuilder->addToSets(logElement); + curr = curr.rightSibling(); } - } - bool ModifierPull::isMatch(mutablebson::ConstElement element) { + return logBuilder->addToSets(logElement); + } +} - // 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? +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()); + dassert(element.hasValue()); - if (!_matchExpr) - return (element.compareWithBSONElement(_exprElt, false) == 0); + 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 (_matcherOnPrimitive) { + // TODO: This is kinda slow. + BSONObj candidate = element.getValue().wrap(""); + return _matchExpr->matchesBSON(candidate); + } - if (element.getType() != Object) - return false; + if (element.getType() != Object) + return false; - return _matchExpr->matchesBSON(element.getValueObject()); - } + return _matchExpr->matchesBSON(element.getValueObject()); +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_pull.h b/src/mongo/db/ops/modifier_pull.h index 4660fd28b62..957750518f0 100644 --- a/src/mongo/db/ops/modifier_pull.h +++ b/src/mongo/db/ops/modifier_pull.h @@ -36,52 +36,49 @@ namespace mongo { - class MatchExpression; +class MatchExpression; - class ModifierPull : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierPull); +class ModifierPull : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierPull); - public: - ModifierPull(); - virtual ~ModifierPull(); +public: + ModifierPull(); + virtual ~ModifierPull(); - /** Evaluates the array items to be removed and the match expression. */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); + /** Evaluates the array items to be removed and the match expression. */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); - /** Decides which portion of the array items will be removed from the provided element */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); + /** Decides which portion of the array items will be removed from the provided element */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - /** Updates the Element used in prepare with the effects of the $pull operation. */ - virtual Status apply() const; + /** Updates the Element used in prepare with the effects of the $pull operation. */ + virtual Status apply() const; - /** Converts the effects of this $pull into one or more equivalent $unset operations. */ - virtual Status log(LogBuilder* logBuilder) const; + /** Converts the effects of this $pull into one or more equivalent $unset operations. */ + virtual Status log(LogBuilder* logBuilder) const; - private: - bool isMatch(mutablebson::ConstElement element); +private: + bool isMatch(mutablebson::ConstElement element); - // Access to each component of fieldName that's the target of this mod. - FieldRef _fieldRef; + // 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; + // 0 or index for $-positional in _fieldRef. + size_t _posDollar; - // If we aren't using a matcher, we just keep modExpr as _exprElt and use that to match - // with woCompare. - BSONElement _exprElt; + // If we aren't using a matcher, we just keep modExpr as _exprElt and use that to match + // with woCompare. + BSONElement _exprElt; - // If we are using a matcher, we need to keep around a BSONObj for it. - BSONObj _exprObj; + // If we are using a matcher, we need to keep around a BSONObj for it. + BSONObj _exprObj; - // If we are using the matcher, this is the match expression we built around _exprObj. - std::unique_ptr<MatchExpression> _matchExpr; - bool _matcherOnPrimitive; + // If we are using the matcher, this is the match expression we built around _exprObj. + std::unique_ptr<MatchExpression> _matchExpr; + bool _matcherOnPrimitive; - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_pull_all.cpp b/src/mongo/db/ops/modifier_pull_all.cpp index c8a73abe1e0..287dc4828b4 100644 --- a/src/mongo/db/ops/modifier_pull_all.cpp +++ b/src/mongo/db/ops/modifier_pull_all.cpp @@ -38,217 +38,198 @@ namespace mongo { - using std::vector; +using std::vector; - namespace mb = mutablebson; - namespace str = mongoutils::str; +namespace mb = mutablebson; +namespace str = mongoutils::str; - struct ModifierPullAll::PreparedState { +struct ModifierPullAll::PreparedState { + PreparedState(mutablebson::Document* targetDoc) + : doc(*targetDoc), + pathFoundIndex(0), + pathFoundElement(doc.end()), + applyCalled(false), + elementsToRemove() {} - PreparedState(mutablebson::Document* targetDoc) - : doc(*targetDoc) - , pathFoundIndex(0) - , pathFoundElement(doc.end()) - , applyCalled(false) - , elementsToRemove() { - } - - // Document that is going to be changed. - mutablebson::Document& doc; + // Document that is going to be changed. + mutablebson::Document& doc; - // Index in _fieldRef for which an Element exist in the document. - size_t pathFoundIndex; + // Index in _fieldRef for which an Element exist in the document. + size_t pathFoundIndex; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element pathFoundElement; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element pathFoundElement; - bool applyCalled; + bool applyCalled; - // Elements to be removed - vector<mutablebson::Element> elementsToRemove; - }; + // Elements to be removed + vector<mutablebson::Element> elementsToRemove; +}; - namespace { +namespace { - struct mutableElementEqualsBSONElement : std::unary_function<BSONElement, bool> - { - mutableElementEqualsBSONElement(const mutablebson::Element& elem) : _what(elem) {} - bool operator()(const BSONElement& elem) const { - return _what.compareWithBSONElement(elem, false) == 0; - } - const mutablebson::Element& _what; - }; - } // namespace - - ModifierPullAll::ModifierPullAll() - : _fieldRef() - , _positionalPathIndex(0) - , _elementsToFind() { +struct mutableElementEqualsBSONElement : std::unary_function<BSONElement, bool> { + mutableElementEqualsBSONElement(const mutablebson::Element& elem) : _what(elem) {} + bool operator()(const BSONElement& elem) const { + return _what.compareWithBSONElement(elem, false) == 0; } + const mutablebson::Element& _what; +}; +} // namespace + +ModifierPullAll::ModifierPullAll() : _fieldRef(), _positionalPathIndex(0), _elementsToFind() {} + +ModifierPullAll::~ModifierPullAll() {} - ModifierPullAll::~ModifierPullAll() { +Status ModifierPullAll::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + // + // field name analysis + // + + // Break down the field name into its 'dotted' components (aka parts) and check that + // there are no empty parts. + _fieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fieldRef); + if (!status.isOK()) { + return status; } - Status ModifierPullAll::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { + // 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, &_positionalPathIndex, &foundCount); - // - // field name analysis - // + if (positional) + *positional = foundDollar; - // Break down the field name into its 'dotted' components (aka parts) and check that - // there are no empty parts. - _fieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fieldRef); - if (! status.isOK()) { - return status; - } + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); + } - // 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, - &_positionalPathIndex, - &foundCount); + // + // value analysis + // - if (positional) - *positional = foundDollar; + if (modExpr.type() != Array) { + return Status(ErrorCodes::BadValue, + str::stream() << "$pullAll requires an array argument but was given a " + << typeName(modExpr.type())); + } - if (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); - } + // store the stuff to remove later + _elementsToFind = modExpr.Array(); + + return Status::OK(); +} - // - // value analysis - // +Status ModifierPullAll::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(&root.getDocument())); - if (modExpr.type() != Array) { + // If we have a $-positional field, it is time to bind it to an actual field part. + if (_positionalPathIndex) { + if (matchedField.empty()) { return Status(ErrorCodes::BadValue, - str::stream() << "$pullAll requires an array argument but was given a " - << typeName(modExpr.type())); + str::stream() << "The positional operator did not find the match " + "needed from the query. Unexpanded update: " + << _fieldRef.dottedField()); } - - // store the stuff to remove later - _elementsToFind = modExpr.Array(); - - return Status::OK(); + _fieldRef.setPart(_positionalPathIndex, matchedField); } - Status ModifierPullAll::prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo) { + // Locate the field name in 'root'. Note that if we don't have the full path in the + // doc, there isn't anything to unset, really. + Status status = pathsupport::findLongestPrefix( + _fieldRef, root, &_preparedState->pathFoundIndex, &_preparedState->pathFoundElement); + // Check if we didn't find the full path + if (status.isOK()) { + const bool destExists = (_preparedState->pathFoundIndex == (_fieldRef.numParts() - 1)); - _preparedState.reset(new PreparedState(&root.getDocument())); - - // If we have a $-positional field, it is time to bind it to an actual field part. - if (_positionalPathIndex) { - if (matchedField.empty()) { + if (!destExists) { + execInfo->noOp = true; + } else { + // If the path exists, we require the target field to be already an + // array. + if (_preparedState->pathFoundElement.getType() != Array) { + mb::Element idElem = mb::findElementNamed(root.leftChild(), "_id"); return Status(ErrorCodes::BadValue, - str::stream() << "The positional operator did not find the match " - "needed from the query. Unexpanded update: " - << _fieldRef.dottedField()); + str::stream() + << "Can only apply $pullAll to an array. " << idElem.toString() + << " has the field " + << _preparedState->pathFoundElement.getFieldName() + << " of non-array type " + << typeName(_preparedState->pathFoundElement.getType())); } - _fieldRef.setPart(_positionalPathIndex, matchedField); - } - // Locate the field name in 'root'. Note that if we don't have the full path in the - // doc, there isn't anything to unset, really. - Status status = pathsupport::findLongestPrefix(_fieldRef, - root, - &_preparedState->pathFoundIndex, - &_preparedState->pathFoundElement); - // Check if we didn't find the full path - if (status.isOK()) { - const bool destExists = (_preparedState->pathFoundIndex == (_fieldRef.numParts()-1)); - - if (!destExists) { + // No children, nothing to do -- not an error state + if (!_preparedState->pathFoundElement.hasChildren()) { execInfo->noOp = true; } else { - // If the path exists, we require the target field to be already an - // array. - if (_preparedState->pathFoundElement.getType() != Array) { - mb::Element idElem = mb::findElementNamed(root.leftChild(), "_id"); - return Status( - ErrorCodes::BadValue, - str::stream() << "Can only apply $pullAll to an array. " - << idElem.toString() - << " has the field " - << _preparedState->pathFoundElement.getFieldName() - << " of non-array type " - << typeName(_preparedState->pathFoundElement.getType())); + mutablebson::Element elem = _preparedState->pathFoundElement.leftChild(); + while (elem.ok()) { + if (std::find_if(_elementsToFind.begin(), + _elementsToFind.end(), + mutableElementEqualsBSONElement(elem)) != + _elementsToFind.end()) { + _preparedState->elementsToRemove.push_back(elem); + } + elem = elem.rightSibling(); } - // No children, nothing to do -- not an error state - if (!_preparedState->pathFoundElement.hasChildren()) { + // Nothing to remove so it is a noOp. + if (_preparedState->elementsToRemove.empty()) execInfo->noOp = true; - } else { - mutablebson::Element elem = _preparedState->pathFoundElement.leftChild(); - while (elem.ok()) { - if (std::find_if(_elementsToFind.begin(), - _elementsToFind.end(), - mutableElementEqualsBSONElement(elem)) - != _elementsToFind.end()) { - _preparedState->elementsToRemove.push_back(elem); - } - elem = elem.rightSibling(); - } - - // Nothing to remove so it is a noOp. - if (_preparedState->elementsToRemove.empty()) - execInfo->noOp = true; - } } - } else { - // Let the caller know we can't do anything given the mod, _fieldRef, and doc. - execInfo->noOp = true; - - - //okay if path not found - if (status.code() == ErrorCodes::NonExistentPath) - status = Status::OK(); } + } else { + // Let the caller know we can't do anything given the mod, _fieldRef, and doc. + execInfo->noOp = true; - // Let the caller know what field we care about - execInfo->fieldRef[0] = &_fieldRef; - return status; + // okay if path not found + if (status.code() == ErrorCodes::NonExistentPath) + status = Status::OK(); } - Status ModifierPullAll::apply() const { - _preparedState->applyCalled = true; + // Let the caller know what field we care about + execInfo->fieldRef[0] = &_fieldRef; - vector<mutablebson::Element>::const_iterator curr = - _preparedState->elementsToRemove.begin(); - const vector<mutablebson::Element>::const_iterator end = - _preparedState->elementsToRemove.end(); - for ( ; curr != end; ++curr) { - const_cast<mutablebson::Element&>(*curr).remove(); - } - return Status::OK(); - } + return status; +} - Status ModifierPullAll::log(LogBuilder* logBuilder) const { - // log document - mutablebson::Document& doc = logBuilder->getDocument(); - const bool pathExists = _preparedState->pathFoundElement.ok() && - (_preparedState->pathFoundIndex == (_fieldRef.numParts() - 1)); - - if (!pathExists) - return logBuilder->addToUnsets(_fieldRef.dottedField()); - - // value for the logElement ("field.path.name": <value>) - mutablebson::Element logElement = doc.makeElementWithNewFieldName( - _fieldRef.dottedField(), - _preparedState->pathFoundElement); - - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, - str::stream() << "Could not append entry to $pullAll oplog entry: " - << "set '" << _fieldRef.dottedField() << "' -> " - << _preparedState->pathFoundElement.toString() ); - } - return logBuilder->addToSets(logElement); +Status ModifierPullAll::apply() const { + _preparedState->applyCalled = true; + + vector<mutablebson::Element>::const_iterator curr = _preparedState->elementsToRemove.begin(); + const vector<mutablebson::Element>::const_iterator end = _preparedState->elementsToRemove.end(); + for (; curr != end; ++curr) { + const_cast<mutablebson::Element&>(*curr).remove(); + } + return Status::OK(); +} + +Status ModifierPullAll::log(LogBuilder* logBuilder) const { + // log document + mutablebson::Document& doc = logBuilder->getDocument(); + const bool pathExists = _preparedState->pathFoundElement.ok() && + (_preparedState->pathFoundIndex == (_fieldRef.numParts() - 1)); + + if (!pathExists) + return logBuilder->addToUnsets(_fieldRef.dottedField()); + + // value for the logElement ("field.path.name": <value>) + mutablebson::Element logElement = + doc.makeElementWithNewFieldName(_fieldRef.dottedField(), _preparedState->pathFoundElement); + + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, + str::stream() << "Could not append entry to $pullAll oplog entry: " + << "set '" << _fieldRef.dottedField() << "' -> " + << _preparedState->pathFoundElement.toString()); } -} // namespace mongo + return logBuilder->addToSets(logElement); +} +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_pull_all.h b/src/mongo/db/ops/modifier_pull_all.h index fef6c7b03f3..9f7efb8ac8d 100644 --- a/src/mongo/db/ops/modifier_pull_all.h +++ b/src/mongo/db/ops/modifier_pull_all.h @@ -38,50 +38,44 @@ namespace mongo { - class LogBuilder; +class LogBuilder; - class ModifierPullAll : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierPullAll); +class ModifierPullAll : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierPullAll); - public: +public: + ModifierPullAll(); + virtual ~ModifierPullAll(); - ModifierPullAll(); - virtual ~ModifierPullAll(); + /** + * The modifier $pullAll takes an array of values to match literally, and remove + * + * Ex. {$pullAll : {<field> : [<values>]}} + * {$pullAll :{ array : [1,2] } } will transform {array: [1,2,3]} -> {array: [3]} + */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); - /** - * The modifier $pullAll takes an array of values to match literally, and remove - * - * Ex. {$pullAll : {<field> : [<values>]}} - * {$pullAll :{ array : [1,2] } } will transform {array: [1,2,3]} -> {array: [3]} - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); + virtual Status apply() const; - virtual Status apply() const; + virtual Status log(LogBuilder* logBuilder) const; - virtual Status log(LogBuilder* logBuilder) const; +private: + // Access to each component of fieldName that's the target of this mod. + FieldRef _fieldRef; - private: + // 0 or index for $-positional in _fieldRef. + size_t _positionalPathIndex; - // Access to each component of fieldName that's the target of this mod. - FieldRef _fieldRef; + // The instance of the field in the provided doc. + // This data is valid after prepare, for use by log and apply + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; - // 0 or index for $-positional in _fieldRef. - size_t _positionalPathIndex; + // User specified elements to remove + std::vector<BSONElement> _elementsToFind; +}; - // The instance of the field in the provided doc. - // This data is valid after prepare, for use by log and apply - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - - // User specified elements to remove - std::vector<BSONElement> _elementsToFind; - - }; - -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_pull_all_test.cpp b/src/mongo/db/ops/modifier_pull_all_test.cpp index 4d689f36ee5..4b22f4d97a8 100644 --- a/src/mongo/db/ops/modifier_pull_all_test.cpp +++ b/src/mongo/db/ops/modifier_pull_all_test.cpp @@ -42,209 +42,206 @@ namespace { - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::ModifierPullAll; - using mongo::ModifierInterface; - using mongo::NumberInt; - using mongo::Status; - using mongo::StringData; - using mongo::fromjson; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate the mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) - : _modObj(modObj) - , _mod() { - ASSERT_OK(_mod.init(_modObj["$pullAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierPullAll& mod() { return _mod; } - - private: - BSONObj _modObj; - ModifierPullAll _mod; - }; - - TEST(Init, BadThings) { - BSONObj modObj; - ModifierPullAll mod; - - modObj = fromjson("{$pullAll: {a:1}}"); - ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - modObj = fromjson("{$pullAll: {a:'test'}}"); - ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - modObj = fromjson("{$pullAll: {a:{}}}"); - ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - modObj = fromjson("{$pullAll: {a:true}}"); - ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - - } - - TEST(PrepareApply, SimpleNumber) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { a : [1] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : ['a', {r:1, b:2}] }"), doc); - } - - TEST(PrepareApply, MissingElement) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { a : ['r'] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [1, 'a', {r:1, b:2}] }"), doc); - } - - TEST(PrepareApply, TwoElements) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { a : [1, 'a'] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [{r:1, b:2}] }"), doc); - } - - TEST(EmptyResult, RemoveEverythingOutOfOrder) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : {a : [ {r:1, b:2}, 1, 'a' ] }}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::ModifierPullAll; +using mongo::ModifierInterface; +using mongo::NumberInt; +using mongo::Status; +using mongo::StringData; +using mongo::fromjson; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** Helper to build and manipulate the mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) : _modObj(modObj), _mod() { + ASSERT_OK(_mod.init(_modObj["$pullAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(EmptyResult, RemoveEverythingInOrder) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { a : [1, 'a', {r:1, b:2} ] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(EmptyResult, RemoveEverythingAndThenSome) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { a : [2,3,1,'r', {r:1, b:2}, 'a' ] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); + Status apply() const { + return _mod.apply(); } - TEST(PrepareLog, MissingPullValue) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { a : [2] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [1, 'a', {r:1, b:2}] } }"), logDoc); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(PrepareLog, MissingPath) { - Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); - Mod mod(fromjson("{ $pullAll : { b : [1] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $unset : { b : true } }"), logDoc); - } - - TEST(Prepare, MissingArrayElementPath) { - Document doc(fromjson("{ a : [1, 2] }")); - Mod mod(fromjson("{ $pullAll : { 'a.2' : [1] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - } - - TEST(Prepare, FromArrayElementPath) { - Document doc(fromjson("{ a : [1, 2] }")); - Mod mod(fromjson("{ $pullAll : { 'a.0' : [1] } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); + ModifierPullAll& mod() { + return _mod; } -} // namespace +private: + BSONObj _modObj; + ModifierPullAll _mod; +}; + +TEST(Init, BadThings) { + BSONObj modObj; + ModifierPullAll mod; + + modObj = fromjson("{$pullAll: {a:1}}"); + ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + modObj = fromjson("{$pullAll: {a:'test'}}"); + ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + modObj = fromjson("{$pullAll: {a:{}}}"); + ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + + modObj = fromjson("{$pullAll: {a:true}}"); + ASSERT_NOT_OK(mod.init(modObj["$pullAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(PrepareApply, SimpleNumber) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { a : [1] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : ['a', {r:1, b:2}] }"), doc); +} + +TEST(PrepareApply, MissingElement) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { a : ['r'] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [1, 'a', {r:1, b:2}] }"), doc); +} + +TEST(PrepareApply, TwoElements) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { a : [1, 'a'] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [{r:1, b:2}] }"), doc); +} + +TEST(EmptyResult, RemoveEverythingOutOfOrder) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : {a : [ {r:1, b:2}, 1, 'a' ] }}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); +} + +TEST(EmptyResult, RemoveEverythingInOrder) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { a : [1, 'a', {r:1, b:2} ] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); +} + +TEST(EmptyResult, RemoveEverythingAndThenSome) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { a : [2,3,1,'r', {r:1, b:2}, 'a' ] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); +} + +TEST(PrepareLog, MissingPullValue) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { a : [2] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [1, 'a', {r:1, b:2}] } }"), logDoc); +} + +TEST(PrepareLog, MissingPath) { + Document doc(fromjson("{ a : [1, 'a', {r:1, b:2}] }")); + Mod mod(fromjson("{ $pullAll : { b : [1] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $unset : { b : true } }"), logDoc); +} + +TEST(Prepare, MissingArrayElementPath) { + Document doc(fromjson("{ a : [1, 2] }")); + Mod mod(fromjson("{ $pullAll : { 'a.2' : [1] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); +} + +TEST(Prepare, FromArrayElementPath) { + Document doc(fromjson("{ a : [1, 2] }")); + Mod mod(fromjson("{ $pullAll : { 'a.0' : [1] } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +} // namespace diff --git a/src/mongo/db/ops/modifier_pull_test.cpp b/src/mongo/db/ops/modifier_pull_test.cpp index e0918b24db5..8862ac62575 100644 --- a/src/mongo/db/ops/modifier_pull_test.cpp +++ b/src/mongo/db/ops/modifier_pull_test.cpp @@ -40,537 +40,516 @@ namespace { - using mongo::BSONObj; - using mongo::LogBuilder; - using mongo::ModifierPull; - using mongo::ModifierInterface; - using mongo::Status; - using mongo::StringData; - using mongo::fromjson; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate a $pull mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) - : _modObj(modObj) - , _mod() { - ASSERT_OK(_mod.init(_modObj["$pull"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierPull& mod() { - return _mod; - } - - private: - BSONObj _modObj; - ModifierPull _mod; - }; - - TEST(SimpleMod, PrepareOKTargetNotFound) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $unset : { a : true } }"), logDoc); - } - - TEST(SimpleMod, PrepareOKTargetFound) { - Document doc(fromjson("{ a : [ 0, 1, 2, 3 ] }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - } - - TEST(SimpleMod, PrepareInvalidTargetString) { - Document doc(fromjson("{ a : 'foo' }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +using mongo::BSONObj; +using mongo::LogBuilder; +using mongo::ModifierPull; +using mongo::ModifierInterface; +using mongo::Status; +using mongo::StringData; +using mongo::fromjson; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** Helper to build and manipulate a $pull mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) : _modObj(modObj), _mod() { + ASSERT_OK(_mod.init(_modObj["$pull"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(SimpleMod, PrepareInvalidTargetObject) { - Document doc(fromjson("{ a : { 'foo' : 'bar' } }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); - } - - TEST(SimpleMod, PrepareAndLogEmptyDocument) { - Document doc(fromjson("{}")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $unset : { a : true } }"), logDoc); - } - - TEST(SimpleMod, PrepareAndLogMissingElementAfterFoundPath) { - Document doc(fromjson("{ a : { b : { c : {} } } }")); - Mod mod(fromjson("{ $pull : { 'a.b.c.d' : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b.c.d"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $unset : { 'a.b.c.d' : true } }"), logDoc); - } - - TEST(SimpleMod, PrepareAndLogEmptyArray) { - Document doc(fromjson("{ a : [] }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); - } - - TEST(SimpleMod, PullMatchesNone) { - Document doc(fromjson("{ a : [2, 3, 4, 5] }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [2, 3, 4, 5] } }"), logDoc); - } - - TEST(SimpleMod, ApplyAndLogPullMatchesOne) { - Document doc(fromjson("{ a : [0, 1, 2, 3, 4, 5] }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3, 4, 5 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [1, 2, 3, 4, 5] } }"), logDoc); - } - - TEST(SimpleMod, ApplyAndLogPullMatchesSeveral) { - Document doc(fromjson("{ a : [0, 1, 0, 2, 0, 3, 0, 4, 0, 5] }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3, 4, 5 ] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [1, 2, 3, 4, 5] } }"), logDoc); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(SimpleMod, ApplyAndLogPullMatchesAll) { - Document doc(fromjson("{ a : [0, -1, -2, -3, -4, -5] }")); - Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ a : [] }"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); + Status apply() const { + return _mod.apply(); } - TEST(ComplexMod, ApplyAndLogComplexDocAndMatching1) { - - const char* const strings[] = { - // Document: - "{ a : { b : [ { x : 1 }, { y : 'y' }, { x : 2 }, { z : 'z' } ] } }", - - // Modifier: - "{ $pull : { 'a.b' : { $or : [ " - " { 'y' : { $exists : true } }, " - " { 'z' : { $exists : true } } " - "] } } }", - - // Document result: - "{ a : { b : [ { x : 1 }, { x : 2 } ] } }", - - // Log result: - "{ $set : { 'a.b' : [ { x : 1 }, { x : 2 } ] } }" - }; - - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(ComplexMod, ApplyAndLogComplexDocAndMatching2) { - - const char* const strings[] = { - // Document: - "{ a : { b : [ { x : 1 }, { y : 'y' }, { x : 2 }, { z : 'z' } ] } }", - - // Modifier: - "{ $pull : { 'a.b' : { 'y' : { $exists : true } } } }", - - // Document result: - "{ a : { b : [ { x : 1 }, { x : 2 }, { z : 'z' } ] } }", - - // Log result: - "{ $set : { 'a.b' : [ { x : 1 }, { x : 2 }, { z : 'z' } ] } }" - }; - - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); + ModifierPull& mod() { + return _mod; } - TEST(ComplexMod, ApplyAndLogComplexDocAndMatching3) { +private: + BSONObj _modObj; + ModifierPull _mod; +}; + +TEST(SimpleMod, PrepareOKTargetNotFound) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $unset : { a : true } }"), logDoc); +} + +TEST(SimpleMod, PrepareOKTargetFound) { + Document doc(fromjson("{ a : [ 0, 1, 2, 3 ] }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); +} + +TEST(SimpleMod, PrepareInvalidTargetString) { + Document doc(fromjson("{ a : 'foo' }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, PrepareInvalidTargetObject) { + Document doc(fromjson("{ a : { 'foo' : 'bar' } }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(mod.prepare(doc.root(), "", &execInfo)); +} + +TEST(SimpleMod, PrepareAndLogEmptyDocument) { + Document doc(fromjson("{}")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $unset : { a : true } }"), logDoc); +} + +TEST(SimpleMod, PrepareAndLogMissingElementAfterFoundPath) { + Document doc(fromjson("{ a : { b : { c : {} } } }")); + Mod mod(fromjson("{ $pull : { 'a.b.c.d' : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b.c.d"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $unset : { 'a.b.c.d' : true } }"), logDoc); +} + +TEST(SimpleMod, PrepareAndLogEmptyArray) { + Document doc(fromjson("{ a : [] }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); +} + +TEST(SimpleMod, PullMatchesNone) { + Document doc(fromjson("{ a : [2, 3, 4, 5] }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [2, 3, 4, 5] } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogPullMatchesOne) { + Document doc(fromjson("{ a : [0, 1, 2, 3, 4, 5] }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3, 4, 5 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [1, 2, 3, 4, 5] } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogPullMatchesSeveral) { + Document doc(fromjson("{ a : [0, 1, 0, 2, 0, 3, 0, 4, 0, 5] }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [ 1, 2, 3, 4, 5 ] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [1, 2, 3, 4, 5] } }"), logDoc); +} + +TEST(SimpleMod, ApplyAndLogPullMatchesAll) { + Document doc(fromjson("{ a : [0, -1, -2, -3, -4, -5] }")); + Mod mod(fromjson("{ $pull : { a : { $lt : 1 } } }")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ a : [] }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{ $set : { a : [] } }"), logDoc); +} + +TEST(ComplexMod, ApplyAndLogComplexDocAndMatching1) { + const char* const strings[] = { + // Document: + "{ a : { b : [ { x : 1 }, { y : 'y' }, { x : 2 }, { z : 'z' } ] } }", + + // Modifier: + "{ $pull : { 'a.b' : { $or : [ " + " { 'y' : { $exists : true } }, " + " { 'z' : { $exists : true } } " + "] } } }", + + // Document result: + "{ a : { b : [ { x : 1 }, { x : 2 } ] } }", + + // Log result: + "{ $set : { 'a.b' : [ { x : 1 }, { x : 2 } ] } }"}; + + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} + +TEST(ComplexMod, ApplyAndLogComplexDocAndMatching2) { + const char* const strings[] = { + // Document: + "{ a : { b : [ { x : 1 }, { y : 'y' }, { x : 2 }, { z : 'z' } ] } }", + + // Modifier: + "{ $pull : { 'a.b' : { 'y' : { $exists : true } } } }", + + // Document result: + "{ a : { b : [ { x : 1 }, { x : 2 }, { z : 'z' } ] } }", + + // Log result: + "{ $set : { 'a.b' : [ { x : 1 }, { x : 2 }, { z : 'z' } ] } }"}; + + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} + +TEST(ComplexMod, ApplyAndLogComplexDocAndMatching3) { + const char* const strings[] = { + // Document: + "{ a : { b : [ { x : 1 }, { y : 'y' }, { x : 2 }, { z : 'z' } ] } }", + + // Modifier: + "{ $pull : { 'a.b' : { $in : [ { x : 1 }, { y : 'y' } ] } } }", - const char* const strings[] = { - // Document: - "{ a : { b : [ { x : 1 }, { y : 'y' }, { x : 2 }, { z : 'z' } ] } }", + // Document result: + "{ a : { b : [ { x : 2 }, { z : 'z' } ] } }", - // Modifier: - "{ $pull : { 'a.b' : { $in : [ { x : 1 }, { y : 'y' } ] } } }", + // Log result: + "{ $set : { 'a.b' : [ { x : 2 }, { z : 'z' } ] } }"}; - // Document result: - "{ a : { b : [ { x : 2 }, { z : 'z' } ] } }", + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - // Log result: - "{ $set : { 'a.b' : [ { x : 2 }, { z : 'z' } ] } }" - }; + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); +TEST(ValueMod, ApplyAndLogScalarValueMod) { + const char* const strings[] = {// Document: + "{ a : [1, 2, 1, 2, 1, 2] }", - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + // Modifier: + "{ $pull : { a : 1 } }", - TEST(ValueMod, ApplyAndLogScalarValueMod) { + // Document result: + "{ a : [ 2, 2, 2] }", - const char* const strings[] = { - // Document: - "{ a : [1, 2, 1, 2, 1, 2] }", + // Log result: + "{ $set : { a : [ 2, 2, 2 ] } }"}; - // Modifier: - "{ $pull : { a : 1 } }", + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - // Document result: - "{ a : [ 2, 2, 2] }", + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - // Log result: - "{ $set : { a : [ 2, 2, 2 ] } }" - }; + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(ValueMod, ApplyAndLogObjectValueMod) { + const char* const strings[] = {// Document: + "{ a : [ { x : 1 }, { y : 2 }, { x : 1 }, { y : 2 } ] }", - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + // Modifier: + "{ $pull : { a : { y : 2 } } }", - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + // Document result: + "{ a : [ { x : 1 }, { x : 1 }] }", - TEST(ValueMod, ApplyAndLogObjectValueMod) { + // Log result: + "{ $set : { a : [ { x : 1 }, { x : 1 } ] } }"}; - const char* const strings[] = { - // Document: - "{ a : [ { x : 1 }, { y : 2 }, { x : 1 }, { y : 2 } ] }", + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - // Modifier: - "{ $pull : { a : { y : 2 } } }", + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - // Document result: - "{ a : [ { x : 1 }, { x : 1 }] }", + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - // Log result: - "{ $set : { a : [ { x : 1 }, { x : 1 } ] } }" - }; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); +TEST(DocumentationTests, Example1) { + const char* const strings[] = { + // Document: + "{ flags: ['vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce' ] }", - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + // Modifier: + "{ $pull: { flags: 'msr' } }", - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + // Document result: + "{ flags: ['vme', 'de', 'pse', 'tsc', 'pae', 'mce' ] }", - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + // Log result: + "{ $set : { flags: ['vme', 'de', 'pse', 'tsc', 'pae', 'mce' ] } }"}; - TEST(DocumentationTests, Example1) { - const char* const strings[] = { - // Document: - "{ flags: ['vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce' ] }", + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - // Modifier: - "{ $pull: { flags: 'msr' } }", + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "flags"); + ASSERT_FALSE(execInfo.noOp); - // Document result: - "{ flags: ['vme', 'de', 'pse', 'tsc', 'pae', 'mce' ] }", + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - // Log result: - "{ $set : { flags: ['vme', 'de', 'pse', 'tsc', 'pae', 'mce' ] } }" - }; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); +TEST(DocumentationTests, Example2a) { + const char* const strings[] = {// Document: + "{ votes: [ 3, 5, 6, 7, 7, 8 ] }", - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "flags"); - ASSERT_FALSE(execInfo.noOp); + // Modifier: + "{ $pull: { votes: 7 } }", - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + // Document result: + "{ votes: [ 3, 5, 6, 8 ] }", - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + // Log result: + "{ $set : { votes: [ 3, 5, 6, 8 ] } }"}; - TEST(DocumentationTests, Example2a) { - const char* const strings[] = { - // Document: - "{ votes: [ 3, 5, 6, 7, 7, 8 ] }", + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - // Modifier: - "{ $pull: { votes: 7 } }", + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "votes"); + ASSERT_FALSE(execInfo.noOp); - // Document result: - "{ votes: [ 3, 5, 6, 8 ] }", + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - // Log result: - "{ $set : { votes: [ 3, 5, 6, 8 ] } }" - }; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); +TEST(DocumentationTests, Example2b) { + const char* const strings[] = {// Document: + "{ votes: [ 3, 5, 6, 7, 7, 8 ] }", - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "votes"); - ASSERT_FALSE(execInfo.noOp); + // Modifier: + "{ $pull: { votes: { $gt: 6 } } }", - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + // Document result: + "{ votes: [ 3, 5, 6 ] }", - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + // Log result: + "{ $set : { votes: [ 3, 5, 6 ] } }"}; - TEST(DocumentationTests, Example2b) { - const char* const strings[] = { - // Document: - "{ votes: [ 3, 5, 6, 7, 7, 8 ] }", + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - // Modifier: - "{ $pull: { votes: { $gt: 6 } } }", + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "votes"); + ASSERT_FALSE(execInfo.noOp); - // Document result: - "{ votes: [ 3, 5, 6 ] }", + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - // Log result: - "{ $set : { votes: [ 3, 5, 6 ] } }" - }; + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); +TEST(MatchingEdgeCases, NonObjectShortCircuit) { + const char* const strings[] = { + "{ a: [ { x: 1 }, 2 ] }", - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "votes"); - ASSERT_FALSE(execInfo.noOp); + "{ $pull: { a: { x: 1 } } }", - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + "{ a: [ 2 ] }", - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } - - TEST(MatchingEdgeCases, NonObjectShortCircuit) { - const char* const strings[] = { - "{ a: [ { x: 1 }, 2 ] }", - - "{ $pull: { a: { x: 1 } } }", - - "{ a: [ 2 ] }", - - "{ $set : { a: [ 2 ] } }", - }; + "{ $set : { a: [ 2 ] } }", + }; - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} - TEST(MatchingRegressions, SERVER_3988) { - const char* const strings[] = { - "{ x: 1, y: [ 2, 3, 4, 'abc', 'xyz' ] }", +TEST(MatchingRegressions, SERVER_3988) { + const char* const strings[] = { + "{ x: 1, y: [ 2, 3, 4, 'abc', 'xyz' ] }", - "{ $pull: { y: /yz/ } }", + "{ $pull: { y: /yz/ } }", - "{ x: 1, y: [ 2, 3, 4, 'abc' ] }", + "{ x: 1, y: [ 2, 3, 4, 'abc' ] }", - "{ $set : { y: [ 2, 3, 4, 'abc' ] } }", - }; + "{ $set : { y: [ 2, 3, 4, 'abc' ] } }", + }; - Document doc(fromjson(strings[0])); - Mod mod(fromjson(strings[1])); + Document doc(fromjson(strings[0])); + Mod mod(fromjson(strings[1])); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "y"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "y"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(mod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson(strings[2]), doc); + ASSERT_OK(mod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson(strings[2]), doc); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod.log(&logBuilder)); - ASSERT_EQUALS(fromjson(strings[3]), logDoc); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod.log(&logBuilder)); + ASSERT_EQUALS(fromjson(strings[3]), logDoc); +} -} // namespace +} // namespace diff --git a/src/mongo/db/ops/modifier_push.cpp b/src/mongo/db/ops/modifier_push.cpp index 70a227fddbd..636a9f4450a 100644 --- a/src/mongo/db/ops/modifier_push.cpp +++ b/src/mongo/db/ops/modifier_push.cpp @@ -44,220 +44,196 @@ namespace mongo { - using std::abs; - using std::numeric_limits; +using std::abs; +using std::numeric_limits; - namespace mb = mutablebson; - namespace str = mongoutils::str; +namespace mb = mutablebson; +namespace str = mongoutils::str; - namespace { +namespace { - const char kEach[] = "$each"; - const char kSlice[] = "$slice"; - const char kSort[] = "$sort"; - const char kPosition[] = "$position"; +const char kEach[] = "$each"; +const char kSlice[] = "$slice"; +const char kSort[] = "$sort"; +const char kPosition[] = "$position"; - bool isPatternElement(const BSONElement& pattern) { - if (!pattern.isNumber()) { - return false; - } +bool isPatternElement(const BSONElement& pattern) { + if (!pattern.isNumber()) { + return false; + } - // Patterns can be only 1 or -1. - double val = pattern.Number(); - if (val != 1 && val != -1) { - return false; - } + // Patterns can be only 1 or -1. + double val = pattern.Number(); + if (val != 1 && val != -1) { + return false; + } - return true; - } + return true; +} - bool inEachMode(const BSONElement& modExpr) { - if (modExpr.type() != Object) { - return false; - } - BSONObj obj = modExpr.embeddedObject(); - if (obj[kEach].type() == EOO) { - return false; - } - return true; +bool inEachMode(const BSONElement& modExpr) { + if (modExpr.type() != Object) { + return false; + } + BSONObj obj = modExpr.embeddedObject(); + if (obj[kEach].type() == EOO) { + return false; + } + return true; +} + +Status parseEachMode(ModifierPush::ModifierPushMode pushMode, + const BSONElement& modExpr, + BSONElement* eachElem, + BSONElement* sliceElem, + BSONElement* sortElem, + BSONElement* positionElem) { + Status status = Status::OK(); + + // If in $pushAll mode, all we need is the array. + if (pushMode == ModifierPush::PUSH_ALL) { + if (modExpr.type() != Array) { + return Status(ErrorCodes::BadValue, "$pushAll requires an array"); } + *eachElem = modExpr; + return Status::OK(); + } - Status parseEachMode(ModifierPush::ModifierPushMode pushMode, - const BSONElement& modExpr, - BSONElement* eachElem, - BSONElement* sliceElem, - BSONElement* sortElem, - BSONElement* positionElem) { - - Status status = Status::OK(); - - // If in $pushAll mode, all we need is the array. - if (pushMode == ModifierPush::PUSH_ALL) { - if (modExpr.type() != Array) { - return Status(ErrorCodes::BadValue, "$pushAll requires an array"); - } - *eachElem = modExpr; - return Status::OK(); - } - - // The $each clause must be an array. - *eachElem = modExpr.embeddedObject()[kEach]; - if (eachElem->type() != Array) { - return Status(ErrorCodes::BadValue, - str::stream() << "The argument to $each in $push must be" - " an array but it was of type " - << typeName(eachElem->type())); - } - - // There must be only one $each clause. - bool seenEach = false; - BSONObjIterator itMod(modExpr.embeddedObject()); - while (itMod.more()) { - BSONElement modElem = itMod.next(); - if (mongoutils::str::equals(modElem.fieldName(), kEach)) { - if (seenEach) { - return Status(ErrorCodes::BadValue, - "Only one $each clause is supported."); - } - seenEach = true; - } - } + // The $each clause must be an array. + *eachElem = modExpr.embeddedObject()[kEach]; + if (eachElem->type() != Array) { + return Status(ErrorCodes::BadValue, + str::stream() << "The argument to $each in $push must be" + " an array but it was of type " + << typeName(eachElem->type())); + } - // Slice, sort, position are optional and may be present in any order. - bool seenSlice = false; - bool seenSort = false; - bool seenPosition = false; - BSONObjIterator itPush(modExpr.embeddedObject()); - while (itPush.more()) { - BSONElement elem = itPush.next(); - if (mongoutils::str::equals(elem.fieldName(), kSlice)) { - if (seenSlice) { - return Status(ErrorCodes::BadValue, - "Only one $slice clause is supported."); - } - *sliceElem = elem; - seenSlice = true; - } - else if (mongoutils::str::equals(elem.fieldName(), kSort)) { - if (seenSort) { - return Status(ErrorCodes::BadValue, - "Only one $sort clause is supported."); - } - *sortElem = elem; - seenSort = true; - } - else if (mongoutils::str::equals(elem.fieldName(), kPosition)) { - if (seenPosition) { - return Status(ErrorCodes::BadValue, - "Only one $position clause is supported."); - } - *positionElem = elem; - seenPosition = true; - } - else if (!mongoutils::str::equals(elem.fieldName(), kEach)) { - return Status(ErrorCodes::BadValue, - str::stream() << "Unrecognized clause in $push: " - << elem.fieldNameStringData()); - } + // There must be only one $each clause. + bool seenEach = false; + BSONObjIterator itMod(modExpr.embeddedObject()); + while (itMod.more()) { + BSONElement modElem = itMod.next(); + if (mongoutils::str::equals(modElem.fieldName(), kEach)) { + if (seenEach) { + return Status(ErrorCodes::BadValue, "Only one $each clause is supported."); } - - return Status::OK(); + seenEach = true; } + } - } // unnamed namespace - - struct ModifierPush::PreparedState { - - PreparedState(mutablebson::Document* targetDoc) - : doc(*targetDoc) - , idxFound(0) - , elemFound(doc.end()) - , arrayPreModSize(0) { + // Slice, sort, position are optional and may be present in any order. + bool seenSlice = false; + bool seenSort = false; + bool seenPosition = false; + BSONObjIterator itPush(modExpr.embeddedObject()); + while (itPush.more()) { + BSONElement elem = itPush.next(); + if (mongoutils::str::equals(elem.fieldName(), kSlice)) { + if (seenSlice) { + return Status(ErrorCodes::BadValue, "Only one $slice clause is supported."); + } + *sliceElem = elem; + seenSlice = true; + } else if (mongoutils::str::equals(elem.fieldName(), kSort)) { + if (seenSort) { + return Status(ErrorCodes::BadValue, "Only one $sort clause is supported."); + } + *sortElem = elem; + seenSort = true; + } else if (mongoutils::str::equals(elem.fieldName(), kPosition)) { + if (seenPosition) { + return Status(ErrorCodes::BadValue, "Only one $position clause is supported."); + } + *positionElem = elem; + seenPosition = true; + } else if (!mongoutils::str::equals(elem.fieldName(), kEach)) { + return Status(ErrorCodes::BadValue, + str::stream() + << "Unrecognized clause in $push: " << elem.fieldNameStringData()); } + } - // Document that is going to be changed. - mutablebson::Document& doc; + return Status::OK(); +} - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; +} // unnamed namespace - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; +struct ModifierPush::PreparedState { + PreparedState(mutablebson::Document* targetDoc) + : doc(*targetDoc), idxFound(0), elemFound(doc.end()), arrayPreModSize(0) {} - size_t arrayPreModSize; + // Document that is going to be changed. + mutablebson::Document& doc; - }; + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; - ModifierPush::ModifierPush(ModifierPush::ModifierPushMode pushMode) - : _fieldRef() - , _posDollar(0) - , _eachMode(false) - , _eachElem() - , _slicePresent(false) - , _slice(0) - , _sortPresent(false) - , _startPosition(std::numeric_limits<std::size_t>::max()) - , _sort() - , _pushMode(pushMode) - , _val() { - } + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; - ModifierPush::~ModifierPush() { - } + size_t arrayPreModSize; +}; - Status ModifierPush::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { +ModifierPush::ModifierPush(ModifierPush::ModifierPushMode pushMode) + : _fieldRef(), + _posDollar(0), + _eachMode(false), + _eachElem(), + _slicePresent(false), + _slice(0), + _sortPresent(false), + _startPosition(std::numeric_limits<std::size_t>::max()), + _sort(), + _pushMode(pushMode), + _val() {} - // - // field name analysis - // +ModifierPush::~ModifierPush() {} - // Break down the field name into its 'dotted' components (aka parts) and check that - // the field is fit for updates. - _fieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fieldRef); - if (! status.isOK()) { - return status; - } +Status ModifierPush::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + // + // field name analysis + // - // 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); + // Break down the field name into its 'dotted' components (aka parts) and check that + // the field is fit for updates. + _fieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fieldRef); + if (!status.isOK()) { + return status; + } - if (positional) - *positional = foundDollar; + // 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 (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); - } + if (positional) + *positional = foundDollar; - // - // value analysis - // + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); + } - // Are the target push values safe to store? - BSONElement sliceElem; - BSONElement sortElem; - BSONElement positionElem; - switch (modExpr.type()) { + // + // value analysis + // + // Are the target push values safe to store? + BSONElement sliceElem; + BSONElement sortElem; + BSONElement positionElem; + switch (modExpr.type()) { case Array: if (_pushMode == PUSH_ALL) { _eachMode = true; - Status status = parseEachMode(PUSH_ALL, - modExpr, - &_eachElem, - &sliceElem, - &sortElem, - &positionElem); + Status status = parseEachMode( + PUSH_ALL, modExpr, &_eachElem, &sliceElem, &sortElem, &positionElem); if (!status.isOK()) { return status; } - } - else { + } else { _val = modExpr; } break; @@ -266,24 +242,19 @@ namespace mongo { if (_pushMode == PUSH_ALL) { return Status(ErrorCodes::BadValue, str::stream() << "$pushAll requires an array of values " - "but was given an embedded document."); + "but was given an embedded document."); } // If any known clause ($each, $slice, or $sort) is present, we'd assume // we're using the $each variation of push and would parse accodingly. _eachMode = inEachMode(modExpr); if (_eachMode) { - Status status = parseEachMode(PUSH_NORMAL, - modExpr, - &_eachElem, - &sliceElem, - &sortElem, - &positionElem); + Status status = parseEachMode( + PUSH_NORMAL, modExpr, &_eachElem, &sliceElem, &sortElem, &positionElem); if (!status.isOK()) { return status; } - } - else { + } else { _val = modExpr; } break; @@ -292,435 +263,400 @@ namespace mongo { if (_pushMode == PUSH_ALL) { return Status(ErrorCodes::BadValue, str::stream() << "$pushAll requires an array of values " - "but was given an " - << typeName(modExpr.type())); + "but was given an " << typeName(modExpr.type())); } _val = modExpr; break; + } + + // Is slice present and correct? + if (sliceElem.type() != EOO) { + if (_pushMode == PUSH_ALL) { + return Status(ErrorCodes::BadValue, "cannot use $slice in $pushAll"); } - // Is slice present and correct? - if (sliceElem.type() != EOO) { - if (_pushMode == PUSH_ALL) { - return Status(ErrorCodes::BadValue, "cannot use $slice in $pushAll"); - } + if (!sliceElem.isNumber()) { + return Status(ErrorCodes::BadValue, + str::stream() << "The value for $slice must " + "be a numeric value not a " + << typeName(sliceElem.type())); + } - if (!sliceElem.isNumber()) { - return Status(ErrorCodes::BadValue, - str::stream() << "The value for $slice must " - "be a numeric value not a " - << typeName(sliceElem.type())); - } + // TODO: Cleanup and unify numbers wrt getting int32/64 bson values (from doubles) - // TODO: Cleanup and unify numbers wrt getting int32/64 bson values (from doubles) + // If the value of slice is not fraction, even if it's a double, we allow it. The + // reason here is that the shell will use doubles by default unless told otherwise. + const double doubleVal = sliceElem.numberDouble(); + if (doubleVal - static_cast<int64_t>(doubleVal) != 0) { + return Status(ErrorCodes::BadValue, "The $slice value in $push cannot be fractional"); + } - // If the value of slice is not fraction, even if it's a double, we allow it. The - // reason here is that the shell will use doubles by default unless told otherwise. - const double doubleVal = sliceElem.numberDouble(); - if (doubleVal - static_cast<int64_t>(doubleVal) != 0) { - return Status(ErrorCodes::BadValue, - "The $slice value in $push cannot be fractional"); - } + _slice = sliceElem.numberLong(); + _slicePresent = true; + } - _slice = sliceElem.numberLong(); - _slicePresent = true; + // Is position present and correct? + if (positionElem.type() != EOO) { + if (_pushMode == PUSH_ALL) { + return Status(ErrorCodes::BadValue, "cannot use $position in $pushAll"); } - // Is position present and correct? - if (positionElem.type() != EOO) { - if (_pushMode == PUSH_ALL) { - return Status(ErrorCodes::BadValue, "cannot use $position in $pushAll"); - } + if (!positionElem.isNumber()) { + return Status(ErrorCodes::BadValue, + str::stream() << "The value for $position must " + "be a positive numeric value not a " + << typeName(positionElem.type())); + } - if (!positionElem.isNumber()) { - return Status(ErrorCodes::BadValue, - str::stream() << "The value for $position must " - "be a positive numeric value not a " - << typeName(positionElem.type())); - } + // TODO: Cleanup and unify numbers wrt getting int32/64 bson values (from doubles) - // TODO: Cleanup and unify numbers wrt getting int32/64 bson values (from doubles) + // If the value of position is not fraction, even if it's a double, we allow it. The + // reason here is that the shell will use doubles by default unless told otherwise. + const double doubleVal = positionElem.numberDouble(); + if (doubleVal - static_cast<int64_t>(doubleVal) != 0) { + return Status(ErrorCodes::BadValue, + "The $position value in $push cannot be fractional"); + } - // If the value of position is not fraction, even if it's a double, we allow it. The - // reason here is that the shell will use doubles by default unless told otherwise. - const double doubleVal = positionElem.numberDouble(); - if (doubleVal - static_cast<int64_t>(doubleVal) != 0) { - return Status(ErrorCodes::BadValue, - "The $position value in $push cannot be fractional"); - } + if (static_cast<double>(numeric_limits<int64_t>::max()) < doubleVal) { + return Status(ErrorCodes::BadValue, + "The $position value in $push is too large a number."); + } - if (static_cast<double>(numeric_limits<int64_t>::max()) < doubleVal) { - return Status(ErrorCodes::BadValue, - "The $position value in $push is too large a number."); - } + if (static_cast<double>(numeric_limits<int64_t>::min()) > doubleVal) { + return Status(ErrorCodes::BadValue, + "The $position value in $push is too small a number."); + } - if (static_cast<double>(numeric_limits<int64_t>::min()) > doubleVal) { - return Status(ErrorCodes::BadValue, - "The $position value in $push is too small a number."); - } + const int64_t tempVal = positionElem.numberLong(); + if (tempVal < 0) + return Status(ErrorCodes::BadValue, "The $position value in $push must be positive."); - const int64_t tempVal = positionElem.numberLong(); - if (tempVal < 0) - return Status(ErrorCodes::BadValue, - "The $position value in $push must be positive."); + _startPosition = size_t(tempVal); + } - _startPosition = size_t(tempVal); + // Is sort present and correct? + if (sortElem.type() != EOO) { + if (_pushMode == PUSH_ALL) { + return Status(ErrorCodes::BadValue, "cannot use $sort in $pushAll"); } - // Is sort present and correct? - if (sortElem.type() != EOO) { - if (_pushMode == PUSH_ALL) { - return Status(ErrorCodes::BadValue, - "cannot use $sort in $pushAll"); - } + if (sortElem.type() != Object && !sortElem.isNumber()) { + return Status(ErrorCodes::BadValue, + "The $sort is invalid: use 1/-1 to sort the whole element, " + "or {field:1/-1} to sort embedded fields"); + } - if (sortElem.type() != Object && !sortElem.isNumber()) { + if (sortElem.isABSONObj()) { + BSONObj sortObj = sortElem.embeddedObject(); + if (sortObj.isEmpty()) { return Status(ErrorCodes::BadValue, - "The $sort is invalid: use 1/-1 to sort the whole element, " - "or {field:1/-1} to sort embedded fields"); + "The $sort pattern is empty when it should be a set of fields."); } - if (sortElem.isABSONObj()) { - BSONObj sortObj = sortElem.embeddedObject(); - if (sortObj.isEmpty()) { + // Check if the sort pattern is sound. + BSONObjIterator sortIter(sortObj); + while (sortIter.more()) { + BSONElement sortPatternElem = sortIter.next(); + + // We require either <field>: 1 or -1 for asc and desc. + if (!isPatternElement(sortPatternElem)) { return Status(ErrorCodes::BadValue, - "The $sort pattern is empty when it should be a set of fields."); + "The $sort element value must be either 1 or -1"); } - // Check if the sort pattern is sound. - BSONObjIterator sortIter(sortObj); - while (sortIter.more()) { - - BSONElement sortPatternElem = sortIter.next(); - - // We require either <field>: 1 or -1 for asc and desc. - if (!isPatternElement(sortPatternElem)) { - return Status(ErrorCodes::BadValue, - "The $sort element value must be either 1 or -1"); - } + // All fields parts must be valid. + FieldRef sortField(sortPatternElem.fieldName()); + if (sortField.numParts() == 0) { + return Status(ErrorCodes::BadValue, "The $sort field cannot be empty"); + } - // All fields parts must be valid. - FieldRef sortField(sortPatternElem.fieldName()); - if (sortField.numParts() == 0) { + for (size_t i = 0; i < sortField.numParts(); i++) { + if (sortField.getPart(i).size() == 0) { return Status(ErrorCodes::BadValue, - "The $sort field cannot be empty"); - } - - for (size_t i = 0; i < sortField.numParts(); i++) { - if (sortField.getPart(i).size() == 0) { - return Status(ErrorCodes::BadValue, - str::stream() << "The $sort field is a dotted field " - "but has an empty part: " - << sortField.dottedField()); - } + str::stream() + << "The $sort field is a dotted field " + "but has an empty part: " << sortField.dottedField()); } } - - _sort = PatternElementCmp(sortElem.embeddedObject()); } - else { - // Ensure the sortElem number is valid. - if (!isPatternElement(sortElem)) { - return Status(ErrorCodes::BadValue, - "The $sort element value must be either 1 or -1"); - } - _sort = PatternElementCmp(BSON("" << sortElem.number())); + _sort = PatternElementCmp(sortElem.embeddedObject()); + } else { + // Ensure the sortElem number is valid. + if (!isPatternElement(sortElem)) { + return Status(ErrorCodes::BadValue, + "The $sort element value must be either 1 or -1"); } - _sortPresent = true; + _sort = PatternElementCmp(BSON("" << sortElem.number())); } - return Status::OK(); + _sortPresent = true; } - Status ModifierPush::prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo) { + return Status::OK(); +} - _preparedState.reset(new PreparedState(&root.getDocument())); +Status ModifierPush::prepare(mutablebson::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); + // 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'. 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 remainin 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()) { + const bool destExists = (_preparedState->idxFound == (_fieldRef.numParts() - 1)); + // If the path exists, we require the target field to be already an + // array. + if (destExists && _preparedState->elemFound.getType() != Array) { + mb::Element idElem = mb::findFirstChildNamed(root, "_id"); + return Status(ErrorCodes::BadValue, + str::stream() << "The field '" << _fieldRef.dottedField() << "'" + << " must be an array but is of type " + << typeName(_preparedState->elemFound.getType()) + << " in document {" << idElem.toString() << "}"); + } + } else { + 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; + + return Status::OK(); +} + +namespace { +Status pushFirstElement(mb::Element& arrayElem, + const size_t arraySize, + const size_t pos, + mb::Element& elem) { + // Empty array or pushing to the front + if (arraySize == 0 || pos == 0) { + return arrayElem.pushFront(elem); + } else { + // Push position is at the end, or beyond + if (pos >= arraySize) { + return arrayElem.pushBack(elem); } - // 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 remainin 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(); + const size_t appendPos = pos - 1; + mutablebson::Element fromElem = getNthChild(arrayElem, appendPos); + // This should not be possible since the checks above should + // cover us but error just in case + if (!fromElem.ok()) { + return Status(ErrorCodes::InvalidLength, + str::stream() << "The specified position (" << appendPos << "/" << pos + << ") is invalid based on the length ( " << arraySize + << ") of the array"); } - else if (status.isOK()) { - const bool destExists = (_preparedState->idxFound == (_fieldRef.numParts()-1)); - // If the path exists, we require the target field to be already an - // array. - if (destExists && _preparedState->elemFound.getType() != Array) { - mb::Element idElem = mb::findFirstChildNamed(root, "_id"); - return Status(ErrorCodes::BadValue, - str::stream() << "The field '" << _fieldRef.dottedField() << "'" - << " must be an array but is of type " - << typeName(_preparedState->elemFound.getType()) - << " in document {" << idElem.toString() << "}"); - } + return fromElem.addSiblingRight(elem); + } +} +} // unamed namespace + +Status ModifierPush::apply() const { + Status status = Status::OK(); + + // + // Applying a $push with an $clause has the following steps + // 1. Create the doc array we'll push into, if it is not there + // 2. Add the items in the $each array (or the simple $push) to the doc array + // 3. Sort the resulting array according to $sort clause, if present + // 4. Trim the resulting array according the $slice clasue, if present + // + // TODO There are _lots_ of optimization opportunities that we'll consider once the + // test coverage is adequate. + // + + // 1. If the array field is not there, create it as an array and attach it to the + // document. + if (!_preparedState->elemFound.ok() || _preparedState->idxFound < (_fieldRef.numParts() - 1)) { + // Creates the array element + mutablebson::Document& doc = _preparedState->doc; + StringData lastPart = _fieldRef.getPart(_fieldRef.numParts() - 1); + mutablebson::Element baseArray = doc.makeElementArray(lastPart); + if (!baseArray.ok()) { + return Status(ErrorCodes::InternalError, "can't create new base array"); } - else { - return status; + + // 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++; } - // 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; + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + status = pathsupport::createPathAt( + _fieldRef, _preparedState->idxFound, _preparedState->elemFound, baseArray); + if (!status.isOK()) { + return status; + } - return Status::OK(); + // Point to the base array just created. The subsequent code expects it to exist + // already. + _preparedState->elemFound = baseArray; } - namespace { - Status pushFirstElement(mb::Element& arrayElem, - const size_t arraySize, - const size_t pos, - mb::Element& elem) { + // This is the count of the array before we change it, or 0 if missing from the doc. + _preparedState->arrayPreModSize = countChildren(_preparedState->elemFound); + // 2. Add new elements to the array either by going over the $each array or by + // appending the (old style $push) element. + if (_eachMode || _pushMode == PUSH_ALL) { + BSONObjIterator itEach(_eachElem.embeddedObject()); - // Empty array or pushing to the front - if (arraySize == 0 || pos == 0) { - return arrayElem.pushFront(elem); - } - else { + // When adding more than one element we keep track of the previous one + // so we can add right siblings to it. + mutablebson::Element prevElem = _preparedState->doc.end(); - // Push position is at the end, or beyond - if (pos >= arraySize) { - return arrayElem.pushBack(elem); - } + // The first element is special below + bool first = true; - const size_t appendPos = pos - 1; - mutablebson::Element fromElem = getNthChild(arrayElem, appendPos); - - // This should not be possible since the checks above should - // cover us but error just in case - if (!fromElem.ok()){ - return Status(ErrorCodes::InvalidLength, - str::stream() << "The specified position (" << appendPos << "/" - << pos - << ") is invalid based on the length ( " - << arraySize - << ") of the array"); - } + while (itEach.more()) { + BSONElement eachItem = itEach.next(); + mutablebson::Element elem = + _preparedState->doc.makeElementWithNewFieldName(StringData(), eachItem); - return fromElem.addSiblingRight(elem); - } - } - } //unamed namespace - - Status ModifierPush::apply() const { - - Status status = Status::OK(); - - // - // Applying a $push with an $clause has the following steps - // 1. Create the doc array we'll push into, if it is not there - // 2. Add the items in the $each array (or the simple $push) to the doc array - // 3. Sort the resulting array according to $sort clause, if present - // 4. Trim the resulting array according the $slice clasue, if present - // - // TODO There are _lots_ of optimization opportunities that we'll consider once the - // test coverage is adequate. - // - - // 1. If the array field is not there, create it as an array and attach it to the - // document. - if (!_preparedState->elemFound.ok() || - _preparedState->idxFound < (_fieldRef.numParts()-1)) { - - // Creates the array element - mutablebson::Document& doc = _preparedState->doc; - StringData lastPart = _fieldRef.getPart(_fieldRef.numParts()-1); - mutablebson::Element baseArray = doc.makeElementArray(lastPart); - if (!baseArray.ok()) { - return Status(ErrorCodes::InternalError, "can't create new base array"); + if (first) { + status = pushFirstElement(_preparedState->elemFound, + _preparedState->arrayPreModSize, + _startPosition, + elem); + } else { + status = prevElem.addSiblingRight(elem); } - // 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. - status = pathsupport::createPathAt(_fieldRef, - _preparedState->idxFound, - _preparedState->elemFound, - baseArray); if (!status.isOK()) { return status; } - // Point to the base array just created. The subsequent code expects it to exist - // already. - _preparedState->elemFound = baseArray; - - } - - // This is the count of the array before we change it, or 0 if missing from the doc. - _preparedState->arrayPreModSize = countChildren(_preparedState->elemFound); - - // 2. Add new elements to the array either by going over the $each array or by - // appending the (old style $push) element. - if (_eachMode || _pushMode == PUSH_ALL) { - BSONObjIterator itEach(_eachElem.embeddedObject()); - - // When adding more than one element we keep track of the previous one - // so we can add right siblings to it. - mutablebson::Element prevElem = _preparedState->doc.end(); - - // The first element is special below - bool first = true; - - while (itEach.more()) { - BSONElement eachItem = itEach.next(); - mutablebson::Element elem = - _preparedState->doc.makeElementWithNewFieldName(StringData(), eachItem); - - if (first) { - status = pushFirstElement(_preparedState->elemFound, - _preparedState->arrayPreModSize, - _startPosition, - elem); } - else { - status = prevElem.addSiblingRight(elem); - } - - if (!status.isOK()) { - return status; - } - - // For the next iteration the previous element will be the left sibling - prevElem = elem; - first = false; - } + // For the next iteration the previous element will be the left sibling + prevElem = elem; + first = false; } - else { - mutablebson::Element elem = - _preparedState->doc.makeElementWithNewFieldName(StringData(), _val); - if (!elem.ok()) { - return Status(ErrorCodes::InternalError, "can't wrap element being $push-ed"); - } - return pushFirstElement(_preparedState->elemFound, - _preparedState->arrayPreModSize, - _startPosition, - elem); - } - - // 3. Sort the resulting array, if $sort was requested. - if (_sortPresent) { - sortChildren(_preparedState->elemFound, _sort); + } else { + mutablebson::Element elem = + _preparedState->doc.makeElementWithNewFieldName(StringData(), _val); + if (!elem.ok()) { + return Status(ErrorCodes::InternalError, "can't wrap element being $push-ed"); } + return pushFirstElement( + _preparedState->elemFound, _preparedState->arrayPreModSize, _startPosition, elem); + } - // 4. Trim the resulting array according to $slice, if present. - if (_slicePresent) { + // 3. Sort the resulting array, if $sort was requested. + if (_sortPresent) { + sortChildren(_preparedState->elemFound, _sort); + } - // Slice 0 means to remove all - if (_slice == 0) { - while(_preparedState->elemFound.ok() && - _preparedState->elemFound.rightChild().ok()) { - _preparedState->elemFound.rightChild().remove(); - } + // 4. Trim the resulting array according to $slice, if present. + if (_slicePresent) { + // Slice 0 means to remove all + if (_slice == 0) { + while (_preparedState->elemFound.ok() && _preparedState->elemFound.rightChild().ok()) { + _preparedState->elemFound.rightChild().remove(); } + } - const int64_t numChildren = mutablebson::countChildren(_preparedState->elemFound); - int64_t countRemoved = std::max(static_cast<int64_t>(0), numChildren - abs(_slice)); + const int64_t numChildren = mutablebson::countChildren(_preparedState->elemFound); + int64_t countRemoved = std::max(static_cast<int64_t>(0), numChildren - abs(_slice)); - // If _slice is negative, remove from the bottom, otherwise from the top - const bool removeFromEnd = (_slice > 0); + // If _slice is negative, remove from the bottom, otherwise from the top + const bool removeFromEnd = (_slice > 0); - // Either start at right or left depending if we are taking from top or bottom - mutablebson::Element curr = removeFromEnd ? - _preparedState->elemFound.rightChild() : - _preparedState->elemFound.leftChild(); - while (curr.ok() && countRemoved > 0) { - mutablebson::Element toRemove = curr; - // Either go right or left depending if we are taking from top or bottom - curr = removeFromEnd ? curr.leftSibling() : curr.rightSibling(); + // Either start at right or left depending if we are taking from top or bottom + mutablebson::Element curr = removeFromEnd ? _preparedState->elemFound.rightChild() + : _preparedState->elemFound.leftChild(); + while (curr.ok() && countRemoved > 0) { + mutablebson::Element toRemove = curr; + // Either go right or left depending if we are taking from top or bottom + curr = removeFromEnd ? curr.leftSibling() : curr.rightSibling(); - status = toRemove.remove(); - if (!status.isOK()) { - return status; - } - countRemoved--; + status = toRemove.remove(); + if (!status.isOK()) { + return status; } + countRemoved--; } - - return status; } - Status ModifierPush::log(LogBuilder* logBuilder) const { + return status; +} - // The start position to use for positional (ordinal) updates to the array - // (We will increment as we append elements to the oplog entry so can't be const) - size_t position = _preparedState->arrayPreModSize; +Status ModifierPush::log(LogBuilder* logBuilder) const { + // The start position to use for positional (ordinal) updates to the array + // (We will increment as we append elements to the oplog entry so can't be const) + size_t position = _preparedState->arrayPreModSize; - // NOTE: Idempotence Requirement - // In the case that the document does't have an array or it is empty we need to make sure - // that the first time the field gets filled with items that it is a full set of the array. + // NOTE: Idempotence Requirement + // In the case that the document does't have an array or it is empty we need to make sure + // that the first time the field gets filled with items that it is a full set of the array. - // If we sorted, sliced, or added the first items to the array, make a full array copy. - const bool doFullCopy = _slicePresent || _sortPresent - || (position == 0) // first element in new/empty array - || (_startPosition < _preparedState->arrayPreModSize); // add in middle + // If we sorted, sliced, or added the first items to the array, make a full array copy. + const bool doFullCopy = _slicePresent || _sortPresent || + (position == 0) // first element in new/empty array + || (_startPosition < _preparedState->arrayPreModSize); // add in middle - if (doFullCopy) { - return logBuilder->addToSetsWithNewFieldName(_fieldRef.dottedField(), - _preparedState->elemFound); - } - else { - // Set only the positional elements appended - if (_eachMode || _pushMode == PUSH_ALL) { - // For each input element log it as a posisional $set - BSONObjIterator itEach(_eachElem.embeddedObject()); - while (itEach.more()) { - BSONElement eachItem = itEach.next(); - // value for the logElement ("field.path.name.N": <value>) - const std::string positionalName = - mongoutils::str::stream() << _fieldRef.dottedField() << "." << position++; - - Status s = logBuilder->addToSetsWithNewFieldName(positionalName, eachItem); - if (!s.isOK()) - return s; - } + if (doFullCopy) { + return logBuilder->addToSetsWithNewFieldName(_fieldRef.dottedField(), + _preparedState->elemFound); + } else { + // Set only the positional elements appended + if (_eachMode || _pushMode == PUSH_ALL) { + // For each input element log it as a posisional $set + BSONObjIterator itEach(_eachElem.embeddedObject()); + while (itEach.more()) { + BSONElement eachItem = itEach.next(); + // value for the logElement ("field.path.name.N": <value>) + const std::string positionalName = mongoutils::str::stream() + << _fieldRef.dottedField() << "." << position++; - return Status::OK(); + Status s = logBuilder->addToSetsWithNewFieldName(positionalName, eachItem); + if (!s.isOK()) + return s; } - else { - // single value for the logElement ("field.path.name.N": <value>) - const std::string positionalName = - mongoutils::str::stream() << _fieldRef.dottedField() << "." << position++; - return logBuilder->addToSetsWithNewFieldName(positionalName, _val); - } + return Status::OK(); + } else { + // single value for the logElement ("field.path.name.N": <value>) + const std::string positionalName = mongoutils::str::stream() << _fieldRef.dottedField() + << "." << position++; + + return logBuilder->addToSetsWithNewFieldName(positionalName, _val); } } +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_push.h b/src/mongo/db/ops/modifier_push.h index 708c6431d97..df9c4b65db9 100644 --- a/src/mongo/db/ops/modifier_push.h +++ b/src/mongo/db/ops/modifier_push.h @@ -39,97 +39,91 @@ namespace mongo { - class LogBuilder; - - class ModifierPush : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierPush); - - public: - - enum ModifierPushMode { PUSH_NORMAL, PUSH_ALL }; - explicit ModifierPush(ModifierPushMode mode= PUSH_NORMAL); - - // - // Modifier interface implementation - // - - virtual ~ModifierPush(); - - /** - * A 'modExpr' here is a BSONElement {<fieldname>: <each clause>, <slice clause>, <sort - * clause>} coming from a $push mod such as {$set: {x: $each[{a:1}], $slice:3, - * $sort{b:1}}}. init() extracts and validates the field name and the clauses. It - * returns OK if successful or a status describing the error. - * - * There are currently a few restrictions concerning the clauses (but all can be - * lifted): - * + $slice can be negative only (ie, slicing from the recent end) - * + $sort requires $slice to be present - * + $sort can only sort objects (as opposed to basic types), so it only takes - * object as patterns - * + Because of the previous, $sort requires that the array being pushed to be made - * of objects - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); - - /** - * Locates the array to be pushed into in the 'root', if it exists, and fills in - * execInfo accordingly. Returns true if $push would succeed in 'root', otherwise - * return a status describing the error. - * - * Note that a $push is never in-place. The cost of checking if it is a no-op makes it - * so that we don't do such check either. As such, execInfo is always filled with - * 'false' for those two options. - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); - - /** - * Pushes the array into the prepared position and "sort/slice"s the resulting array - * according to that call's instructions. - */ - virtual Status apply() const; - - /** - * $push currently logs the entire resulting array as a $set. - * - * TODO Log a positional $set in the array, whenever possible. - */ - virtual Status log(LogBuilder* logBuilder) 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; - - // Clauses for the $push that are filled when the $each variation of the command is used. - bool _eachMode; - BSONElement _eachElem; - bool _slicePresent; - int64_t _slice; - bool _sortPresent; - size_t _startPosition; - - PatternElementCmp _sort; - - // Whether this mod is supposed to be parsed as a $pushAll. - const ModifierPushMode _pushMode; - - // Simple (old style) push value when the $each variation of the command is not - // used. The _eachMode flag would be off if we're this mode. - 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; - std::unique_ptr<PreparedState> _preparedState; - - }; - -} // namespace mongo +class LogBuilder; + +class ModifierPush : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierPush); + +public: + enum ModifierPushMode { PUSH_NORMAL, PUSH_ALL }; + explicit ModifierPush(ModifierPushMode mode = PUSH_NORMAL); + + // + // Modifier interface implementation + // + + virtual ~ModifierPush(); + + /** + * A 'modExpr' here is a BSONElement {<fieldname>: <each clause>, <slice clause>, <sort + * clause>} coming from a $push mod such as {$set: {x: $each[{a:1}], $slice:3, + * $sort{b:1}}}. init() extracts and validates the field name and the clauses. It + * returns OK if successful or a status describing the error. + * + * There are currently a few restrictions concerning the clauses (but all can be + * lifted): + * + $slice can be negative only (ie, slicing from the recent end) + * + $sort requires $slice to be present + * + $sort can only sort objects (as opposed to basic types), so it only takes + * object as patterns + * + Because of the previous, $sort requires that the array being pushed to be made + * of objects + */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); + + /** + * Locates the array to be pushed into in the 'root', if it exists, and fills in + * execInfo accordingly. Returns true if $push would succeed in 'root', otherwise + * return a status describing the error. + * + * Note that a $push is never in-place. The cost of checking if it is a no-op makes it + * so that we don't do such check either. As such, execInfo is always filled with + * 'false' for those two options. + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); + + /** + * Pushes the array into the prepared position and "sort/slice"s the resulting array + * according to that call's instructions. + */ + virtual Status apply() const; + + /** + * $push currently logs the entire resulting array as a $set. + * + * TODO Log a positional $set in the array, whenever possible. + */ + virtual Status log(LogBuilder* logBuilder) 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; + + // Clauses for the $push that are filled when the $each variation of the command is used. + bool _eachMode; + BSONElement _eachElem; + bool _slicePresent; + int64_t _slice; + bool _sortPresent; + size_t _startPosition; + + PatternElementCmp _sort; + + // Whether this mod is supposed to be parsed as a $pushAll. + const ModifierPushMode _pushMode; + + // Simple (old style) push value when the $each variation of the command is not + // used. The _eachMode flag would be off if we're this mode. + 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; + std::unique_ptr<PreparedState> _preparedState; +}; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_push_sorter.h b/src/mongo/db/ops/modifier_push_sorter.h index 930ab539d57..c942f4e5da3 100644 --- a/src/mongo/db/ops/modifier_push_sorter.h +++ b/src/mongo/db/ops/modifier_push_sorter.h @@ -34,44 +34,37 @@ namespace mongo { - // Extracts the value for 'pattern' for both 'lhs' and 'rhs' and return true if 'lhs' < - // 'rhs'. We expect that both 'lhs' and 'rhs' be key patterns. - struct PatternElementCmp { - BSONObj sortPattern; - bool useWholeValue; +// Extracts the value for 'pattern' for both 'lhs' and 'rhs' and return true if 'lhs' < +// 'rhs'. We expect that both 'lhs' and 'rhs' be key patterns. +struct PatternElementCmp { + BSONObj sortPattern; + bool useWholeValue; - PatternElementCmp() - : sortPattern(BSONObj()) - , useWholeValue(true) {} + PatternElementCmp() : sortPattern(BSONObj()), useWholeValue(true) {} - PatternElementCmp(const BSONObj& pattern) - : sortPattern(pattern) - , useWholeValue(pattern.hasField("")){ - } + PatternElementCmp(const BSONObj& pattern) + : sortPattern(pattern), useWholeValue(pattern.hasField("")) {} - bool operator()(const mutablebson::Element& lhs, const mutablebson::Element& rhs) const { - if (useWholeValue) { - const int comparedValue = lhs.compareWithElement( rhs, false ); + bool operator()(const mutablebson::Element& lhs, const mutablebson::Element& rhs) const { + if (useWholeValue) { + const int comparedValue = lhs.compareWithElement(rhs, false); - const bool reversed = (sortPattern.firstElement().number() < 0 ); + const bool reversed = (sortPattern.firstElement().number() < 0); - return (reversed ? comparedValue > 0 : comparedValue < 0); - } - else { - //TODO: Push on to mutable in the future, and to support non-contiguous Elements. - BSONObj lhsObj = lhs.getType() == Object ? - lhs.getValueObject() : - lhs.getValue().wrap(""); - BSONObj rhsObj = rhs.getType() == Object ? - rhs.getValueObject() : - rhs.getValue().wrap(""); + return (reversed ? comparedValue > 0 : comparedValue < 0); + } else { + // TODO: Push on to mutable in the future, and to support non-contiguous Elements. + BSONObj lhsObj = + lhs.getType() == Object ? lhs.getValueObject() : lhs.getValue().wrap(""); + BSONObj rhsObj = + rhs.getType() == Object ? rhs.getValueObject() : rhs.getValue().wrap(""); - BSONObj lhsKey = lhsObj.extractFields(sortPattern, true); - BSONObj rhsKey = rhsObj.extractFields(sortPattern, true); + BSONObj lhsKey = lhsObj.extractFields(sortPattern, true); + BSONObj rhsKey = rhsObj.extractFields(sortPattern, true); - return lhsKey.woCompare(rhsKey, sortPattern) < 0; - } + return lhsKey.woCompare(rhsKey, sortPattern) < 0; } - }; + } +}; -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_push_sorter_test.cpp b/src/mongo/db/ops/modifier_push_sorter_test.cpp index 4df1c4dfadc..d60eea1e942 100644 --- a/src/mongo/db/ops/modifier_push_sorter_test.cpp +++ b/src/mongo/db/ops/modifier_push_sorter_test.cpp @@ -37,143 +37,143 @@ namespace { - using mongo::BSONObj; - using mongo::fromjson; - using mongo::PatternElementCmp; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - using mongo::mutablebson::sortChildren; - - class ObjectArray : public mongo::unittest::Test { - public: - ObjectArray() : _doc(), _size(0) {} - - virtual void setUp() { - Element arr = _doc.makeElementArray("x"); - ASSERT_TRUE(arr.ok()); - ASSERT_OK(_doc.root().pushBack(arr)); - } - - void addObj(BSONObj obj) { - ASSERT_LESS_THAN_OR_EQUALS(_size, 3u); - _objs[_size] = obj; - _size++; - - ASSERT_OK(_doc.root()["x"].appendObject(mongo::StringData(), obj)); - } - - BSONObj getOrigObj(size_t i) { - return _objs[i]; - } - - BSONObj getSortedObj(size_t i) { - return getArray()[i].getValueObject(); - } - - Element getArray() { - return _doc.root()["x"]; - } - - private: - Document _doc; - BSONObj _objs[3]; - size_t _size; - }; - - TEST_F(ObjectArray, NormalOrder) { - addObj(fromjson("{b:1, a:1}")); - addObj(fromjson("{a:3, b:2}")); - addObj(fromjson("{b:3, a:2}")); - - sortChildren(getArray(), PatternElementCmp(fromjson("{'a':1,'b':1}"))); - - ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(2)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); +using mongo::BSONObj; +using mongo::fromjson; +using mongo::PatternElementCmp; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; +using mongo::mutablebson::sortChildren; + +class ObjectArray : public mongo::unittest::Test { +public: + ObjectArray() : _doc(), _size(0) {} + + virtual void setUp() { + Element arr = _doc.makeElementArray("x"); + ASSERT_TRUE(arr.ok()); + ASSERT_OK(_doc.root().pushBack(arr)); } - TEST_F(ObjectArray, MixedOrder) { - addObj(fromjson("{b:1, a:1}")); - addObj(fromjson("{a:3, b:2}")); - addObj(fromjson("{b:3, a:2}")); + void addObj(BSONObj obj) { + ASSERT_LESS_THAN_OR_EQUALS(_size, 3u); + _objs[_size] = obj; + _size++; - sortChildren(getArray(), PatternElementCmp(fromjson("{b:1,a:-1}"))); - - ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(1)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); + ASSERT_OK(_doc.root()["x"].appendObject(mongo::StringData(), obj)); } - TEST_F(ObjectArray, ExtraFields) { - addObj(fromjson("{b:1, c:2, a:1}")); - addObj(fromjson("{c:1, a:3, b:2}")); - addObj(fromjson("{b:3, a:2}")); + BSONObj getOrigObj(size_t i) { + return _objs[i]; + } - sortChildren(getArray(), PatternElementCmp(fromjson("{a:1,b:1}"))); + BSONObj getSortedObj(size_t i) { + return getArray()[i].getValueObject(); + } - ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(2)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); + Element getArray() { + return _doc.root()["x"]; } - TEST_F(ObjectArray, MissingFields) { - addObj(fromjson("{a:2, b:2}")); - addObj(fromjson("{a:1}")); - addObj(fromjson("{a:3, b:3, c:3}")); +private: + Document _doc; + BSONObj _objs[3]; + size_t _size; +}; - sortChildren(getArray(), PatternElementCmp(fromjson("{b:1,c:1}"))); +TEST_F(ObjectArray, NormalOrder) { + addObj(fromjson("{b:1, a:1}")); + addObj(fromjson("{a:3, b:2}")); + addObj(fromjson("{b:3, a:2}")); - ASSERT_EQUALS(getOrigObj(0), getSortedObj(1)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); - } + sortChildren(getArray(), PatternElementCmp(fromjson("{'a':1,'b':1}"))); - TEST_F(ObjectArray, NestedFields) { - addObj(fromjson("{a:{b:{c:2, d:0}}}")); - addObj(fromjson("{a:{b:{c:1, d:2}}}")); - addObj(fromjson("{a:{b:{c:3, d:1}}}")); + ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(2)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); +} - sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b':1}"))); +TEST_F(ObjectArray, MixedOrder) { + addObj(fromjson("{b:1, a:1}")); + addObj(fromjson("{a:3, b:2}")); + addObj(fromjson("{b:3, a:2}")); - ASSERT_EQUALS(getOrigObj(0), getSortedObj(1)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); - } + sortChildren(getArray(), PatternElementCmp(fromjson("{b:1,a:-1}"))); - TEST_F(ObjectArray, SimpleNestedFields) { - addObj(fromjson("{a:{b: -1}}")); - addObj(fromjson("{a:{b: -100}}")); - addObj(fromjson("{a:{b: 34}}")); + ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(1)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); +} - sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b':1}"))); +TEST_F(ObjectArray, ExtraFields) { + addObj(fromjson("{b:1, c:2, a:1}")); + addObj(fromjson("{c:1, a:3, b:2}")); + addObj(fromjson("{b:3, a:2}")); - ASSERT_EQUALS(getOrigObj(0), getSortedObj(1)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); - } + sortChildren(getArray(), PatternElementCmp(fromjson("{a:1,b:1}"))); - TEST_F(ObjectArray, NestedInnerObjectDescending) { - addObj(fromjson("{a:{b:{c:2, d:0}}}")); - addObj(fromjson("{a:{b:{c:1, d:2}}}")); - addObj(fromjson("{a:{b:{c:3, d:1}}}")); + ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(2)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); +} - sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b.d':-1}"))); +TEST_F(ObjectArray, MissingFields) { + addObj(fromjson("{a:2, b:2}")); + addObj(fromjson("{a:1}")); + addObj(fromjson("{a:3, b:3, c:3}")); - ASSERT_EQUALS(getOrigObj(0), getSortedObj(2)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); - } + sortChildren(getArray(), PatternElementCmp(fromjson("{b:1,c:1}"))); - TEST_F(ObjectArray, NestedInnerObjectAscending) { - addObj(fromjson("{a:{b:{c:2, d:0}}}")); - addObj(fromjson("{a:{b:{c:1, d:2}}}")); - addObj(fromjson("{a:{b:{c:3, d:1}}}")); + ASSERT_EQUALS(getOrigObj(0), getSortedObj(1)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); +} - sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b.d':1}"))); +TEST_F(ObjectArray, NestedFields) { + addObj(fromjson("{a:{b:{c:2, d:0}}}")); + addObj(fromjson("{a:{b:{c:1, d:2}}}")); + addObj(fromjson("{a:{b:{c:3, d:1}}}")); - ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); - ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); - ASSERT_EQUALS(getOrigObj(1), getSortedObj(2)); - } + sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b':1}"))); + + ASSERT_EQUALS(getOrigObj(0), getSortedObj(1)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); +} + +TEST_F(ObjectArray, SimpleNestedFields) { + addObj(fromjson("{a:{b: -1}}")); + addObj(fromjson("{a:{b: -100}}")); + addObj(fromjson("{a:{b: 34}}")); + + sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b':1}"))); + + ASSERT_EQUALS(getOrigObj(0), getSortedObj(1)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(2)); +} + +TEST_F(ObjectArray, NestedInnerObjectDescending) { + addObj(fromjson("{a:{b:{c:2, d:0}}}")); + addObj(fromjson("{a:{b:{c:1, d:2}}}")); + addObj(fromjson("{a:{b:{c:3, d:1}}}")); + + sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b.d':-1}"))); + + ASSERT_EQUALS(getOrigObj(0), getSortedObj(2)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); +} + +TEST_F(ObjectArray, NestedInnerObjectAscending) { + addObj(fromjson("{a:{b:{c:2, d:0}}}")); + addObj(fromjson("{a:{b:{c:1, d:2}}}")); + addObj(fromjson("{a:{b:{c:3, d:1}}}")); + + sortChildren(getArray(), PatternElementCmp(fromjson("{'a.b.d':1}"))); + + ASSERT_EQUALS(getOrigObj(0), getSortedObj(0)); + ASSERT_EQUALS(getOrigObj(2), getSortedObj(1)); + ASSERT_EQUALS(getOrigObj(1), getSortedObj(2)); +} -} // unnamed namespace +} // unnamed namespace diff --git a/src/mongo/db/ops/modifier_push_test.cpp b/src/mongo/db/ops/modifier_push_test.cpp index 884566d48ac..275e1a4945e 100644 --- a/src/mongo/db/ops/modifier_push_test.cpp +++ b/src/mongo/db/ops/modifier_push_test.cpp @@ -47,1409 +47,1394 @@ namespace { - using mongo::BSONObj; - using mongo::BSONObjBuilder; - using mongo::BSONArrayBuilder; - using mongo::fromjson; - using mongo::LogBuilder; - using mongo::ModifierInterface; - using mongo::ModifierPush; - using mongo::NumberInt; - using mongo::Ordering; - using mongo::Status; - using mongo::StringData; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::countChildren; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - using std::sort; - using std::vector; - - void combineVec(const vector<int>& origVec, - const vector<int>& modVec, - int32_t slice, - vector<int>* combined) { - - using namespace std; - combined->clear(); - - // Slice 0 means the result is empty - if (slice == 0) - return; - - // Combine both vectors - *combined = origVec; - combined->insert(combined->end(), modVec.begin(), modVec.end()); - - // Remove sliced items - bool removeFromFront = (slice < 0); - - // if abs(slice) is larger than the size, nothing to do. - if (abs(slice) >= int32_t(combined->size())) - return; - - if (removeFromFront) { - // Slice is negative. - int32_t removeCount = combined->size() + slice; - combined->erase(combined->begin(), combined->begin() + removeCount); - } - else { - combined->resize(std::min(combined->size(), size_t(slice))); - } - } - - /** - * Comparator between two BSONObjects that takes in consideration only the keys and - * direction described in the sort pattern. - */ - struct ProjectKeyCmp { - BSONObj sortPattern; - bool useWholeValue; - - ProjectKeyCmp(BSONObj pattern) : sortPattern(pattern) { - useWholeValue = pattern.hasField(""); - } - - int operator()(const BSONObj& left, const BSONObj& right) const { - int ret = 0; - if (useWholeValue) { - ret = left.woCompare( right, Ordering::make(sortPattern), false ); - } else { - BSONObj lhsKey = left.extractFields(sortPattern, true); - BSONObj rhsKey = right.extractFields(sortPattern, true); - ret = lhsKey.woCompare(rhsKey, sortPattern); - } - return ret < 0; - } - }; - - void combineAndSortVec(const vector<BSONObj>& origVec, - const vector<BSONObj>& modVec, - int32_t slice, - BSONObj sortOrder, - vector<BSONObj>* combined) { - - combined->clear(); +using mongo::BSONObj; +using mongo::BSONObjBuilder; +using mongo::BSONArrayBuilder; +using mongo::fromjson; +using mongo::LogBuilder; +using mongo::ModifierInterface; +using mongo::ModifierPush; +using mongo::NumberInt; +using mongo::Ordering; +using mongo::Status; +using mongo::StringData; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::countChildren; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; +using std::sort; +using std::vector; + +void combineVec(const vector<int>& origVec, + const vector<int>& modVec, + int32_t slice, + vector<int>* combined) { + using namespace std; + combined->clear(); + + // Slice 0 means the result is empty + if (slice == 0) + return; + + // Combine both vectors + *combined = origVec; + combined->insert(combined->end(), modVec.begin(), modVec.end()); + + // Remove sliced items + bool removeFromFront = (slice < 0); + + // if abs(slice) is larger than the size, nothing to do. + if (abs(slice) >= int32_t(combined->size())) + return; + + if (removeFromFront) { + // Slice is negative. + int32_t removeCount = combined->size() + slice; + combined->erase(combined->begin(), combined->begin() + removeCount); + } else { + combined->resize(std::min(combined->size(), size_t(slice))); + } +} - // Slice 0 means the result is empty - if (slice == 0) - return; - - *combined = origVec; - combined->insert(combined->end(), modVec.begin(), modVec.end()); - - sort(combined->begin(), combined->end(), ProjectKeyCmp(sortOrder)); - - // Remove sliced items - bool removeFromFront = (slice < 0); - - // if abs(slice) is larger than the size, nothing to do. - if (abs(slice) >= int32_t(combined->size())) - return; - - if (removeFromFront) { - // Slice is negative. - int32_t removeCount = combined->size() + slice; - combined->erase(combined->begin(), combined->begin() + removeCount); - } - else { - combined->resize(std::min(combined->size(), size_t(slice))); +/** + * Comparator between two BSONObjects that takes in consideration only the keys and + * direction described in the sort pattern. + */ +struct ProjectKeyCmp { + BSONObj sortPattern; + bool useWholeValue; + + ProjectKeyCmp(BSONObj pattern) : sortPattern(pattern) { + useWholeValue = pattern.hasField(""); + } + + int operator()(const BSONObj& left, const BSONObj& right) const { + int ret = 0; + if (useWholeValue) { + ret = left.woCompare(right, Ordering::make(sortPattern), false); + } else { + BSONObj lhsKey = left.extractFields(sortPattern, true); + BSONObj rhsKey = right.extractFields(sortPattern, true); + ret = lhsKey.woCompare(rhsKey, sortPattern); } - } - - // - // Init testing (module field checking, which is done in 'fieldchecker' - // - - TEST(Init, SimplePush) { - BSONObj modObj = fromjson("{$push: {x: 0}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + return ret < 0; + } +}; + +void combineAndSortVec(const vector<BSONObj>& origVec, + const vector<BSONObj>& modVec, + int32_t slice, + BSONObj sortOrder, + vector<BSONObj>* combined) { + combined->clear(); + + // Slice 0 means the result is empty + if (slice == 0) + return; + + *combined = origVec; + combined->insert(combined->end(), modVec.begin(), modVec.end()); + + sort(combined->begin(), combined->end(), ProjectKeyCmp(sortOrder)); + + // Remove sliced items + bool removeFromFront = (slice < 0); + + // if abs(slice) is larger than the size, nothing to do. + if (abs(slice) >= int32_t(combined->size())) + return; + + if (removeFromFront) { + // Slice is negative. + int32_t removeCount = combined->size() + slice; + combined->erase(combined->begin(), combined->begin() + removeCount); + } else { + combined->resize(std::min(combined->size(), size_t(slice))); + } +} + +// +// Init testing (module field checking, which is done in 'fieldchecker' +// + +TEST(Init, SimplePush) { + BSONObj modObj = fromjson("{$push: {x: 0}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +// +// If present, is the $each clause valid? +// + +TEST(Init, PushEachNormal) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2]}}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachMixed) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, {a: 2}]}}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachObject) { + // $each must be an array + BSONObj modObj = fromjson("{$push: {x: {$each: {'0': 1}}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } +} - // - // If present, is the $each clause valid? - // - - TEST(Init, PushEachNormal) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2]}}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), +TEST(Init, PushEachSimpleType) { + // $each must be an array. + BSONObj modObj = fromjson("{$push: {x: {$each: 1}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachMixed) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, {a: 2}]}}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), +} + +TEST(Init, PushEachEmpty) { + BSONObj modObj = fromjson("{$push: {x: {$each: []}}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachInvalidType) { + // $each must be an array. + BSONObj modObj = fromjson("{$push: {x: {$each: {b: 1}}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachObject) { - // $each must be an array - BSONObj modObj = fromjson("{$push: {x: {$each: {'0': 1}}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachSimpleType) { - // $each must be an array. - BSONObj modObj = fromjson("{$push: {x: {$each: 1}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachEmpty) { - BSONObj modObj = fromjson("{$push: {x: {$each: []}}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), +} + +// +// If present, is the $slice clause valid? +// + +TEST(Init, PushEachWithSliceBottom) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: -3}}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachWithSliceTop) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: 3}}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachWithInvalidSliceObject) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: {a: 1}}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachInvalidType) { - // $each must be an array. - BSONObj modObj = fromjson("{$push: {x: {$each: {b: 1}}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +} - // - // If present, is the $slice clause valid? - // - - TEST(Init, PushEachWithSliceBottom) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: -3}}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), +TEST(Init, PushEachWithInvalidSliceDouble) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: -2.1}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithSliceTop) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: 3}}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithInvalidSliceObject) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: {a: 1}}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithInvalidSliceDouble) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: -2.1}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithValidSliceDouble) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: -2.0}}}"); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), +} + +TEST(Init, PushEachWithValidSliceDouble) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: -2.0}}}"); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachWithUnsupportedFullSlice) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: [1,2]}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } +} - TEST(Init, PushEachWithUnsupportedFullSlice) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: [1,2]}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithWrongTypeSlice) { - BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: '-1'}}}"); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - // - // If present, is the sort $sort clause valid? - // - - TEST(Init, PushEachWithObjectSort) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), +TEST(Init, PushEachWithWrongTypeSlice) { + BSONObj modObj = fromjson("{$push: {x: {$each: [1, 2], $slice: '-1'}}}"); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithNumbericSort) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort:1 }}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithInvalidSortType) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: [{a:1}]}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachDuplicateSortPattern) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: [{a:1,a:1}]}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(Init, PushEachWithInvalidSortValue) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {a:100}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +} + +// +// If present, is the sort $sort clause valid? +// + +TEST(Init, PushEachWithObjectSort) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachWithNumbericSort) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort:1 }}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachWithInvalidSortType) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: [{a:1}]}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithEmptySortField) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'':1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachDuplicateSortPattern) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: [{a:1,a:1}]}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithEmptyDottedSortField) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'.':1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachWithInvalidSortValue) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {a:100}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithMissingSortFieldSuffix) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'a.':1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachWithEmptySortField) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'':1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithMissingSortFieldPreffix) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'.b':1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachWithEmptyDottedSortField) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'.':1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithMissingSortFieldMiddle) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'a..b':1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachWithMissingSortFieldSuffix) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'a.':1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithEmptySort) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort:{} }}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachWithMissingSortFieldPreffix) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'.b':1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - // - // If in $pushAll semantics, do we check the array and that nothing else is there? - // +TEST(Init, PushEachWithMissingSortFieldMiddle) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {'a..b':1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushAllSimple) { - BSONObj modObj = fromjson("{$pushAll: {x: [0]}}"); - ModifierPush mod(ModifierPush::PUSH_ALL); - ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), +TEST(Init, PushEachWithEmptySort) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort:{} }}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } +} + +// +// If in $pushAll semantics, do we check the array and that nothing else is there? +// + +TEST(Init, PushAllSimple) { + BSONObj modObj = fromjson("{$pushAll: {x: [0]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushAllMultiple) { + BSONObj modObj = fromjson("{$pushAll: {x: [1,2,3]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushAllObject) { + BSONObj modObj = fromjson("{$pushAll: {x: [{a:1},{a:2}]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushAllMixed) { + BSONObj modObj = fromjson("{$pushAll: {x: [1,{a:2}]}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushAllWrongType) { + BSONObj modObj = fromjson("{$pushAll: {x: 1}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_NOT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushAllMultiple) { - BSONObj modObj = fromjson("{$pushAll: {x: [1,2,3]}}"); - ModifierPush mod(ModifierPush::PUSH_ALL); - ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), +TEST(Init, PushAllNotArray) { + BSONObj modObj = fromjson("{$pushAll: {x: {a:1}}}"); + ModifierPush mod(ModifierPush::PUSH_ALL); + ASSERT_NOT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } +} + +// +// Are all clauses present? Is anything extroneous? Is anything duplicated? +// + +TEST(Init, PushEachWithSortMissingSlice) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $sort:{a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} + +TEST(Init, PushEachInvalidClause) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $xxx: -1, $sort:{a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushAllObject) { - BSONObj modObj = fromjson("{$pushAll: {x: [{a:1},{a:2}]}}"); - ModifierPush mod(ModifierPush::PUSH_ALL); - ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), +TEST(Init, PushEachExtraField) { + const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {a:1}, b: 1}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } +} - TEST(Init, PushAllMixed) { - BSONObj modObj = fromjson("{$pushAll: {x: [1,{a:2}]}}"); - ModifierPush mod(ModifierPush::PUSH_ALL); - ASSERT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), +TEST(Init, PushEachDuplicateSortClause) { + const char* c = "{$push: {x:{$each:[{a:1},{a:2}], $slice:-2.0, $sort:{a:1}, $sort:{a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), ModifierInterface::Options::normal())); - } +} - TEST(Init, PushAllWrongType) { - BSONObj modObj = fromjson("{$pushAll: {x: 1}}"); - ModifierPush mod(ModifierPush::PUSH_ALL); - ASSERT_NOT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachDuplicateSliceClause) { + const char* c = "{$push: {x: {$each:[{a:1},{a:2}], $slice:-2.0, $slice:-2, $sort:{a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushAllNotArray) { - BSONObj modObj = fromjson("{$pushAll: {x: {a:1}}}"); - ModifierPush mod(ModifierPush::PUSH_ALL); - ASSERT_NOT_OK(mod.init(modObj["$pushAll"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachDuplicateEachClause) { + const char* c = "{$push: {x: {$each:[{a:1}], $each:[{a:2}], $slice:-3, $sort:{a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - // - // Are all clauses present? Is anything extroneous? Is anything duplicated? - // +TEST(Init, PushEachWithSliceFirst) { + const char* c = "{$push: {x: {$slice: -2.0, $each: [{a:1},{a:2}], $sort: {a:1}}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachWithSortMissingSlice) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $sort:{a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +TEST(Init, PushEachWithSortFirst) { + const char* c = "{$push: {x: {$sort: {a:1}, $slice: -2.0, $each: [{a:1},{a:2}]}}}"; + BSONObj modObj = fromjson(c); + ModifierPush mod; + ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - TEST(Init, PushEachInvalidClause) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $xxx: -1, $sort:{a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +// +// Simple mod +// - TEST(Init, PushEachExtraField) { - const char* c = "{$push: {x: {$each: [{a:1},{a:2}], $slice: -2.0, $sort: {a:1}, b: 1}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } +/** Helper to build and manipulate a $push or a $pushAll mod. */ +class Mod { +public: + Mod() : _mod() {} - TEST(Init, PushEachDuplicateSortClause) { - const char* c = "{$push: {x:{$each:[{a:1},{a:2}], $slice:-2.0, $sort:{a:1}, $sort:{a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + explicit Mod(BSONObj modObj) + : _mod(mongoutils::str::equals(modObj.firstElement().fieldName(), "$pushAll") + ? ModifierPush::PUSH_ALL + : ModifierPush::PUSH_NORMAL) { + _modObj = modObj; + StringData modName = modObj.firstElement().fieldName(); + ASSERT_OK(_mod.init(_modObj[modName].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(Init, PushEachDuplicateSliceClause) { - const char* c = "{$push: {x: {$each:[{a:1},{a:2}], $slice:-2.0, $slice:-2, $sort:{a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(Init, PushEachDuplicateEachClause) { - const char* c = "{$push: {x: {$each:[{a:1}], $each:[{a:2}], $slice:-3, $sort:{a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_NOT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + Status apply() const { + return _mod.apply(); } - TEST(Init, PushEachWithSliceFirst) { - const char* c = "{$push: {x: {$slice: -2.0, $each: [{a:1},{a:2}], $sort: {a:1}}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(Init, PushEachWithSortFirst) { - const char* c = "{$push: {x: {$sort: {a:1}, $slice: -2.0, $each: [{a:1},{a:2}]}}}"; - BSONObj modObj = fromjson(c); - ModifierPush mod; - ASSERT_OK(mod.init(modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); + ModifierPush& mod() { + return _mod; } - // - // Simple mod - // - - /** Helper to build and manipulate a $push or a $pushAll mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) - : _mod(mongoutils::str::equals(modObj.firstElement().fieldName(), "$pushAll") ? - ModifierPush::PUSH_ALL : ModifierPush::PUSH_NORMAL) { - _modObj = modObj; - StringData modName = modObj.firstElement().fieldName(); - ASSERT_OK(_mod.init(_modObj[modName].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - StringData matchedField, - ModifierInterface::ExecInfo* execInfo) { - return _mod.prepare(root, matchedField, execInfo); - } - - Status apply() const { - return _mod.apply(); - } +private: + ModifierPush _mod; + BSONObj _modObj; +}; - Status log(LogBuilder* logBuilder) const { - return _mod.log(logBuilder); - } +TEST(SimpleMod, PrepareNonArray) { + Document doc(fromjson("{a: 1}")); + Mod pushMod(fromjson("{$push: {a: 1}}")); - ModifierPush& mod() { return _mod; } + ModifierInterface::ExecInfo dummy; + ASSERT_NOT_OK(pushMod.prepare(doc.root(), "", &dummy)); +} - private: - ModifierPush _mod; - BSONObj _modObj; - }; +TEST(SimpleMod, PrepareApplyEmpty) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: 1}}")); - TEST(SimpleMod, PrepareNonArray) { - Document doc(fromjson("{a: 1}")); - Mod pushMod(fromjson("{$push: {a: 1}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo dummy; - ASSERT_NOT_OK(pushMod.prepare(doc.root(), "", &dummy)); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(SimpleMod, PrepareApplyEmpty) { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: 1}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareApplyInexistent) { + Document doc(fromjson("{}")); + Mod pushMod(fromjson("{$push: {a: 1}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(SimpleMod, PrepareApplyInexistent) { - Document doc(fromjson("{}")); - Mod pushMod(fromjson("{$push: {a: 1}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareApplyNormal) { + Document doc(fromjson("{a: [0]}")); + Mod pushMod(fromjson("{$push: {a: 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); - } + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [0,1]}"), doc); - TEST(SimpleMod, PrepareApplyNormal) { - Document doc(fromjson("{a: [0]}")); - Mod pushMod(fromjson("{$push: {a: 1}}")); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.1':1}}"), logDoc); +} + +// +// Simple object mod +// - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); +TEST(SimpleObjMod, PrepareNonArray) { + Document doc(fromjson("{a: 1}")); + Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); + + ModifierInterface::ExecInfo dummy; + ASSERT_NOT_OK(pushMod.prepare(doc.root(), "", &dummy)); +} + +TEST(SimpleObjMod, PrepareApplyEmpty) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{b:1}]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [{b:1}]}}"), logDoc); +} + +TEST(SimpleObjMod, PrepareApplyInexistent) { + Document doc(fromjson("{}")); + Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{b:1}]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [{b:1}]}}"), logDoc); +} + +TEST(SimpleObjMod, PrepareApplyNormal) { + Document doc(fromjson("{a: [{b:0}]}")); + Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{b:0},{b:1}]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.1':{b:1}}}"), logDoc); +} + +TEST(SimpleObjMod, PrepareApplyDotted) { + Document doc(fromjson( + "{ _id : 1 , " + " question : 'a', " + " choices : { " + " first : { choice : 'b' }, " + " second : { choice : 'c' } }" + "}")); + Mod pushMod(fromjson("{$push: {'choices.first.votes': 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "choices.first.votes"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson( + "{ _id : 1 , " + " question : 'a', " + " choices : { " + " first : { choice : 'b', votes: [1]}, " + " second : { choice : 'c' } }" + "}"), + doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'choices.first.votes':[1]}}"), logDoc); +} + + +// +// $pushAll Variation +// + +TEST(PushAll, PrepareApplyEmpty) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$pushAll: {a: [1]}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{a: [1]}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(logDoc, fromjson("{$set: {a: [1]}}")); +} + +TEST(PushAll, PrepareApplyInexistent) { + Document doc(fromjson("{}")); + Mod pushMod(fromjson("{$pushAll: {a: [1]}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{a: [1]}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(logDoc, fromjson("{$set: {a: [1]}}")); +} + +TEST(PushAll, PrepareApplyNormal) { + Document doc(fromjson("{a: [0]}")); + Mod pushMod(fromjson("{$pushAll: {a: [1,2]}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{a: [0,1,2]}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(logDoc, fromjson("{$set: {'a.1': 1, 'a.2':2}}")); +} + +// +// Simple $each mod +// + +TEST(SimpleEachMod, PrepareNonArray) { + Document doc(fromjson("{a: 1}")); + Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); + + ModifierInterface::ExecInfo dummy; + ASSERT_NOT_OK(pushMod.prepare(doc.root(), "", &dummy)); +} + +TEST(SimpleEachMod, PrepareApplyEmpty) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); +} + +TEST(SimpleEachMod, PrepareApplyInexistent) { + Document doc(fromjson("{}")); + Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); +} + +TEST(SimpleEachMod, PrepareApplyInexistentMultiple) { + Document doc(fromjson("{}")); + Mod pushMod(fromjson("{$push: {a: {$each: [1, 2]}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1, 2]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [1, 2]}}"), logDoc); +} + +TEST(SimpleEachMod, PrepareApplyNormal) { + Document doc(fromjson("{a: [0]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [0,1]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.1': 1}}"), logDoc); +} + +TEST(SimpleEachMod, PrepareApplyNormalMultiple) { + Document doc(fromjson("{a: [0]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [1,2]}}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [0,1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.1':1}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [0,1,2]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.1': 1, 'a.2':2}}"), logDoc); +} - // - // Simple object mod - // +/** + * Slice variants + */ +TEST(SlicePushEach, TopOne) { + Document doc(fromjson("{a: [3]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [2, -1], $slice:1}}}")); - TEST(SimpleObjMod, PrepareNonArray) { - Document doc(fromjson("{a: 1}")); - Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo dummy; - ASSERT_NOT_OK(pushMod.prepare(doc.root(), "", &dummy)); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(SimpleObjMod, PrepareApplyEmpty) { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [3]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [3]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +/** + * Sort for scalar (whole) array elements + */ +TEST(SortPushEach, NumberSort) { + Document doc(fromjson("{a: [3]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [2, -1], $sort:1}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [-1,2,3]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [-1, 2, 3]}}"), logDoc); +} + +TEST(SortPushEach, NumberSortReverse) { + Document doc(fromjson("{a: [3]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:-1}}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [4,3,-1]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: [4,3,-1]}}"), logDoc); +} + +TEST(SortPushEach, MixedSortWhole) { + Document doc(fromjson("{a: [3, 't', {b:1}, {a:1}]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:1}}}")); + const BSONObj expectedObj = fromjson("{a: [-1,3,4,'t', {a:1}, {b:1}]}"); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(expectedObj, doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(BSON("$set" << expectedObj), logDoc); +} + +TEST(SortPushEach, MixedSortWholeReverse) { + Document doc(fromjson("{a: [3, 't', {b:1}, {a:1}]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:-1}}}")); + const BSONObj expectedObj = fromjson("{a: [{b:1}, {a:1}, 't', 4, 3, -1]}"); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(expectedObj, doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(BSON("$set" << expectedObj), logDoc); +} + +TEST(SortPushEach, MixedSortEmbeddedField) { + Document doc(fromjson("{a: [3, 't', {b:1}, {a:1}]}")); + Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:{a:1}}}}")); + const BSONObj expectedObj = fromjson("{a: [3, 't', {b: 1}, 4, -1, {a: 1}]}"); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(expectedObj, doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(BSON("$set" << expectedObj), logDoc); +} - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{b:1}]}"), doc); +/** + * This fixture supports building $push mods with parameterized $each arrays and $slices. + * It always assume that the array being operated on is called 'a'. To build a mod, one + * issues a set*Mod() call. + * + * The setSimpleMod() call will build a $each array of numbers. The setObjectMod() call + * will build a $each array with object. Both these calls take the slice as a parameter as + * well. + * + * Here's a typical test case flow: + * + Determine what the original document's 'a' array would contain + * + Ditto for the $push's $each arrray + * + Loop over slice value + * + Apply the $push with current slice value to the doc + * + Use the fixture/helpers to combine and slice the mod's and original's 'a' + * array + * + Build a document with the above and check against the one generated by the mod apply + */ +class SlicedMod : public mongo::unittest::Test { +public: + SlicedMod() : _mod() {} - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [{b:1}]}}"), logDoc); + virtual void setUp() { + // no op; set all state using the setMod() call } - TEST(SimpleObjMod, PrepareApplyInexistent) { - Document doc(fromjson("{}")); - Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + /** Sets up the mod to be {$push: {a: {$each: [<eachArray>], $slice: <slice>}}} */ + void setSimpleMod(int32_t slice, const vector<int>& eachArray) { + BSONArrayBuilder arrBuilder; + for (vector<int>::const_iterator it = eachArray.begin(); it != eachArray.end(); ++it) { + arrBuilder.append(*it); + } - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{b:1}]}"), doc); + _modObj = + BSON("$push" << BSON("a" << BSON("$each" << arrBuilder.arr() << "$slice" << slice))); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [{b:1}]}}"), logDoc); + ASSERT_OK(_mod.init(_modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(SimpleObjMod, PrepareApplyNormal) { - Document doc(fromjson("{a: [{b:0}]}")); - Mod pushMod(fromjson("{$push: {a: {b: 1}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + /** Sets up the mod to be {$push: {a: {$each:[<Obj>,...], $slice:<slice>, $sort:<Obj>}}} */ + void setSortMod(int32_t slice, const vector<BSONObj>& eachArray, BSONObj sort) { + BSONArrayBuilder arrBuilder; + for (vector<BSONObj>::const_iterator it = eachArray.begin(); it != eachArray.end(); ++it) { + arrBuilder.append(*it); + } - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{b:0},{b:1}]}"), doc); + _modObj = BSON("$push" << BSON("a" << BSON("$each" << arrBuilder.arr() << "$slice" << slice + << "$sort" << sort))); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.1':{b:1}}}"), logDoc); + ASSERT_OK(_mod.init(_modObj["$push"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(SimpleObjMod, PrepareApplyDotted) { - Document doc(fromjson("{ _id : 1 , " - " question : 'a', " - " choices : { " - " first : { choice : 'b' }, " - " second : { choice : 'c' } }" - "}")); - Mod pushMod(fromjson("{$push: {'choices.first.votes': 1}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + /** Returns an object {a: [<'vec's content>]} */ + BSONObj getObjectUsing(const vector<int>& vec) { + BSONArrayBuilder arrBuilder; + for (vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it) { + arrBuilder.append(*it); + } - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "choices.first.votes"); - ASSERT_FALSE(execInfo.noOp); + BSONObjBuilder builder; + builder.appendArray("a", arrBuilder.obj()); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson( "{ _id : 1 , " - " question : 'a', " - " choices : { " - " first : { choice : 'b', votes: [1]}, " - " second : { choice : 'c' } }" - "}"), - doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'choices.first.votes':[1]}}"), logDoc); + return builder.obj(); } + /** Returns an object {a: [<'vec's content>]} */ + BSONObj getObjectUsing(const vector<BSONObj>& vec) { + BSONArrayBuilder arrBuilder; + for (vector<BSONObj>::const_iterator it = vec.begin(); it != vec.end(); ++it) { + arrBuilder.append(*it); + } - // - // $pushAll Variation - // - - TEST(PushAll, PrepareApplyEmpty) { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$pushAll: {a: [1]}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{a: [1]}")); + BSONObjBuilder builder; + builder.appendArray("a", arrBuilder.obj()); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(logDoc, fromjson("{$set: {a: [1]}}")); + return builder.obj(); } - TEST(PushAll, PrepareApplyInexistent) { - Document doc(fromjson("{}")); - Mod pushMod(fromjson("{$pushAll: {a: [1]}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{a: [1]}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(logDoc, fromjson("{$set: {a: [1]}}")); + ModifierPush& mod() { + return _mod; } - TEST(PushAll, PrepareApplyNormal) { - Document doc(fromjson("{a: [0]}")); - Mod pushMod(fromjson("{$pushAll: {a: [1,2]}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{a: [0,1,2]}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(logDoc, fromjson("{$set: {'a.1': 1, 'a.2':2}}")); + BSONObj modObj() { + return _modObj; } - // - // Simple $each mod - // +private: + ModifierPush _mod; + BSONObj _modObj; + vector<int> _eachArray; +}; - TEST(SimpleEachMod, PrepareNonArray) { - Document doc(fromjson("{a: 1}")); - Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); +TEST_F(SlicedMod, SimpleArrayFromEmpty) { + // We'll simulate the original document having {a: []} and the mod being + // {$push: {a: {$each: [1], $slice: <-2..0>}}} + vector<int> docArray; + vector<int> eachArray; + eachArray.push_back(1); - ModifierInterface::ExecInfo dummy; - ASSERT_NOT_OK(pushMod.prepare(doc.root(), "", &dummy)); - } - - TEST(SimpleEachMod, PrepareApplyEmpty) { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); + for (int32_t slice = -2; slice <= 0; slice++) { + setSimpleMod(slice, eachArray); + Document doc(getObjectUsing(docArray /* {a: []} */)); ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(pushMod.apply()); + ASSERT_OK(mod().apply()); ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); + vector<int> combinedVec; + combineVec(docArray, /* a: [] */ + eachArray, /* a: [1] */ + slice, + &combinedVec); + ASSERT_EQUALS(getObjectUsing(combinedVec), doc); } +} - TEST(SimpleEachMod, PrepareApplyInexistent) { - Document doc(fromjson("{}")); - Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); +TEST_F(SlicedMod, SimpleArrayFromExisting) { + // We'll simulate the original document having {a: [2,3]} and the mod being + // {$push: {a: {$each: [1], $slice: <-4..0>}}} + vector<int> docArray; + docArray.push_back(2); + docArray.push_back(3); + vector<int> eachArray; + eachArray.push_back(1); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [1]}}"), logDoc); - } - - TEST(SimpleEachMod, PrepareApplyInexistentMultiple) { - Document doc(fromjson("{}")); - Mod pushMod(fromjson("{$push: {a: {$each: [1, 2]}}}")); + for (int32_t slice = -4; slice <= 0; slice++) { + setSimpleMod(slice, eachArray); + Document doc(getObjectUsing(docArray /* {a: [2, 3]} */)); ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(pushMod.apply()); + ASSERT_OK(mod().apply()); ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1, 2]}"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [1, 2]}}"), logDoc); - } - - TEST(SimpleEachMod, PrepareApplyNormal) { - Document doc(fromjson("{a: [0]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [1]}}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + vector<int> combinedVec; + combineVec(docArray, /* a: [2, 3] */ + eachArray, /* a: [1] */ + slice, + &combinedVec); + ASSERT_EQUALS(getObjectUsing(combinedVec), doc); + } +} + +TEST_F(SlicedMod, ObjectArrayFromEmpty) { + // We'll simulate the original document having {a: []} and the mod being + // {$push: {a: {$each: [{a:2,b:1}], $slice: <-4..0>}, $sort: {a:-1/1,b:-1/1}} + vector<BSONObj> docArray; + vector<BSONObj> eachArray; + eachArray.push_back(fromjson("{a:2,b:1}")); + eachArray.push_back(fromjson("{a:1,b:2}")); + + for (int32_t aOrB = 0; aOrB < 2; aOrB++) { + for (int32_t sortA = 0; sortA < 2; sortA++) { + for (int32_t sortB = 0; sortB < 2; sortB++) { + for (int32_t slice = -3; slice <= 3; slice++) { + BSONObj sortOrder; + if (aOrB == 0) { + sortOrder = BSON("a" << (sortA ? 1 : -1) << "b" << (sortB ? 1 : -1)); + } else { + sortOrder = BSON("b" << (sortB ? 1 : -1) << "a" << (sortA ? 1 : -1)); + } - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + setSortMod(slice, eachArray, sortOrder); + Document doc(getObjectUsing(docArray /* {a: []} */)); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [0,1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.1': 1}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(SimpleEachMod, PrepareApplyNormalMultiple) { - Document doc(fromjson("{a: [0]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [1,2]}}}")); + ASSERT_OK(mod().apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + vector<BSONObj> combinedVec; + combineAndSortVec(docArray, /* a: [] */ + eachArray, /* a: [{a:2,b:1},{a:1,b:2}] */ + slice, + sortOrder, + &combinedVec); + ASSERT_EQUALS(getObjectUsing(combinedVec), doc); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [0,1,2]}"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.1': 1, 'a.2':2}}"), logDoc); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod().log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(BSON("$set" << getObjectUsing(combinedVec)), logDoc); + } + } + } } +} + +TEST_F(SlicedMod, ObjectArrayFromExisting) { + // We'll simulate the original document having {a: [{a:2,b:3},{a:3,:b1}]} and the mod being + // {$push: {a: {$each: [{a:2,b:1}], $slice: <-4..0>}, $sort: {a:-1/1,b:-1/1}} + vector<BSONObj> docArray; + docArray.push_back(fromjson("{a:2,b:3}")); + docArray.push_back(fromjson("{a:3,b:1}")); + vector<BSONObj> eachArray; + eachArray.push_back(fromjson("{a:2,b:1}")); + + for (int32_t aOrB = 0; aOrB < 2; aOrB++) { + for (int32_t sortA = 0; sortA < 2; sortA++) { + for (int32_t sortB = 0; sortB < 2; sortB++) { + for (int32_t slice = -4; slice <= 4; slice++) { + BSONObj sortOrder; + if (aOrB == 0) { + sortOrder = BSON("a" << (sortA ? 1 : -1) << "b" << (sortB ? 1 : -1)); + } else { + sortOrder = BSON("b" << (sortB ? 1 : -1) << "a" << (sortA ? 1 : -1)); + } - /** - * Slice variants - */ - TEST(SlicePushEach, TopOne) { - Document doc(fromjson("{a: [3]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [2, -1], $slice:1}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [3]}"), doc); + setSortMod(slice, eachArray, sortOrder); + Document doc(getObjectUsing(docArray /* {a: [{a:2,b:b},{a:3,:b1}]} */)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [3]}}"), logDoc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); - /** - * Sort for scalar (whole) array elements - */ - TEST(SortPushEach, NumberSort) { - Document doc(fromjson("{a: [3]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [2, -1], $sort:1}}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(mod().apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + vector<BSONObj> combinedVec; + combineAndSortVec(docArray, /* a: [{a:2,b:3},{a:3,:b1}] */ + eachArray, /* a: [{a:2,b:1}] */ + slice, + sortOrder, + &combinedVec); + ASSERT_EQUALS(getObjectUsing(combinedVec), doc); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [-1,2,3]}"), doc); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(mod().log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(BSON("$set" << getObjectUsing(combinedVec)), logDoc); + } + } + } + } +} + +// Push to position tests + +TEST(ToPosition, BadInputs) { + const char* const bad[] = { + "{$push: {a: { $each: [1], $position:-1}}}", + "{$push: {a: { $each: [1], $position:'s'}}}", + "{$push: {a: { $each: [1], $position:{}}}}", + "{$push: {a: { $each: [1], $position:[0]}}}", + "{$push: {a: { $each: [1], $position:1.1211212}}}", + "{$push: {a: { $each: [1], $position:3.000000000001}}}", + "{$push: {a: { $each: [1], $position:1.2}}}", + "{$push: {a: { $each: [1], $position:-1.2}}}", + "{$push: {a: { $each: [1], $position:9.0e19}}}", + "{$push: {a: { $each: [1], $position:-9.0e19}}}", + NULL, + }; - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [-1, 2, 3]}}"), logDoc); + int i = 0; + while (bad[i] != NULL) { + ModifierPush pushMod(ModifierPush::PUSH_NORMAL); + BSONObj modObj = fromjson(bad[i]); + ASSERT_NOT_OK(pushMod.init(modObj.firstElement().embeddedObject().firstElement(), + ModifierInterface::Options::normal())); + i++; } +} - TEST(SortPushEach, NumberSortReverse) { - Document doc(fromjson("{a: [3]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:-1}}}")); +TEST(ToPosition, GoodInputs) { + { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position: NumberLong(1)}}}")); ModifierInterface::ExecInfo execInfo; ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [4,3,-1]}"), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: [4,3,-1]}}"), logDoc); } - - TEST(SortPushEach, MixedSortWhole) { - Document doc(fromjson("{a: [3, 't', {b:1}, {a:1}]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:1}}}")); - const BSONObj expectedObj = fromjson("{a: [-1,3,4,'t', {a:1}, {b:1}]}"); + { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:NumberInt(100)}}}")); ModifierInterface::ExecInfo execInfo; ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(expectedObj, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(BSON("$set" << expectedObj), logDoc); } - - TEST(SortPushEach, MixedSortWholeReverse) { - Document doc(fromjson("{a: [3, 't', {b:1}, {a:1}]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:-1}}}")); - const BSONObj expectedObj = fromjson("{a: [{b:1}, {a:1}, 't', 4, 3, -1]}"); + { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1.0}}}")); ModifierInterface::ExecInfo execInfo; ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(expectedObj, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(BSON("$set" << expectedObj), logDoc); } - - TEST(SortPushEach, MixedSortEmbeddedField) { - Document doc(fromjson("{a: [3, 't', {b:1}, {a:1}]}")); - Mod pushMod(fromjson("{$push: {a: {$each: [4, -1], $sort:{a:1}}}}")); - const BSONObj expectedObj = fromjson("{a: [3, 't', {b: 1}, 4, -1, {a: 1}]}"); + { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1000000}}}")); ModifierInterface::ExecInfo execInfo; ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(expectedObj, doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(BSON("$set" << expectedObj), logDoc); - } - - /** - * This fixture supports building $push mods with parameterized $each arrays and $slices. - * It always assume that the array being operated on is called 'a'. To build a mod, one - * issues a set*Mod() call. - * - * The setSimpleMod() call will build a $each array of numbers. The setObjectMod() call - * will build a $each array with object. Both these calls take the slice as a parameter as - * well. - * - * Here's a typical test case flow: - * + Determine what the original document's 'a' array would contain - * + Ditto for the $push's $each arrray - * + Loop over slice value - * + Apply the $push with current slice value to the doc - * + Use the fixture/helpers to combine and slice the mod's and original's 'a' - * array - * + Build a document with the above and check against the one generated by the mod apply - */ - class SlicedMod : public mongo::unittest::Test { - public: - SlicedMod() : _mod() {} - - virtual void setUp() { - // no op; set all state using the setMod() call - } - - /** Sets up the mod to be {$push: {a: {$each: [<eachArray>], $slice: <slice>}}} */ - void setSimpleMod(int32_t slice, const vector<int>& eachArray) { - - BSONArrayBuilder arrBuilder; - for (vector<int>::const_iterator it = eachArray.begin(); it != eachArray.end(); ++it) { - arrBuilder.append(*it); - } - - _modObj = BSON("$push" << - BSON("a" - << BSON("$each" << arrBuilder.arr() << - "$slice" << slice))); - - ASSERT_OK(_mod.init(_modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - /** Sets up the mod to be {$push: {a: {$each:[<Obj>,...], $slice:<slice>, $sort:<Obj>}}} */ - void setSortMod(int32_t slice, const vector<BSONObj>& eachArray, BSONObj sort) { - - BSONArrayBuilder arrBuilder; - for (vector<BSONObj>::const_iterator it = eachArray.begin(); - it != eachArray.end(); - ++it) { - arrBuilder.append(*it); - } - - _modObj = BSON("$push" << - BSON("a" - << BSON("$each" << arrBuilder.arr() << - "$slice" << slice << - "$sort" << sort))); - - ASSERT_OK(_mod.init(_modObj["$push"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - /** Returns an object {a: [<'vec's content>]} */ - BSONObj getObjectUsing(const vector<int>& vec) { - - BSONArrayBuilder arrBuilder; - for (vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it) { - arrBuilder.append(*it); - } - - BSONObjBuilder builder; - builder.appendArray("a", arrBuilder.obj()); - - return builder.obj(); - } - - /** Returns an object {a: [<'vec's content>]} */ - BSONObj getObjectUsing(const vector<BSONObj>& vec) { - - BSONArrayBuilder arrBuilder; - for (vector<BSONObj>::const_iterator it = vec.begin(); it != vec.end(); ++it) { - arrBuilder.append(*it); - } - - BSONObjBuilder builder; - builder.appendArray("a", arrBuilder.obj()); - - return builder.obj(); - } - - ModifierPush& mod() { return _mod; } - - BSONObj modObj() { return _modObj; } - - private: - ModifierPush _mod; - BSONObj _modObj; - vector<int> _eachArray; - }; - - TEST_F(SlicedMod, SimpleArrayFromEmpty) { - // We'll simulate the original document having {a: []} and the mod being - // {$push: {a: {$each: [1], $slice: <-2..0>}}} - vector<int> docArray; - vector<int> eachArray; - eachArray.push_back(1); - - for (int32_t slice = -2; slice <= 0; slice++) { - setSimpleMod(slice, eachArray); - Document doc(getObjectUsing(docArray /* {a: []} */)); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod().apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - vector<int> combinedVec; - combineVec(docArray, /* a: [] */ - eachArray, /* a: [1] */ - slice, - &combinedVec); - ASSERT_EQUALS(getObjectUsing(combinedVec), doc); - } - } - - TEST_F(SlicedMod, SimpleArrayFromExisting) { - // We'll simulate the original document having {a: [2,3]} and the mod being - // {$push: {a: {$each: [1], $slice: <-4..0>}}} - vector<int> docArray; - docArray.push_back(2); - docArray.push_back(3); - vector<int> eachArray; - eachArray.push_back(1); - - for (int32_t slice = -4; slice <= 0; slice++) { - setSimpleMod(slice, eachArray); - Document doc(getObjectUsing(docArray /* {a: [2, 3]} */)); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod().apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - vector<int> combinedVec; - combineVec(docArray, /* a: [2, 3] */ - eachArray, /* a: [1] */ - slice, - &combinedVec); - ASSERT_EQUALS(getObjectUsing(combinedVec), doc); - } - } - - TEST_F(SlicedMod, ObjectArrayFromEmpty) { - // We'll simulate the original document having {a: []} and the mod being - // {$push: {a: {$each: [{a:2,b:1}], $slice: <-4..0>}, $sort: {a:-1/1,b:-1/1}} - vector<BSONObj> docArray; - vector<BSONObj> eachArray; - eachArray.push_back(fromjson("{a:2,b:1}")); - eachArray.push_back(fromjson("{a:1,b:2}")); - - for (int32_t aOrB = 0; aOrB < 2 ; aOrB++) { - for (int32_t sortA = 0; sortA < 2; sortA++) { - for (int32_t sortB = 0; sortB < 2; sortB++) { - for (int32_t slice = -3; slice <= 3; slice++) { - - BSONObj sortOrder; - if (aOrB == 0) { - sortOrder = BSON("a" << (sortA ? 1 : -1) << "b" << (sortB ? 1 : -1)); - } - else { - sortOrder = BSON("b" << (sortB ? 1 : -1) << "a" << (sortA ? 1 : -1)); - } - - setSortMod(slice, eachArray, sortOrder); - Document doc(getObjectUsing(docArray /* {a: []} */)); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod().apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - vector<BSONObj> combinedVec; - combineAndSortVec(docArray, /* a: [] */ - eachArray, /* a: [{a:2,b:1},{a:1,b:2}] */ - slice, - sortOrder, - &combinedVec); - ASSERT_EQUALS(getObjectUsing(combinedVec), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod().log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(BSON("$set" << getObjectUsing(combinedVec)), logDoc); - - } - } - } - } - } - - TEST_F(SlicedMod, ObjectArrayFromExisting) { - // We'll simulate the original document having {a: [{a:2,b:3},{a:3,:b1}]} and the mod being - // {$push: {a: {$each: [{a:2,b:1}], $slice: <-4..0>}, $sort: {a:-1/1,b:-1/1}} - vector<BSONObj> docArray; - docArray.push_back(fromjson("{a:2,b:3}")); - docArray.push_back(fromjson("{a:3,b:1}")); - vector<BSONObj> eachArray; - eachArray.push_back(fromjson("{a:2,b:1}")); - - for (int32_t aOrB = 0; aOrB < 2 ; aOrB++) { - for (int32_t sortA = 0; sortA < 2; sortA++) { - for (int32_t sortB = 0; sortB < 2; sortB++) { - for (int32_t slice = -4; slice <= 4; slice++) { - - BSONObj sortOrder; - if (aOrB == 0) { - sortOrder = BSON("a" << (sortA ? 1 : -1) << "b" << (sortB ? 1 : -1)); - } - else { - sortOrder = BSON("b" << (sortB ? 1 : -1) << "a" << (sortA ? 1 : -1)); - } - - setSortMod(slice, eachArray, sortOrder); - Document doc(getObjectUsing(docArray /* {a: [{a:2,b:b},{a:3,:b1}]} */)); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(mod().prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(mod().apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - - vector<BSONObj> combinedVec; - combineAndSortVec(docArray, /* a: [{a:2,b:3},{a:3,:b1}] */ - eachArray, /* a: [{a:2,b:1}] */ - slice, - sortOrder, - &combinedVec); - ASSERT_EQUALS(getObjectUsing(combinedVec), doc); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(mod().log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(BSON("$set" << getObjectUsing(combinedVec)), logDoc); - - } - } - } - } - } - - // Push to position tests - - TEST(ToPosition, BadInputs) { - const char* const bad[] = { - "{$push: {a: { $each: [1], $position:-1}}}", - "{$push: {a: { $each: [1], $position:'s'}}}", - "{$push: {a: { $each: [1], $position:{}}}}", - "{$push: {a: { $each: [1], $position:[0]}}}", - "{$push: {a: { $each: [1], $position:1.1211212}}}", - "{$push: {a: { $each: [1], $position:3.000000000001}}}", - "{$push: {a: { $each: [1], $position:1.2}}}", - "{$push: {a: { $each: [1], $position:-1.2}}}", - "{$push: {a: { $each: [1], $position:9.0e19}}}", - "{$push: {a: { $each: [1], $position:-9.0e19}}}", - NULL, - }; - - int i = 0; - while(bad[i] != NULL) - { - ModifierPush pushMod(ModifierPush::PUSH_NORMAL); - BSONObj modObj = fromjson(bad[i]); - ASSERT_NOT_OK(pushMod.init(modObj.firstElement().embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - i++; - } } - - TEST(ToPosition, GoodInputs) { - { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position: NumberLong(1)}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - } - { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:NumberInt(100)}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - } - { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1.0}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - } - { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1000000}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - } - { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:0}}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - } - } - - TEST(ToPosition, EmptyArrayFront) { + { Document doc(fromjson("{a: []}")); Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:0}}}")); ModifierInterface::ExecInfo execInfo; ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + } +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(ToPosition, EmptyArrayFront) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:0}}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a':[1]}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(ToPosition, EmptyArrayBackBigPosition) { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1000}}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a':[1]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(ToPosition, EmptyArrayBackBigPosition) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1000}}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a':[1]}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(ToPosition, EmptyArrayBack) { - Document doc(fromjson("{a: []}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1}}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a':[1]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(ToPosition, EmptyArrayBack) { + Document doc(fromjson("{a: []}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:1}}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a':[1]}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(ToPosition, Front) { - Document doc(fromjson("{a: [0]}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:0}}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a':[1]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(ToPosition, Front) { + Document doc(fromjson("{a: [0]}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:0}}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [1, 0]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a':[1, 0]}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(ToPosition, Back) { - Document doc(fromjson("{a: [0]}")); - Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:100}}}")); + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [1, 0]}"), doc); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a':[1, 0]}}"), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(ToPosition, Back) { + Document doc(fromjson("{a: [0]}")); + Mod pushMod(fromjson("{$push: {a: { $each: [1], $position:100}}}")); - ASSERT_OK(pushMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [0,1]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(pushMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(pushMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.1':1}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(pushMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [0,1]}"), doc); -} // unnamed namespace + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(pushMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.1':1}}"), logDoc); +} + +} // unnamed namespace diff --git a/src/mongo/db/ops/modifier_rename.cpp b/src/mongo/db/ops/modifier_rename.cpp index 13ec0c70beb..123b253f227 100644 --- a/src/mongo/db/ops/modifier_rename.cpp +++ b/src/mongo/db/ops/modifier_rename.cpp @@ -38,279 +38,258 @@ namespace mongo { - namespace str = mongoutils::str; +namespace str = mongoutils::str; - struct ModifierRename::PreparedState { +struct ModifierRename::PreparedState { + PreparedState(mutablebson::Element root) + : doc(root.getDocument()), + fromElemFound(doc.end()), + toIdxFound(0), + toElemFound(doc.end()), + applyCalled(false) {} - PreparedState(mutablebson::Element root) - : doc(root.getDocument()) - , fromElemFound(doc.end()) - , toIdxFound(0) - , toElemFound(doc.end()) - , applyCalled(false){ - } - - // Document that is going to be changed. - mutablebson::Document& doc; + // Document that is going to be changed. + mutablebson::Document& doc; - // The element to rename - mutablebson::Element fromElemFound; + // The element to rename + mutablebson::Element fromElemFound; - // Index in _fieldRef for which an Element exist in the document. - size_t toIdxFound; + // Index in _fieldRef for which an Element exist in the document. + size_t toIdxFound; - // Element to remove (in the destination position) - mutablebson::Element toElemFound; + // Element to remove (in the destination position) + mutablebson::Element toElemFound; - // Was apply called? - bool applyCalled; + // Was apply called? + bool applyCalled; +}; - }; +ModifierRename::ModifierRename() : _fromFieldRef(), _toFieldRef() {} - ModifierRename::ModifierRename() - : _fromFieldRef() - , _toFieldRef() { - } +ModifierRename::~ModifierRename() {} - ModifierRename::~ModifierRename() { +Status ModifierRename::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + if (modExpr.type() != String) { + return Status(ErrorCodes::BadValue, + str::stream() << "The 'to' field for $rename must be a string: " << modExpr); } - Status ModifierRename::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - - if (modExpr.type() != String) { - return Status(ErrorCodes::BadValue, - str::stream() << "The 'to' field for $rename must be a string: " - << modExpr); - } + // Extract the field names from the mod expression - // Extract the field names from the mod expression + _fromFieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fromFieldRef); + if (!status.isOK()) + return status; - _fromFieldRef.parse(modExpr.fieldName()); - Status status = fieldchecker::isUpdatable(_fromFieldRef); - if (!status.isOK()) - return status; + _toFieldRef.parse(modExpr.String()); + status = fieldchecker::isUpdatable(_toFieldRef); + if (!status.isOK()) + return status; - _toFieldRef.parse(modExpr.String()); - status = fieldchecker::isUpdatable(_toFieldRef); - if (!status.isOK()) + // TODO: Remove this restriction and make a noOp to lift restriction + // Old restriction is that if the fields are the same then it is not allowed. + if (_fromFieldRef == _toFieldRef) + return Status(ErrorCodes::BadValue, + str::stream() + << "The source and target field for $rename must differ: " << modExpr); + + // TODO: Remove this restriction by allowing moving deeping from the 'from' path + // Old restriction is that if the to/from is on the same path it fails + if (_fromFieldRef.isPrefixOf(_toFieldRef) || _toFieldRef.isPrefixOf(_fromFieldRef)) { + return Status(ErrorCodes::BadValue, + str::stream() << "The source and target field for $rename must " + "not be on the same path: " << modExpr); + } + // TODO: We can remove this restriction as long as there is only one, + // or it is the same array -- should think on this a bit. + // + // If a $-positional operator was used it is an error + size_t dummyPos; + if (fieldchecker::isPositional(_fromFieldRef, &dummyPos)) + return Status(ErrorCodes::BadValue, + str::stream() << "The source field for $rename may not be dynamic: " + << _fromFieldRef.dottedField()); + else if (fieldchecker::isPositional(_toFieldRef, &dummyPos)) + return Status(ErrorCodes::BadValue, + str::stream() << "The destination field for $rename may not be dynamic: " + << _toFieldRef.dottedField()); + + if (positional) + *positional = false; + + return Status::OK(); +} + +Status ModifierRename::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + // Rename doesn't work with positional fields ($) + dassert(matchedField.empty()); + + _preparedState.reset(new PreparedState(root)); + + // Locate the to field name in 'root', which must exist. + size_t fromIdxFound; + Status status = pathsupport::findLongestPrefix( + _fromFieldRef, root, &fromIdxFound, &_preparedState->fromElemFound); + + const bool sourceExists = + (_preparedState->fromElemFound.ok() && fromIdxFound == (_fromFieldRef.numParts() - 1)); + + // If we can't find the full element in the from field then we can't do anything. + if (!status.isOK() || !sourceExists) { + execInfo->noOp = true; + _preparedState->fromElemFound = root.getDocument().end(); + + // TODO: remove this special case from existing behavior + if (status.code() == ErrorCodes::PathNotViable) { return status; - - // TODO: Remove this restriction and make a noOp to lift restriction - // Old restriction is that if the fields are the same then it is not allowed. - if (_fromFieldRef == _toFieldRef) - return Status(ErrorCodes::BadValue, - str::stream() << "The source and target field for $rename must differ: " - << modExpr); - - // TODO: Remove this restriction by allowing moving deeping from the 'from' path - // Old restriction is that if the to/from is on the same path it fails - if (_fromFieldRef.isPrefixOf(_toFieldRef) || _toFieldRef.isPrefixOf(_fromFieldRef)){ - return Status(ErrorCodes::BadValue, - str::stream() << "The source and target field for $rename must " - "not be on the same path: " - << modExpr); } - // TODO: We can remove this restriction as long as there is only one, - // or it is the same array -- should think on this a bit. - // - // If a $-positional operator was used it is an error - size_t dummyPos; - if (fieldchecker::isPositional(_fromFieldRef, &dummyPos)) - return Status(ErrorCodes::BadValue, - str::stream() << "The source field for $rename may not be dynamic: " - << _fromFieldRef.dottedField()); - else if (fieldchecker::isPositional(_toFieldRef, &dummyPos)) - return Status(ErrorCodes::BadValue, - str::stream() << "The destination field for $rename may not be dynamic: " - << _toFieldRef.dottedField()); - - if (positional) - *positional = false; return Status::OK(); } - Status ModifierRename::prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo) { - // Rename doesn't work with positional fields ($) - dassert(matchedField.empty()); - - _preparedState.reset(new PreparedState(root)); - - // Locate the to field name in 'root', which must exist. - size_t fromIdxFound; - Status status = pathsupport::findLongestPrefix(_fromFieldRef, - root, - &fromIdxFound, - &_preparedState->fromElemFound); + // Ensure no array in ancestry if what we found is not at the root + mutablebson::Element curr = _preparedState->fromElemFound.parent(); + if (curr != curr.getDocument().root()) + while (curr.ok() && (curr != curr.getDocument().root())) { + if (curr.getType() == Array) + return Status(ErrorCodes::BadValue, + str::stream() << "The source field cannot be an array element, '" + << _fromFieldRef.dottedField() << "' in doc with " + << findElementNamed(root.leftChild(), "_id").toString() + << " has an array field called '" << curr.getFieldName() + << "'"); + curr = curr.parent(); + } - const bool sourceExists = (_preparedState->fromElemFound.ok() && - fromIdxFound == (_fromFieldRef.numParts() - 1)); + // "To" side validation below - // If we can't find the full element in the from field then we can't do anything. - if (!status.isOK() || !sourceExists) { - execInfo->noOp = true; - _preparedState->fromElemFound = root.getDocument().end(); + status = pathsupport::findLongestPrefix( + _toFieldRef, root, &_preparedState->toIdxFound, &_preparedState->toElemFound); - // TODO: remove this special case from existing behavior - if (status.code() == ErrorCodes::PathNotViable) { - return status; - } + // FindLongestPrefix may return not viable or any other error and then we cannot proceed. + if (status.code() == ErrorCodes::NonExistentPath) { + // Not an error condition as we will create the "to" path as needed. + } else if (!status.isOK()) { + return status; + } - return Status::OK(); + const bool destExists = _preparedState->toElemFound.ok() && + (_preparedState->toIdxFound == (_toFieldRef.numParts() - 1)); + + // Ensure no array in ancestry of "to" Element + // Set to either parent, or node depending on if the full path element was found + curr = (destExists ? _preparedState->toElemFound.parent() : _preparedState->toElemFound); + if (curr != curr.getDocument().root()) { + while (curr.ok()) { + if (curr.getType() == Array) + return Status(ErrorCodes::BadValue, + str::stream() << "The destination field cannot be an array element, '" + << _fromFieldRef.dottedField() << "' in doc with " + << findElementNamed(root.leftChild(), "_id").toString() + << " has an array field called '" << curr.getFieldName() + << "'"); + curr = curr.parent(); } + } - // Ensure no array in ancestry if what we found is not at the root - mutablebson::Element curr = _preparedState->fromElemFound.parent(); - if (curr != curr.getDocument().root()) - while (curr.ok() && (curr != curr.getDocument().root())) { - if (curr.getType() == Array) - return Status(ErrorCodes::BadValue, - str::stream() << "The source field cannot be an array element, '" - << _fromFieldRef.dottedField() << "' in doc with " - << findElementNamed(root.leftChild(), "_id").toString() - << " has an array field called '" << curr.getFieldName() << "'"); - curr = curr.parent(); - } - - // "To" side validation below - - status = pathsupport::findLongestPrefix(_toFieldRef, - root, - &_preparedState->toIdxFound, - &_preparedState->toElemFound); - - // FindLongestPrefix may return not viable or any other error and then we cannot proceed. - if (status.code() == ErrorCodes::NonExistentPath) { - // Not an error condition as we will create the "to" path as needed. - } 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] = &_fromFieldRef; + execInfo->fieldRef[1] = &_toFieldRef; - const bool destExists = _preparedState->toElemFound.ok() && - (_preparedState->toIdxFound == (_toFieldRef.numParts()-1)); - - // Ensure no array in ancestry of "to" Element - // Set to either parent, or node depending on if the full path element was found - curr = (destExists ? _preparedState->toElemFound.parent() : _preparedState->toElemFound); - if (curr != curr.getDocument().root()) { - while (curr.ok()) { - if (curr.getType() == Array) - return Status(ErrorCodes::BadValue, - str::stream() - << "The destination field cannot be an array element, '" - << _fromFieldRef.dottedField() << "' in doc with " - << findElementNamed(root.leftChild(), "_id").toString() - << " has an array field called '" << curr.getFieldName() << "'"); - curr = curr.parent(); - } - } + execInfo->noOp = false; - // 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] = &_fromFieldRef; - execInfo->fieldRef[1] = &_toFieldRef; + return Status::OK(); +} - execInfo->noOp = false; +Status ModifierRename::apply() const { + dassert(_preparedState->fromElemFound.ok()); - return Status::OK(); - } + _preparedState->applyCalled = true; - Status ModifierRename::apply() const { - dassert(_preparedState->fromElemFound.ok()); + // Remove from source + Status removeStatus = _preparedState->fromElemFound.remove(); + if (!removeStatus.isOK()) { + return removeStatus; + } - _preparedState->applyCalled = true; + // If there's no need to create any further field part, the op is simply a value + // assignment. + const bool destExists = _preparedState->toElemFound.ok() && + (_preparedState->toIdxFound == (_toFieldRef.numParts() - 1)); - // Remove from source - Status removeStatus = _preparedState->fromElemFound.remove(); + if (destExists) { + removeStatus = _preparedState->toElemFound.remove(); if (!removeStatus.isOK()) { return removeStatus; } + } - // If there's no need to create any further field part, the op is simply a value - // assignment. - const bool destExists = _preparedState->toElemFound.ok() && - (_preparedState->toIdxFound == (_toFieldRef.numParts()-1)); - - if (destExists) { - removeStatus = _preparedState->toElemFound.remove(); - if (!removeStatus.isOK()) { - return removeStatus; - } - } + // Creates the final element that's going to be the in 'doc'. + mutablebson::Document& doc = _preparedState->doc; + StringData lastPart = _toFieldRef.getPart(_toFieldRef.numParts() - 1); + mutablebson::Element elemToSet = + doc.makeElementWithNewFieldName(lastPart, _preparedState->fromElemFound); + if (!elemToSet.ok()) { + return Status(ErrorCodes::InternalError, "can't create new element"); + } - // Creates the final element that's going to be the in 'doc'. - mutablebson::Document& doc = _preparedState->doc; - StringData lastPart = _toFieldRef.getPart(_toFieldRef.numParts()-1); - mutablebson::Element elemToSet = doc.makeElementWithNewFieldName( - lastPart, - _preparedState->fromElemFound); - if (!elemToSet.ok()) { - return Status(ErrorCodes::InternalError, "can't create new element"); - } + // Find the new place to put the "to" element: + // createPathAt does not use existing prefix elements so we + // need to get the prefix match position for createPathAt below + size_t tempIdx = 0; + mutablebson::Element tempElem = doc.end(); + Status status = pathsupport::findLongestPrefix(_toFieldRef, doc.root(), &tempIdx, &tempElem); + + // createPathAt will complete the path and attach 'elemToSet' at the end of it. + return pathsupport::createPathAt(_toFieldRef, + tempElem == doc.end() ? 0 : tempIdx + 1, + tempElem == doc.end() ? doc.root() : tempElem, + elemToSet); +} + +Status ModifierRename::log(LogBuilder* logBuilder) const { + // If there was no element found then it was a noop, so return immediately + if (!_preparedState->fromElemFound.ok()) + return Status::OK(); - // Find the new place to put the "to" element: - // createPathAt does not use existing prefix elements so we - // need to get the prefix match position for createPathAt below - size_t tempIdx = 0; - mutablebson::Element tempElem = doc.end(); - Status status = pathsupport::findLongestPrefix(_toFieldRef, - doc.root(), - &tempIdx, - &tempElem); - - // createPathAt will complete the path and attach 'elemToSet' at the end of it. - return pathsupport::createPathAt(_toFieldRef, - tempElem == doc.end() ? 0 : tempIdx + 1, - tempElem == doc.end() ? doc.root() : tempElem, - elemToSet); - } + // debug assert if apply not called, since we found an element to move. + dassert(_preparedState->applyCalled); - Status ModifierRename::log(LogBuilder* logBuilder) const { + const bool isPrefix = _fromFieldRef.isPrefixOf(_toFieldRef); + const StringData setPath = (isPrefix ? _fromFieldRef : _toFieldRef).dottedField(); + const StringData unsetPath = isPrefix ? StringData() : _fromFieldRef.dottedField(); + const bool doUnset = !isPrefix; - // If there was no element found then it was a noop, so return immediately - if (!_preparedState->fromElemFound.ok()) - return Status::OK(); + // We'd like to create an entry such as {$set: {<fieldname>: <value>}} under 'logRoot'. + // We start by creating the {$set: ...} Element. + mutablebson::Document& doc = logBuilder->getDocument(); - // debug assert if apply not called, since we found an element to move. - dassert(_preparedState->applyCalled); + // Create the {<fieldname>: <value>} Element. Note that we log the mod with a + // dotted field, if it was applied over a dotted field. The rationale is that the + // secondary may be in a different state than the primary and thus make different + // decisions about creating the intermediate path in _fieldRef or not. + mutablebson::Element logElement = + doc.makeElementWithNewFieldName(setPath, _preparedState->fromElemFound.getValue()); - const bool isPrefix = _fromFieldRef.isPrefixOf(_toFieldRef); - const StringData setPath = - (isPrefix ? _fromFieldRef : _toFieldRef).dottedField(); - const StringData unsetPath = - isPrefix ? StringData() : _fromFieldRef.dottedField(); - const bool doUnset = !isPrefix; + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, "cannot create details for $rename mod"); + } - // We'd like to create an entry such as {$set: {<fieldname>: <value>}} under 'logRoot'. - // We start by creating the {$set: ...} Element. - mutablebson::Document& doc = logBuilder->getDocument(); + // Now, we attach the {<fieldname>: <value>} Element under the {$set: ...} section. + Status status = logBuilder->addToSets(logElement); + if (status.isOK() && doUnset) { // Create the {<fieldname>: <value>} Element. Note that we log the mod with a // dotted field, if it was applied over a dotted field. The rationale is that the // secondary may be in a different state than the primary and thus make different // decisions about creating the intermediate path in _fieldRef or not. - mutablebson::Element logElement = doc.makeElementWithNewFieldName( - setPath, _preparedState->fromElemFound.getValue()); - - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, "cannot create details for $rename mod"); - } - - // Now, we attach the {<fieldname>: <value>} Element under the {$set: ...} section. - Status status = logBuilder->addToSets(logElement); - - if (status.isOK() && doUnset) { - // Create the {<fieldname>: <value>} Element. Note that we log the mod with a - // dotted field, if it was applied over a dotted field. The rationale is that the - // secondary may be in a different state than the primary and thus make different - // decisions about creating the intermediate path in _fieldRef or not. - status = logBuilder->addToUnsets(unsetPath); - } - - return status; + status = logBuilder->addToUnsets(unsetPath); } -} // namespace mongo + return status; +} + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_rename.h b/src/mongo/db/ops/modifier_rename.h index a43523f5320..883443893b3 100644 --- a/src/mongo/db/ops/modifier_rename.h +++ b/src/mongo/db/ops/modifier_rename.h @@ -38,61 +38,56 @@ namespace mongo { - class LogBuilder; +class LogBuilder; - /** - * The $rename modifier moves the field from source to the destination to perform - * the rename. - * - * Example: {$rename: {<source>:<dest>}} where both <source/dest> are field names - * Start with {a:1} and applying a {$rename: {"a":"b"} } produces {b:1} - **/ - class ModifierRename : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierRename); - - public: - - ModifierRename(); - virtual ~ModifierRename(); - - /** - * We will check that the to/from are valid paths; in prepare more validation is done - */ - virtual Status init(const BSONElement& modExpr, const Options& opts, - bool* positional = NULL); - - /** - * In prepare we will ensure that all restrictions are met: - * -- The 'from' field exists, and is valid, else it is a no-op - * -- The 'to' field is valid as a destination - * -- The 'to' field is not on the path (or the same path) as the 'from' field - * -- Neither 'to' nor 'from' have an array ancestor - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); - - /** - * We will transform the document by first making sure that the 'to' element - * is empty before moving the 'from' element there. - */ - virtual Status apply() const; - - /** - * For the oplog entry we will generate an $unset on the 'from' field, and $set for - * the 'to' field. If no 'from' element is found then function will return immediately. - */ - virtual Status log(LogBuilder* logBuilder) const; +/** +* The $rename modifier moves the field from source to the destination to perform +* the rename. +* +* Example: {$rename: {<source>:<dest>}} where both <source/dest> are field names +* Start with {a:1} and applying a {$rename: {"a":"b"} } produces {b:1} +**/ +class ModifierRename : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierRename); + +public: + ModifierRename(); + virtual ~ModifierRename(); - private: + /** + * We will check that the to/from are valid paths; in prepare more validation is done + */ + virtual Status init(const BSONElement& modExpr, const Options& opts, bool* positional = NULL); - // The source and destination fields - FieldRef _fromFieldRef; - FieldRef _toFieldRef; + /** + * In prepare we will ensure that all restrictions are met: + * -- The 'from' field exists, and is valid, else it is a no-op + * -- The 'to' field is valid as a destination + * -- The 'to' field is not on the path (or the same path) as the 'from' field + * -- Neither 'to' nor 'from' have an array ancestor + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); - // The state carried over from prepare for apply/log - struct PreparedState; - std::unique_ptr<PreparedState> _preparedState; - }; + /** + * We will transform the document by first making sure that the 'to' element + * is empty before moving the 'from' element there. + */ + virtual Status apply() const; -} // namespace mongo + /** + * For the oplog entry we will generate an $unset on the 'from' field, and $set for + * the 'to' field. If no 'from' element is found then function will return immediately. + */ + virtual Status log(LogBuilder* logBuilder) const; + +private: + // The source and destination fields + FieldRef _fromFieldRef; + FieldRef _toFieldRef; + + // The state carried over from prepare for apply/log + struct PreparedState; + std::unique_ptr<PreparedState> _preparedState; +}; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_rename_test.cpp b/src/mongo/db/ops/modifier_rename_test.cpp index 637926da8e1..4841c4b15a1 100644 --- a/src/mongo/db/ops/modifier_rename_test.cpp +++ b/src/mongo/db/ops/modifier_rename_test.cpp @@ -41,355 +41,355 @@ namespace { - using mongo::BSONObj; - using mongo::fromjson; - using mongo::LogBuilder; - using mongo::ModifierInterface; - using mongo::NumberInt; - using mongo::ModifierRename; - using mongo::Status; - using mongo::StringData; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate the mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) { - _modObj = modObj; - ASSERT_OK(_mod.init(_modObj["$rename"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierRename& mod() { return _mod; } - - private: - ModifierRename _mod; - BSONObj _modObj; - }; - - /** - * These test negative cases: - * -- No '$' support for positional operator - * -- No empty field names (ex. .a, b. ) - * -- Can't rename to an invalid fieldname (empty fieldname part) - */ - TEST(InvalidInit, FromDbTests) { - ModifierRename mod; - ASSERT_NOT_OK(mod.init(fromjson("{'a.$':'b'}").firstElement(), - ModifierInterface::Options::normal())); - ASSERT_NOT_OK(mod.init(fromjson("{'a':'b.$'}").firstElement(), - ModifierInterface::Options::normal())); - ASSERT_NOT_OK(mod.init(fromjson("{'.b':'a'}").firstElement(), - ModifierInterface::Options::normal())); - ASSERT_NOT_OK(mod.init(fromjson("{'b.':'a'}").firstElement(), - ModifierInterface::Options::normal())); - ASSERT_NOT_OK(mod.init(fromjson("{'b':'.a'}").firstElement(), - ModifierInterface::Options::normal())); - ASSERT_NOT_OK(mod.init(fromjson("{'b':'a.'}").firstElement(), - ModifierInterface::Options::normal())); +using mongo::BSONObj; +using mongo::fromjson; +using mongo::LogBuilder; +using mongo::ModifierInterface; +using mongo::NumberInt; +using mongo::ModifierRename; +using mongo::Status; +using mongo::StringData; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** Helper to build and manipulate the mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) { + _modObj = modObj; + ASSERT_OK(_mod.init(_modObj["$rename"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(MissingFrom, InitPrepLog) { - Document doc(fromjson("{a: 2}")); - Mod setMod(fromjson("{$rename: {'b':'a'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(MissingFromDotted, InitPrepLog) { - Document doc(fromjson("{a: {r:2}}")); - Mod setMod(fromjson("{$rename: {'a.b':'a.c'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_TRUE(execInfo.noOp); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(BasicInit, DifferentRoots) { - Document doc(fromjson("{a: 2}")); - Mod setMod(fromjson("{$rename: {'a':'f.g'}}")); - } - - TEST(MoveOnSamePath, MoveUp) { - ModifierRename mod; - ASSERT_NOT_OK(mod.init(fromjson("{'b.a':'b'}").firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(MoveOnSamePath, MoveDown) { - ModifierRename mod; - ASSERT_NOT_OK(mod.init(fromjson("{'b':'b.a'}").firstElement(), - ModifierInterface::Options::normal())); - } - - TEST(MissingTo, SimpleNumberAtRoot) { - Document doc(fromjson("{a: 2}")); - Mod setMod(fromjson("{$rename: {'a':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b:2}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b': 2}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(SimpleReplace, SameLevel) { - Document doc(fromjson("{a: 2, b: 1}")); - Mod setMod(fromjson("{$rename: {'a':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b:2}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b': 2}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(SimpleReplace, FromDottedElement) { - Document doc(fromjson("{a: {c: {d: 6}}, b: 1}")); - Mod setMod(fromjson("{$rename: {'a.c':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.c"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{a: {}, b:{ d: 6}}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b': {d: 6}}, $unset: {'a.c': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(DottedTo, MissingCompleteTo) { - Document doc(fromjson("{a: 2, b: 1, c: {}}")); - Mod setMod(fromjson("{$rename: {'a':'c.r.d'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "c.r.d"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b:1, c: { r: { d: 2}}}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'c.r.d': 2}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(DottedTo, ToIsCompletelyMissing) { - Document doc(fromjson("{a: 2}")); - Mod setMod(fromjson("{$rename: {'a':'b.c.d'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b.c.d"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b: {c: {d: 2}}}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b.c.d': 2}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(FromArrayOfEmbeddedDocs, ToMissingDottedField) { - Document doc(fromjson("{a: [ {a:2, b:1} ] }")); - Mod setMod(fromjson("{$rename: {'a':'b.c.d'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b.c.d"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b: {c: {d: [ {a:2, b:1} ]}}}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b.c.d': [ {a:2, b:1} ]}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); + Status apply() const { + return _mod.apply(); } - TEST(FromArrayOfEmbeddedDocs, ToArray) { - Document doc(fromjson("{a: [ {a:2, b:1} ] }")); - Mod setMod(fromjson("{$rename: {'a.a':'a.b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(Arrays, MoveInto) { - Document doc(fromjson("{a: [1, 2], b:2}")); - Mod setMod(fromjson("{$rename: {'b':'a.2'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierRename& mod() { + return _mod; } - TEST(Arrays, MoveOut) { - Document doc(fromjson("{a: [1, 2]}")); - Mod setMod(fromjson("{$rename: {'a.0':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } - - TEST(Arrays, MoveNonexistantEmbeddedFieldOut) { - Document doc(fromjson("{a: [{a:1}, {b:2}]}")); - Mod setMod(fromjson("{$rename: {'a.a':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } - - TEST(Arrays, MoveEmbeddedFieldOutWithElementNumber) { - Document doc(fromjson("{a: [{a:1}, {b:2}]}")); - Mod setMod(fromjson("{$rename: {'a.0.a':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } +private: + ModifierRename _mod; + BSONObj _modObj; +}; - TEST(Arrays, ReplaceArrayField) { - Document doc(fromjson("{a: 2, b: []}")); - Mod setMod(fromjson("{$rename: {'a':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b:2}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b': 2}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - - TEST(Arrays, ReplaceWithArrayField) { - Document doc(fromjson("{a: [], b: 2}")); - Mod setMod(fromjson("{$rename: {'a':'b'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{b:[]}")); - - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'b': []}, $unset: {'a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } - - TEST(LegacyData, CanRenameFromInvalidFieldName) { - Document doc(fromjson("{$a: 2}")); - Mod setMod(fromjson("{$rename: {'$a':'a'}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "$a"); - ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +/** + * These test negative cases: + * -- No '$' support for positional operator + * -- No empty field names (ex. .a, b. ) + * -- Can't rename to an invalid fieldname (empty fieldname part) + */ +TEST(InvalidInit, FromDbTests) { + ModifierRename mod; + ASSERT_NOT_OK( + mod.init(fromjson("{'a.$':'b'}").firstElement(), ModifierInterface::Options::normal())); + ASSERT_NOT_OK( + mod.init(fromjson("{'a':'b.$'}").firstElement(), ModifierInterface::Options::normal())); + ASSERT_NOT_OK( + mod.init(fromjson("{'.b':'a'}").firstElement(), ModifierInterface::Options::normal())); + ASSERT_NOT_OK( + mod.init(fromjson("{'b.':'a'}").firstElement(), ModifierInterface::Options::normal())); + ASSERT_NOT_OK( + mod.init(fromjson("{'b':'.a'}").firstElement(), ModifierInterface::Options::normal())); + ASSERT_NOT_OK( + mod.init(fromjson("{'b':'a.'}").firstElement(), ModifierInterface::Options::normal())); +} + +TEST(MissingFrom, InitPrepLog) { + Document doc(fromjson("{a: 2}")); + Mod setMod(fromjson("{$rename: {'b':'a'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(MissingFromDotted, InitPrepLog) { + Document doc(fromjson("{a: {r:2}}")); + Mod setMod(fromjson("{$rename: {'a.b':'a.c'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_TRUE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(BasicInit, DifferentRoots) { + Document doc(fromjson("{a: 2}")); + Mod setMod(fromjson("{$rename: {'a':'f.g'}}")); +} + +TEST(MoveOnSamePath, MoveUp) { + ModifierRename mod; + ASSERT_NOT_OK( + mod.init(fromjson("{'b.a':'b'}").firstElement(), ModifierInterface::Options::normal())); +} + +TEST(MoveOnSamePath, MoveDown) { + ModifierRename mod; + ASSERT_NOT_OK( + mod.init(fromjson("{'b':'b.a'}").firstElement(), ModifierInterface::Options::normal())); +} + +TEST(MissingTo, SimpleNumberAtRoot) { + Document doc(fromjson("{a: 2}")); + Mod setMod(fromjson("{$rename: {'a':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b:2}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b': 2}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(SimpleReplace, SameLevel) { + Document doc(fromjson("{a: 2, b: 1}")); + Mod setMod(fromjson("{$rename: {'a':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b:2}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b': 2}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(SimpleReplace, FromDottedElement) { + Document doc(fromjson("{a: {c: {d: 6}}, b: 1}")); + Mod setMod(fromjson("{$rename: {'a.c':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.c"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{a: {}, b:{ d: 6}}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b': {d: 6}}, $unset: {'a.c': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(DottedTo, MissingCompleteTo) { + Document doc(fromjson("{a: 2, b: 1, c: {}}")); + Mod setMod(fromjson("{$rename: {'a':'c.r.d'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "c.r.d"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b:1, c: { r: { d: 2}}}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'c.r.d': 2}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(DottedTo, ToIsCompletelyMissing) { + Document doc(fromjson("{a: 2}")); + Mod setMod(fromjson("{$rename: {'a':'b.c.d'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b.c.d"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b: {c: {d: 2}}}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b.c.d': 2}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(FromArrayOfEmbeddedDocs, ToMissingDottedField) { + Document doc(fromjson("{a: [ {a:2, b:1} ] }")); + Mod setMod(fromjson("{$rename: {'a':'b.c.d'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b.c.d"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b: {c: {d: [ {a:2, b:1} ]}}}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b.c.d': [ {a:2, b:1} ]}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(FromArrayOfEmbeddedDocs, ToArray) { + Document doc(fromjson("{a: [ {a:2, b:1} ] }")); + Mod setMod(fromjson("{$rename: {'a.a':'a.b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} + +TEST(Arrays, MoveInto) { + Document doc(fromjson("{a: [1, 2], b:2}")); + Mod setMod(fromjson("{$rename: {'b':'a.2'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} + +TEST(Arrays, MoveOut) { + Document doc(fromjson("{a: [1, 2]}")); + Mod setMod(fromjson("{$rename: {'a.0':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} + +TEST(Arrays, MoveNonexistantEmbeddedFieldOut) { + Document doc(fromjson("{a: [{a:1}, {b:2}]}")); + Mod setMod(fromjson("{$rename: {'a.a':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} + +TEST(Arrays, MoveEmbeddedFieldOutWithElementNumber) { + Document doc(fromjson("{a: [{a:1}, {b:2}]}")); + Mod setMod(fromjson("{$rename: {'a.0.a':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} + +TEST(Arrays, ReplaceArrayField) { + Document doc(fromjson("{a: 2, b: []}")); + Mod setMod(fromjson("{$rename: {'a':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b:2}")); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b': 2}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + + +TEST(Arrays, ReplaceWithArrayField) { + Document doc(fromjson("{a: [], b: 2}")); + Mod setMod(fromjson("{$rename: {'a':'b'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{b:[]}")); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(doc, fromjson("{a:2}")); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'b': []}, $unset: {'a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} + +TEST(LegacyData, CanRenameFromInvalidFieldName) { + Document doc(fromjson("{$a: 2}")); + Mod setMod(fromjson("{$rename: {'$a':'a'}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "$a"); + ASSERT_EQUALS(execInfo.fieldRef[1]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(doc, fromjson("{a:2}")); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - BSONObj logObj = fromjson("{$set:{ 'a': 2}, $unset: {'$a': true}}"); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(logDoc, logObj); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + BSONObj logObj = fromjson("{$set:{ 'a': 2}, $unset: {'$a': true}}"); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(logDoc, logObj); +} -} // namespace +} // namespace diff --git a/src/mongo/db/ops/modifier_set.cpp b/src/mongo/db/ops/modifier_set.cpp index fe808138c68..93abc04f892 100644 --- a/src/mongo/db/ops/modifier_set.cpp +++ b/src/mongo/db/ops/modifier_set.cpp @@ -37,253 +37,224 @@ namespace mongo { - namespace str = mongoutils::str; +namespace str = mongoutils::str; - struct ModifierSet::PreparedState { +struct ModifierSet::PreparedState { + PreparedState(mutablebson::Document* targetDoc) + : doc(*targetDoc), idxFound(0), elemFound(doc.end()), noOp(false), elemIsBlocking(false) {} - PreparedState(mutablebson::Document* targetDoc) - : doc(*targetDoc) - , idxFound(0) - , elemFound(doc.end()) - , noOp(false) - , elemIsBlocking(false) { - } + // Document that is going to be changed. + mutablebson::Document& doc; - // Document that is going to be changed. - mutablebson::Document& doc; + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; + // This $set is a no-op? + bool noOp; - // This $set is a no-op? - bool noOp; + // The element we find during a replication operation that blocks our update path + bool elemIsBlocking; +}; - // The element we find during a replication operation that blocks our update path - bool elemIsBlocking; +ModifierSet::ModifierSet(ModifierSet::ModifierSetMode mode) + : _fieldRef(), _posDollar(0), _setMode(mode), _val(), _modOptions() {} - }; +ModifierSet::~ModifierSet() {} - ModifierSet::ModifierSet(ModifierSet::ModifierSetMode mode) - : _fieldRef() - , _posDollar(0) - , _setMode(mode) - , _val() - , _modOptions() { - } +Status ModifierSet::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + // + // field name analysis + // - ModifierSet::~ModifierSet() { + // Break down the field name into its 'dotted' components (aka parts) and check that + // the field is fit for updates + _fieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fieldRef); + if (!status.isOK()) { + return status; } - Status ModifierSet::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - - // - // field name analysis - // - - // Break down the field name into its 'dotted' components (aka parts) and check that - // the field is fit for updates - _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 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 (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); - } + if (positional) + *positional = foundDollar; - // - // value analysis - // - - if (!modExpr.ok()) - return Status(ErrorCodes::BadValue, "cannot $set an empty value"); - - _val = modExpr; - _modOptions = opts; - - return Status::OK(); + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); } - Status ModifierSet::prepare(mutablebson::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); - } + // + // value analysis + // - // 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 remainin 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 (_modOptions.fromReplication && status.code() == ErrorCodes::PathNotViable) { - // If we are coming from replication and it is an invalid path, - // then push on indicating that we had a blocking element, which we stopped at - _preparedState->elemIsBlocking = true; - } - else if (!status.isOK()) { - return status; - } + if (!modExpr.ok()) + return Status(ErrorCodes::BadValue, "cannot $set an empty value"); - if (_setMode == SET_ON_INSERT) { - execInfo->context = ModifierInterface::ExecInfo::INSERT_CONTEXT; - } + _val = modExpr; + _modOptions = opts; - // 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; + return Status::OK(); +} - // - // in-place and no-op logic - // +Status ModifierSet::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(&root.getDocument())); - // 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 < (_fieldRef.numParts()-1)) { - return Status::OK(); + // 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); + } - // If the value being $set is the same as the one already in the doc, than this is a - // noOp. - if (_preparedState->elemFound.ok() && - _preparedState->idxFound == (_fieldRef.numParts()-1) && - _preparedState->elemFound.compareWithBSONElement(_val, false /*ignore field*/) == 0) { - execInfo->noOp = _preparedState->noOp = true; - } + // 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 remainin 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 (_modOptions.fromReplication && status.code() == ErrorCodes::PathNotViable) { + // If we are coming from replication and it is an invalid path, + // then push on indicating that we had a blocking element, which we stopped at + _preparedState->elemIsBlocking = true; + } else if (!status.isOK()) { + return status; + } - return Status::OK(); + if (_setMode == SET_ON_INSERT) { + execInfo->context = ModifierInterface::ExecInfo::INSERT_CONTEXT; } - Status ModifierSet::apply() const { - dassert(!_preparedState->noOp); + // 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; - const bool destExists = _preparedState->elemFound.ok() && - _preparedState->idxFound == (_fieldRef.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); - } + // + // in-place and no-op logic + // - // - // Complete document path 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 < (_fieldRef.numParts() - 1)) { + return Status::OK(); + } - // 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.makeElementWithNewFieldName(lastPart, _val); - if (!elemToSet.ok()) { - return Status(ErrorCodes::InternalError, "can't create new element"); - } + // If the value being $set is the same as the one already in the doc, than this is a + // noOp. + if (_preparedState->elemFound.ok() && _preparedState->idxFound == (_fieldRef.numParts() - 1) && + _preparedState->elemFound.compareWithBSONElement(_val, false /*ignore field*/) == 0) { + execInfo->noOp = _preparedState->noOp = true; + } - // 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++; - } + return Status::OK(); +} - // Remove the blocking element, if we are from replication applier. See comment below. - if (_modOptions.fromReplication && !destExists && _preparedState->elemFound.ok() && - _preparedState->elemIsBlocking && - (!(_preparedState->elemFound.isType(Array)) || - !(_preparedState->elemFound.isType(Object))) - ) { - - /** - * With replication we want to be able to remove blocking elements for $set (only). - * The reason they are blocking elements is that they are not embedded documents - * (objects) nor an array (a special type of an embedded doc) and we cannot - * add children to them (because the $set path requires adding children past - * the blocking element). - * - * Imagine that we started with this: - * {_id:1, a:1} + {$set : {"a.b.c" : 1}} -> {_id:1, a: {b: {c:1}}} - * Above we found that element (a:1) is blocking at position 1. We now will replace - * it with an empty object so the normal logic below can be - * applied from the root (in this example case). - * - * Here is an array example: - * {_id:1, a:[1, 2]} + {$set : {"a.0.c" : 1}} -> {_id:1, a: [ {c:1}, 2]} - * The blocking element is "a.0" since it is a number, non-object, and we must - * then replace it with an empty object so we can add c:1 to that empty object - */ - - mutablebson::Element blockingElem = _preparedState->elemFound; - BSONObj newObj; - // Replace blocking non-object with an empty object - Status status = blockingElem.setValueObject(newObj); - if (!status.isOK()) { - return status; - } - } +Status ModifierSet::apply() const { + dassert(!_preparedState->noOp); - // createPathAt() will complete the path and attach 'elemToSet' at the end of it. - return pathsupport::createPathAt(_fieldRef, - _preparedState->idxFound, - _preparedState->elemFound, - elemToSet); + const bool destExists = + _preparedState->elemFound.ok() && _preparedState->idxFound == (_fieldRef.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); } - Status ModifierSet::log(LogBuilder* logBuilder) const { + // + // Complete document path logic + // - // We'd like to create an entry such as {$set: {<fieldname>: <value>}} under 'logRoot'. - // We start by creating the {$set: ...} Element. - mutablebson::Document& doc = logBuilder->getDocument(); + // 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.makeElementWithNewFieldName(lastPart, _val); + if (!elemToSet.ok()) { + return Status(ErrorCodes::InternalError, "can't create new element"); + } - // Create the {<fieldname>: <value>} Element. Note that we log the mod with a - // dotted field, if it was applied over a dotted field. The rationale is that the - // secondary may be in a different state than the primary and thus make different - // decisions about creating the intermediate path in _fieldRef or not. - mutablebson::Element logElement = doc.makeElementWithNewFieldName( - _fieldRef.dottedField(), _val); + // 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++; + } - if (!logElement.ok()) { - return Status(ErrorCodes::InternalError, "cannot create details for $set mod"); + // Remove the blocking element, if we are from replication applier. See comment below. + if (_modOptions.fromReplication && !destExists && _preparedState->elemFound.ok() && + _preparedState->elemIsBlocking && (!(_preparedState->elemFound.isType(Array)) || + !(_preparedState->elemFound.isType(Object)))) { + /** + * With replication we want to be able to remove blocking elements for $set (only). + * The reason they are blocking elements is that they are not embedded documents + * (objects) nor an array (a special type of an embedded doc) and we cannot + * add children to them (because the $set path requires adding children past + * the blocking element). + * + * Imagine that we started with this: + * {_id:1, a:1} + {$set : {"a.b.c" : 1}} -> {_id:1, a: {b: {c:1}}} + * Above we found that element (a:1) is blocking at position 1. We now will replace + * it with an empty object so the normal logic below can be + * applied from the root (in this example case). + * + * Here is an array example: + * {_id:1, a:[1, 2]} + {$set : {"a.0.c" : 1}} -> {_id:1, a: [ {c:1}, 2]} + * The blocking element is "a.0" since it is a number, non-object, and we must + * then replace it with an empty object so we can add c:1 to that empty object + */ + + mutablebson::Element blockingElem = _preparedState->elemFound; + BSONObj newObj; + // Replace blocking non-object with an empty object + Status status = blockingElem.setValueObject(newObj); + if (!status.isOK()) { + return status; } + } - return logBuilder->addToSets(logElement); + // createPathAt() will complete the path and attach 'elemToSet' at the end of it. + return pathsupport::createPathAt( + _fieldRef, _preparedState->idxFound, _preparedState->elemFound, elemToSet); +} + +Status ModifierSet::log(LogBuilder* logBuilder) 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 = logBuilder->getDocument(); + + // Create the {<fieldname>: <value>} Element. Note that we log the mod with a + // dotted field, if it was applied over a dotted field. The rationale is that the + // secondary may be in a different state than the primary and thus make different + // decisions about creating the intermediate path in _fieldRef or not. + mutablebson::Element logElement = + doc.makeElementWithNewFieldName(_fieldRef.dottedField(), _val); + + if (!logElement.ok()) { + return Status(ErrorCodes::InternalError, "cannot create details for $set mod"); } -} // namespace mongo + return logBuilder->addToSets(logElement); +} + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_set.h b/src/mongo/db/ops/modifier_set.h index cc961163234..84d6f0ba8cf 100644 --- a/src/mongo/db/ops/modifier_set.h +++ b/src/mongo/db/ops/modifier_set.h @@ -38,77 +38,71 @@ namespace mongo { - class LogBuilder; - - class ModifierSet : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierSet); - - public: - - enum ModifierSetMode { SET_NORMAL, SET_ON_INSERT }; - explicit ModifierSet(ModifierSetMode mode = SET_NORMAL); - - // - // Modifier interface implementation - // - - virtual ~ModifierSet(); - - /** - * 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, - bool* positional = NULL); - - /** - * 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, - 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: - - // 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; - - // If on 'on insert' mode, We'd like to apply this mod only if we're in a upsert. - const ModifierSetMode _setMode; - - // Element of the $set expression. - BSONElement _val; - - // See the class comments in modifier_interface.h - ModifierInterface::Options _modOptions; - - // 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; - std::unique_ptr<PreparedState> _preparedState; - - }; - -} // namespace mongo +class LogBuilder; + +class ModifierSet : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierSet); + +public: + enum ModifierSetMode { SET_NORMAL, SET_ON_INSERT }; + explicit ModifierSet(ModifierSetMode mode = SET_NORMAL); + + // + // Modifier interface implementation + // + + virtual ~ModifierSet(); + + /** + * 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, bool* positional = NULL); + + /** + * 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, 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: + // 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; + + // If on 'on insert' mode, We'd like to apply this mod only if we're in a upsert. + const ModifierSetMode _setMode; + + // Element of the $set expression. + BSONElement _val; + + // See the class comments in modifier_interface.h + ModifierInterface::Options _modOptions; + + // 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; + std::unique_ptr<PreparedState> _preparedState; +}; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_set_test.cpp b/src/mongo/db/ops/modifier_set_test.cpp index 1a7e1db5a68..315da4e113d 100644 --- a/src/mongo/db/ops/modifier_set_test.cpp +++ b/src/mongo/db/ops/modifier_set_test.cpp @@ -42,715 +42,715 @@ namespace { - using mongo::BSONObj; - using mongo::fromjson; - using mongo::LogBuilder; - using mongo::ModifierInterface; - using mongo::NumberInt; - using mongo::ModifierSet; - using mongo::Status; - using mongo::StringData; - using mongo::mutablebson::ConstElement; - using mongo::mutablebson::countChildren; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate a $set mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj, bool fromRepl = false) : - _mod(mongoutils::str::equals(modObj.firstElement().fieldName(), "$setOnInsert") ? - ModifierSet::SET_ON_INSERT : ModifierSet::SET_NORMAL) { - - _modObj = modObj; - StringData modName = modObj.firstElement().fieldName(); - ASSERT_OK(_mod.init(_modObj[modName].embeddedObject().firstElement(), - !fromRepl ? ModifierInterface::Options::normal(): - ModifierInterface::Options::fromRepl())); - } - - Status prepare(Element root, - 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); - } - - ModifierSet& mod() { return _mod; } - - private: - ModifierSet _mod; - BSONObj _modObj; - }; - - // - // Init tests - // - - TEST(Init, EmptyOperation) { - BSONObj modObj = fromjson("{$set: {}}"); - ModifierSet mod; - ASSERT_NOT_OK(mod.init(modObj["$set"].embeddedObject().firstElement(), - ModifierInterface::Options::normal() )); - } - - // - // Simple Mods - // +using mongo::BSONObj; +using mongo::fromjson; +using mongo::LogBuilder; +using mongo::ModifierInterface; +using mongo::NumberInt; +using mongo::ModifierSet; +using mongo::Status; +using mongo::StringData; +using mongo::mutablebson::ConstElement; +using mongo::mutablebson::countChildren; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; - TEST(SimpleMod, PrepareNoOp) { - Document doc(fromjson("{a: 2}")); - Mod setMod(fromjson("{$set: {a: 2}}")); +/** Helper to build and manipulate a $set mod. */ +class Mod { +public: + Mod() : _mod() {} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); + explicit Mod(BSONObj modObj, bool fromRepl = false) + : _mod(mongoutils::str::equals(modObj.firstElement().fieldName(), "$setOnInsert") + ? ModifierSet::SET_ON_INSERT + : ModifierSet::SET_NORMAL) { + _modObj = modObj; + StringData modName = modObj.firstElement().fieldName(); + ASSERT_OK(_mod.init(_modObj[modName].embeddedObject().firstElement(), + !fromRepl ? ModifierInterface::Options::normal() + : ModifierInterface::Options::fromRepl())); } - TEST(SimpleMod, PrepareSetOnInsert) { - Document doc(fromjson("{a: 1}")); - Mod setMod(fromjson("{$setOnInsert: {a: 2}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS(execInfo.context, ModifierInterface::ExecInfo::INSERT_CONTEXT); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); } - TEST(SimpleMod, PrepareApplyEmptyDocument) { - Document doc(fromjson("{}")); - Mod setMod(fromjson("{$set: {a: 2}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: 2}"), doc); + Status apply() const { + return _mod.apply(); } - TEST(SimpleMod, PrepareApplyInPlace) { - Document doc(fromjson("{a: 1}")); - Mod setMod(fromjson("{$set: {a: 2}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: 2}"), doc); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); } - TEST(SimpleMod, PrepareApplyOverridePath) { - Document doc(fromjson("{a: {b: 1}}")); - Mod setMod(fromjson("{$set: {a: 2}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: 2}"), doc); + ModifierSet& mod() { + return _mod; } - TEST(SimpleMod, PrepareApplyChangeType) { - Document doc(fromjson("{a: 'str'}")); - Mod setMod(fromjson("{$set: {a: 2}}")); +private: + ModifierSet _mod; + BSONObj _modObj; +}; - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +// +// Init tests +// - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(Init, EmptyOperation) { + BSONObj modObj = fromjson("{$set: {}}"); + ModifierSet mod; + ASSERT_NOT_OK(mod.init(modObj["$set"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: 2}"), doc); - } +// +// Simple Mods +// - TEST(SimpleMod, PrepareApplyNewPath) { - Document doc(fromjson("{b: 1}")); - Mod setMod(fromjson("{$set: {a: 2}}")); +TEST(SimpleMod, PrepareNoOp) { + Document doc(fromjson("{a: 2}")); + Mod setMod(fromjson("{$set: {a: 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{b: 1, a: 2}"), doc); - } +TEST(SimpleMod, PrepareSetOnInsert) { + Document doc(fromjson("{a: 1}")); + Mod setMod(fromjson("{$setOnInsert: {a: 2}}")); - TEST(SimpleMod, LogNormal) { - BSONObj obj = fromjson("{a: 1}"); - Mod setMod(fromjson("{$set: {a: 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - Document doc(obj); - ModifierInterface::ExecInfo dummy; - ASSERT_OK(setMod.prepare(doc.root(), "", &dummy)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.context, ModifierInterface::ExecInfo::INSERT_CONTEXT); +} - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {a: 2}}"), logDoc); - } - - // - // Simple dotted mod - // - - TEST(DottedMod, PrepareNoOp) { - Document doc(fromjson("{a: {b: 2}}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(SimpleMod, PrepareApplyEmptyDocument) { + Document doc(fromjson("{}")); + Mod setMod(fromjson("{$set: {a: 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_TRUE(execInfo.noOp); - } - - TEST(DottedMod, PreparePathNotViable) { - Document doc(fromjson("{a:1}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(DottedMod, PreparePathNotViableArrray) { - Document doc(fromjson("{a:[{b:1}]}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } +TEST(SimpleMod, PrepareApplyInPlace) { + Document doc(fromjson("{a: 1}")); + Mod setMod(fromjson("{$set: {a: 2}}")); - TEST(DottedMod, PrepareApplyInPlace) { - Document doc(fromjson("{a: {b: 1}}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); - } - - TEST(DottedMod, PrepareApplyChangeType) { - Document doc(fromjson("{a: {b: 'str'}}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); - - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); - } + ASSERT_OK(setMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); +} - TEST(DottedMod, PrepareApplyChangePath) { - Document doc(fromjson("{a: {b: {c: 1}}}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); +TEST(SimpleMod, PrepareApplyOverridePath) { + Document doc(fromjson("{a: {b: 1}}")); + Mod setMod(fromjson("{$set: {a: 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); - } + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); +} - TEST(DottedMod, PrepareApplyExtendPath) { - Document doc(fromjson("{a: {c: 1}}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); +TEST(SimpleMod, PrepareApplyChangeType) { + Document doc(fromjson("{a: 'str'}")); + Mod setMod(fromjson("{$set: {a: 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {c: 1, b: 2}}"), doc); - } + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: 2}"), doc); +} - TEST(DottedMod, PrepareApplyNewPath) { - Document doc(fromjson("{c: 1}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); +TEST(SimpleMod, PrepareApplyNewPath) { + Document doc(fromjson("{b: 1}")); + Mod setMod(fromjson("{$set: {a: 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{c: 1, a: {b: 2}}"), doc); - } + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{b: 1, a: 2}"), doc); +} - TEST(DottedMod, PrepareApplyEmptyDoc) { - Document doc(fromjson("{}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); +TEST(SimpleMod, LogNormal) { + BSONObj obj = fromjson("{a: 1}"); + Mod setMod(fromjson("{$set: {a: 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + Document doc(obj); + ModifierInterface::ExecInfo dummy; + ASSERT_OK(setMod.prepare(doc.root(), "", &dummy)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {a: 2}}"), logDoc); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); - } +// +// Simple dotted mod +// - TEST(DottedMod, PrepareApplyFieldWithDot) { - Document doc(fromjson("{'a.b':4}")); - Mod setMod(fromjson("{$set: {'a.b': 2}}")); +TEST(DottedMod, PrepareNoOp) { + Document doc(fromjson("{a: {b: 2}}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{'a.b':4, a: {b: 2}}"), doc); - } +TEST(DottedMod, PreparePathNotViable) { + Document doc(fromjson("{a:1}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - // - // Indexed mod - // + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} - TEST(IndexedMod, PrepareNoOp) { - Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); +TEST(DottedMod, PreparePathNotViableArrray) { + Document doc(fromjson("{a:[{b:1}]}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_TRUE(execInfo.noOp); - } +TEST(DottedMod, PrepareApplyInPlace) { + Document doc(fromjson("{a: {b: 1}}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - TEST(IndexedMod, PrepareNonViablePath) { - Document doc(fromjson("{a: 0}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyInPlace) { - Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 1}]}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyChangeType) { + Document doc(fromjson("{a: {b: 'str'}}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyNormalArray) { - Document doc(fromjson("{a: [{b: 0},{b: 1}]}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyChangePath) { + Document doc(fromjson("{a: {b: {c: 1}}}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyPaddingArray) { - Document doc(fromjson("{a: [{b: 0}]}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyExtendPath) { + Document doc(fromjson("{a: {c: 1}}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{b: 0},null,{b: 2}]}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyNumericObject) { - Document doc(fromjson("{a: {b: 0}}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {c: 1, b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyNewPath) { + Document doc(fromjson("{c: 1}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {b: 0, '2': {b: 2}}}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyNumericField) { - Document doc(fromjson("{a: {'2': {b: 1}}}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{c: 1, a: {b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyEmptyDoc) { + Document doc(fromjson("{}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyExtendNumericField) { - Document doc(fromjson("{a: {'2': {c: 1}}}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyFieldWithDot) { + Document doc(fromjson("{'a.b':4}")); + Mod setMod(fromjson("{$set: {'a.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {'2': {c: 1, b: 2}}}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareApplyEmptyObject) { - Document doc(fromjson("{a: {}}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{'a.b':4, a: {b: 2}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +// +// Indexed mod +// - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); +TEST(IndexedMod, PrepareNoOp) { + Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - TEST(IndexedMod, PrepareApplyEmptyArray) { - Document doc(fromjson("{a: []}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_TRUE(execInfo.noOp); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(IndexedMod, PrepareNonViablePath) { + Document doc(fromjson("{a: 0}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [null, null, {b: 2}]}"), doc); - } +TEST(IndexedMod, PrepareApplyInPlace) { + Document doc(fromjson("{a: [{b: 0},{b: 1},{b: 1}]}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - TEST(IndexedMod, PrepareApplyInexistent) { - Document doc(fromjson("{}")); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_OK(setMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); - } +TEST(IndexedMod, PrepareApplyNormalArray) { + Document doc(fromjson("{a: [{b: 0},{b: 1}]}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - TEST(IndexedMod, LogNormal) { - BSONObj obj = fromjson("{a: [{b:0}, {b:1}]}"); - Document doc(obj); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); - } + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{b: 0},{b: 1},{b: 2}]}"), doc); +} - TEST(IndexedMod, LogEmptyArray) { - BSONObj obj = fromjson("{a: []}"); - Document doc(obj); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); +TEST(IndexedMod, PrepareApplyPaddingArray) { + Document doc(fromjson("{a: [{b: 0}]}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, LogEmptyObject) { - BSONObj obj = fromjson("{a: []}"); - Document doc(obj); - Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{b: 0},null,{b: 2}]}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(IndexedMod, PrepareApplyNumericObject) { + Document doc(fromjson("{a: {b: 0}}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - // - // Indexed complex mod - // + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedComplexMod, PrepareNoOp) { - Document doc(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, d: 1}}]}}")); - Mod setMod(fromjson("{$set: {'a.1.b': {c: 1, d: 1}}}")); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {b: 0, '2': {b: 2}}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(IndexedMod, PrepareApplyNumericField) { + Document doc(fromjson("{a: {'2': {b: 1}}}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); - ASSERT_TRUE(execInfo.noOp); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - TEST(IndexedComplexMod, PrepareSameStructure) { - Document doc(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, xxx: 1}}]}}")); - Mod setMod(fromjson("{$set: {'a.1.b': {c: 1, d: 1}}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(setMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); - ASSERT_FALSE(execInfo.noOp); - } +TEST(IndexedMod, PrepareApplyExtendNumericField) { + Document doc(fromjson("{a: {'2': {c: 1}}}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - // - // Replication version where viable paths don't block modification - // - TEST(NonViablePathWithoutRepl, ControlRun) { - Document doc(fromjson("{a: 1}")); - Mod setMod(fromjson("{$set: {'a.1.b': 1}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(NonViablePathWithRepl, SingleField) { - Document doc(fromjson("{_id:1, a: 1}")); - Mod setMod(fromjson("{$set: {'a.1.b': 1}}"), true); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {'2': {c: 1, b: 2}}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(IndexedMod, PrepareApplyEmptyObject) { + Document doc(fromjson("{a: {}}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{_id:1, a: {'1': {b: 1}}}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(NonViablePathWithRepl, SingleFieldNoId) { - Document doc(fromjson("{a: 1}")); - Mod setMod(fromjson("{$set: {'a.1.b': 1}}"), true); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(IndexedMod, PrepareApplyEmptyArray) { + Document doc(fromjson("{a: []}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {'1': {b: 1}}}"), doc); - } + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [null, null, {b: 2}]}"), doc); +} + +TEST(IndexedMod, PrepareApplyInexistent) { + Document doc(fromjson("{}")); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - TEST(NonViablePathWithRepl, NestedField) { - Document doc(fromjson("{_id:1, a: {a: 1}}")); - Mod setMod(fromjson("{$set: {'a.a.1.b': 1}}"), true); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.2.b"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.a.1.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {'2': {b: 2}}}"), doc); +} + +TEST(IndexedMod, LogNormal) { + BSONObj obj = fromjson("{a: [{b:0}, {b:1}]}"); + Document doc(obj); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{_id:1, a: {a: {'1': {b: 1}}}}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - TEST(NonViablePathWithRepl, DoubleNestedField) { - Document doc(fromjson("{_id:1, a: {b: {c: 1}}}")); - Mod setMod(fromjson("{$set: {'a.b.c.d': 2}}"), true); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); +} + +TEST(IndexedMod, LogEmptyArray) { + BSONObj obj = fromjson("{a: []}"); + Document doc(obj); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b.c.d"); - ASSERT_FALSE(execInfo.noOp); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); +} + +TEST(IndexedMod, LogEmptyObject) { + BSONObj obj = fromjson("{a: []}"); + Document doc(obj); + Mod setMod(fromjson("{$set: {'a.2.b': 2}}")); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{_id:1, a: {b: {c: {d: 2}}}}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - TEST(NonViablePathWithRepl, NestedFieldNoId) { - Document doc(fromjson("{a: {a: 1}}")); - Mod setMod(fromjson("{$set: {'a.a.1.b': 1}}"), true); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'a.2.b': 2}}"), logDoc); +} + +// +// Indexed complex mod +// - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(IndexedComplexMod, PrepareNoOp) { + Document doc(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, d: 1}}]}}")); + Mod setMod(fromjson("{$set: {'a.1.b': {c: 1, d: 1}}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.a.1.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); + ASSERT_TRUE(execInfo.noOp); +} + +TEST(IndexedComplexMod, PrepareSameStructure) { + Document doc(fromjson("{a: [{b: {c: 0, d: 0}}, {b: {c: 1, xxx: 1}}]}}")); + Mod setMod(fromjson("{$set: {'a.1.b': {c: 1, d: 1}}}")); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: {a: {'1': {b: 1}}}}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - TEST(NonViablePathWithRepl, ReplayArrayFieldNotAppendedItermediate) { - Document doc(fromjson("{_id: 0, a: [1, {b: [1]}]}")); - Mod setMod(fromjson("{$set: {'a.0.b': [0,2]}}}"), true); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); + ASSERT_FALSE(execInfo.noOp); +} + +// +// Replication version where viable paths don't block modification +// +TEST(NonViablePathWithoutRepl, ControlRun) { + Document doc(fromjson("{a: 1}")); + Mod setMod(fromjson("{$set: {'a.1.b': 1}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(setMod.prepare(doc.root(), "", &execInfo)); +} + +TEST(NonViablePathWithRepl, SingleField) { + Document doc(fromjson("{_id:1, a: 1}")); + Mod setMod(fromjson("{$set: {'a.1.b': 1}}"), true); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{_id:1, a: {'1': {b: 1}}}"), doc); +} + +TEST(NonViablePathWithRepl, SingleFieldNoId) { + Document doc(fromjson("{a: 1}")); + Mod setMod(fromjson("{$set: {'a.1.b': 1}}"), true); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {'1': {b: 1}}}"), doc); +} + +TEST(NonViablePathWithRepl, NestedField) { + Document doc(fromjson("{_id:1, a: {a: 1}}")); + Mod setMod(fromjson("{$set: {'a.a.1.b': 1}}"), true); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.a.1.b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{_id:1, a: {a: {'1': {b: 1}}}}"), doc); +} + +TEST(NonViablePathWithRepl, DoubleNestedField) { + Document doc(fromjson("{_id:1, a: {b: {c: 1}}}")); + Mod setMod(fromjson("{$set: {'a.b.c.d': 2}}"), true); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b.c.d"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{_id:1, a: {b: {c: {d: 2}}}}"), doc); +} - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{_id: 0, a: [{b: [0,2]}, {b: [1]}]}"), doc); - } +TEST(NonViablePathWithRepl, NestedFieldNoId) { + Document doc(fromjson("{a: {a: 1}}")); + Mod setMod(fromjson("{$set: {'a.a.1.b': 1}}"), true); - // Cases from users/issues/jstests - TEST(JsTestIssues, Set6) { - Document doc(fromjson("{_id: 1, r: {a:1, b:2}}")); - Mod setMod(fromjson("{$set: {'r.a': 2}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.a.1.b"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "r.a"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: {a: {'1': {b: 1}}}}"), doc); +} + +TEST(NonViablePathWithRepl, ReplayArrayFieldNotAppendedItermediate) { + Document doc(fromjson("{_id: 0, a: [1, {b: [1]}]}")); + Mod setMod(fromjson("{$set: {'a.0.b': [0,2]}}}"), true); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{_id: 1, r: {a:2, b:2}}"), doc); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); + ASSERT_FALSE(execInfo.noOp); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'r.a': 2}}"), logDoc); - } + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{_id: 0, a: [{b: [0,2]}, {b: [1]}]}"), doc); +} + +// Cases from users/issues/jstests +TEST(JsTestIssues, Set6) { + Document doc(fromjson("{_id: 1, r: {a:1, b:2}}")); + Mod setMod(fromjson("{$set: {'r.a': 2}}")); - // Test which failed before and is here to verify correct execution. - TEST(JsTestIssues, Set6FromRepl) { - Document doc(fromjson("{_id: 1, r: {a:1, b:2}}")); - Mod setMod(fromjson("{$set: { 'r.a': 2}}"), true); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "r.a"); + ASSERT_FALSE(execInfo.noOp); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "r.a"); - ASSERT_FALSE(execInfo.noOp); + ASSERT_OK(setMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{_id: 1, r: {a:2, b:2}}"), doc); - ASSERT_OK(setMod.apply()); - ASSERT_TRUE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{_id: 1, r: {a:2, b:2} }"), doc); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'r.a': 2}}"), logDoc); +} - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(setMod.log(&logBuilder)); - ASSERT_EQUALS(countChildren(logDoc.root()), 1u); - ASSERT_EQUALS(fromjson("{$set: {'r.a': 2}}"), logDoc); - } +// Test which failed before and is here to verify correct execution. +TEST(JsTestIssues, Set6FromRepl) { + Document doc(fromjson("{_id: 1, r: {a:1, b:2}}")); + Mod setMod(fromjson("{$set: { 'r.a': 2}}"), true); - TEST(Ephemeral, ApplySetModToEphemeralDocument) { - // The following mod when applied to a document constructed node by node exposed a - // latent debug only defect in mutable BSON, so this is more a test of mutable than - // $set. - Document doc; - Element x = doc.makeElementObject("x"); - doc.root().pushBack(x); - Element a = doc.makeElementInt("a", 100); - x.pushBack(a); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - Mod setMod(fromjson("{ $set: { x: { a: 100, b: 2 }}}"), true); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "r.a"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(setMod.apply()); + ASSERT_TRUE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{_id: 1, r: {a:2, b:2} }"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(setMod.log(&logBuilder)); + ASSERT_EQUALS(countChildren(logDoc.root()), 1u); + ASSERT_EQUALS(fromjson("{$set: {'r.a': 2}}"), logDoc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); +TEST(Ephemeral, ApplySetModToEphemeralDocument) { + // The following mod when applied to a document constructed node by node exposed a + // latent debug only defect in mutable BSON, so this is more a test of mutable than + // $set. + Document doc; + Element x = doc.makeElementObject("x"); + doc.root().pushBack(x); + Element a = doc.makeElementInt("a", 100); + x.pushBack(a); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "x"); - ASSERT_FALSE(execInfo.noOp); + Mod setMod(fromjson("{ $set: { x: { a: 100, b: 2 }}}"), true); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(setMod.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(setMod.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{ x : { a : 100, b : 2 } }"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "x"); + ASSERT_FALSE(execInfo.noOp); -} // unnamed namespace + ASSERT_OK(setMod.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{ x : { a : 100, b : 2 } }"), doc); +} + +} // unnamed namespace diff --git a/src/mongo/db/ops/modifier_table.cpp b/src/mongo/db/ops/modifier_table.cpp index f35e79cf5c5..81bc612df48 100644 --- a/src/mongo/db/ops/modifier_table.cpp +++ b/src/mongo/db/ops/modifier_table.cpp @@ -49,96 +49,93 @@ namespace mongo { - using std::make_pair; - using std::string; +using std::make_pair; +using std::string; namespace modifiertable { - namespace { +namespace { - struct ModifierEntry { - string name; - ModifierType type; +struct ModifierEntry { + string name; + ModifierType type; - ModifierEntry(StringData name, ModifierType type) - : name(name.toString()) - , type(type) { - } - }; + ModifierEntry(StringData name, ModifierType type) : name(name.toString()), type(type) {} +}; - typedef unordered_map<StringData, ModifierEntry*, StringData::Hasher> NameMap; +typedef unordered_map<StringData, ModifierEntry*, StringData::Hasher> NameMap; - NameMap* MODIFIER_NAME_MAP; +NameMap* MODIFIER_NAME_MAP; - void init(NameMap* nameMap) { - ModifierEntry* entryAddToSet = new ModifierEntry("$addToSet", MOD_ADD_TO_SET); - nameMap->insert(make_pair(StringData(entryAddToSet->name), entryAddToSet)); +void init(NameMap* nameMap) { + ModifierEntry* entryAddToSet = new ModifierEntry("$addToSet", MOD_ADD_TO_SET); + nameMap->insert(make_pair(StringData(entryAddToSet->name), entryAddToSet)); - ModifierEntry* entryBit = new ModifierEntry("$bit", MOD_BIT); - nameMap->insert(make_pair(StringData(entryBit->name), entryBit)); + ModifierEntry* entryBit = new ModifierEntry("$bit", MOD_BIT); + nameMap->insert(make_pair(StringData(entryBit->name), entryBit)); - ModifierEntry* entryCurrentDate = new ModifierEntry("$currentDate", MOD_CURRENTDATE); - nameMap->insert(make_pair(StringData(entryCurrentDate->name), entryCurrentDate)); + ModifierEntry* entryCurrentDate = new ModifierEntry("$currentDate", MOD_CURRENTDATE); + nameMap->insert(make_pair(StringData(entryCurrentDate->name), entryCurrentDate)); - ModifierEntry* entryInc = new ModifierEntry("$inc", MOD_INC); - nameMap->insert(make_pair(StringData(entryInc->name), entryInc)); + ModifierEntry* entryInc = new ModifierEntry("$inc", MOD_INC); + nameMap->insert(make_pair(StringData(entryInc->name), entryInc)); - ModifierEntry* entryMax = new ModifierEntry("$max", MOD_MAX); - nameMap->insert(make_pair(StringData(entryMax->name), entryMax)); + ModifierEntry* entryMax = new ModifierEntry("$max", MOD_MAX); + nameMap->insert(make_pair(StringData(entryMax->name), entryMax)); - ModifierEntry* entryMin = new ModifierEntry("$min", MOD_MIN); - nameMap->insert(make_pair(StringData(entryMin->name), entryMin)); + ModifierEntry* entryMin = new ModifierEntry("$min", MOD_MIN); + nameMap->insert(make_pair(StringData(entryMin->name), entryMin)); - ModifierEntry* entryMul = new ModifierEntry("$mul", MOD_MUL); - nameMap->insert(make_pair(StringData(entryMul->name), entryMul)); + ModifierEntry* entryMul = new ModifierEntry("$mul", MOD_MUL); + nameMap->insert(make_pair(StringData(entryMul->name), entryMul)); - ModifierEntry* entryPop = new ModifierEntry("$pop", MOD_POP); - nameMap->insert(make_pair(StringData(entryPop->name), entryPop)); + ModifierEntry* entryPop = new ModifierEntry("$pop", MOD_POP); + nameMap->insert(make_pair(StringData(entryPop->name), entryPop)); - ModifierEntry* entryPull = new ModifierEntry("$pull", MOD_PULL); - nameMap->insert(make_pair(StringData(entryPull->name), entryPull)); + ModifierEntry* entryPull = new ModifierEntry("$pull", MOD_PULL); + nameMap->insert(make_pair(StringData(entryPull->name), entryPull)); - ModifierEntry* entryPullAll = new ModifierEntry("$pullAll", MOD_PULL_ALL); - nameMap->insert(make_pair(StringData(entryPullAll->name), entryPullAll)); + ModifierEntry* entryPullAll = new ModifierEntry("$pullAll", MOD_PULL_ALL); + nameMap->insert(make_pair(StringData(entryPullAll->name), entryPullAll)); - ModifierEntry* entryPush = new ModifierEntry("$push", MOD_PUSH); - nameMap->insert(make_pair(StringData(entryPush->name), entryPush)); + ModifierEntry* entryPush = new ModifierEntry("$push", MOD_PUSH); + nameMap->insert(make_pair(StringData(entryPush->name), entryPush)); - ModifierEntry* entryPushAll = new ModifierEntry("$pushAll", MOD_PUSH_ALL); - nameMap->insert(make_pair(StringData(entryPushAll->name), entryPushAll)); + ModifierEntry* entryPushAll = new ModifierEntry("$pushAll", MOD_PUSH_ALL); + nameMap->insert(make_pair(StringData(entryPushAll->name), entryPushAll)); - ModifierEntry* entrySet = new ModifierEntry("$set", MOD_SET); - nameMap->insert(make_pair(StringData(entrySet->name), entrySet)); + ModifierEntry* entrySet = new ModifierEntry("$set", MOD_SET); + nameMap->insert(make_pair(StringData(entrySet->name), entrySet)); - ModifierEntry* entrySetOnInsert = new ModifierEntry("$setOnInsert", MOD_SET_ON_INSERT); - nameMap->insert(make_pair(StringData(entrySetOnInsert->name), entrySetOnInsert)); + ModifierEntry* entrySetOnInsert = new ModifierEntry("$setOnInsert", MOD_SET_ON_INSERT); + nameMap->insert(make_pair(StringData(entrySetOnInsert->name), entrySetOnInsert)); - ModifierEntry* entryRename = new ModifierEntry("$rename", MOD_RENAME); - nameMap->insert(make_pair(StringData(entryRename->name), entryRename)); + ModifierEntry* entryRename = new ModifierEntry("$rename", MOD_RENAME); + nameMap->insert(make_pair(StringData(entryRename->name), entryRename)); - ModifierEntry* entryUnset = new ModifierEntry("$unset", MOD_UNSET); - nameMap->insert(make_pair(StringData(entryUnset->name), entryUnset)); - } + ModifierEntry* entryUnset = new ModifierEntry("$unset", MOD_UNSET); + nameMap->insert(make_pair(StringData(entryUnset->name), entryUnset)); +} - } // unnamed namespace +} // unnamed namespace - MONGO_INITIALIZER(ModifierTable)(InitializerContext* context) { - MODIFIER_NAME_MAP = new NameMap; - init(MODIFIER_NAME_MAP); +MONGO_INITIALIZER(ModifierTable)(InitializerContext* context) { + MODIFIER_NAME_MAP = new NameMap; + init(MODIFIER_NAME_MAP); - return Status::OK(); - } + return Status::OK(); +} - ModifierType getType(StringData typeStr) { - NameMap::const_iterator it = MODIFIER_NAME_MAP->find(typeStr); - if (it == MODIFIER_NAME_MAP->end()) { - return MOD_UNKNOWN; - } - return it->second->type; +ModifierType getType(StringData typeStr) { + NameMap::const_iterator it = MODIFIER_NAME_MAP->find(typeStr); + if (it == MODIFIER_NAME_MAP->end()) { + return MOD_UNKNOWN; } + return it->second->type; +} - ModifierInterface* makeUpdateMod(ModifierType modType) { - switch (modType) { +ModifierInterface* makeUpdateMod(ModifierType modType) { + switch (modType) { case MOD_ADD_TO_SET: return new ModifierAddToSet; case MOD_BIT: @@ -173,8 +170,8 @@ namespace modifiertable { return new ModifierUnset; default: return NULL; - } } +} -} // namespace modifiertable -} // namespace mongo +} // namespace modifiertable +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_table.h b/src/mongo/db/ops/modifier_table.h index 8087114e657..acefc3290dc 100644 --- a/src/mongo/db/ops/modifier_table.h +++ b/src/mongo/db/ops/modifier_table.h @@ -33,38 +33,38 @@ namespace mongo { namespace modifiertable { - // NOTE: Please update jstests/verify_update_mods.js or include a jstest for any new mods - enum ModifierType { - MOD_ADD_TO_SET, - MOD_BIT, - MOD_CURRENTDATE, - MOD_INC, - MOD_MAX, - MOD_MIN, - MOD_MUL, - MOD_POP, - MOD_PULL, - MOD_PULL_ALL, - MOD_PUSH, - MOD_PUSH_ALL, - MOD_SET, - MOD_SET_ON_INSERT, - MOD_RENAME, - MOD_UNSET, - MOD_UNKNOWN - }; +// NOTE: Please update jstests/verify_update_mods.js or include a jstest for any new mods +enum ModifierType { + MOD_ADD_TO_SET, + MOD_BIT, + MOD_CURRENTDATE, + MOD_INC, + MOD_MAX, + MOD_MIN, + MOD_MUL, + MOD_POP, + MOD_PULL, + MOD_PULL_ALL, + MOD_PUSH, + MOD_PUSH_ALL, + MOD_SET, + MOD_SET_ON_INSERT, + MOD_RENAME, + MOD_UNSET, + MOD_UNKNOWN +}; - /** - * Returns the modifier type for 'typeStr', if it was recognized as an existing update - * mod, or MOD_UNKNOWN otherwise. - */ - ModifierType getType(StringData typeStr); +/** + * Returns the modifier type for 'typeStr', if it was recognized as an existing update + * mod, or MOD_UNKNOWN otherwise. + */ +ModifierType getType(StringData typeStr); - /** - * Instantiate an update mod that corresponds to 'modType' or NULL if 'modType' is not - * valid. The ownership of the new object is the caller's. - */ - ModifierInterface* makeUpdateMod(ModifierType modType); +/** + * Instantiate an update mod that corresponds to 'modType' or NULL if 'modType' is not + * valid. The ownership of the new object is the caller's. + */ +ModifierInterface* makeUpdateMod(ModifierType modType); -} // namespace modifiertable -} // namespace mongo +} // namespace modifiertable +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_table_test.cpp b/src/mongo/db/ops/modifier_table_test.cpp index 239f9eb5a34..c5225142668 100644 --- a/src/mongo/db/ops/modifier_table_test.cpp +++ b/src/mongo/db/ops/modifier_table_test.cpp @@ -35,25 +35,25 @@ namespace { - using namespace mongo::modifiertable; +using namespace mongo::modifiertable; - using mongo::ModifierInterface; - using std::unique_ptr; +using mongo::ModifierInterface; +using std::unique_ptr; - TEST(getType, Normal) { - ASSERT_EQUALS(getType("$set"), MOD_SET); - ASSERT_EQUALS(getType("$AModThatDoesn'tExist"), MOD_UNKNOWN); - ASSERT_EQUALS(getType("NotAModExpression"), MOD_UNKNOWN); - } +TEST(getType, Normal) { + ASSERT_EQUALS(getType("$set"), MOD_SET); + ASSERT_EQUALS(getType("$AModThatDoesn'tExist"), MOD_UNKNOWN); + ASSERT_EQUALS(getType("NotAModExpression"), MOD_UNKNOWN); +} - TEST(makeUpdateMod, Normal) { - unique_ptr<ModifierInterface> mod; +TEST(makeUpdateMod, Normal) { + unique_ptr<ModifierInterface> mod; - mod.reset(makeUpdateMod(MOD_SET)); - ASSERT_NOT_EQUALS(mod.get(), static_cast<ModifierInterface*>(0)); + mod.reset(makeUpdateMod(MOD_SET)); + ASSERT_NOT_EQUALS(mod.get(), static_cast<ModifierInterface*>(0)); - mod.reset(makeUpdateMod(MOD_UNKNOWN)); - ASSERT_EQUALS(mod.get(), static_cast<ModifierInterface*>(0)); - } + mod.reset(makeUpdateMod(MOD_UNKNOWN)); + ASSERT_EQUALS(mod.get(), static_cast<ModifierInterface*>(0)); +} -} // unnamed namespace +} // unnamed namespace diff --git a/src/mongo/db/ops/modifier_unset.cpp b/src/mongo/db/ops/modifier_unset.cpp index 9b54480c62d..673cbdb8d16 100644 --- a/src/mongo/db/ops/modifier_unset.cpp +++ b/src/mongo/db/ops/modifier_unset.cpp @@ -37,149 +37,131 @@ namespace mongo { - namespace str = mongoutils::str; +namespace str = mongoutils::str; - struct ModifierUnset::PreparedState { +struct ModifierUnset::PreparedState { + PreparedState(mutablebson::Document* targetDoc) + : doc(*targetDoc), idxFound(0), elemFound(doc.end()), noOp(false) {} - PreparedState(mutablebson::Document* targetDoc) - : doc(*targetDoc) - , idxFound(0) - , elemFound(doc.end()) - , noOp(false) { - } + // Document that is going to be changed. + mutablebson::Document& doc; - // Document that is going to be changed. - mutablebson::Document& doc; + // Index in _fieldRef for which an Element exist in the document. + size_t idxFound; - // Index in _fieldRef for which an Element exist in the document. - size_t idxFound; + // Element corresponding to _fieldRef[0.._idxFound]. + mutablebson::Element elemFound; - // Element corresponding to _fieldRef[0.._idxFound]. - mutablebson::Element elemFound; + // This $set is a no-op? + bool noOp; +}; - // This $set is a no-op? - bool noOp; +ModifierUnset::ModifierUnset() : _fieldRef(), _posDollar(0), _val() {} - }; +ModifierUnset::~ModifierUnset() {} - ModifierUnset::ModifierUnset() - : _fieldRef() - , _posDollar(0) - , _val() { - } +Status ModifierUnset::init(const BSONElement& modExpr, const Options& opts, bool* positional) { + // + // field name analysis + // - ModifierUnset::~ModifierUnset() { + // Perform standard field name and updateable checks. + _fieldRef.parse(modExpr.fieldName()); + Status status = fieldchecker::isUpdatable(_fieldRef); + if (!status.isOK()) { + return status; } - Status ModifierUnset::init(const BSONElement& modExpr, const Options& opts, - bool* positional) { - - // - // 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 + // and ensure only one occurrence. + size_t foundCount; + bool foundDollar = fieldchecker::isPositional(_fieldRef, &_posDollar, &foundCount); - // 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 (positional) - *positional = foundDollar; + if (foundDollar && foundCount > 1) { + return Status(ErrorCodes::BadValue, + str::stream() << "Too many positional (i.e. '$') elements found in path '" + << _fieldRef.dottedField() << "'"); + } - if (foundDollar && foundCount > 1) { - return Status(ErrorCodes::BadValue, - str::stream() << "Too many positional (i.e. '$') elements found in path '" - << _fieldRef.dottedField() << "'"); - } + // + // value analysis + // - // - // value analysis - // + // Unset takes any value, since there is no semantics attached to such value. + _val = modExpr; - // Unset takes any value, since there is no semantics attached to such value. - _val = modExpr; + return Status::OK(); +} - return Status::OK(); - } +Status ModifierUnset::prepare(mutablebson::Element root, + StringData matchedField, + ExecInfo* execInfo) { + _preparedState.reset(new PreparedState(&root.getDocument())); - Status ModifierUnset::prepare(mutablebson::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); + // 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'. Note that if we don't have the full path in the - // doc, there isn't anything to unset, really. - Status status = pathsupport::findLongestPrefix(_fieldRef, - root, - &_preparedState->idxFound, - &_preparedState->elemFound); - if (!status.isOK() || - _preparedState->idxFound != (_fieldRef.numParts() -1)) { - execInfo->noOp = _preparedState->noOp = true; - execInfo->fieldRef[0] = &_fieldRef; - - return Status::OK(); - } - - // If there is indeed something to unset, we register so, along with the interest in - // the field name. The driver needs this info to sort out if there is any conflict - // among mods. + // Locate the field name in 'root'. Note that if we don't have the full path in the + // doc, there isn't anything to unset, really. + Status status = pathsupport::findLongestPrefix( + _fieldRef, root, &_preparedState->idxFound, &_preparedState->elemFound); + if (!status.isOK() || _preparedState->idxFound != (_fieldRef.numParts() - 1)) { + execInfo->noOp = _preparedState->noOp = true; execInfo->fieldRef[0] = &_fieldRef; - // The only way for an $unset to be inplace is for its target field to be the last one - // of the object. That is, it is always the right child on its paths. The current - // rationale is that there should be no holes in a BSONObj and, to be in place, no - // field boundaries must change. - // - // TODO: - // mutablebson::Element curr = _preparedState->elemFound; - // while (curr.ok()) { - // if (curr.rightSibling().ok()) { - // } - // curr = curr.parent(); - // } - return Status::OK(); } - Status ModifierUnset::apply() const { - dassert(!_preparedState->noOp); - - // Our semantics says that, if we're unseting an element of an array, we swap that - // value to null. The rationale is that we don't want other array elements to change - // indices. (That could be achieved with $pull-ing element from it.) - if (_preparedState->elemFound.parent().ok() && - _preparedState->elemFound.parent().getType() == Array) { - return _preparedState->elemFound.setValueNull(); - } - else { - return _preparedState->elemFound.remove(); - } + // If there is indeed something to unset, we register so, along with the interest in + // the field name. The driver needs this info to sort out if there is any conflict + // among mods. + execInfo->fieldRef[0] = &_fieldRef; + + // The only way for an $unset to be inplace is for its target field to be the last one + // of the object. That is, it is always the right child on its paths. The current + // rationale is that there should be no holes in a BSONObj and, to be in place, no + // field boundaries must change. + // + // TODO: + // mutablebson::Element curr = _preparedState->elemFound; + // while (curr.ok()) { + // if (curr.rightSibling().ok()) { + // } + // curr = curr.parent(); + // } + + return Status::OK(); +} + +Status ModifierUnset::apply() const { + dassert(!_preparedState->noOp); + + // Our semantics says that, if we're unseting an element of an array, we swap that + // value to null. The rationale is that we don't want other array elements to change + // indices. (That could be achieved with $pull-ing element from it.) + if (_preparedState->elemFound.parent().ok() && + _preparedState->elemFound.parent().getType() == Array) { + return _preparedState->elemFound.setValueNull(); + } else { + return _preparedState->elemFound.remove(); } +} - Status ModifierUnset::log(LogBuilder* logBuilder) const { - return logBuilder->addToUnsets(_fieldRef.dottedField()); - } +Status ModifierUnset::log(LogBuilder* logBuilder) const { + return logBuilder->addToUnsets(_fieldRef.dottedField()); +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_unset.h b/src/mongo/db/ops/modifier_unset.h index 53417a3c37e..a36b16ac19c 100644 --- a/src/mongo/db/ops/modifier_unset.h +++ b/src/mongo/db/ops/modifier_unset.h @@ -38,66 +38,60 @@ namespace mongo { - class LogBuilder; - - class ModifierUnset : public ModifierInterface { - MONGO_DISALLOW_COPYING(ModifierUnset); - - public: - - ModifierUnset(); - - // - // Modifier interface implementation - // - - virtual ~ModifierUnset(); - - /** - * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming from a $set mod such as - * {$unset: {<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, - bool* positional = NULL); - - /** - * Locates the field to be removed under the 'root' element, if it exist, and fills in - * 'execInfo' accordingly. Return OK if successful or a status describing the error. - */ - virtual Status prepare(mutablebson::Element root, - StringData matchedField, - ExecInfo* execInfo); - - /** - * Removes the found element from the document. If such element was inside an array, - * removal means setting that array position to 'null'. - */ - virtual Status apply() const; - - /** - * Adds the exact $unset mod to the log. - */ - virtual Status log(LogBuilder* logBuilder) 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. - 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; - std::unique_ptr<PreparedState> _preparedState; - - }; - -} // namespace mongo +class LogBuilder; + +class ModifierUnset : public ModifierInterface { + MONGO_DISALLOW_COPYING(ModifierUnset); + +public: + ModifierUnset(); + + // + // Modifier interface implementation + // + + virtual ~ModifierUnset(); + + /** + * A 'modExpr' is a BSONElement {<fieldname>: <value>} coming from a $set mod such as + * {$unset: {<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, bool* positional = NULL); + + /** + * Locates the field to be removed under the 'root' element, if it exist, and fills in + * 'execInfo' accordingly. Return OK if successful or a status describing the error. + */ + virtual Status prepare(mutablebson::Element root, StringData matchedField, ExecInfo* execInfo); + + /** + * Removes the found element from the document. If such element was inside an array, + * removal means setting that array position to 'null'. + */ + virtual Status apply() const; + + /** + * Adds the exact $unset mod to the log. + */ + virtual Status log(LogBuilder* logBuilder) 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. + 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; + std::unique_ptr<PreparedState> _preparedState; +}; + +} // namespace mongo diff --git a/src/mongo/db/ops/modifier_unset_test.cpp b/src/mongo/db/ops/modifier_unset_test.cpp index 3ca4957e94c..d68f471c7e7 100644 --- a/src/mongo/db/ops/modifier_unset_test.cpp +++ b/src/mongo/db/ops/modifier_unset_test.cpp @@ -42,410 +42,412 @@ namespace { - using mongo::Array; - using mongo::BSONObj; - using mongo::fromjson; - using mongo::LogBuilder; - using mongo::ModifierInterface; - using mongo::ModifierUnset; - using mongo::Status; - using mongo::StringData; - using mongo::mutablebson::Document; - using mongo::mutablebson::Element; - - /** Helper to build and manipulate a $set mod. */ - class Mod { - public: - Mod() : _mod() {} - - explicit Mod(BSONObj modObj) { - _modObj = modObj; - ASSERT_OK(_mod.init(_modObj["$unset"].embeddedObject().firstElement(), - ModifierInterface::Options::normal())); - } - - Status prepare(Element root, - 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); - } - - ModifierUnset& mod() { return _mod; } - - BSONObj modObj() { return _modObj; } - - private: - ModifierUnset _mod; - BSONObj _modObj; - }; - - // - // Simple mod - // - - TEST(SimpleMod, PrepareNoOp) { - Document doc(fromjson("{}")); - Mod modUnset(fromjson("{$unset: {a: true}}")); - - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_TRUE(execInfo.noOp); +using mongo::Array; +using mongo::BSONObj; +using mongo::fromjson; +using mongo::LogBuilder; +using mongo::ModifierInterface; +using mongo::ModifierUnset; +using mongo::Status; +using mongo::StringData; +using mongo::mutablebson::Document; +using mongo::mutablebson::Element; + +/** Helper to build and manipulate a $set mod. */ +class Mod { +public: + Mod() : _mod() {} + + explicit Mod(BSONObj modObj) { + _modObj = modObj; + ASSERT_OK(_mod.init(_modObj["$unset"].embeddedObject().firstElement(), + ModifierInterface::Options::normal())); } - TEST(SimpleMod, PrepareApplyNormal) { - Document doc(fromjson("{a: 1, b: 2}")); - Mod modUnset(fromjson("{$unset: {a: true}}")); + Status prepare(Element root, StringData matchedField, ModifierInterface::ExecInfo* execInfo) { + return _mod.prepare(root, matchedField, execInfo); + } - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + Status apply() const { + return _mod.apply(); + } - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); + Status log(LogBuilder* logBuilder) const { + return _mod.log(logBuilder); + } - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{b: 2}"), doc); + ModifierUnset& mod() { + return _mod; } - TEST(SimpleMod, PrepareApplyInPlace) { - Document doc(fromjson("{x: 0, a: 1}")); - Mod modUnset(fromjson("{$unset: {a: true}}")); + BSONObj modObj() { + return _modObj; + } - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); +private: + ModifierUnset _mod; + BSONObj _modObj; +}; - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +// +// Simple mod +// - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. - ASSERT_EQUALS(fromjson("{x: 0}"), doc); - } +TEST(SimpleMod, PrepareNoOp) { + Document doc(fromjson("{}")); + Mod modUnset(fromjson("{$unset: {a: true}}")); - TEST(SimpleMod, PrepareApplyGeneratesEmptyDocument) { - Document doc(fromjson("{a: 1}")); - Mod modUnset(fromjson("{$unset: {a: true}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareApplyNormal) { + Document doc(fromjson("{a: 1, b: 2}")); + Mod modUnset(fromjson("{$unset: {a: true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. - ASSERT_EQUALS(fromjson("{}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(SimpleMod, PrepareApplyUnsetSubtree) { - Document doc(fromjson("{a: {b: 1}, c: 2}")); - Mod modUnset(fromjson("{$unset: {a: true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{b: 2}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareApplyInPlace) { + Document doc(fromjson("{x: 0, a: 1}")); + Mod modUnset(fromjson("{$unset: {a: true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{c: 2}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(SimpleMod, LogNormal) { - BSONObj obj = fromjson("{a: 1}"); - Document doc(obj); - Mod modUnset(fromjson("{$unset: {a: true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. + ASSERT_EQUALS(fromjson("{x: 0}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, PrepareApplyGeneratesEmptyDocument) { + Document doc(fromjson("{a: 1}")); + Mod modUnset(fromjson("{$unset: {a: true}}")); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(modUnset.log(&logBuilder)); - ASSERT_EQUALS(modUnset.modObj(), logDoc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - // - // Dotted mod - // + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - TEST(DottedMod, PrepareNoOp) { - Document doc(fromjson("{c:2}")); - Mod modUnset(fromjson("{$unset: {'a.b': true}}")); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. + ASSERT_EQUALS(fromjson("{}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); +TEST(SimpleMod, PrepareApplyUnsetSubtree) { + Document doc(fromjson("{a: {b: 1}, c: 2}")); + Mod modUnset(fromjson("{$unset: {a: true}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_TRUE(execInfo.noOp); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(DottedMod, PrepareApplyNormal) { - Document doc(fromjson("{a: {b: 1}, c: 2}")); - Mod modUnset(fromjson("{$unset: {'a.b': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{c: 2}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); +TEST(SimpleMod, LogNormal) { + BSONObj obj = fromjson("{a: 1}"); + Document doc(obj); + Mod modUnset(fromjson("{$unset: {a: true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a:{}, c:2}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(DottedMod, PrepareApplyInPlace) { - Document doc(fromjson("{x: 0, a: {b: 1}}")); - Mod modUnset(fromjson("{$unset: {'a.b': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(modUnset.log(&logBuilder)); + ASSERT_EQUALS(modUnset.modObj(), logDoc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); +// +// Dotted mod +// - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. - ASSERT_EQUALS(fromjson("{x: 0, a:{}}"), doc); - } +TEST(DottedMod, PrepareNoOp) { + Document doc(fromjson("{c:2}")); + Mod modUnset(fromjson("{$unset: {'a.b': true}}")); - TEST(DottedMod, PrepareApplyUnsetNestedSubobject) { - Document doc(fromjson("{a: {b: {c: 1}}}")); - Mod modUnset(fromjson("{$unset: {'a.b': true}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); - ASSERT_FALSE(execInfo.noOp); +TEST(DottedMod, PrepareApplyNormal) { + Document doc(fromjson("{a: {b: 1}, c: 2}")); + Mod modUnset(fromjson("{$unset: {'a.b': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. - ASSERT_EQUALS(fromjson("{a: {}}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - // - // Indexed mod - // + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - TEST(IndexedMod, PrepareNoOp) { - Document doc(fromjson("{a:[]}")); - Mod modUnset(fromjson("{$unset: {'a.0': true}}")); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a:{}, c:2}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); +TEST(DottedMod, PrepareApplyInPlace) { + Document doc(fromjson("{x: 0, a: {b: 1}}")); + Mod modUnset(fromjson("{$unset: {'a.b': true}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_TRUE(execInfo.noOp); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(IndexedMod, PrepareApplyNormal) { - Document doc(fromjson("{a:[0,1,2]}")); - Mod modUnset(fromjson("{$unset: {'a.0': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. + ASSERT_EQUALS(fromjson("{x: 0, a:{}}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); +TEST(DottedMod, PrepareApplyUnsetNestedSubobject) { + Document doc(fromjson("{a: {b: {c: 1}}}")); + Mod modUnset(fromjson("{$unset: {'a.b': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a:[null,1,2]}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(IndexedMod, PrepareApplyInPlace) { - Document doc(fromjson("{b:1, a:[1]}")); - Mod modUnset(fromjson("{$unset: {'a.0': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.b"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. + ASSERT_EQUALS(fromjson("{a: {}}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); +// +// Indexed mod +// - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. - ASSERT_EQUALS(fromjson("{b:1, a:[null]}"), doc); - } +TEST(IndexedMod, PrepareNoOp) { + Document doc(fromjson("{a:[]}")); + Mod modUnset(fromjson("{$unset: {'a.0': true}}")); - TEST(IndexedMod, PrepareApplyInPlaceNuance) { - // Can't change the encoding in the middle of a bson stream. - Document doc(fromjson("{a:[1], b:1}")); - Mod modUnset(fromjson("{$unset: {'a.0': true}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_TRUE(execInfo.noOp); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); +TEST(IndexedMod, PrepareApplyNormal) { + Document doc(fromjson("{a:[0,1,2]}")); + Mod modUnset(fromjson("{$unset: {'a.0': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a:[null], b:1}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(IndexedMod, PrepareApplyInnerObject) { - Document doc(fromjson("{a:[{b:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.0.b': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a:[null,1,2]}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); - ASSERT_FALSE(execInfo.noOp); +TEST(IndexedMod, PrepareApplyInPlace) { + Document doc(fromjson("{b:1, a:[1]}")); + Mod modUnset(fromjson("{$unset: {'a.0': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a:[{}]}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(IndexedMod, PrepareApplyObject) { - Document doc(fromjson("{a:[{b:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.0': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. + ASSERT_EQUALS(fromjson("{b:1, a:[null]}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); +TEST(IndexedMod, PrepareApplyInPlaceNuance) { + // Can't change the encoding in the middle of a bson stream. + Document doc(fromjson("{a:[1], b:1}")); + Mod modUnset(fromjson("{$unset: {'a.0': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a:[null]}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(IndexedMod, LogNormal) { - Document doc(fromjson("{a:[0,1,2]}")); - Mod modUnset(fromjson("{$unset: {'a.0': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a:[null], b:1}"), doc); +} - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(modUnset.log(&logBuilder)); - ASSERT_EQUALS(modUnset.modObj(), logDoc); - } +TEST(IndexedMod, PrepareApplyInnerObject) { + Document doc(fromjson("{a:[{b:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.0.b': true}}")); - // - // Positional mod - // + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - TEST(PositionalMod, PrepareNoOp) { - Document doc(fromjson("{a:[{b:0}]}")); - Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "1", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a:[{}]}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); - ASSERT_TRUE(execInfo.noOp); - } +TEST(IndexedMod, PrepareApplyObject) { + Document doc(fromjson("{a:[{b:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.0': true}}")); - TEST(PositionalMod, PrepareMissingPositional) { - Document doc(fromjson("{a:[{b:0},{c:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - ModifierInterface::ExecInfo execInfo; - ASSERT_NOT_OK(modUnset.prepare(doc.root(), "" /* no position */, &execInfo)); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - TEST(PositionalMod, PrepareApplyNormal) { - Document doc(fromjson("{a:[{b:0},{c:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a:[null]}"), doc); +} - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); +TEST(IndexedMod, LogNormal) { + Document doc(fromjson("{a:[0,1,2]}")); + Mod modUnset(fromjson("{$unset: {'a.0': true}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "", &execInfo)); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [{}, {c:1}]}"), doc); - } + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(modUnset.log(&logBuilder)); + ASSERT_EQUALS(modUnset.modObj(), logDoc); +} - TEST(PositionalMod, PrepareApplyObject) { - Document doc(fromjson("{a:[{b:0},{c:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.$': true}}")); +// +// Positional mod +// - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); +TEST(PositionalMod, PrepareNoOp) { + Document doc(fromjson("{a:[{b:0}]}")); + Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); - ASSERT_FALSE(execInfo.noOp); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "1", &execInfo)); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{a: [null, {c:1}]}"), doc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.1.b"); + ASSERT_TRUE(execInfo.noOp); +} - TEST(PositionalMod, PrepareApplyInPlace) { - Document doc(fromjson("{b:1, a:[{b:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); +TEST(PositionalMod, PrepareMissingPositional) { + Document doc(fromjson("{a:[{b:0},{c:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); + ModifierInterface::ExecInfo execInfo; + ASSERT_NOT_OK(modUnset.prepare(doc.root(), "" /* no position */, &execInfo)); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); - ASSERT_FALSE(execInfo.noOp); +TEST(PositionalMod, PrepareApplyNormal) { + Document doc(fromjson("{a:[{b:0},{c:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. - ASSERT_EQUALS(fromjson("{b:1, a:[{}]}"), doc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); - TEST(PositionalMod, LogNormal) { - Document doc(fromjson("{b:1, a:[{b:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [{}, {c:1}]}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); - ASSERT_FALSE(execInfo.noOp); +TEST(PositionalMod, PrepareApplyObject) { + Document doc(fromjson("{a:[{b:0},{c:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.$': true}}")); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(modUnset.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$unset: {'a.0.b': true}}"), logDoc); - } + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); - TEST(LegacyData, CanUnsetInvalidField) { - Document doc(fromjson("{b:1, a:[{$b:1}]}")); - Mod modUnset(fromjson("{$unset: {'a.$.$b': true}}")); + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0"); + ASSERT_FALSE(execInfo.noOp); - ModifierInterface::ExecInfo execInfo; - ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{a: [null, {c:1}]}"), doc); +} - ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.$b"); - ASSERT_FALSE(execInfo.noOp); +TEST(PositionalMod, PrepareApplyInPlace) { + Document doc(fromjson("{b:1, a:[{b:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); - ASSERT_OK(modUnset.apply()); - ASSERT_FALSE(doc.isInPlaceModeEnabled()); - ASSERT_EQUALS(fromjson("{b:1, a:[{}]}"), doc); + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); - Document logDoc; - LogBuilder logBuilder(logDoc.root()); - ASSERT_OK(modUnset.log(&logBuilder)); - ASSERT_EQUALS(fromjson("{$unset: {'a.0.$b': true}}"), logDoc); - } + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); // TODO turn in-place on for this. + ASSERT_EQUALS(fromjson("{b:1, a:[{}]}"), doc); +} + +TEST(PositionalMod, LogNormal) { + Document doc(fromjson("{b:1, a:[{b:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.$.b': true}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.b"); + ASSERT_FALSE(execInfo.noOp); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(modUnset.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$unset: {'a.0.b': true}}"), logDoc); +} + +TEST(LegacyData, CanUnsetInvalidField) { + Document doc(fromjson("{b:1, a:[{$b:1}]}")); + Mod modUnset(fromjson("{$unset: {'a.$.$b': true}}")); + + ModifierInterface::ExecInfo execInfo; + ASSERT_OK(modUnset.prepare(doc.root(), "0", &execInfo)); + + ASSERT_EQUALS(execInfo.fieldRef[0]->dottedField(), "a.0.$b"); + ASSERT_FALSE(execInfo.noOp); + + ASSERT_OK(modUnset.apply()); + ASSERT_FALSE(doc.isInPlaceModeEnabled()); + ASSERT_EQUALS(fromjson("{b:1, a:[{}]}"), doc); + + Document logDoc; + LogBuilder logBuilder(logDoc.root()); + ASSERT_OK(modUnset.log(&logBuilder)); + ASSERT_EQUALS(fromjson("{$unset: {'a.0.$b': true}}"), logDoc); +} -} // unnamed namespace +} // unnamed namespace diff --git a/src/mongo/db/ops/parsed_delete.cpp b/src/mongo/db/ops/parsed_delete.cpp index 62243d0818d..46a2a33752d 100644 --- a/src/mongo/db/ops/parsed_delete.cpp +++ b/src/mongo/db/ops/parsed_delete.cpp @@ -46,88 +46,86 @@ namespace mongo { - ParsedDelete::ParsedDelete(OperationContext* txn, const DeleteRequest* request) : - _txn(txn), - _request(request) { } +ParsedDelete::ParsedDelete(OperationContext* txn, const DeleteRequest* request) + : _txn(txn), _request(request) {} - Status ParsedDelete::parseRequest() { - dassert(!_canonicalQuery.get()); - // It is invalid to request that the DeleteStage return the deleted document during a - // multi-remove. - invariant(!(_request->shouldReturnDeleted() && _request->isMulti())); +Status ParsedDelete::parseRequest() { + dassert(!_canonicalQuery.get()); + // It is invalid to request that the DeleteStage return the deleted document during a + // multi-remove. + invariant(!(_request->shouldReturnDeleted() && _request->isMulti())); - // It is invalid to request that a ProjectionStage be applied to the DeleteStage if the - // DeleteStage would not return the deleted document. - invariant(_request->getProj().isEmpty() || _request->shouldReturnDeleted()); + // It is invalid to request that a ProjectionStage be applied to the DeleteStage if the + // DeleteStage would not return the deleted document. + invariant(_request->getProj().isEmpty() || _request->shouldReturnDeleted()); - if (CanonicalQuery::isSimpleIdQuery(_request->getQuery())) { - return Status::OK(); - } - - return parseQueryToCQ(); + if (CanonicalQuery::isSimpleIdQuery(_request->getQuery())) { + return Status::OK(); } - Status ParsedDelete::parseQueryToCQ() { - dassert(!_canonicalQuery.get()); - - CanonicalQuery* cqRaw; - const WhereCallbackReal whereCallback(_txn, _request->getNamespaceString().db()); - - // Limit should only used for the findAndModify command when a sort is specified. If a sort - // is requested, we want to use a top-k sort for efficiency reasons, so should pass the - // limit through. Generally, a delete stage expects to be able to skip documents that were - // deleted out from under it, but a limit could inhibit that and give an EOF when the delete - // has not actually deleted a document. This behavior is fine for findAndModify, but should - // not apply to deletes in general. - long long limit = (!_request->isMulti() && !_request->getSort().isEmpty()) ? -1 : 0; - - // The projection needs to be applied after the delete operation, so we specify an empty - // BSONObj as the projection during canonicalization. - const BSONObj emptyObj; - Status status = CanonicalQuery::canonicalize(_request->getNamespaceString().ns(), - _request->getQuery(), - _request->getSort(), - emptyObj, // projection - 0, // skip - limit, - emptyObj, // hint - emptyObj, // min - emptyObj, // max - false, // snapshot - _request->isExplain(), - &cqRaw, - whereCallback); - - if (status.isOK()) { - _canonicalQuery.reset(cqRaw); - } - - return status; + return parseQueryToCQ(); +} + +Status ParsedDelete::parseQueryToCQ() { + dassert(!_canonicalQuery.get()); + + CanonicalQuery* cqRaw; + const WhereCallbackReal whereCallback(_txn, _request->getNamespaceString().db()); + + // Limit should only used for the findAndModify command when a sort is specified. If a sort + // is requested, we want to use a top-k sort for efficiency reasons, so should pass the + // limit through. Generally, a delete stage expects to be able to skip documents that were + // deleted out from under it, but a limit could inhibit that and give an EOF when the delete + // has not actually deleted a document. This behavior is fine for findAndModify, but should + // not apply to deletes in general. + long long limit = (!_request->isMulti() && !_request->getSort().isEmpty()) ? -1 : 0; + + // The projection needs to be applied after the delete operation, so we specify an empty + // BSONObj as the projection during canonicalization. + const BSONObj emptyObj; + Status status = CanonicalQuery::canonicalize(_request->getNamespaceString().ns(), + _request->getQuery(), + _request->getSort(), + emptyObj, // projection + 0, // skip + limit, + emptyObj, // hint + emptyObj, // min + emptyObj, // max + false, // snapshot + _request->isExplain(), + &cqRaw, + whereCallback); + + if (status.isOK()) { + _canonicalQuery.reset(cqRaw); } - const DeleteRequest* ParsedDelete::getRequest() const { - return _request; - } + return status; +} - bool ParsedDelete::canYield() const { - return !_request->isGod() && - PlanExecutor::YIELD_AUTO == _request->getYieldPolicy() && - !isIsolated(); - } +const DeleteRequest* ParsedDelete::getRequest() const { + return _request; +} - bool ParsedDelete::isIsolated() const { - return _canonicalQuery.get() - ? QueryPlannerCommon::hasNode(_canonicalQuery->root(), MatchExpression::ATOMIC) - : LiteParsedQuery::isQueryIsolated(_request->getQuery()); - } +bool ParsedDelete::canYield() const { + return !_request->isGod() && PlanExecutor::YIELD_AUTO == _request->getYieldPolicy() && + !isIsolated(); +} - bool ParsedDelete::hasParsedQuery() const { - return _canonicalQuery.get() != NULL; - } +bool ParsedDelete::isIsolated() const { + return _canonicalQuery.get() + ? QueryPlannerCommon::hasNode(_canonicalQuery->root(), MatchExpression::ATOMIC) + : LiteParsedQuery::isQueryIsolated(_request->getQuery()); +} - CanonicalQuery* ParsedDelete::releaseParsedQuery() { - invariant(_canonicalQuery.get() != NULL); - return _canonicalQuery.release(); - } +bool ParsedDelete::hasParsedQuery() const { + return _canonicalQuery.get() != NULL; +} + +CanonicalQuery* ParsedDelete::releaseParsedQuery() { + invariant(_canonicalQuery.get() != NULL); + return _canonicalQuery.release(); +} } // namespace mongo diff --git a/src/mongo/db/ops/parsed_delete.h b/src/mongo/db/ops/parsed_delete.h index c65f0da69e9..0745cbb85e8 100644 --- a/src/mongo/db/ops/parsed_delete.h +++ b/src/mongo/db/ops/parsed_delete.h @@ -34,85 +34,85 @@ namespace mongo { - class CanonicalQuery; - class Database; - class DeleteRequest; - class OperationContext; +class CanonicalQuery; +class Database; +class DeleteRequest; +class OperationContext; +/** + * This class takes a pointer to a DeleteRequest, and converts that request into a parsed form + * via the parseRequest() method. A ParsedDelete can then be used to retrieve a PlanExecutor + * capable of executing the delete. + * + * It is invalid to request that the DeleteStage return the deleted document during a + * multi-remove. It is also invalid to request that a ProjectionStage be applied to the + * DeleteStage if the DeleteStage would not return the deleted document. + * + * A delete request is parsed to a CanonicalQuery, so this class is a thin, delete-specific + * wrapper around canonicalization. + * + * No locks need to be held during parsing. + */ +class ParsedDelete { + MONGO_DISALLOW_COPYING(ParsedDelete); + +public: /** - * This class takes a pointer to a DeleteRequest, and converts that request into a parsed form - * via the parseRequest() method. A ParsedDelete can then be used to retrieve a PlanExecutor - * capable of executing the delete. - * - * It is invalid to request that the DeleteStage return the deleted document during a - * multi-remove. It is also invalid to request that a ProjectionStage be applied to the - * DeleteStage if the DeleteStage would not return the deleted document. - * - * A delete request is parsed to a CanonicalQuery, so this class is a thin, delete-specific - * wrapper around canonicalization. + * Constructs a parsed delete. * - * No locks need to be held during parsing. + * The object pointed to by "request" must stay in scope for the life of the constructed + * ParsedDelete. + */ + ParsedDelete(OperationContext* txn, const DeleteRequest* request); + + /** + * Parses the delete request to a canonical query. On success, the parsed delete can be + * used to create a PlanExecutor capable of executing this delete. + */ + Status parseRequest(); + + /** + * As an optimization, we do not create a canonical query if the predicate is a simple + * _id equality. This method can be used to force full parsing to a canonical query, + * as a fallback if the idhack path is not available (e.g. no _id index). + */ + Status parseQueryToCQ(); + + /** + * Get the raw request. + */ + const DeleteRequest* getRequest() const; + + /** + * Is this delete allowed to yield? + */ + bool canYield() const; + + /** + * Is this update supposed to be isolated? + */ + bool isIsolated() const; + + /** + * As an optimization, we don't create a canonical query for updates with simple _id + * queries. Use this method to determine whether or not we actually parsed the query. + */ + bool hasParsedQuery() const; + + /** + * Releases ownership of the canonical query to the caller. */ - class ParsedDelete { - MONGO_DISALLOW_COPYING(ParsedDelete); - public: - /** - * Constructs a parsed delete. - * - * The object pointed to by "request" must stay in scope for the life of the constructed - * ParsedDelete. - */ - ParsedDelete(OperationContext* txn, const DeleteRequest* request); - - /** - * Parses the delete request to a canonical query. On success, the parsed delete can be - * used to create a PlanExecutor capable of executing this delete. - */ - Status parseRequest(); - - /** - * As an optimization, we do not create a canonical query if the predicate is a simple - * _id equality. This method can be used to force full parsing to a canonical query, - * as a fallback if the idhack path is not available (e.g. no _id index). - */ - Status parseQueryToCQ(); - - /** - * Get the raw request. - */ - const DeleteRequest* getRequest() const; - - /** - * Is this delete allowed to yield? - */ - bool canYield() const; - - /** - * Is this update supposed to be isolated? - */ - bool isIsolated() const; - - /** - * As an optimization, we don't create a canonical query for updates with simple _id - * queries. Use this method to determine whether or not we actually parsed the query. - */ - bool hasParsedQuery() const; - - /** - * Releases ownership of the canonical query to the caller. - */ - CanonicalQuery* releaseParsedQuery(); - - private: - // Transactional context. Not owned by us. - OperationContext* _txn; - - // Unowned pointer to the request object that this executor will process. - const DeleteRequest* const _request; - - // Parsed query object, or NULL if the query proves to be an id hack query. - std::unique_ptr<CanonicalQuery> _canonicalQuery; - - }; + CanonicalQuery* releaseParsedQuery(); + +private: + // Transactional context. Not owned by us. + OperationContext* _txn; + + // Unowned pointer to the request object that this executor will process. + const DeleteRequest* const _request; + + // Parsed query object, or NULL if the query proves to be an id hack query. + std::unique_ptr<CanonicalQuery> _canonicalQuery; +}; } // namespace mongo diff --git a/src/mongo/db/ops/parsed_update.cpp b/src/mongo/db/ops/parsed_update.cpp index d4651371e3f..80138e0385d 100644 --- a/src/mongo/db/ops/parsed_update.cpp +++ b/src/mongo/db/ops/parsed_update.cpp @@ -35,126 +35,120 @@ namespace mongo { - ParsedUpdate::ParsedUpdate(OperationContext* txn, const UpdateRequest* request) : - _txn(txn), - _request(request), - _driver(UpdateDriver::Options()), - _canonicalQuery() { } - - Status ParsedUpdate::parseRequest() { - // It is invalid to request that the UpdateStage return the prior or newly-updated version - // of a document during a multi-update. - invariant(!(_request->shouldReturnAnyDocs() && _request->isMulti())); - - // It is invalid to request that a ProjectionStage be applied to the UpdateStage if the - // UpdateStage would not return any document. - invariant(_request->getProj().isEmpty() || _request->shouldReturnAnyDocs()); - - // We parse the update portion before the query portion because the dispostion of the update - // may determine whether or not we need to produce a CanonicalQuery at all. For example, if - // the update involves the positional-dollar operator, we must have a CanonicalQuery even if - // it isn't required for query execution. - Status status = parseUpdate(); - if (!status.isOK()) - return status; - status = parseQuery(); - if (!status.isOK()) - return status; - return Status::OK(); - } - - Status ParsedUpdate::parseQuery() { - dassert(!_canonicalQuery.get()); +ParsedUpdate::ParsedUpdate(OperationContext* txn, const UpdateRequest* request) + : _txn(txn), _request(request), _driver(UpdateDriver::Options()), _canonicalQuery() {} + +Status ParsedUpdate::parseRequest() { + // It is invalid to request that the UpdateStage return the prior or newly-updated version + // of a document during a multi-update. + invariant(!(_request->shouldReturnAnyDocs() && _request->isMulti())); + + // It is invalid to request that a ProjectionStage be applied to the UpdateStage if the + // UpdateStage would not return any document. + invariant(_request->getProj().isEmpty() || _request->shouldReturnAnyDocs()); + + // We parse the update portion before the query portion because the dispostion of the update + // may determine whether or not we need to produce a CanonicalQuery at all. For example, if + // the update involves the positional-dollar operator, we must have a CanonicalQuery even if + // it isn't required for query execution. + Status status = parseUpdate(); + if (!status.isOK()) + return status; + status = parseQuery(); + if (!status.isOK()) + return status; + return Status::OK(); +} - if (!_driver.needMatchDetails() && CanonicalQuery::isSimpleIdQuery(_request->getQuery())) { - return Status::OK(); - } +Status ParsedUpdate::parseQuery() { + dassert(!_canonicalQuery.get()); - return parseQueryToCQ(); + if (!_driver.needMatchDetails() && CanonicalQuery::isSimpleIdQuery(_request->getQuery())) { + return Status::OK(); } - Status ParsedUpdate::parseQueryToCQ() { - dassert(!_canonicalQuery.get()); - - CanonicalQuery* cqRaw; - const WhereCallbackReal whereCallback(_txn, _request->getNamespaceString().db()); - - // Limit should only used for the findAndModify command when a sort is specified. If a sort - // is requested, we want to use a top-k sort for efficiency reasons, so should pass the - // limit through. Generally, a update stage expects to be able to skip documents that were - // deleted/modified under it, but a limit could inhibit that and give an EOF when the update - // has not actually updated a document. This behavior is fine for findAndModify, but should - // not apply to update in general. - long long limit = (!_request->isMulti() && !_request->getSort().isEmpty()) ? -1 : 0; - - // The projection needs to be applied after the update operation, so we specify an empty - // BSONObj as the projection during canonicalization. - const BSONObj emptyObj; - Status status = CanonicalQuery::canonicalize(_request->getNamespaceString().ns(), - _request->getQuery(), - _request->getSort(), - emptyObj, // projection - 0, // skip - limit, - emptyObj, // hint - emptyObj, // min - emptyObj, // max - false, // snapshot - _request->isExplain(), - &cqRaw, - whereCallback); - if (status.isOK()) { - _canonicalQuery.reset(cqRaw); - } - - return status; + return parseQueryToCQ(); +} + +Status ParsedUpdate::parseQueryToCQ() { + dassert(!_canonicalQuery.get()); + + CanonicalQuery* cqRaw; + const WhereCallbackReal whereCallback(_txn, _request->getNamespaceString().db()); + + // Limit should only used for the findAndModify command when a sort is specified. If a sort + // is requested, we want to use a top-k sort for efficiency reasons, so should pass the + // limit through. Generally, a update stage expects to be able to skip documents that were + // deleted/modified under it, but a limit could inhibit that and give an EOF when the update + // has not actually updated a document. This behavior is fine for findAndModify, but should + // not apply to update in general. + long long limit = (!_request->isMulti() && !_request->getSort().isEmpty()) ? -1 : 0; + + // The projection needs to be applied after the update operation, so we specify an empty + // BSONObj as the projection during canonicalization. + const BSONObj emptyObj; + Status status = CanonicalQuery::canonicalize(_request->getNamespaceString().ns(), + _request->getQuery(), + _request->getSort(), + emptyObj, // projection + 0, // skip + limit, + emptyObj, // hint + emptyObj, // min + emptyObj, // max + false, // snapshot + _request->isExplain(), + &cqRaw, + whereCallback); + if (status.isOK()) { + _canonicalQuery.reset(cqRaw); } - Status ParsedUpdate::parseUpdate() { - const NamespaceString& ns(_request->getNamespaceString()); + return status; +} - // Should the modifiers validate their embedded docs via okForStorage - // Only user updates should be checked. Any system or replication stuff should pass through. - // Config db docs shouldn't get checked for valid field names since the shard key can have - // a dot (".") in it. - const bool shouldValidate = !(!_txn->writesAreReplicated() || - ns.isConfigDB() || - _request->isFromMigration()); +Status ParsedUpdate::parseUpdate() { + const NamespaceString& ns(_request->getNamespaceString()); - _driver.setLogOp(true); - _driver.setModOptions(ModifierInterface::Options(!_txn->writesAreReplicated(), - shouldValidate)); + // Should the modifiers validate their embedded docs via okForStorage + // Only user updates should be checked. Any system or replication stuff should pass through. + // Config db docs shouldn't get checked for valid field names since the shard key can have + // a dot (".") in it. + const bool shouldValidate = + !(!_txn->writesAreReplicated() || ns.isConfigDB() || _request->isFromMigration()); - return _driver.parse(_request->getUpdates(), _request->isMulti()); - } + _driver.setLogOp(true); + _driver.setModOptions(ModifierInterface::Options(!_txn->writesAreReplicated(), shouldValidate)); - bool ParsedUpdate::canYield() const { - return !_request->isGod() && - PlanExecutor::YIELD_AUTO == _request->getYieldPolicy() && - !isIsolated(); - } + return _driver.parse(_request->getUpdates(), _request->isMulti()); +} - bool ParsedUpdate::isIsolated() const { - return _canonicalQuery.get() - ? QueryPlannerCommon::hasNode(_canonicalQuery->root(), MatchExpression::ATOMIC) - : LiteParsedQuery::isQueryIsolated(_request->getQuery()); - } +bool ParsedUpdate::canYield() const { + return !_request->isGod() && PlanExecutor::YIELD_AUTO == _request->getYieldPolicy() && + !isIsolated(); +} - bool ParsedUpdate::hasParsedQuery() const { - return _canonicalQuery.get() != NULL; - } +bool ParsedUpdate::isIsolated() const { + return _canonicalQuery.get() + ? QueryPlannerCommon::hasNode(_canonicalQuery->root(), MatchExpression::ATOMIC) + : LiteParsedQuery::isQueryIsolated(_request->getQuery()); +} - CanonicalQuery* ParsedUpdate::releaseParsedQuery() { - invariant(_canonicalQuery.get() != NULL); - return _canonicalQuery.release(); - } +bool ParsedUpdate::hasParsedQuery() const { + return _canonicalQuery.get() != NULL; +} - const UpdateRequest* ParsedUpdate::getRequest() const { - return _request; - } +CanonicalQuery* ParsedUpdate::releaseParsedQuery() { + invariant(_canonicalQuery.get() != NULL); + return _canonicalQuery.release(); +} - UpdateDriver* ParsedUpdate::getDriver() { - return &_driver; - } +const UpdateRequest* ParsedUpdate::getRequest() const { + return _request; +} + +UpdateDriver* ParsedUpdate::getDriver() { + return &_driver; +} } // namespace mongo diff --git a/src/mongo/db/ops/parsed_update.h b/src/mongo/db/ops/parsed_update.h index d109904d1bd..8ce08aaabc3 100644 --- a/src/mongo/db/ops/parsed_update.h +++ b/src/mongo/db/ops/parsed_update.h @@ -34,102 +34,103 @@ namespace mongo { - class CanonicalQuery; - class OperationContext; - class UpdateRequest; +class CanonicalQuery; +class OperationContext; +class UpdateRequest; +/** + * This class takes a pointer to an UpdateRequest, and converts that request into a parsed form + * via the parseRequest() method. A ParsedUpdate can then be used to retrieve a PlanExecutor + * capable of executing the update. + * + * It is invalid to request that the UpdateStage return the prior or newly-updated version of a + * document during a multi-update. It is also invalid to request that a ProjectionStage be + * applied to the UpdateStage if the UpdateStage would not return any document. + * + * No locks need to be held during parsing. + * + * The query part of the update is parsed to a CanonicalQuery, and the update part is parsed + * using the UpdateDriver. + */ +class ParsedUpdate { + MONGO_DISALLOW_COPYING(ParsedUpdate); + +public: /** - * This class takes a pointer to an UpdateRequest, and converts that request into a parsed form - * via the parseRequest() method. A ParsedUpdate can then be used to retrieve a PlanExecutor - * capable of executing the update. + * Constructs a parsed update. * - * It is invalid to request that the UpdateStage return the prior or newly-updated version of a - * document during a multi-update. It is also invalid to request that a ProjectionStage be - * applied to the UpdateStage if the UpdateStage would not return any document. - * - * No locks need to be held during parsing. - * - * The query part of the update is parsed to a CanonicalQuery, and the update part is parsed - * using the UpdateDriver. + * The object pointed to by "request" must stay in scope for the life of the constructed + * ParsedUpdate. + */ + ParsedUpdate(OperationContext* txn, const UpdateRequest* request); + + /** + * Parses the update request to a canonical query and an update driver. On success, the + * parsed update can be used to create a PlanExecutor for this update. + */ + Status parseRequest(); + + /** + * As an optimization, we do not create a canonical query if the predicate is a simple + * _id equality. This method can be used to force full parsing to a canonical query, + * as a fallback if the idhack path is not available (e.g. no _id index). + */ + Status parseQueryToCQ(); + + /** + * Get the raw request. + */ + const UpdateRequest* getRequest() const; + + /** + * Get a pointer to the update driver, the abstraction which both parses the update and + * is capable of applying mods / computing damages. + */ + UpdateDriver* getDriver(); + + /** + * Is this update allowed to yield? + */ + bool canYield() const; + + /** + * Is this update supposed to be isolated? + */ + bool isIsolated() const; + + /** + * As an optimization, we don't create a canonical query for updates with simple _id + * queries. Use this method to determine whether or not we actually parsed the query. + */ + bool hasParsedQuery() const; + + /** + * Releases ownership of the canonical query to the caller. */ - class ParsedUpdate { - MONGO_DISALLOW_COPYING(ParsedUpdate); - public: - /** - * Constructs a parsed update. - * - * The object pointed to by "request" must stay in scope for the life of the constructed - * ParsedUpdate. - */ - ParsedUpdate(OperationContext* txn, const UpdateRequest* request); - - /** - * Parses the update request to a canonical query and an update driver. On success, the - * parsed update can be used to create a PlanExecutor for this update. - */ - Status parseRequest(); - - /** - * As an optimization, we do not create a canonical query if the predicate is a simple - * _id equality. This method can be used to force full parsing to a canonical query, - * as a fallback if the idhack path is not available (e.g. no _id index). - */ - Status parseQueryToCQ(); - - /** - * Get the raw request. - */ - const UpdateRequest* getRequest() const; - - /** - * Get a pointer to the update driver, the abstraction which both parses the update and - * is capable of applying mods / computing damages. - */ - UpdateDriver* getDriver(); - - /** - * Is this update allowed to yield? - */ - bool canYield() const; - - /** - * Is this update supposed to be isolated? - */ - bool isIsolated() const; - - /** - * As an optimization, we don't create a canonical query for updates with simple _id - * queries. Use this method to determine whether or not we actually parsed the query. - */ - bool hasParsedQuery() const; - - /** - * Releases ownership of the canonical query to the caller. - */ - CanonicalQuery* releaseParsedQuery(); - - private: - /** - * Parses the query portion of the update request. - */ - Status parseQuery(); - - /** - * Parses the update-descriptor portion of the update request. - */ - Status parseUpdate(); - - // Unowned pointer to the transactional context. - OperationContext* _txn; - - // Unowned pointer to the request object to process. - const UpdateRequest* const _request; - - // Driver for processing updates on matched documents. - UpdateDriver _driver; - - // Parsed query object, or NULL if the query proves to be an id hack query. - std::unique_ptr<CanonicalQuery> _canonicalQuery; - }; + CanonicalQuery* releaseParsedQuery(); + +private: + /** + * Parses the query portion of the update request. + */ + Status parseQuery(); + + /** + * Parses the update-descriptor portion of the update request. + */ + Status parseUpdate(); + + // Unowned pointer to the transactional context. + OperationContext* _txn; + + // Unowned pointer to the request object to process. + const UpdateRequest* const _request; + + // Driver for processing updates on matched documents. + UpdateDriver _driver; + + // Parsed query object, or NULL if the query proves to be an id hack query. + std::unique_ptr<CanonicalQuery> _canonicalQuery; +}; } // namespace mongo diff --git a/src/mongo/db/ops/path_support.cpp b/src/mongo/db/ops/path_support.cpp index 4ac4b03fb36..894b0117393 100644 --- a/src/mongo/db/ops/path_support.cpp +++ b/src/mongo/db/ops/path_support.cpp @@ -38,78 +38,72 @@ namespace mongo { namespace pathsupport { - using std::string; - using mongoutils::str::stream; - - namespace { - - bool isNumeric(StringData str, size_t* num) { - size_t res = 0; - for (size_t i = 0; i < str.size(); ++i) { - if (str[i] < '0' || str[i] > '9') { - return false; - } - else { - res = res * 10 + (str[i] - '0'); - } - } - *num = res; - return true; +using std::string; +using mongoutils::str::stream; + +namespace { + +bool isNumeric(StringData str, size_t* num) { + size_t res = 0; + for (size_t i = 0; i < str.size(); ++i) { + if (str[i] < '0' || str[i] > '9') { + return false; + } else { + res = res * 10 + (str[i] - '0'); } + } + *num = res; + return true; +} - Status maybePadTo(mutablebson::Element* elemArray, - size_t sizeRequired) { - dassert(elemArray->getType() == Array); +Status maybePadTo(mutablebson::Element* elemArray, size_t sizeRequired) { + dassert(elemArray->getType() == Array); - if (sizeRequired > kMaxPaddingAllowed) { - return Status(ErrorCodes::CannotBackfillArray, - mongoutils::str::stream() << "can't backfill array to larger than " - << kMaxPaddingAllowed << " elements"); - } + if (sizeRequired > kMaxPaddingAllowed) { + return Status(ErrorCodes::CannotBackfillArray, + mongoutils::str::stream() << "can't backfill array to larger than " + << kMaxPaddingAllowed << " elements"); + } - size_t currSize = mutablebson::countChildren(*elemArray); - if (sizeRequired > currSize) { - size_t toPad = sizeRequired - currSize; - for (size_t i = 0; i < toPad; i++) { - Status status = elemArray->appendNull(""); - if (!status.isOK()) { - return status; - } - } + size_t currSize = mutablebson::countChildren(*elemArray); + if (sizeRequired > currSize) { + size_t toPad = sizeRequired - currSize; + for (size_t i = 0; i < toPad; i++) { + Status status = elemArray->appendNull(""); + if (!status.isOK()) { + return status; } - return Status::OK(); } + } + return Status::OK(); +} + +} // unnamed namespace + +Status findLongestPrefix(const FieldRef& prefix, + mutablebson::Element root, + size_t* idxFound, + mutablebson::Element* elemFound) { + // If root is empty or the prefix is so, there's no point in looking for a prefix. + const size_t prefixSize = prefix.numParts(); + if (!root.hasChildren() || prefixSize == 0) { + return Status(ErrorCodes::NonExistentPath, "either the document or the path are empty"); + } - } // unnamed namespace - - Status findLongestPrefix(const FieldRef& prefix, - mutablebson::Element root, - size_t* idxFound, - mutablebson::Element* elemFound) { - - // If root is empty or the prefix is so, there's no point in looking for a prefix. - const size_t prefixSize = prefix.numParts(); - if (!root.hasChildren() || prefixSize == 0) { - return Status(ErrorCodes::NonExistentPath, - "either the document or the path are empty"); - } - - // Loop through prefix's parts. At each iteration, check that the part ('curr') exists - // in 'root' and that the type of the previous part ('prev') allows for children. - mutablebson::Element curr = root; - mutablebson::Element prev = root; - size_t i = 0; - size_t numericPart = 0; - bool viable = true; - for (;i < prefixSize; i++) { - - // If prefix wants to reach 'curr' by applying a non-numeric index to an array - // 'prev', or if 'curr' wants to traverse a leaf 'prev', then we'd be in a - // non-viable path (see definition on the header file). - StringData prefixPart = prefix.getPart(i); - prev = curr; - switch (curr.getType()) { - + // Loop through prefix's parts. At each iteration, check that the part ('curr') exists + // in 'root' and that the type of the previous part ('prev') allows for children. + mutablebson::Element curr = root; + mutablebson::Element prev = root; + size_t i = 0; + size_t numericPart = 0; + bool viable = true; + for (; i < prefixSize; i++) { + // If prefix wants to reach 'curr' by applying a non-numeric index to an array + // 'prev', or if 'curr' wants to traverse a leaf 'prev', then we'd be in a + // non-viable path (see definition on the header file). + StringData prefixPart = prefix.getPart(i); + prev = curr; + switch (curr.getType()) { case Object: curr = prev[prefixPart]; break; @@ -124,338 +118,307 @@ namespace pathsupport { default: viable = false; - } - - // If we couldn't find the next field part of the prefix in the document or if the - // field part we're in constitutes a non-viable path, we can stop looking. - if (!curr.ok() || !viable) { - break; - } } - // We broke out of the loop because one of four things happened. (a) 'prefix' and - // 'root' have nothing in common, (b) 'prefix' is not viable in 'root', (c) not all the - // parts in 'prefix' exist in 'root', or (d) all parts do. In each case, we need to - // figure out what index and Element pointer to return. - if (i == 0) { - return Status(ErrorCodes::NonExistentPath, - "cannot find path in the document"); - } - else if (!viable) { - *idxFound = i - 1; - *elemFound = prev; - return Status(ErrorCodes::PathNotViable, - mongoutils::str::stream() << "cannot use the part (" << - prefix.getPart(i-1) << " of " << prefix.dottedField() << - ") to traverse the element ({" << - curr.toString() << "})"); - } - else if (curr.ok()) { - *idxFound = i - 1; - *elemFound = curr; - return Status::OK(); - } - else { - *idxFound = i - 1; - *elemFound = prev; - return Status::OK(); + // If we couldn't find the next field part of the prefix in the document or if the + // field part we're in constitutes a non-viable path, we can stop looking. + if (!curr.ok() || !viable) { + break; } } - Status createPathAt(const FieldRef& prefix, - size_t idxFound, - mutablebson::Element elemFound, - mutablebson::Element newElem) { - Status status = Status::OK(); - - // Sanity check that 'idxField' is an actual part. - const size_t size = prefix.numParts(); - if (idxFound >= size) { - return Status(ErrorCodes::BadValue, "index larger than path size"); - } - - mutablebson::Document& doc = elemFound.getDocument(); - - // If we are creating children under an array and a numeric index is next, then perhaps - // we need padding. - size_t i = idxFound; - bool inArray = false; - if (elemFound.getType() == mongo::Array) { - size_t newIdx = 0; - if (!isNumeric(prefix.getPart(idxFound), &newIdx)) { - return Status(ErrorCodes::InvalidPath, "Array require numeric fields"); - } + // We broke out of the loop because one of four things happened. (a) 'prefix' and + // 'root' have nothing in common, (b) 'prefix' is not viable in 'root', (c) not all the + // parts in 'prefix' exist in 'root', or (d) all parts do. In each case, we need to + // figure out what index and Element pointer to return. + if (i == 0) { + return Status(ErrorCodes::NonExistentPath, "cannot find path in the document"); + } else if (!viable) { + *idxFound = i - 1; + *elemFound = prev; + return Status(ErrorCodes::PathNotViable, + mongoutils::str::stream() << "cannot use the part (" << prefix.getPart(i - 1) + << " of " << prefix.dottedField() + << ") to traverse the element ({" << curr.toString() + << "})"); + } else if (curr.ok()) { + *idxFound = i - 1; + *elemFound = curr; + return Status::OK(); + } else { + *idxFound = i - 1; + *elemFound = prev; + return Status::OK(); + } +} + +Status createPathAt(const FieldRef& prefix, + size_t idxFound, + mutablebson::Element elemFound, + mutablebson::Element newElem) { + Status status = Status::OK(); + + // Sanity check that 'idxField' is an actual part. + const size_t size = prefix.numParts(); + if (idxFound >= size) { + return Status(ErrorCodes::BadValue, "index larger than path size"); + } - status = maybePadTo(&elemFound, newIdx); - if (!status.isOK()) { - return status; - } + mutablebson::Document& doc = elemFound.getDocument(); - // If there is a next field, that would be an array element. We'd like to mark that - // field because we create array elements differently than we do regular objects. - if (++i < size) { - inArray = true; - } + // If we are creating children under an array and a numeric index is next, then perhaps + // we need padding. + size_t i = idxFound; + bool inArray = false; + if (elemFound.getType() == mongo::Array) { + size_t newIdx = 0; + if (!isNumeric(prefix.getPart(idxFound), &newIdx)) { + return Status(ErrorCodes::InvalidPath, "Array require numeric fields"); } - // Create all the remaining parts but the last one. - for (; i < size - 1 ; i++) { - mutablebson::Element elem = doc.makeElementObject(prefix.getPart(i)); - if (!elem.ok()) { - return Status(ErrorCodes::InternalError, "cannot create path"); - } + status = maybePadTo(&elemFound, newIdx); + if (!status.isOK()) { + return status; + } - // If this field is an array element, we wrap it in an object (because array - // elements are wraped in { "N": <element> } objects. - if (inArray) { - // TODO pass empty StringData to makeElementObject, when that's supported. - mutablebson::Element arrayObj = doc.makeElementObject("" /* it's an array */); - if (!arrayObj.ok()) { - return Status(ErrorCodes::InternalError, "cannot create item on array"); - } - status = arrayObj.pushBack(elem); - if (!status.isOK()) { - return status; - } - status = elemFound.pushBack(arrayObj); - if (!status.isOK()) { - return status; - } - inArray = false; - } - else { - status = elemFound.pushBack(elem); - if (!status.isOK()) { - return status; - } - } + // If there is a next field, that would be an array element. We'd like to mark that + // field because we create array elements differently than we do regular objects. + if (++i < size) { + inArray = true; + } + } - elemFound = elem; + // Create all the remaining parts but the last one. + for (; i < size - 1; i++) { + mutablebson::Element elem = doc.makeElementObject(prefix.getPart(i)); + if (!elem.ok()) { + return Status(ErrorCodes::InternalError, "cannot create path"); } - // Attach the last element. Here again, if we're in a field that is an array element, - // we wrap it in an object first. + // If this field is an array element, we wrap it in an object (because array + // elements are wraped in { "N": <element> } objects. if (inArray) { // TODO pass empty StringData to makeElementObject, when that's supported. mutablebson::Element arrayObj = doc.makeElementObject("" /* it's an array */); if (!arrayObj.ok()) { return Status(ErrorCodes::InternalError, "cannot create item on array"); } - - status = arrayObj.pushBack(newElem); + status = arrayObj.pushBack(elem); if (!status.isOK()) { return status; } - status = elemFound.pushBack(arrayObj); if (!status.isOK()) { return status; } - - } - else { - status = elemFound.pushBack(newElem); + inArray = false; + } else { + status = elemFound.pushBack(elem); if (!status.isOK()) { return status; } } - return Status::OK(); + elemFound = elem; } - Status setElementAtPath(const FieldRef& path, - const BSONElement& value, - mutablebson::Document* doc) { - - size_t deepestElemPathPart; - mutablebson::Element deepestElem(doc->end()); - - // Get the existing parents of this path - Status status = findLongestPrefix(path, - doc->root(), - &deepestElemPathPart, - &deepestElem); - - // TODO: All this is pretty awkward, why not return the position immediately after the - // consumed path or use a signed sentinel? Why is it a special case when we've consumed the - // whole path? + // Attach the last element. Here again, if we're in a field that is an array element, + // we wrap it in an object first. + if (inArray) { + // TODO pass empty StringData to makeElementObject, when that's supported. + mutablebson::Element arrayObj = doc.makeElementObject("" /* it's an array */); + if (!arrayObj.ok()) { + return Status(ErrorCodes::InternalError, "cannot create item on array"); + } - if (!status.isOK() && status.code() != ErrorCodes::NonExistentPath) + status = arrayObj.pushBack(newElem); + if (!status.isOK()) { return status; - - // Inc the path by one *unless* we matched nothing - if (status.code() != ErrorCodes::NonExistentPath) { - ++deepestElemPathPart; - } - else { - deepestElemPathPart = 0; - deepestElem = doc->root(); } - if (deepestElemPathPart == path.numParts()) { - // The full path exists already in the document, so just set a value - return deepestElem.setValueBSONElement(value); + status = elemFound.pushBack(arrayObj); + if (!status.isOK()) { + return status; } - else { - // Construct the rest of the path we need with empty documents and set the value - StringData leafFieldName = path.getPart(path.numParts() - 1); - mutablebson::Element leafElem = doc->makeElementWithNewFieldName(leafFieldName, - value); - dassert(leafElem.ok()); - return createPathAt(path, deepestElemPathPart, deepestElem, leafElem); + + } else { + status = elemFound.pushBack(newElem); + if (!status.isOK()) { + return status; } } - const BSONElement& findParentEqualityElement(const EqualityMatches& equalities, - const FieldRef& path, - int* parentPathParts) { + return Status::OK(); +} - // We may have an equality match to an object at a higher point in the pattern path, check - // all path prefixes for equality matches - // ex: path: 'a.b', query : { 'a' : { b : <value> } } - // ex: path: 'a.b.c', query : { 'a.b' : { c : <value> } } - for (int i = static_cast<int>(path.numParts()); i >= 0; --i) { +Status setElementAtPath(const FieldRef& path, + const BSONElement& value, + mutablebson::Document* doc) { + size_t deepestElemPathPart; + mutablebson::Element deepestElem(doc->end()); - // "" element is *not* a parent of anyone but itself - if (i == 0 && path.numParts() != 0) - continue; + // Get the existing parents of this path + Status status = findLongestPrefix(path, doc->root(), &deepestElemPathPart, &deepestElem); - StringData subPathStr = path.dottedSubstring(0, i); - EqualityMatches::const_iterator seenIt = equalities.find(subPathStr); - if (seenIt == equalities.end()) - continue; + // TODO: All this is pretty awkward, why not return the position immediately after the + // consumed path or use a signed sentinel? Why is it a special case when we've consumed the + // whole path? - *parentPathParts = i; - return seenIt->second->getData(); - } + if (!status.isOK() && status.code() != ErrorCodes::NonExistentPath) + return status; - *parentPathParts = -1; - static const BSONElement eooElement; - return eooElement; + // Inc the path by one *unless* we matched nothing + if (status.code() != ErrorCodes::NonExistentPath) { + ++deepestElemPathPart; + } else { + deepestElemPathPart = 0; + deepestElem = doc->root(); } - /** - * Helper function to check if the current equality match paths conflict with a new path. - */ - static Status checkEqualityConflicts(const EqualityMatches& equalities, const FieldRef& path) { - - int parentPathPart = -1; - const BSONElement& parentEl = findParentEqualityElement(equalities, - path, - &parentPathPart); - - if (parentEl.eoo()) - return Status::OK(); + if (deepestElemPathPart == path.numParts()) { + // The full path exists already in the document, so just set a value + return deepestElem.setValueBSONElement(value); + } else { + // Construct the rest of the path we need with empty documents and set the value + StringData leafFieldName = path.getPart(path.numParts() - 1); + mutablebson::Element leafElem = doc->makeElementWithNewFieldName(leafFieldName, value); + dassert(leafElem.ok()); + return createPathAt(path, deepestElemPathPart, deepestElem, leafElem); + } +} + +const BSONElement& findParentEqualityElement(const EqualityMatches& equalities, + const FieldRef& path, + int* parentPathParts) { + // We may have an equality match to an object at a higher point in the pattern path, check + // all path prefixes for equality matches + // ex: path: 'a.b', query : { 'a' : { b : <value> } } + // ex: path: 'a.b.c', query : { 'a.b' : { c : <value> } } + for (int i = static_cast<int>(path.numParts()); i >= 0; --i) { + // "" element is *not* a parent of anyone but itself + if (i == 0 && path.numParts() != 0) + continue; + + StringData subPathStr = path.dottedSubstring(0, i); + EqualityMatches::const_iterator seenIt = equalities.find(subPathStr); + if (seenIt == equalities.end()) + continue; + + *parentPathParts = i; + return seenIt->second->getData(); + } - string errMsg = "cannot infer query fields to set, "; + *parentPathParts = -1; + static const BSONElement eooElement; + return eooElement; +} - StringData pathStr = path.dottedField(); - StringData prefixStr = path.dottedSubstring(0, parentPathPart); - StringData suffixStr = path.dottedSubstring(parentPathPart, path.numParts()); +/** + * Helper function to check if the current equality match paths conflict with a new path. + */ +static Status checkEqualityConflicts(const EqualityMatches& equalities, const FieldRef& path) { + int parentPathPart = -1; + const BSONElement& parentEl = findParentEqualityElement(equalities, path, &parentPathPart); - if (suffixStr.size() != 0) - errMsg += stream() << "both paths '" << pathStr << "' and '" << prefixStr - << "' are matched"; - else - errMsg += stream() << "path '" << pathStr << "' is matched twice"; + if (parentEl.eoo()) + return Status::OK(); - return Status(ErrorCodes::NotSingleValueField, errMsg); - } + string errMsg = "cannot infer query fields to set, "; - /** - * Helper function to check if path conflicts are all prefixes. - */ - static Status checkPathIsPrefixOf(const FieldRef& path, const FieldRefSet& conflictPaths) { + StringData pathStr = path.dottedField(); + StringData prefixStr = path.dottedSubstring(0, parentPathPart); + StringData suffixStr = path.dottedSubstring(parentPathPart, path.numParts()); - for (FieldRefSet::const_iterator it = conflictPaths.begin(); it != conflictPaths.end(); - ++it) { + if (suffixStr.size() != 0) + errMsg += stream() << "both paths '" << pathStr << "' and '" << prefixStr + << "' are matched"; + else + errMsg += stream() << "path '" << pathStr << "' is matched twice"; - const FieldRef* conflictingPath = *it; - // Conflicts are always prefixes (or equal to) the path, or vice versa - if (path.numParts() > conflictingPath->numParts()) { + return Status(ErrorCodes::NotSingleValueField, errMsg); +} - string errMsg = stream() << "field at '" << conflictingPath->dottedField() - << "' must be exactly specified, field at sub-path '" - << path.dottedField() << "'found"; - return Status(ErrorCodes::NotExactValueField, errMsg); - } +/** + * Helper function to check if path conflicts are all prefixes. + */ +static Status checkPathIsPrefixOf(const FieldRef& path, const FieldRefSet& conflictPaths) { + for (FieldRefSet::const_iterator it = conflictPaths.begin(); it != conflictPaths.end(); ++it) { + const FieldRef* conflictingPath = *it; + // Conflicts are always prefixes (or equal to) the path, or vice versa + if (path.numParts() > conflictingPath->numParts()) { + string errMsg = stream() << "field at '" << conflictingPath->dottedField() + << "' must be exactly specified, field at sub-path '" + << path.dottedField() << "'found"; + return Status(ErrorCodes::NotExactValueField, errMsg); } - - return Status::OK(); } - static Status _extractFullEqualityMatches(const MatchExpression& root, - const FieldRefSet* fullPathsToExtract, - EqualityMatches* equalities) { - - if (root.matchType() == MatchExpression::EQ) { - - // Extract equality matches - const EqualityMatchExpression& eqChild = - static_cast<const EqualityMatchExpression&>(root); - - FieldRef path(eqChild.path()); + return Status::OK(); +} - if (fullPathsToExtract) { +static Status _extractFullEqualityMatches(const MatchExpression& root, + const FieldRefSet* fullPathsToExtract, + EqualityMatches* equalities) { + if (root.matchType() == MatchExpression::EQ) { + // Extract equality matches + const EqualityMatchExpression& eqChild = static_cast<const EqualityMatchExpression&>(root); - FieldRefSet conflictPaths; - fullPathsToExtract->findConflicts(&path, &conflictPaths); + FieldRef path(eqChild.path()); - // Ignore if this path is unrelated to the full paths - if (conflictPaths.empty()) - return Status::OK(); + if (fullPathsToExtract) { + FieldRefSet conflictPaths; + fullPathsToExtract->findConflicts(&path, &conflictPaths); - // Make sure we're a prefix of all the conflict paths - Status status = checkPathIsPrefixOf(path, conflictPaths); - if (!status.isOK()) - return status; - } + // Ignore if this path is unrelated to the full paths + if (conflictPaths.empty()) + return Status::OK(); - Status status = checkEqualityConflicts(*equalities, path); + // Make sure we're a prefix of all the conflict paths + Status status = checkPathIsPrefixOf(path, conflictPaths); if (!status.isOK()) return status; - - equalities->insert(std::make_pair(eqChild.path(), &eqChild)); - } - else if (root.matchType() == MatchExpression::AND) { - - // Further explore $and matches - for (size_t i = 0; i < root.numChildren(); ++i) { - MatchExpression* child = root.getChild(i); - Status status = _extractFullEqualityMatches(*child, fullPathsToExtract, equalities); - if (!status.isOK()) - return status; - } } - return Status::OK(); - } - - Status extractFullEqualityMatches(const MatchExpression& root, - const FieldRefSet& fullPathsToExtract, - EqualityMatches* equalities) { - return _extractFullEqualityMatches(root, &fullPathsToExtract, equalities); - } + Status status = checkEqualityConflicts(*equalities, path); + if (!status.isOK()) + return status; - Status extractEqualityMatches(const MatchExpression& root, EqualityMatches* equalities) { - return _extractFullEqualityMatches(root, NULL, equalities); + equalities->insert(std::make_pair(eqChild.path(), &eqChild)); + } else if (root.matchType() == MatchExpression::AND) { + // Further explore $and matches + for (size_t i = 0; i < root.numChildren(); ++i) { + MatchExpression* child = root.getChild(i); + Status status = _extractFullEqualityMatches(*child, fullPathsToExtract, equalities); + if (!status.isOK()) + return status; + } } - Status addEqualitiesToDoc(const EqualityMatches& equalities, mutablebson::Document* doc) { + return Status::OK(); +} - for (EqualityMatches::const_iterator it = equalities.begin(); it != equalities.end(); - ++it) { +Status extractFullEqualityMatches(const MatchExpression& root, + const FieldRefSet& fullPathsToExtract, + EqualityMatches* equalities) { + return _extractFullEqualityMatches(root, &fullPathsToExtract, equalities); +} - FieldRef path(it->first); - const BSONElement& data = it->second->getData(); +Status extractEqualityMatches(const MatchExpression& root, EqualityMatches* equalities) { + return _extractFullEqualityMatches(root, NULL, equalities); +} - Status status = setElementAtPath(path, data, doc); - if (!status.isOK()) - return status; - } +Status addEqualitiesToDoc(const EqualityMatches& equalities, mutablebson::Document* doc) { + for (EqualityMatches::const_iterator it = equalities.begin(); it != equalities.end(); ++it) { + FieldRef path(it->first); + const BSONElement& data = it->second->getData(); - return Status::OK(); + Status status = setElementAtPath(path, data, doc); + if (!status.isOK()) + return status; } -} // namespace pathsupport -} // namespace mongo + return Status::OK(); +} + +} // namespace pathsupport +} // namespace mongo diff --git a/src/mongo/db/ops/path_support.h b/src/mongo/db/ops/path_support.h index ed4515506b8..95d0a3da44c 100644 --- a/src/mongo/db/ops/path_support.h +++ b/src/mongo/db/ops/path_support.h @@ -40,152 +40,149 @@ namespace mongo { - namespace pathsupport { - - // Cap on the number of nulls we'll add to an array if we're inserting to an index that - // doesn't exist. - static const size_t kMaxPaddingAllowed = 1500000; - - // Convenience type to hold equality matches at particular paths from a MatchExpression - typedef std::map<StringData, const EqualityMatchExpression*> EqualityMatches; - - /** - * Finds the longest portion of 'prefix' that exists in document rooted at 'root' and is - * "viable." A viable path is one that, if fully created on a given doc, would not - * change the existing types of any fields in that doc. (See examples below.) - * - * If a prefix indeed exists, 'idxFound' is set to indicate how many parts in common - * 'prefix' and 'doc' have. 'elemFound' would point to the Element corresponding to - * prefix[idxFound] in 'doc'. The call would return an OK status in this case. - * - * If a prefix is not viable, returns a status "PathNotViable". 'idxFound' is set to - * indicate the part in the document that caused the path to be not viable. 'elemFound' - * would point to the Element corresponding to prefix[idxFound] in 'doc'. - * - * If a prefix does not exist, the call returns "NonExistentPath". 'elemFound' and - * 'idxFound' are indeterminate in this case. - * - * Definition of a "Viable Path": - * - * A field reference 'p_1.p_2.[...].p_n', where 'p_i' is a field part, is said to be - * a viable path in a given document D if the creation of each part 'p_i', 0 <= i < n - * in D does not force 'p_i' to change types. In other words, no existing 'p_i' in D - * may have a different type, other than the 'p_n'. - * - * 'a.b.c' is a viable path in {a: {b: {c: 1}}} - * 'a.b.c' is a viable path in {a: {b: {c: {d: 1}}}} - * 'a.b.c' is NOT a viable path in {a: {b: 1}}, because b would have changed types - * 'a.0.b' is a viable path in {a: [{b: 1}, {c: 1}]} - * 'a.0.b' is a viable path in {a: {"0": {b: 1}}} - * 'a.0.b' is NOT a viable path in {a: 1}, because a would have changed types - * 'a.5.b' is a viable path in in {a: []} (padding would occur) - */ - Status findLongestPrefix(const FieldRef& prefix, - mutablebson::Element root, - size_t* idxFound, - mutablebson::Element* elemFound); - - /** - * Creates the parts 'prefix[idxRoot]', 'prefix[idxRoot+1]', ..., - * 'prefix[<numParts>-1]' under 'elemFound' and adds 'newElem' as a child of that - * path. Returns OK, if successful, or an error code describing why not, otherwise. - * - * createPathAt is designed to work with 'findLongestPrefix' in that it can create the - * field parts in 'prefix' that are missing from a given document. 'elemFound' points - * to the element in the doc that is the parent of prefix[idxRoot]. - */ - Status createPathAt(const FieldRef& prefix, - size_t idxRoot, - mutablebson::Element elemFound, - mutablebson::Element newElem); - - /** - * Uses the above methods to set the given value at the specified path in a mutable - * Document, creating parents of the path if necessary. - * - * Returns PathNotViable if the path cannot be created without modifying the type of another - * element, see above. - */ - Status setElementAtPath(const FieldRef& path, - const BSONElement& value, - mutablebson::Document* doc); - - /** - * Finds and returns-by-path all the equality matches in a particular MatchExpression. - * - * This method is meant to be used with the methods below, which allow efficient use of the - * equality matches without needing to serialize to a BSONObj. - * - * Returns NotSingleValueField if the match expression has equality operators for - * conflicting paths - equality paths conflict if they are the same or one path is a prefix - * of the other. - * - * Ex: - * { a : 1, b : 1 } -> no conflict - * { a : 1, a.b : 1 } -> conflict - * { _id : { x : 1 }, _id.y : 1 } -> conflict - * { a : 1, a : 1 } -> conflict - */ - Status extractEqualityMatches(const MatchExpression& root, EqualityMatches* equalities); - - /** - * Same as the above, but ignores all paths except for paths in a specified set. - * Equality matches with paths completely distinct from these paths are ignored. - * - * For a full equality match, the path of an equality found must not be a suffix of one of - * the specified path - otherwise it isn't clear how to construct a full value for a field - * at that path. - * - * Generally this is useful for shard keys and _ids which need unambiguous extraction from - * queries. - * - * Ex: - * { a : 1 }, full path 'a' -> a $eq 1 extracted - * { a : 1 }, full path 'a.b' -> a $eq 1 extracted - * { 'a.b' : 1 }, full path 'a' -> NotExactValueField error - * ('a.b' doesn't specify 'a' fully) - * { 'a.b' : 1 }, full path 'a.b' -> 'a.b' $eq 1 extracted - * { '_id' : 1 }, full path '_id' -> '_id' $eq 1 extracted - * { '_id.x' : 1 }, full path '_id' -> NotExactValueFieldError - */ - Status extractFullEqualityMatches(const MatchExpression& root, - const FieldRefSet& fullPathsToExtract, - EqualityMatches* equalities); - - /** - * Returns the equality match which is at or a parent of the specified path string. The - * path string must be a valid dotted path. - * - * If a parent equality is found, returns the BSONElement data from that equality (which - * includes the BSON value), the path of the parent element (prefixStr), and the remainder - * of the path (which may be empty). - * - * EOO() is returned if there were no equalities at any point along the path. - * - * Ex: - * Given equality matches of: - * 'a.b' : 1, 'c' : 2 - * Path 'a' has no equality match parent (EOO) - * Path 'c' has an eqmatch parent of 'c' : 2 - * Path 'c.d' has an eqmatch parent of 'c' : 2 - * Path 'a.b' has an eqmatch parent of 'a.b' : 1 - * Path 'a.b.c' has an eqmatch parent of 'a.b' : 1 - * - */ - const BSONElement& findParentEqualityElement(const EqualityMatches& equalities, - const FieldRef& path, - int* parentPathParts); - - /** - * Adds the BSON values from equality matches into the given document at the equality match - * paths. - * - * Returns PathNotViable similar to setElementAtPath above. If equality paths do not - * conflict, as is enforced by extractEqualityMatches, this function should return OK. - */ - Status addEqualitiesToDoc(const EqualityMatches& equalities, - mutablebson::Document* doc); - - } // namespace pathsupport - -} // namespace mongo +namespace pathsupport { + +// Cap on the number of nulls we'll add to an array if we're inserting to an index that +// doesn't exist. +static const size_t kMaxPaddingAllowed = 1500000; + +// Convenience type to hold equality matches at particular paths from a MatchExpression +typedef std::map<StringData, const EqualityMatchExpression*> EqualityMatches; + +/** + * Finds the longest portion of 'prefix' that exists in document rooted at 'root' and is + * "viable." A viable path is one that, if fully created on a given doc, would not + * change the existing types of any fields in that doc. (See examples below.) + * + * If a prefix indeed exists, 'idxFound' is set to indicate how many parts in common + * 'prefix' and 'doc' have. 'elemFound' would point to the Element corresponding to + * prefix[idxFound] in 'doc'. The call would return an OK status in this case. + * + * If a prefix is not viable, returns a status "PathNotViable". 'idxFound' is set to + * indicate the part in the document that caused the path to be not viable. 'elemFound' + * would point to the Element corresponding to prefix[idxFound] in 'doc'. + * + * If a prefix does not exist, the call returns "NonExistentPath". 'elemFound' and + * 'idxFound' are indeterminate in this case. + * + * Definition of a "Viable Path": + * + * A field reference 'p_1.p_2.[...].p_n', where 'p_i' is a field part, is said to be + * a viable path in a given document D if the creation of each part 'p_i', 0 <= i < n + * in D does not force 'p_i' to change types. In other words, no existing 'p_i' in D + * may have a different type, other than the 'p_n'. + * + * 'a.b.c' is a viable path in {a: {b: {c: 1}}} + * 'a.b.c' is a viable path in {a: {b: {c: {d: 1}}}} + * 'a.b.c' is NOT a viable path in {a: {b: 1}}, because b would have changed types + * 'a.0.b' is a viable path in {a: [{b: 1}, {c: 1}]} + * 'a.0.b' is a viable path in {a: {"0": {b: 1}}} + * 'a.0.b' is NOT a viable path in {a: 1}, because a would have changed types + * 'a.5.b' is a viable path in in {a: []} (padding would occur) + */ +Status findLongestPrefix(const FieldRef& prefix, + mutablebson::Element root, + size_t* idxFound, + mutablebson::Element* elemFound); + +/** + * Creates the parts 'prefix[idxRoot]', 'prefix[idxRoot+1]', ..., + * 'prefix[<numParts>-1]' under 'elemFound' and adds 'newElem' as a child of that + * path. Returns OK, if successful, or an error code describing why not, otherwise. + * + * createPathAt is designed to work with 'findLongestPrefix' in that it can create the + * field parts in 'prefix' that are missing from a given document. 'elemFound' points + * to the element in the doc that is the parent of prefix[idxRoot]. + */ +Status createPathAt(const FieldRef& prefix, + size_t idxRoot, + mutablebson::Element elemFound, + mutablebson::Element newElem); + +/** + * Uses the above methods to set the given value at the specified path in a mutable + * Document, creating parents of the path if necessary. + * + * Returns PathNotViable if the path cannot be created without modifying the type of another + * element, see above. + */ +Status setElementAtPath(const FieldRef& path, const BSONElement& value, mutablebson::Document* doc); + +/** + * Finds and returns-by-path all the equality matches in a particular MatchExpression. + * + * This method is meant to be used with the methods below, which allow efficient use of the + * equality matches without needing to serialize to a BSONObj. + * + * Returns NotSingleValueField if the match expression has equality operators for + * conflicting paths - equality paths conflict if they are the same or one path is a prefix + * of the other. + * + * Ex: + * { a : 1, b : 1 } -> no conflict + * { a : 1, a.b : 1 } -> conflict + * { _id : { x : 1 }, _id.y : 1 } -> conflict + * { a : 1, a : 1 } -> conflict + */ +Status extractEqualityMatches(const MatchExpression& root, EqualityMatches* equalities); + +/** + * Same as the above, but ignores all paths except for paths in a specified set. + * Equality matches with paths completely distinct from these paths are ignored. + * + * For a full equality match, the path of an equality found must not be a suffix of one of + * the specified path - otherwise it isn't clear how to construct a full value for a field + * at that path. + * + * Generally this is useful for shard keys and _ids which need unambiguous extraction from + * queries. + * + * Ex: + * { a : 1 }, full path 'a' -> a $eq 1 extracted + * { a : 1 }, full path 'a.b' -> a $eq 1 extracted + * { 'a.b' : 1 }, full path 'a' -> NotExactValueField error + * ('a.b' doesn't specify 'a' fully) + * { 'a.b' : 1 }, full path 'a.b' -> 'a.b' $eq 1 extracted + * { '_id' : 1 }, full path '_id' -> '_id' $eq 1 extracted + * { '_id.x' : 1 }, full path '_id' -> NotExactValueFieldError + */ +Status extractFullEqualityMatches(const MatchExpression& root, + const FieldRefSet& fullPathsToExtract, + EqualityMatches* equalities); + +/** + * Returns the equality match which is at or a parent of the specified path string. The + * path string must be a valid dotted path. + * + * If a parent equality is found, returns the BSONElement data from that equality (which + * includes the BSON value), the path of the parent element (prefixStr), and the remainder + * of the path (which may be empty). + * + * EOO() is returned if there were no equalities at any point along the path. + * + * Ex: + * Given equality matches of: + * 'a.b' : 1, 'c' : 2 + * Path 'a' has no equality match parent (EOO) + * Path 'c' has an eqmatch parent of 'c' : 2 + * Path 'c.d' has an eqmatch parent of 'c' : 2 + * Path 'a.b' has an eqmatch parent of 'a.b' : 1 + * Path 'a.b.c' has an eqmatch parent of 'a.b' : 1 + * + */ +const BSONElement& findParentEqualityElement(const EqualityMatches& equalities, + const FieldRef& path, + int* parentPathParts); + +/** + * Adds the BSON values from equality matches into the given document at the equality match + * paths. + * + * Returns PathNotViable similar to setElementAtPath above. If equality paths do not + * conflict, as is enforced by extractEqualityMatches, this function should return OK. + */ +Status addEqualitiesToDoc(const EqualityMatches& equalities, mutablebson::Document* doc); + +} // namespace pathsupport + +} // namespace mongo diff --git a/src/mongo/db/ops/path_support_test.cpp b/src/mongo/db/ops/path_support_test.cpp index a502a39a0c3..62e6ded46c2 100644 --- a/src/mongo/db/ops/path_support_test.cpp +++ b/src/mongo/db/ops/path_support_test.cpp @@ -49,834 +49,852 @@ namespace { - using namespace mongo; - using namespace mutablebson; - using namespace pathsupport; - using mongoutils::str::stream; - using std::unique_ptr; - using std::string; +using namespace mongo; +using namespace mutablebson; +using namespace pathsupport; +using mongoutils::str::stream; +using std::unique_ptr; +using std::string; - class EmptyDoc : public mongo::unittest::Test { - public: - EmptyDoc() : _doc() {} +class EmptyDoc : public mongo::unittest::Test { +public: + EmptyDoc() : _doc() {} - Document& doc() { return _doc; } - - Element root() { return _doc.root(); } - - FieldRef& field() { return _field; } - - void setField(StringData str) { _field.parse(str); } - - private: - Document _doc; - FieldRef _field; - }; - - TEST_F(EmptyDoc, EmptyPath) { - setField(""); - - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); - } - - TEST_F(EmptyDoc, NewField) { - setField("a"); - - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); - - Element newElem = doc().makeElementInt("a", 1); - ASSERT_TRUE(newElem.ok()); - ASSERT_OK(createPathAt(field(), 0, root(), newElem)); - ASSERT_EQUALS(fromjson("{a: 1}"), doc()); - } - - class SimpleDoc : public mongo::unittest::Test { - public: - SimpleDoc() : _doc() {} - - virtual void setUp() { - // {a: 1} - ASSERT_OK(root().appendInt("a", 1)); - } - - Document& doc() { return _doc; } - - Element root() { return _doc.root(); } - - FieldRef& field() { return _field; } - void setField(StringData str) { _field.parse(str); } - - private: - Document _doc; - FieldRef _field; - }; - - TEST_F(SimpleDoc, EmptyPath) { - setField(""); - - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); - } - - TEST_F(SimpleDoc, SimplePath) { - setField("a"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); - } - - TEST_F(SimpleDoc, LongerPath) { - setField("a.b"); - - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status, ErrorCodes::PathNotViable); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); + Document& doc() { + return _doc; } - TEST_F(SimpleDoc, NotCommonPrefix) { - setField("b"); - - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); - - // From this point on, handles the creation of the '.b' part that wasn't found. - Element newElem = doc().makeElementInt("b", 1); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(root()), 1u); - - ASSERT_OK(createPathAt(field(), 0, root(), newElem)); - ASSERT_EQUALS(newElem.getFieldName(), "b"); - ASSERT_EQUALS(newElem.getType(), NumberInt); - ASSERT_TRUE(newElem.hasValue()); - ASSERT_EQUALS(newElem.getValueInt(), 1); - - ASSERT_TRUE(newElem.parent().ok() /* root an ok parent */); - ASSERT_EQUALS(countChildren(root()), 2u); - ASSERT_EQUALS(root().leftChild().getFieldName(), "a"); - ASSERT_EQUALS(root().leftChild().rightSibling().getFieldName(), "b"); - ASSERT_EQUALS(root().rightChild().getFieldName(), "b"); - ASSERT_EQUALS(root().rightChild().leftSibling().getFieldName(), "a"); + Element root() { + return _doc.root(); } - class NestedDoc : public mongo::unittest::Test { - public: - NestedDoc() : _doc() {} - - virtual void setUp() { - // {a: {b: {c: 1}}} - Element elemA = _doc.makeElementObject("a"); - ASSERT_TRUE(elemA.ok()); - Element elemB = _doc.makeElementObject("b"); - ASSERT_TRUE(elemB.ok()); - Element elemC = _doc.makeElementInt("c", 1); - ASSERT_TRUE(elemC.ok()); - - ASSERT_OK(elemB.pushBack(elemC)); - ASSERT_OK(elemA.pushBack(elemB)); - ASSERT_OK(root().pushBack(elemA)); - } - - Document& doc() { return _doc; } - - Element root() { return _doc.root(); } - - FieldRef& field() { return _field; } - void setField(StringData str) { _field.parse(str); } - - private: - Document _doc; - FieldRef _field; - }; - - TEST_F(NestedDoc, SimplePath) { - setField("a"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); + FieldRef& field() { + return _field; } - TEST_F(NestedDoc, ShorterPath) { - setField("a.b"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_EQUALS(idxFound, 1U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]), 0); + void setField(StringData str) { + _field.parse(str); } - TEST_F(NestedDoc, ExactPath) { - setField("a.b.c"); +private: + Document _doc; + FieldRef _field; +}; - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 2U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]["c"]), 0); - } - - TEST_F(NestedDoc, LongerPath) { - // This would for 'c' to change from NumberInt to Object, which is invalid. - setField("a.b.c.d"); +TEST_F(EmptyDoc, EmptyPath) { + setField(""); - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status.code(), ErrorCodes::PathNotViable); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 2U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]["c"]), 0); + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); +} - } +TEST_F(EmptyDoc, NewField) { + setField("a"); - TEST_F(NestedDoc, NewFieldNested) { - setField("a.b.d"); + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_EQUALS(idxFound, 1U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]), 0); + Element newElem = doc().makeElementInt("a", 1); + ASSERT_TRUE(newElem.ok()); + ASSERT_OK(createPathAt(field(), 0, root(), newElem)); + ASSERT_EQUALS(fromjson("{a: 1}"), doc()); +} - // From this point on, handles the creation of the '.d' part that wasn't found. - Element newElem = doc().makeElementInt("d", 1); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(elemFound), 1u); // 'c' is a child of 'b' +class SimpleDoc : public mongo::unittest::Test { +public: + SimpleDoc() : _doc() {} - ASSERT_OK(createPathAt(field(), idxFound+1, elemFound, newElem)); - ASSERT_EQUALS(fromjson("{a: {b: {c: 1, d: 1}}}"), doc()); + virtual void setUp() { + // {a: 1} + ASSERT_OK(root().appendInt("a", 1)); } - TEST_F(NestedDoc, NotStartingFromRoot) { - setField("b.c"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root()["a"], &idxFound, &elemFound)); - ASSERT_EQUALS(idxFound, 1U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]["c"]), 0); + Document& doc() { + return _doc; } - class ArrayDoc : public mongo::unittest::Test { - public: - ArrayDoc() : _doc() {} - - virtual void setUp() { - // {a: []} - Element elemA = _doc.makeElementArray("a"); - ASSERT_TRUE(elemA.ok()); - ASSERT_OK(root().pushBack(elemA)); - - // {a: [], b: [{c: 1}]} - Element elemB = _doc.makeElementArray("b"); - ASSERT_TRUE(elemB.ok()); - Element elemObj = _doc.makeElementObject("dummy" /* field name not used in array */); - ASSERT_TRUE(elemObj.ok()); - ASSERT_OK(elemObj.appendInt("c",1)); - ASSERT_OK(elemB.pushBack(elemObj)); - ASSERT_OK(root().pushBack(elemB)); - } - - Document& doc() { return _doc; } - - Element root() { return _doc.root(); } - - FieldRef& field() { return _field; } - - void setField(StringData str) { _field.parse(str); } - - private: - Document _doc; - FieldRef _field; - }; - - TEST_F(ArrayDoc, PathOnEmptyArray) { - setField("a.0"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); + Element root() { + return _doc.root(); } - TEST_F(ArrayDoc, PathOnPopulatedArray) { - setField("b.0"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 1U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"][0]), 0); + FieldRef& field() { + return _field; } - - TEST_F(ArrayDoc, MixedArrayAndObjectPath) { - setField("b.0.c"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 2U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"][0]["c"]), 0); + void setField(StringData str) { + _field.parse(str); } - TEST_F(ArrayDoc, ExtendingExistingObject) { - setField("b.0.d"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 1U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"][0]), 0); +private: + Document _doc; + FieldRef _field; +}; - // From this point on, handles the creation of the '.0.d' part that wasn't found. - Element newElem = doc().makeElementInt("d", 1); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of b.0 +TEST_F(SimpleDoc, EmptyPath) { + setField(""); - ASSERT_OK(createPathAt(field(), idxFound+1, elemFound, newElem)); - ASSERT_EQUALS(fromjson("{a: [], b: [{c:1, d:1}]}"), doc()); - } + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); +} - TEST_F(ArrayDoc, NewObjectInsideArray) { - setField("b.1.c"); +TEST_F(SimpleDoc, SimplePath) { + setField("a"); - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); +} - // From this point on, handles the creation of the '.1.c' part that wasn't found. - Element newElem = doc().makeElementInt("c", 2); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' - - ASSERT_OK(createPathAt(field(), idxFound+1, elemFound, newElem)); - ASSERT_EQUALS(fromjson("{a: [], b: [{c:1},{c:2}]}"), doc()); - } +TEST_F(SimpleDoc, LongerPath) { + setField("a.b"); - TEST_F(ArrayDoc, NewNestedObjectInsideArray) { - setField("b.1.c.d"); + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status, ErrorCodes::PathNotViable); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); +} - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); +TEST_F(SimpleDoc, NotCommonPrefix) { + setField("b"); - // From this point on, handles the creation of the '.1.c.d' part that wasn't found. - Element newElem = doc().makeElementInt("d", 2); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status, ErrorCodes::NonExistentPath); + + // From this point on, handles the creation of the '.b' part that wasn't found. + Element newElem = doc().makeElementInt("b", 1); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(root()), 1u); + + ASSERT_OK(createPathAt(field(), 0, root(), newElem)); + ASSERT_EQUALS(newElem.getFieldName(), "b"); + ASSERT_EQUALS(newElem.getType(), NumberInt); + ASSERT_TRUE(newElem.hasValue()); + ASSERT_EQUALS(newElem.getValueInt(), 1); + + ASSERT_TRUE(newElem.parent().ok() /* root an ok parent */); + ASSERT_EQUALS(countChildren(root()), 2u); + ASSERT_EQUALS(root().leftChild().getFieldName(), "a"); + ASSERT_EQUALS(root().leftChild().rightSibling().getFieldName(), "b"); + ASSERT_EQUALS(root().rightChild().getFieldName(), "b"); + ASSERT_EQUALS(root().rightChild().leftSibling().getFieldName(), "a"); +} + +class NestedDoc : public mongo::unittest::Test { +public: + NestedDoc() : _doc() {} + + virtual void setUp() { + // {a: {b: {c: 1}}} + Element elemA = _doc.makeElementObject("a"); + ASSERT_TRUE(elemA.ok()); + Element elemB = _doc.makeElementObject("b"); + ASSERT_TRUE(elemB.ok()); + Element elemC = _doc.makeElementInt("c", 1); + ASSERT_TRUE(elemC.ok()); + + ASSERT_OK(elemB.pushBack(elemC)); + ASSERT_OK(elemA.pushBack(elemB)); + ASSERT_OK(root().pushBack(elemA)); + } + + Document& doc() { + return _doc; + } + + Element root() { + return _doc.root(); + } + + FieldRef& field() { + return _field; + } + void setField(StringData str) { + _field.parse(str); + } + +private: + Document _doc; + FieldRef _field; +}; + +TEST_F(NestedDoc, SimplePath) { + setField("a"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); +} + +TEST_F(NestedDoc, ShorterPath) { + setField("a.b"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_EQUALS(idxFound, 1U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]), 0); +} + +TEST_F(NestedDoc, ExactPath) { + setField("a.b.c"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 2U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]["c"]), 0); +} + +TEST_F(NestedDoc, LongerPath) { + // This would for 'c' to change from NumberInt to Object, which is invalid. + setField("a.b.c.d"); + + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status.code(), ErrorCodes::PathNotViable); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 2U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]["c"]), 0); +} + +TEST_F(NestedDoc, NewFieldNested) { + setField("a.b.d"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_EQUALS(idxFound, 1U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]), 0); + + // From this point on, handles the creation of the '.d' part that wasn't found. + Element newElem = doc().makeElementInt("d", 1); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(elemFound), 1u); // 'c' is a child of 'b' + + ASSERT_OK(createPathAt(field(), idxFound + 1, elemFound, newElem)); + ASSERT_EQUALS(fromjson("{a: {b: {c: 1, d: 1}}}"), doc()); +} + +TEST_F(NestedDoc, NotStartingFromRoot) { + setField("b.c"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root()["a"], &idxFound, &elemFound)); + ASSERT_EQUALS(idxFound, 1U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]["b"]["c"]), 0); +} + +class ArrayDoc : public mongo::unittest::Test { +public: + ArrayDoc() : _doc() {} + + virtual void setUp() { + // {a: []} + Element elemA = _doc.makeElementArray("a"); + ASSERT_TRUE(elemA.ok()); + ASSERT_OK(root().pushBack(elemA)); + + // {a: [], b: [{c: 1}]} + Element elemB = _doc.makeElementArray("b"); + ASSERT_TRUE(elemB.ok()); + Element elemObj = _doc.makeElementObject("dummy" /* field name not used in array */); + ASSERT_TRUE(elemObj.ok()); + ASSERT_OK(elemObj.appendInt("c", 1)); + ASSERT_OK(elemB.pushBack(elemObj)); + ASSERT_OK(root().pushBack(elemB)); + } + + Document& doc() { + return _doc; + } + + Element root() { + return _doc.root(); + } + + FieldRef& field() { + return _field; + } + + void setField(StringData str) { + _field.parse(str); + } + +private: + Document _doc; + FieldRef _field; +}; + +TEST_F(ArrayDoc, PathOnEmptyArray) { + setField("a.0"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["a"]), 0); +} + +TEST_F(ArrayDoc, PathOnPopulatedArray) { + setField("b.0"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 1U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"][0]), 0); +} + +TEST_F(ArrayDoc, MixedArrayAndObjectPath) { + setField("b.0.c"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 2U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"][0]["c"]), 0); +} + +TEST_F(ArrayDoc, ExtendingExistingObject) { + setField("b.0.d"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 1U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"][0]), 0); + + // From this point on, handles the creation of the '.0.d' part that wasn't found. + Element newElem = doc().makeElementInt("d", 1); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of b.0 + + ASSERT_OK(createPathAt(field(), idxFound + 1, elemFound, newElem)); + ASSERT_EQUALS(fromjson("{a: [], b: [{c:1, d:1}]}"), doc()); +} + +TEST_F(ArrayDoc, NewObjectInsideArray) { + setField("b.1.c"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); + + // From this point on, handles the creation of the '.1.c' part that wasn't found. + Element newElem = doc().makeElementInt("c", 2); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' + + ASSERT_OK(createPathAt(field(), idxFound + 1, elemFound, newElem)); + ASSERT_EQUALS(fromjson("{a: [], b: [{c:1},{c:2}]}"), doc()); +} + +TEST_F(ArrayDoc, NewNestedObjectInsideArray) { + setField("b.1.c.d"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); + + // From this point on, handles the creation of the '.1.c.d' part that wasn't found. + Element newElem = doc().makeElementInt("d", 2); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' + + ASSERT_OK(createPathAt(field(), idxFound + 1, elemFound, newElem)); + ASSERT_EQUALS(fromjson("{a: [], b: [{c:1},{c:{d:2}}]}"), doc()); +} + +TEST_F(ArrayDoc, ArrayPaddingNecessary) { + setField("b.5"); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); + + // From this point on, handles the creation of the '.5' part that wasn't found. + Element newElem = doc().makeElementInt("", 1); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' + + ASSERT_OK(createPathAt(field(), idxFound + 1, elemFound, newElem)); + ASSERT_EQUALS(fromjson("{a: [], b: [{c:1},null,null,null,null,1]}"), doc()); +} + +TEST_F(ArrayDoc, ExcessivePaddingRequested) { + // Try to create an array item beyond what we're allowed to pad. + string paddedField = stream() << "b." << mongo::pathsupport::kMaxPaddingAllowed + 1; + ; + setField(paddedField); + + size_t idxFound; + Element elemFound = root(); + ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); + + // From this point on, try to create the padded part that wasn't found. + Element newElem = doc().makeElementInt("", 1); + ASSERT_TRUE(newElem.ok()); + ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' + + Status status = createPathAt(field(), idxFound + 1, elemFound, newElem); + ASSERT_EQUALS(status.code(), ErrorCodes::CannotBackfillArray); +} + +TEST_F(ArrayDoc, NonNumericPathInArray) { + setField("b.z"); + + size_t idxFound; + Element elemFound = root(); + Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); + ASSERT_EQUALS(status.code(), ErrorCodes::PathNotViable); + ASSERT_TRUE(elemFound.ok()); + ASSERT_EQUALS(idxFound, 0U); + ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); +} + +// +// Tests of equality extraction from MatchExpressions +// NONGOAL: Testing query/match expression parsing and optimization +// + +static MatchExpression* makeExpr(const BSONObj& exprBSON) { + static const WhereCallbackNoop callbackNoop; + return MatchExpressionParser::parse(exprBSON, callbackNoop).getValue(); +} + +static void assertContains(const EqualityMatches& equalities, const BSONObj& wrapped) { + BSONElement value = wrapped.firstElement(); + StringData path = value.fieldNameStringData(); + + EqualityMatches::const_iterator it = equalities.find(path); + if (it == equalities.end()) { + FAIL(stream() << "Equality matches did not contain path \"" << path << "\""); + } + if (!it->second->getData().valuesEqual(value)) { + FAIL(stream() << "Equality match at path \"" << path << "\" contains value " + << it->second->getData() << ", not value " << value); + } +} + +static void assertContains(const EqualityMatches& equalities, StringData path, int value) { + assertContains(equalities, BSON(path << value)); +} + +// NOTE: For tests below, BSONObj expr must exist for lifetime of MatchExpression + +TEST(ExtractEqualities, Basic) { + BSONObj exprBSON = fromjson("{a:1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "a", 1); +} + +TEST(ExtractEqualities, Multiple) { + BSONObj exprBSON = fromjson("{a:1, b:2}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 2u); + assertContains(equalities, "a", 1); + assertContains(equalities, "b", 2); +} + +TEST(ExtractEqualities, EqOperator) { + BSONObj exprBSON = fromjson("{a:{$eq:1}}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "a", 1); +} + +TEST(ExtractEqualities, AndOperator) { + BSONObj exprBSON = fromjson("{$and:[{a:{$eq:1}},{b:2}]}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 2u); + assertContains(equalities, "a", 1); + assertContains(equalities, "b", 2); +} + +TEST(ExtractEqualities, NestedAndOperator) { + BSONObj exprBSON = fromjson("{$and:[{$and:[{a:{$eq:1}},{b:2}]},{c:3}]}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 3u); + assertContains(equalities, "a", 1); + assertContains(equalities, "b", 2); + assertContains(equalities, "c", 3); +} + +TEST(ExtractEqualities, NestedPaths) { + BSONObj exprBSON = fromjson("{'a.a':1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "a.a", 1); +} + +TEST(ExtractEqualities, SiblingPaths) { + BSONObj exprBSON = fromjson("{'a.a':1,'a.b':{$eq:2}}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 2u); + assertContains(equalities, "a.a", 1); + assertContains(equalities, "a.b", 2); +} + +TEST(ExtractEqualities, NestedAndNestedPaths) { + BSONObj exprBSON = fromjson("{$and:[{$and:[{'a.a':{$eq:1}},{'a.b':2}]},{'c.c.c':3}]}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 3u); + assertContains(equalities, "a.a", 1); + assertContains(equalities, "a.b", 2); + assertContains(equalities, "c.c.c", 3); +} + +TEST(ExtractEqualities, IdOnly) { + BSONObj exprBSON = fromjson("{_id:1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "_id", 1); +} - ASSERT_OK(createPathAt(field(), idxFound+1, elemFound, newElem)); - ASSERT_EQUALS(fromjson("{a: [], b: [{c:1},{c:{d:2}}]}"), doc()); - } - - TEST_F(ArrayDoc, ArrayPaddingNecessary) { - setField("b.5"); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); - - // From this point on, handles the creation of the '.5' part that wasn't found. - Element newElem = doc().makeElementInt("", 1); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' - - ASSERT_OK(createPathAt(field(), idxFound+1, elemFound, newElem)); - ASSERT_EQUALS(fromjson("{a: [], b: [{c:1},null,null,null,null,1]}"), doc()); - } - - TEST_F(ArrayDoc, ExcessivePaddingRequested) { - // Try to create an array item beyond what we're allowed to pad. - string paddedField = stream() << "b." << mongo::pathsupport::kMaxPaddingAllowed + 1;; - setField(paddedField); - - size_t idxFound; - Element elemFound = root(); - ASSERT_OK(findLongestPrefix(field(), root(), &idxFound, &elemFound)); - - // From this point on, try to create the padded part that wasn't found. - Element newElem = doc().makeElementInt("", 1); - ASSERT_TRUE(newElem.ok()); - ASSERT_EQUALS(countChildren(elemFound), 1u); // '{c:1}' is a child of 'b' - - Status status = createPathAt(field(), idxFound+1, elemFound, newElem); - ASSERT_EQUALS(status.code(), ErrorCodes::CannotBackfillArray); - } - - TEST_F(ArrayDoc, NonNumericPathInArray) { - setField("b.z"); - - size_t idxFound; - Element elemFound = root(); - Status status = findLongestPrefix(field(), root(), &idxFound, &elemFound); - ASSERT_EQUALS(status.code(), ErrorCodes::PathNotViable); - ASSERT_TRUE(elemFound.ok()); - ASSERT_EQUALS(idxFound, 0U); - ASSERT_EQUALS(elemFound.compareWithElement(root()["b"]), 0); - } - - // - // Tests of equality extraction from MatchExpressions - // NONGOAL: Testing query/match expression parsing and optimization - // - - static MatchExpression* makeExpr(const BSONObj& exprBSON) { - static const WhereCallbackNoop callbackNoop; - return MatchExpressionParser::parse(exprBSON, callbackNoop).getValue(); - } - - static void assertContains(const EqualityMatches& equalities, const BSONObj& wrapped) { - - BSONElement value = wrapped.firstElement(); - StringData path = value.fieldNameStringData(); - - EqualityMatches::const_iterator it = equalities.find(path); - if (it == equalities.end()) { - FAIL(stream() << "Equality matches did not contain path \"" << path << "\""); - } - if (!it->second->getData().valuesEqual(value)) { - FAIL(stream() << "Equality match at path \"" << path << "\" contains value " - << it->second->getData() << ", not value " << value); - } - } - - static void assertContains(const EqualityMatches& equalities, - StringData path, - int value) { - assertContains(equalities, BSON(path << value)); - } - - // NOTE: For tests below, BSONObj expr must exist for lifetime of MatchExpression - - TEST(ExtractEqualities, Basic) { - BSONObj exprBSON = fromjson("{a:1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "a", 1); - } - - TEST(ExtractEqualities, Multiple) { - BSONObj exprBSON = fromjson("{a:1, b:2}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 2u); - assertContains(equalities, "a", 1); - assertContains(equalities, "b", 2); - } - - TEST(ExtractEqualities, EqOperator) { - BSONObj exprBSON = fromjson("{a:{$eq:1}}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "a", 1); - } - - TEST(ExtractEqualities, AndOperator) { - BSONObj exprBSON = fromjson("{$and:[{a:{$eq:1}},{b:2}]}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 2u); - assertContains(equalities, "a", 1); - assertContains(equalities, "b", 2); - } - - TEST(ExtractEqualities, NestedAndOperator) { - BSONObj exprBSON = fromjson("{$and:[{$and:[{a:{$eq:1}},{b:2}]},{c:3}]}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 3u); - assertContains(equalities, "a", 1); - assertContains(equalities, "b", 2); - assertContains(equalities, "c", 3); - } - - TEST(ExtractEqualities, NestedPaths) { - BSONObj exprBSON = fromjson("{'a.a':1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "a.a", 1); - } - - TEST(ExtractEqualities, SiblingPaths) { - BSONObj exprBSON = fromjson("{'a.a':1,'a.b':{$eq:2}}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 2u); - assertContains(equalities, "a.a", 1); - assertContains(equalities, "a.b", 2); - } - - TEST(ExtractEqualities, NestedAndNestedPaths) { - BSONObj exprBSON = fromjson("{$and:[{$and:[{'a.a':{$eq:1}},{'a.b':2}]},{'c.c.c':3}]}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 3u); - assertContains(equalities, "a.a", 1); - assertContains(equalities, "a.b", 2); - assertContains(equalities, "c.c.c", 3); - } - - TEST(ExtractEqualities, IdOnly) { - BSONObj exprBSON = fromjson("{_id:1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "_id", 1); - } - - /** - * Helper class to allow easy construction of immutable paths - */ - class ImmutablePaths { - public: - ImmutablePaths() {} - - void addPath(const string& path) { - _ownedPaths.mutableVector().push_back(new FieldRef(path)); - FieldRef const* conflictPath = NULL; - ASSERT(_immutablePathSet.insert(_ownedPaths.vector().back(), &conflictPath)); - } - - const FieldRefSet& getPathSet() { - return _immutablePathSet; - } - - private: - - FieldRefSet _immutablePathSet; - OwnedPointerVector<FieldRef> _ownedPaths; - }; - - TEST(ExtractEqualities, IdOnlyMulti) { - BSONObj exprBSON = fromjson("{_id:{$eq:1},a:1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - ImmutablePaths immutablePaths; - immutablePaths.addPath("_id"); - - EqualityMatches equalities; - ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "_id", 1); - } - - TEST(ExtractEqualities, IdOnlyIgnoreConflict) { - BSONObj exprBSON = fromjson("{_id:1,a:1,'a.b':1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - ImmutablePaths immutablePaths; - immutablePaths.addPath("_id"); - - EqualityMatches equalities; - ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "_id", 1); - } - - TEST(ExtractEqualities, IdOnlyNested) { - BSONObj exprBSON = fromjson("{'_id.a':1,'_id.b':{$eq:2},c:3}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - ImmutablePaths immutablePaths; - immutablePaths.addPath("_id"); - - EqualityMatches equalities; - Status status = extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities); - ASSERT_EQUALS(status.code(), ErrorCodes::NotExactValueField); - } - - TEST(ExtractEqualities, IdAndOtherImmutable) { - BSONObj exprBSON = fromjson("{_id:1,a:1,b:2}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - ImmutablePaths immutablePaths; - immutablePaths.addPath("_id"); - immutablePaths.addPath("a"); - - EqualityMatches equalities; - ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); - ASSERT_EQUALS(equalities.size(), 2u); - assertContains(equalities, "_id", 1); - assertContains(equalities, "a", 1); - } - - TEST(ExtractEqualities, IdAndNestedImmutable) { - BSONObj exprBSON = fromjson("{_id:1,a:1,'c.d':3}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - ImmutablePaths immutablePaths; - immutablePaths.addPath("_id"); - immutablePaths.addPath("a.b"); - immutablePaths.addPath("c.d"); - - EqualityMatches equalities; - ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); - ASSERT_EQUALS(equalities.size(), 3u); - assertContains(equalities, "_id", 1); - assertContains(equalities, "a", 1); - assertContains(equalities, "c.d", 3); - } - - TEST(ExtractEqualities, NonFullImmutable) { - BSONObj exprBSON = fromjson("{'a.b':1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - ImmutablePaths immutablePaths; - immutablePaths.addPath("a"); - - EqualityMatches equalities; - Status status = extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities); - ASSERT_EQUALS(status.code(), ErrorCodes::NotExactValueField); - } - - TEST(ExtractEqualities, Empty) { - BSONObj exprBSON = fromjson("{'':0}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 1u); - assertContains(equalities, "", 0); - } - - TEST(ExtractEqualities, EmptyMulti) { - BSONObj exprBSON = fromjson("{'':0,a:{$eq:1}}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - ASSERT_EQUALS(equalities.size(), 2u); - assertContains(equalities, "", 0); - assertContains(equalities, "a", 1); - } - - TEST(ExtractEqualities, EqConflict) { - BSONObj exprBSON = fromjson("{a:1,a:1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), - ErrorCodes::NotSingleValueField); - } - - TEST(ExtractEqualities, PrefixConflict) { - BSONObj exprBSON = fromjson("{a:1,'a.b':{$eq:1}}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), - ErrorCodes::NotSingleValueField); - } - - TEST(ExtractEqualities, AndPrefixConflict) { - BSONObj exprBSON = fromjson("{$and:[{a:1},{'a.b':{$eq:1}}]}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), - ErrorCodes::NotSingleValueField); - } - - TEST(ExtractEqualities, EmptyConflict) { - BSONObj exprBSON = fromjson("{'':0,'':{$eq:0}}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - - EqualityMatches equalities; - ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), - ErrorCodes::NotSingleValueField); - } - - // - // Tests for finding parent equality from equalities found in expression - // NONGOALS: Testing complex equality match extraction - tested above - // - - static void assertParent(const EqualityMatches& equalities, - StringData pathStr, - const BSONObj& wrapped) { - - FieldRef path(pathStr); - BSONElement value = wrapped.firstElement(); - StringData parentPath = value.fieldNameStringData(); - - int parentPathPart; - BSONElement parentEl = findParentEqualityElement(equalities, path, &parentPathPart); - - if (parentEl.eoo()) { - FAIL(stream() << "Equality matches did not contain parent for \"" << pathStr - << "\""); - } +/** + * Helper class to allow easy construction of immutable paths + */ +class ImmutablePaths { +public: + ImmutablePaths() {} + + void addPath(const string& path) { + _ownedPaths.mutableVector().push_back(new FieldRef(path)); + FieldRef const* conflictPath = NULL; + ASSERT(_immutablePathSet.insert(_ownedPaths.vector().back(), &conflictPath)); + } + + const FieldRefSet& getPathSet() { + return _immutablePathSet; + } + +private: + FieldRefSet _immutablePathSet; + OwnedPointerVector<FieldRef> _ownedPaths; +}; + +TEST(ExtractEqualities, IdOnlyMulti) { + BSONObj exprBSON = fromjson("{_id:{$eq:1},a:1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + ImmutablePaths immutablePaths; + immutablePaths.addPath("_id"); + + EqualityMatches equalities; + ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "_id", 1); +} + +TEST(ExtractEqualities, IdOnlyIgnoreConflict) { + BSONObj exprBSON = fromjson("{_id:1,a:1,'a.b':1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + ImmutablePaths immutablePaths; + immutablePaths.addPath("_id"); + + EqualityMatches equalities; + ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "_id", 1); +} + +TEST(ExtractEqualities, IdOnlyNested) { + BSONObj exprBSON = fromjson("{'_id.a':1,'_id.b':{$eq:2},c:3}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + ImmutablePaths immutablePaths; + immutablePaths.addPath("_id"); + + EqualityMatches equalities; + Status status = extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities); + ASSERT_EQUALS(status.code(), ErrorCodes::NotExactValueField); +} + +TEST(ExtractEqualities, IdAndOtherImmutable) { + BSONObj exprBSON = fromjson("{_id:1,a:1,b:2}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + ImmutablePaths immutablePaths; + immutablePaths.addPath("_id"); + immutablePaths.addPath("a"); + + EqualityMatches equalities; + ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); + ASSERT_EQUALS(equalities.size(), 2u); + assertContains(equalities, "_id", 1); + assertContains(equalities, "a", 1); +} + +TEST(ExtractEqualities, IdAndNestedImmutable) { + BSONObj exprBSON = fromjson("{_id:1,a:1,'c.d':3}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + ImmutablePaths immutablePaths; + immutablePaths.addPath("_id"); + immutablePaths.addPath("a.b"); + immutablePaths.addPath("c.d"); + + EqualityMatches equalities; + ASSERT_OK(extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities)); + ASSERT_EQUALS(equalities.size(), 3u); + assertContains(equalities, "_id", 1); + assertContains(equalities, "a", 1); + assertContains(equalities, "c.d", 3); +} + +TEST(ExtractEqualities, NonFullImmutable) { + BSONObj exprBSON = fromjson("{'a.b':1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + ImmutablePaths immutablePaths; + immutablePaths.addPath("a"); + + EqualityMatches equalities; + Status status = extractFullEqualityMatches(*expr, immutablePaths.getPathSet(), &equalities); + ASSERT_EQUALS(status.code(), ErrorCodes::NotExactValueField); +} + +TEST(ExtractEqualities, Empty) { + BSONObj exprBSON = fromjson("{'':0}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 1u); + assertContains(equalities, "", 0); +} + +TEST(ExtractEqualities, EmptyMulti) { + BSONObj exprBSON = fromjson("{'':0,a:{$eq:1}}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + ASSERT_EQUALS(equalities.size(), 2u); + assertContains(equalities, "", 0); + assertContains(equalities, "a", 1); +} + +TEST(ExtractEqualities, EqConflict) { + BSONObj exprBSON = fromjson("{a:1,a:1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), + ErrorCodes::NotSingleValueField); +} + +TEST(ExtractEqualities, PrefixConflict) { + BSONObj exprBSON = fromjson("{a:1,'a.b':{$eq:1}}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), + ErrorCodes::NotSingleValueField); +} + +TEST(ExtractEqualities, AndPrefixConflict) { + BSONObj exprBSON = fromjson("{$and:[{a:1},{'a.b':{$eq:1}}]}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), + ErrorCodes::NotSingleValueField); +} + +TEST(ExtractEqualities, EmptyConflict) { + BSONObj exprBSON = fromjson("{'':0,'':{$eq:0}}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + + EqualityMatches equalities; + ASSERT_EQUALS(extractEqualityMatches(*expr, &equalities).code(), + ErrorCodes::NotSingleValueField); +} + +// +// Tests for finding parent equality from equalities found in expression +// NONGOALS: Testing complex equality match extraction - tested above +// + +static void assertParent(const EqualityMatches& equalities, + StringData pathStr, + const BSONObj& wrapped) { + FieldRef path(pathStr); + BSONElement value = wrapped.firstElement(); + StringData parentPath = value.fieldNameStringData(); + + int parentPathPart; + BSONElement parentEl = findParentEqualityElement(equalities, path, &parentPathPart); + + if (parentEl.eoo()) { + FAIL(stream() << "Equality matches did not contain parent for \"" << pathStr << "\""); + } + + StringData foundParentPath = path.dottedSubstring(0, parentPathPart); + if (foundParentPath != parentPath) { + FAIL(stream() << "Equality match parent at path \"" << foundParentPath + << "\" does not match \"" << parentPath << "\""); + } + + if (!parentEl.valuesEqual(value)) { + FAIL(stream() << "Equality match parent for \"" << pathStr << "\" at path \"" << parentPath + << "\" contains value " << parentEl << ", not value " << value); + } +} +static void assertParent(const EqualityMatches& equalities, + StringData path, + StringData parentPath, + int value) { + assertParent(equalities, path, BSON(parentPath << value)); +} + +static void assertNoParent(const EqualityMatches& equalities, StringData pathStr) { + FieldRef path(pathStr); + + int parentPathPart; + BSONElement parentEl = findParentEqualityElement(equalities, path, &parentPathPart); + + if (!parentEl.eoo()) { StringData foundParentPath = path.dottedSubstring(0, parentPathPart); - if (foundParentPath != parentPath) { - FAIL(stream() << "Equality match parent at path \"" << foundParentPath - << "\" does not match \"" << parentPath << "\""); - } - - if (!parentEl.valuesEqual(value)) { - FAIL(stream() << "Equality match parent for \"" << pathStr << "\" at path \"" - << parentPath << "\" contains value " << parentEl << ", not value " - << value); - } - } - - static void assertParent(const EqualityMatches& equalities, - StringData path, - StringData parentPath, - int value) { - assertParent(equalities, path, BSON(parentPath << value)); - } - - static void assertNoParent(const EqualityMatches& equalities, StringData pathStr) { - - FieldRef path(pathStr); - - int parentPathPart; - BSONElement parentEl = findParentEqualityElement(equalities, path, &parentPathPart); - - if (!parentEl.eoo()) { - StringData foundParentPath = path.dottedSubstring(0, parentPathPart); - FAIL(stream() << "Equality matches contained parent for \"" << pathStr << "\" at \"" - << foundParentPath << "\""); - } - } - - - TEST(FindParentEquality, Basic) { - - BSONObj exprBSON = fromjson("{a:1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - - assertNoParent(equalities, ""); - assertParent(equalities, "a", "a", 1); - assertParent(equalities, "a.b", "a", 1); - } - - TEST(FindParentEquality, Multi) { - - BSONObj exprBSON = fromjson("{a:1,b:2}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - - assertNoParent(equalities, ""); - assertParent(equalities, "a", "a", 1); - assertParent(equalities, "a.b", "a", 1); - assertParent(equalities, "b", "b", 2); - assertParent(equalities, "b.b", "b", 2); - } - - TEST(FindParentEquality, Nested) { - - BSONObj exprBSON = fromjson("{'a.a':1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - - assertNoParent(equalities, ""); - assertNoParent(equalities, "a"); - assertParent(equalities, "a.a", "a.a", 1); - assertParent(equalities, "a.a.b", "a.a", 1); - } - - TEST(FindParentEquality, NestedMulti) { - - BSONObj exprBSON = fromjson("{'a.a':1,'a.b':2,'c.c':3}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - - assertNoParent(equalities, ""); - assertNoParent(equalities, "a"); - assertNoParent(equalities, "c"); - assertParent(equalities, "a.a", "a.a", 1); - assertParent(equalities, "a.a.a", "a.a", 1); - assertParent(equalities, "a.b", "a.b", 2); - assertParent(equalities, "a.b.b", "a.b", 2); - assertParent(equalities, "c.c", "c.c", 3); - assertParent(equalities, "c.c.c", "c.c", 3); - } - - TEST(FindParentEquality, Empty) { - - BSONObj exprBSON = fromjson("{'':0}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - - assertParent(equalities, "", "", 0); - } - - TEST(FindParentEquality, EmptyMulti) { - - BSONObj exprBSON = fromjson("{'':0,a:1}"); - unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); - EqualityMatches equalities; - ASSERT_OK(extractEqualityMatches(*expr, &equalities)); - - assertParent(equalities, "", "", 0); - assertParent(equalities, "a", "a", 1); - assertParent(equalities, "a.b", "a", 1); - } - -} // unnamed namespace + FAIL(stream() << "Equality matches contained parent for \"" << pathStr << "\" at \"" + << foundParentPath << "\""); + } +} + + +TEST(FindParentEquality, Basic) { + BSONObj exprBSON = fromjson("{a:1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + + assertNoParent(equalities, ""); + assertParent(equalities, "a", "a", 1); + assertParent(equalities, "a.b", "a", 1); +} + +TEST(FindParentEquality, Multi) { + BSONObj exprBSON = fromjson("{a:1,b:2}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + + assertNoParent(equalities, ""); + assertParent(equalities, "a", "a", 1); + assertParent(equalities, "a.b", "a", 1); + assertParent(equalities, "b", "b", 2); + assertParent(equalities, "b.b", "b", 2); +} + +TEST(FindParentEquality, Nested) { + BSONObj exprBSON = fromjson("{'a.a':1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + + assertNoParent(equalities, ""); + assertNoParent(equalities, "a"); + assertParent(equalities, "a.a", "a.a", 1); + assertParent(equalities, "a.a.b", "a.a", 1); +} + +TEST(FindParentEquality, NestedMulti) { + BSONObj exprBSON = fromjson("{'a.a':1,'a.b':2,'c.c':3}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + + assertNoParent(equalities, ""); + assertNoParent(equalities, "a"); + assertNoParent(equalities, "c"); + assertParent(equalities, "a.a", "a.a", 1); + assertParent(equalities, "a.a.a", "a.a", 1); + assertParent(equalities, "a.b", "a.b", 2); + assertParent(equalities, "a.b.b", "a.b", 2); + assertParent(equalities, "c.c", "c.c", 3); + assertParent(equalities, "c.c.c", "c.c", 3); +} + +TEST(FindParentEquality, Empty) { + BSONObj exprBSON = fromjson("{'':0}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + + assertParent(equalities, "", "", 0); +} + +TEST(FindParentEquality, EmptyMulti) { + BSONObj exprBSON = fromjson("{'':0,a:1}"); + unique_ptr<MatchExpression> expr(makeExpr(exprBSON)); + EqualityMatches equalities; + ASSERT_OK(extractEqualityMatches(*expr, &equalities)); + + assertParent(equalities, "", "", 0); + assertParent(equalities, "a", "a", 1); + assertParent(equalities, "a.b", "a", 1); +} + +} // unnamed namespace diff --git a/src/mongo/db/ops/update.cpp b/src/mongo/db/ops/update.cpp index cf60978b4b8..43c9be211ce 100644 --- a/src/mongo/db/ops/update.cpp +++ b/src/mongo/db/ops/update.cpp @@ -54,74 +54,74 @@ namespace mongo { - UpdateResult update(OperationContext* txn, - Database* db, - const UpdateRequest& request, - OpDebug* opDebug) { - invariant(db); - - // Explain should never use this helper. - invariant(!request.isExplain()); - - const NamespaceString& nsString = request.getNamespaceString(); - Collection* collection = db->getCollection(nsString.ns()); - - // The update stage does not create its own collection. As such, if the update is - // an upsert, create the collection that the update stage inserts into beforehand. - if (!collection && request.isUpsert()) { - // We have to have an exclusive lock on the db to be allowed to create the collection. - // Callers should either get an X or create the collection. - const Locker* locker = txn->lockState(); - invariant(locker->isW() || - locker->isLockHeldForMode(ResourceId(RESOURCE_DATABASE, nsString.db()), - MODE_X)); - - MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { - ScopedTransaction transaction(txn, MODE_IX); - Lock::DBLock lk(txn->lockState(), nsString.db(), MODE_X); - - const bool userInitiatedWritesAndNotPrimary = txn->writesAreReplicated() && - !repl::getGlobalReplicationCoordinator()->canAcceptWritesFor(nsString); - - if (userInitiatedWritesAndNotPrimary) { - uassertStatusOK(Status(ErrorCodes::NotMaster, str::stream() - << "Not primary while creating collection " << nsString.ns() - << " during upsert")); - } - WriteUnitOfWork wuow(txn); - collection = db->createCollection(txn, nsString.ns(), CollectionOptions()); - invariant(collection); - wuow.commit(); - } MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "createCollection", nsString.ns()); +UpdateResult update(OperationContext* txn, + Database* db, + const UpdateRequest& request, + OpDebug* opDebug) { + invariant(db); + + // Explain should never use this helper. + invariant(!request.isExplain()); + + const NamespaceString& nsString = request.getNamespaceString(); + Collection* collection = db->getCollection(nsString.ns()); + + // The update stage does not create its own collection. As such, if the update is + // an upsert, create the collection that the update stage inserts into beforehand. + if (!collection && request.isUpsert()) { + // We have to have an exclusive lock on the db to be allowed to create the collection. + // Callers should either get an X or create the collection. + const Locker* locker = txn->lockState(); + invariant(locker->isW() || + locker->isLockHeldForMode(ResourceId(RESOURCE_DATABASE, nsString.db()), MODE_X)); + + MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN { + ScopedTransaction transaction(txn, MODE_IX); + Lock::DBLock lk(txn->lockState(), nsString.db(), MODE_X); + + const bool userInitiatedWritesAndNotPrimary = txn->writesAreReplicated() && + !repl::getGlobalReplicationCoordinator()->canAcceptWritesFor(nsString); + + if (userInitiatedWritesAndNotPrimary) { + uassertStatusOK(Status(ErrorCodes::NotMaster, + str::stream() << "Not primary while creating collection " + << nsString.ns() << " during upsert")); + } + WriteUnitOfWork wuow(txn); + collection = db->createCollection(txn, nsString.ns(), CollectionOptions()); + invariant(collection); + wuow.commit(); } + MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "createCollection", nsString.ns()); + } - // Parse the update, get an executor for it, run the executor, get stats out. - ParsedUpdate parsedUpdate(txn, &request); - uassertStatusOK(parsedUpdate.parseRequest()); - - PlanExecutor* rawExec; - uassertStatusOK(getExecutorUpdate(txn, collection, &parsedUpdate, opDebug, &rawExec)); - std::unique_ptr<PlanExecutor> exec(rawExec); + // Parse the update, get an executor for it, run the executor, get stats out. + ParsedUpdate parsedUpdate(txn, &request); + uassertStatusOK(parsedUpdate.parseRequest()); - uassertStatusOK(exec->executePlan()); - return UpdateStage::makeUpdateResult(exec.get(), opDebug); - } + PlanExecutor* rawExec; + uassertStatusOK(getExecutorUpdate(txn, collection, &parsedUpdate, opDebug, &rawExec)); + std::unique_ptr<PlanExecutor> exec(rawExec); - BSONObj applyUpdateOperators(const BSONObj& from, const BSONObj& operators) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - Status status = driver.parse(operators); - if (!status.isOK()) { - uasserted(16838, status.reason()); - } + uassertStatusOK(exec->executePlan()); + return UpdateStage::makeUpdateResult(exec.get(), opDebug); +} - mutablebson::Document doc(from, mutablebson::Document::kInPlaceDisabled); - status = driver.update(StringData(), &doc); - if (!status.isOK()) { - uasserted(16839, status.reason()); - } +BSONObj applyUpdateOperators(const BSONObj& from, const BSONObj& operators) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + Status status = driver.parse(operators); + if (!status.isOK()) { + uasserted(16838, status.reason()); + } - return doc.getObject(); + mutablebson::Document doc(from, mutablebson::Document::kInPlaceDisabled); + status = driver.update(StringData(), &doc); + if (!status.isOK()) { + uasserted(16839, status.reason()); } + return doc.getObject(); +} + } // namespace mongo diff --git a/src/mongo/db/ops/update.h b/src/mongo/db/ops/update.h index c4c5e71edff..06cec7fa90f 100644 --- a/src/mongo/db/ops/update.h +++ b/src/mongo/db/ops/update.h @@ -37,27 +37,27 @@ namespace mongo { - class CanonicalQuery; - class Database; - class OperationContext; - class UpdateDriver; +class CanonicalQuery; +class Database; +class OperationContext; +class UpdateDriver; - /** - * Utility method to execute an update described by "request". - * - * Caller must hold the appropriate database locks. - */ - UpdateResult update(OperationContext* txn, - Database* db, - const UpdateRequest& request, - OpDebug* opDebug); +/** + * Utility method to execute an update described by "request". + * + * Caller must hold the appropriate database locks. + */ +UpdateResult update(OperationContext* txn, + Database* db, + const UpdateRequest& request, + OpDebug* opDebug); - /** - * takes the from document and returns a new document - * after apply all the operators - * e.g. - * applyUpdateOperators( BSON( "x" << 1 ) , BSON( "$inc" << BSON( "x" << 1 ) ) ); - * returns: { x : 2 } - */ - BSONObj applyUpdateOperators( const BSONObj& from, const BSONObj& operators ); +/** + * takes the from document and returns a new document + * after apply all the operators + * e.g. + * applyUpdateOperators( BSON( "x" << 1 ) , BSON( "$inc" << BSON( "x" << 1 ) ) ); + * returns: { x : 2 } + */ +BSONObj applyUpdateOperators(const BSONObj& from, const BSONObj& operators); } // namespace mongo diff --git a/src/mongo/db/ops/update_driver.cpp b/src/mongo/db/ops/update_driver.cpp index 28ae06215ae..ccd966dbb81 100644 --- a/src/mongo/db/ops/update_driver.cpp +++ b/src/mongo/db/ops/update_driver.cpp @@ -44,353 +44,341 @@ namespace mongo { - namespace str = mongoutils::str; - namespace mb = mongo::mutablebson; +namespace str = mongoutils::str; +namespace mb = mongo::mutablebson; - using std::unique_ptr; - using std::vector; +using std::unique_ptr; +using std::vector; - using pathsupport::EqualityMatches; +using pathsupport::EqualityMatches; - UpdateDriver::UpdateDriver(const Options& opts) - : _replacementMode(false) - , _indexedFields(NULL) - , _logOp(opts.logOp) - , _modOptions(opts.modOptions) - , _affectIndices(false) - , _positional(false) { - } - - UpdateDriver::~UpdateDriver() { - clear(); - } +UpdateDriver::UpdateDriver(const Options& opts) + : _replacementMode(false), + _indexedFields(NULL), + _logOp(opts.logOp), + _modOptions(opts.modOptions), + _affectIndices(false), + _positional(false) {} - Status UpdateDriver::parse(const BSONObj& updateExpr, const bool multi) { - clear(); +UpdateDriver::~UpdateDriver() { + clear(); +} - // Check if the update expression is a full object replacement. - if (*updateExpr.firstElementFieldName() != '$') { - if (multi) { - return Status(ErrorCodes::FailedToParse, - "multi update only works with $ operators"); - } +Status UpdateDriver::parse(const BSONObj& updateExpr, const bool multi) { + clear(); - // Modifiers expect BSONElements as input. But the input to object replace is, by - // definition, an object. We wrap the 'updateExpr' as the mod is expecting. Note - // that the wrapper is temporary so the object replace mod should make a copy of - // the object. - unique_ptr<ModifierObjectReplace> mod(new ModifierObjectReplace); - BSONObj wrapper = BSON( "dummy" << updateExpr ); - Status status = mod->init(wrapper.firstElement(), _modOptions); - if (!status.isOK()) { - return status; - } - - _mods.push_back(mod.release()); - - // Register the fact that this driver will only do full object replacements. - _replacementMode = true; + // Check if the update expression is a full object replacement. + if (*updateExpr.firstElementFieldName() != '$') { + if (multi) { + return Status(ErrorCodes::FailedToParse, "multi update only works with $ operators"); + } - return Status::OK(); + // Modifiers expect BSONElements as input. But the input to object replace is, by + // definition, an object. We wrap the 'updateExpr' as the mod is expecting. Note + // that the wrapper is temporary so the object replace mod should make a copy of + // the object. + unique_ptr<ModifierObjectReplace> mod(new ModifierObjectReplace); + BSONObj wrapper = BSON("dummy" << updateExpr); + Status status = mod->init(wrapper.firstElement(), _modOptions); + if (!status.isOK()) { + return status; } - // The update expression is made of mod operators, that is - // { <$mod>: {...}, <$mod>: {...}, ... } - BSONObjIterator outerIter(updateExpr); - while (outerIter.more()) { - BSONElement outerModElem = outerIter.next(); - - // Check whether this is a valid mod type. - modifiertable::ModifierType modType = modifiertable::getType(outerModElem.fieldName()); - if (modType == modifiertable::MOD_UNKNOWN) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "Unknown modifier: " << outerModElem.fieldName()); - } + _mods.push_back(mod.release()); - // Check whether there is indeed a list of mods under this modifier. - if (outerModElem.type() != Object) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "Modifiers operate on fields but we found a " - << typeName(outerModElem.type()) - << " instead. For example: {$mod: {<field>: ...}}" - << " not {" << outerModElem.toString() << "}"); - } + // Register the fact that this driver will only do full object replacements. + _replacementMode = true; - // Check whether there are indeed mods under this modifier. - if (outerModElem.embeddedObject().isEmpty()) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "'" << outerModElem.fieldName() - << "' is empty. You must specify a field like so: " - "{" << outerModElem.fieldName() << ": {<field>: ...}}"); - } + return Status::OK(); + } - BSONObjIterator innerIter(outerModElem.embeddedObject()); - while (innerIter.more()) { - BSONElement innerModElem = innerIter.next(); + // The update expression is made of mod operators, that is + // { <$mod>: {...}, <$mod>: {...}, ... } + BSONObjIterator outerIter(updateExpr); + while (outerIter.more()) { + BSONElement outerModElem = outerIter.next(); - Status status = addAndParse(modType, innerModElem); - if (!status.isOK()) { - return status; - } - } + // Check whether this is a valid mod type. + modifiertable::ModifierType modType = modifiertable::getType(outerModElem.fieldName()); + if (modType == modifiertable::MOD_UNKNOWN) { + return Status(ErrorCodes::FailedToParse, + str::stream() << "Unknown modifier: " << outerModElem.fieldName()); } - // Register the fact that there will be only $mod's in this driver -- no object - // replacement. - _replacementMode = false; - - return Status::OK(); - } + // Check whether there is indeed a list of mods under this modifier. + if (outerModElem.type() != Object) { + return Status(ErrorCodes::FailedToParse, + str::stream() << "Modifiers operate on fields but we found a " + << typeName(outerModElem.type()) + << " instead. For example: {$mod: {<field>: ...}}" + << " not {" << outerModElem.toString() << "}"); + } - inline Status UpdateDriver::addAndParse(const modifiertable::ModifierType type, - const BSONElement& elem) { - if (elem.eoo()) { + // Check whether there are indeed mods under this modifier. + if (outerModElem.embeddedObject().isEmpty()) { return Status(ErrorCodes::FailedToParse, - str::stream() << "'" << elem.fieldName() - << "' has no value in : " << elem - << " which is not allowed for any $" << type << " mod."); + str::stream() << "'" << outerModElem.fieldName() + << "' is empty. You must specify a field like so: " + "{" << outerModElem.fieldName() << ": {<field>: ...}}"); } - unique_ptr<ModifierInterface> mod(modifiertable::makeUpdateMod(type)); - dassert(mod.get()); + BSONObjIterator innerIter(outerModElem.embeddedObject()); + while (innerIter.more()) { + BSONElement innerModElem = innerIter.next(); - bool positional = false; - Status status = mod->init(elem, _modOptions, &positional); - if (!status.isOK()) { - return status; + Status status = addAndParse(modType, innerModElem); + if (!status.isOK()) { + return status; + } } + } - // If any modifier indicates that it requires a positional match, toggle the - // _positional flag to true. - _positional = _positional || positional; + // Register the fact that there will be only $mod's in this driver -- no object + // replacement. + _replacementMode = false; - _mods.push_back(mod.release()); + return Status::OK(); +} - return Status::OK(); +inline Status UpdateDriver::addAndParse(const modifiertable::ModifierType type, + const BSONElement& elem) { + if (elem.eoo()) { + return Status(ErrorCodes::FailedToParse, + str::stream() << "'" << elem.fieldName() << "' has no value in : " << elem + << " which is not allowed for any $" << type << " mod."); } - Status UpdateDriver::populateDocumentWithQueryFields(const BSONObj& query, - const vector<FieldRef*>* immutablePaths, - mutablebson::Document& doc) const { - CanonicalQuery* rawCG; - // We canonicalize the query to collapse $and/$or, and the first arg (ns) is not needed - // Also, because this is for the upsert case, where we insert a new document if one was - // not found, the $where clause does not make sense, hence empty WhereCallback. - Status s = CanonicalQuery::canonicalize("", query, &rawCG, WhereCallbackNoop()); - if (!s.isOK()) - return s; - unique_ptr<CanonicalQuery> cq(rawCG); - return populateDocumentWithQueryFields(rawCG, immutablePaths, doc); - } + unique_ptr<ModifierInterface> mod(modifiertable::makeUpdateMod(type)); + dassert(mod.get()); - Status UpdateDriver::populateDocumentWithQueryFields(const CanonicalQuery* query, - const vector<FieldRef*>* immutablePathsPtr, - mutablebson::Document& doc) const { - EqualityMatches equalities; - Status status = Status::OK(); + bool positional = false; + Status status = mod->init(elem, _modOptions, &positional); + if (!status.isOK()) { + return status; + } - if (isDocReplacement()) { + // If any modifier indicates that it requires a positional match, toggle the + // _positional flag to true. + _positional = _positional || positional; + + _mods.push_back(mod.release()); + + return Status::OK(); +} + +Status UpdateDriver::populateDocumentWithQueryFields(const BSONObj& query, + const vector<FieldRef*>* immutablePaths, + mutablebson::Document& doc) const { + CanonicalQuery* rawCG; + // We canonicalize the query to collapse $and/$or, and the first arg (ns) is not needed + // Also, because this is for the upsert case, where we insert a new document if one was + // not found, the $where clause does not make sense, hence empty WhereCallback. + Status s = CanonicalQuery::canonicalize("", query, &rawCG, WhereCallbackNoop()); + if (!s.isOK()) + return s; + unique_ptr<CanonicalQuery> cq(rawCG); + return populateDocumentWithQueryFields(rawCG, immutablePaths, doc); +} + +Status UpdateDriver::populateDocumentWithQueryFields(const CanonicalQuery* query, + const vector<FieldRef*>* immutablePathsPtr, + mutablebson::Document& doc) const { + EqualityMatches equalities; + Status status = Status::OK(); + + if (isDocReplacement()) { + FieldRefSet pathsToExtract; + + // TODO: Refactor update logic, make _id just another immutable field + static const FieldRef idPath("_id"); + static const vector<FieldRef*> emptyImmutablePaths; + const vector<FieldRef*>& immutablePaths = + immutablePathsPtr ? *immutablePathsPtr : emptyImmutablePaths; + + pathsToExtract.fillFrom(immutablePaths); + pathsToExtract.insert(&idPath); + + // Extract only immutable fields from replacement-style + status = + pathsupport::extractFullEqualityMatches(*query->root(), pathsToExtract, &equalities); + } else { + // Extract all fields from op-style + status = pathsupport::extractEqualityMatches(*query->root(), &equalities); + } - FieldRefSet pathsToExtract; + if (!status.isOK()) + return status; - // TODO: Refactor update logic, make _id just another immutable field - static const FieldRef idPath("_id"); - static const vector<FieldRef*> emptyImmutablePaths; - const vector<FieldRef*>& immutablePaths = - immutablePathsPtr ? *immutablePathsPtr : emptyImmutablePaths; + status = pathsupport::addEqualitiesToDoc(equalities, &doc); + return status; +} + +Status UpdateDriver::update(StringData matchedField, + mutablebson::Document* doc, + BSONObj* logOpRec, + FieldRefSet* updatedFields, + bool* docWasModified) { + // TODO: assert that update() is called at most once in a !_multi case. + + // Use the passed in FieldRefSet + FieldRefSet* targetFields = updatedFields; + + // If we didn't get a FieldRefSet* from the caller, allocate storage and use + // the unique_ptr for lifecycle management + unique_ptr<FieldRefSet> targetFieldScopedPtr; + if (!targetFields) { + targetFieldScopedPtr.reset(new FieldRefSet()); + targetFields = targetFieldScopedPtr.get(); + } - pathsToExtract.fillFrom(immutablePaths); - pathsToExtract.insert(&idPath); + _affectIndices = (isDocReplacement() && (_indexedFields != NULL)); - // Extract only immutable fields from replacement-style - status = pathsupport::extractFullEqualityMatches(*query->root(), - pathsToExtract, - &equalities); - } - else { - // Extract all fields from op-style - status = pathsupport::extractEqualityMatches(*query->root(), &equalities); - } + _logDoc.reset(); + LogBuilder logBuilder(_logDoc.root()); - if (!status.isOK()) + // Ask each of the mods to type check whether they can operate over the current document + // and, if so, to change that document accordingly. + for (vector<ModifierInterface*>::iterator it = _mods.begin(); it != _mods.end(); ++it) { + ModifierInterface::ExecInfo execInfo; + Status status = (*it)->prepare(doc->root(), matchedField, &execInfo); + if (!status.isOK()) { return status; + } - status = pathsupport::addEqualitiesToDoc(equalities, &doc); - return status; - } + // If a mod wants to be applied only if this is an upsert (or only if this is a + // strict update), we should respect that. If a mod doesn't care, it would state + // it is fine with ANY update context. + const bool validContext = (execInfo.context == ModifierInterface::ExecInfo::ANY_CONTEXT || + execInfo.context == _context); - Status UpdateDriver::update(StringData matchedField, - mutablebson::Document* doc, - BSONObj* logOpRec, - FieldRefSet* updatedFields, - bool* docWasModified) { - // TODO: assert that update() is called at most once in a !_multi case. - - // Use the passed in FieldRefSet - FieldRefSet* targetFields = updatedFields; - - // If we didn't get a FieldRefSet* from the caller, allocate storage and use - // the unique_ptr for lifecycle management - unique_ptr<FieldRefSet> targetFieldScopedPtr; - if (!targetFields) { - targetFieldScopedPtr.reset(new FieldRefSet()); - targetFields = targetFieldScopedPtr.get(); + // Nothing to do if not in a valid context. + if (!validContext) { + continue; } - _affectIndices = (isDocReplacement() && (_indexedFields != NULL)); - _logDoc.reset(); - LogBuilder logBuilder(_logDoc.root()); - - // Ask each of the mods to type check whether they can operate over the current document - // and, if so, to change that document accordingly. - for (vector<ModifierInterface*>::iterator it = _mods.begin(); it != _mods.end(); ++it) { - ModifierInterface::ExecInfo execInfo; - Status status = (*it)->prepare(doc->root(), matchedField, &execInfo); - if (!status.isOK()) { - return status; + // Gather which fields this mod is interested on and whether these fields were + // "taken" by previous mods. Note that not all mods are multi-field mods. When we + // see an empty field, we may stop looking for others. + for (int i = 0; i < ModifierInterface::ExecInfo::MAX_NUM_FIELDS; i++) { + if (execInfo.fieldRef[i] == 0) { + break; } - // If a mod wants to be applied only if this is an upsert (or only if this is a - // strict update), we should respect that. If a mod doesn't care, it would state - // it is fine with ANY update context. - const bool validContext = (execInfo.context == ModifierInterface::ExecInfo::ANY_CONTEXT || - execInfo.context == _context); - - // Nothing to do if not in a valid context. - if (!validContext) { - continue; + // Record each field being updated but check for conflicts first + const FieldRef* other; + if (!targetFields->insert(execInfo.fieldRef[i], &other)) { + return Status(ErrorCodes::ConflictingUpdateOperators, + str::stream() << "Cannot update '" << other->dottedField() + << "' and '" << execInfo.fieldRef[i]->dottedField() + << "' at the same time"); } - - // Gather which fields this mod is interested on and whether these fields were - // "taken" by previous mods. Note that not all mods are multi-field mods. When we - // see an empty field, we may stop looking for others. - for (int i = 0; i < ModifierInterface::ExecInfo::MAX_NUM_FIELDS; i++) { - if (execInfo.fieldRef[i] == 0) { - break; - } - - // Record each field being updated but check for conflicts first - const FieldRef* other; - if (!targetFields->insert(execInfo.fieldRef[i], &other)) { - return Status(ErrorCodes::ConflictingUpdateOperators, - str::stream() << "Cannot update '" - << other->dottedField() - << "' and '" - << execInfo.fieldRef[i]->dottedField() - << "' at the same time"); - } - - // We start with the expectation that a mod will be in-place. But if the mod - // touched an indexed field and the mod will indeed be executed -- that is, it - // is not a no-op and it is in a valid context -- then we switch back to a - // non-in-place mode. - // - // TODO: make mightBeIndexed and fieldRef like each other. - if (!_affectIndices && - !execInfo.noOp && - _indexedFields && - _indexedFields->mightBeIndexed(execInfo.fieldRef[i]->dottedField())) { - _affectIndices = true; - doc->disableInPlaceUpdates(); - } + // We start with the expectation that a mod will be in-place. But if the mod + // touched an indexed field and the mod will indeed be executed -- that is, it + // is not a no-op and it is in a valid context -- then we switch back to a + // non-in-place mode. + // + // TODO: make mightBeIndexed and fieldRef like each other. + if (!_affectIndices && !execInfo.noOp && _indexedFields && + _indexedFields->mightBeIndexed(execInfo.fieldRef[i]->dottedField())) { + _affectIndices = true; + doc->disableInPlaceUpdates(); } + } - if (!execInfo.noOp) { - status = (*it)->apply(); + if (!execInfo.noOp) { + status = (*it)->apply(); - if (docWasModified) - *docWasModified = true; + if (docWasModified) + *docWasModified = true; - if (!status.isOK()) { - return status; - } + if (!status.isOK()) { + return status; } + } - // If we require a replication oplog entry for this update, go ahead and generate one. - if (!execInfo.noOp && _logOp && logOpRec) { - status = (*it)->log(&logBuilder); - if (!status.isOK()) { - return status; - } + // If we require a replication oplog entry for this update, go ahead and generate one. + if (!execInfo.noOp && _logOp && logOpRec) { + status = (*it)->log(&logBuilder); + if (!status.isOK()) { + return status; } - } - - if (_logOp && logOpRec) - *logOpRec = _logDoc.getObject(); - - return Status::OK(); } - size_t UpdateDriver::numMods() const { - return _mods.size(); + if (_logOp && logOpRec) + *logOpRec = _logDoc.getObject(); + + return Status::OK(); +} + +size_t UpdateDriver::numMods() const { + return _mods.size(); +} + +bool UpdateDriver::isDocReplacement() const { + return _replacementMode; +} + +bool UpdateDriver::modsAffectIndices() const { + return _affectIndices; +} + +void UpdateDriver::refreshIndexKeys(const UpdateIndexData* indexedFields) { + _indexedFields = indexedFields; +} + +bool UpdateDriver::logOp() const { + return _logOp; +} + +void UpdateDriver::setLogOp(bool logOp) { + _logOp = logOp; +} + +ModifierInterface::Options UpdateDriver::modOptions() const { + return _modOptions; +} + +void UpdateDriver::setModOptions(ModifierInterface::Options modOpts) { + _modOptions = modOpts; +} + +ModifierInterface::ExecInfo::UpdateContext UpdateDriver::context() const { + return _context; +} + +void UpdateDriver::setContext(ModifierInterface::ExecInfo::UpdateContext context) { + _context = context; +} + +BSONObj UpdateDriver::makeOplogEntryQuery(const BSONObj& doc, bool multi) const { + BSONObjBuilder idPattern; + BSONElement id; + // NOTE: If the matching object lacks an id, we'll log + // with the original pattern. This isn't replay-safe. + // It might make sense to suppress the log instead + // if there's no id. + if (doc.getObjectID(id)) { + idPattern.append(id); + return idPattern.obj(); + } else { + uassert(16980, + str::stream() << "Multi-update operations require all documents to " + "have an '_id' field. " << doc.toString(false, false), + !multi); + return doc; } - - bool UpdateDriver::isDocReplacement() const { - return _replacementMode; - } - - bool UpdateDriver::modsAffectIndices() const { - return _affectIndices; - } - - void UpdateDriver::refreshIndexKeys(const UpdateIndexData* indexedFields) { - _indexedFields = indexedFields; - } - - bool UpdateDriver::logOp() const { - return _logOp; - } - - void UpdateDriver::setLogOp(bool logOp) { - _logOp = logOp; - } - - ModifierInterface::Options UpdateDriver::modOptions() const { - return _modOptions; - } - - void UpdateDriver::setModOptions(ModifierInterface::Options modOpts) { - _modOptions = modOpts; - } - - ModifierInterface::ExecInfo::UpdateContext UpdateDriver::context() const { - return _context; - } - - void UpdateDriver::setContext(ModifierInterface::ExecInfo::UpdateContext context) { - _context = context; - } - - BSONObj UpdateDriver::makeOplogEntryQuery(const BSONObj& doc, bool multi) const { - BSONObjBuilder idPattern; - BSONElement id; - // NOTE: If the matching object lacks an id, we'll log - // with the original pattern. This isn't replay-safe. - // It might make sense to suppress the log instead - // if there's no id. - if ( doc.getObjectID( id ) ) { - idPattern.append( id ); - return idPattern.obj(); - } - else { - uassert( 16980, - str::stream() << "Multi-update operations require all documents to " - "have an '_id' field. " << doc.toString(false, false), - ! multi ); - return doc; - } - } - void UpdateDriver::clear() { - for (vector<ModifierInterface*>::iterator it = _mods.begin(); it != _mods.end(); ++it) { - delete *it; - } - _mods.clear(); - _indexedFields = NULL; - _replacementMode = false; - _positional = false; +} +void UpdateDriver::clear() { + for (vector<ModifierInterface*>::iterator it = _mods.begin(); it != _mods.end(); ++it) { + delete *it; } + _mods.clear(); + _indexedFields = NULL; + _replacementMode = false; + _positional = false; +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/update_driver.h b/src/mongo/db/ops/update_driver.h index 067c432ebe2..00e3e2eb10f 100644 --- a/src/mongo/db/ops/update_driver.h +++ b/src/mongo/db/ops/update_driver.h @@ -43,153 +43,150 @@ namespace mongo { - class UpdateDriver { - public: - - struct Options; - UpdateDriver(const Options& opts); - - ~UpdateDriver(); - - /** - * Returns OK and fills in '_mods' if 'updateExpr' is correct. Otherwise returns an - * error status with a corresponding description. - */ - Status parse(const BSONObj& updateExpr, const bool multi = false); - - /** - * Fills in document with any fields in the query which are valid. - * - * Valid fields include equality matches like "a":1, or "a.b":false - * - * Each valid field will be expanded (from dot notation) and conflicts will be - * checked for all fields added to the underlying document. - * - * Returns Status::OK() if the document can be used. If there are any error or - * conflicts along the way then those errors will be returned. - */ - Status populateDocumentWithQueryFields(const BSONObj& query, - const std::vector<FieldRef*>* immutablePaths, - mutablebson::Document& doc) const; - - Status populateDocumentWithQueryFields(const CanonicalQuery* query, - const std::vector<FieldRef*>* immutablePaths, - mutablebson::Document& doc) const; - - /** - * return a BSONObj with the _id field of the doc passed in, or the doc itself. - * If no _id and multi, error. - */ - BSONObj makeOplogEntryQuery(const BSONObj& doc, bool multi) const; - - /** - * Returns OK and executes '_mods' over 'doc', generating 'newObj'. If any mod is - * positional, use 'matchedField' (index of the array item matched). If doc allows - * mods to be applied in place and no index updating is involved, then the mods may - * be applied "in place" over 'doc'. - * - * If the driver's '_logOp' mode is turned on, and if 'logOpRec' is not NULL, fills in - * the latter with the oplog entry corresponding to the update. If '_mods' can't be - * applied, returns an error status with a corresponding description. - * - * If a non-NULL updatedField vector* is supplied, - * then all updated fields will be added to it. - */ - Status update(StringData matchedField, - mutablebson::Document* doc, - BSONObj* logOpRec = NULL, - FieldRefSet* updatedFields = NULL, - bool* docWasModified = NULL); - - // - // Accessors - // - - size_t numMods() const; - - bool isDocReplacement() const; - - bool modsAffectIndices() const; - void refreshIndexKeys(const UpdateIndexData* indexedFields); - - bool logOp() const; - void setLogOp(bool logOp); - - ModifierInterface::Options modOptions() const; - void setModOptions(ModifierInterface::Options modOpts); - - ModifierInterface::ExecInfo::UpdateContext context() const; - void setContext(ModifierInterface::ExecInfo::UpdateContext context); - - mutablebson::Document& getDocument() { - return _objDoc; - } - - const mutablebson::Document& getDocument() const { - return _objDoc; - } - - bool needMatchDetails() const { - return _positional; - } - - private: - - /** Resets the state of the class associated with mods (not the error state) */ - void clear(); - - /** Create the modifier and add it to the back of the modifiers vector */ - inline Status addAndParse(const modifiertable::ModifierType type, - const BSONElement& elem); - - // - // immutable properties after parsing - // - - // Is there a list of $mod's on '_mods' or is it just full object replacement? - bool _replacementMode; - - // Collection of update mod instances. Owned here. - std::vector<ModifierInterface*> _mods; - - // What are the list of fields in the collection over which the update is going to be - // applied that participate in indices? - // - // NOTE: Owned by the collection's info cache!. - const UpdateIndexData* _indexedFields; - - // - // mutable properties after parsing - // - - // Should this driver generate an oplog record when it applies the update? - bool _logOp; - - // The options to initiate the mods with - ModifierInterface::Options _modOptions; +class UpdateDriver { +public: + struct Options; + UpdateDriver(const Options& opts); + + ~UpdateDriver(); + + /** + * Returns OK and fills in '_mods' if 'updateExpr' is correct. Otherwise returns an + * error status with a corresponding description. + */ + Status parse(const BSONObj& updateExpr, const bool multi = false); + + /** + * Fills in document with any fields in the query which are valid. + * + * Valid fields include equality matches like "a":1, or "a.b":false + * + * Each valid field will be expanded (from dot notation) and conflicts will be + * checked for all fields added to the underlying document. + * + * Returns Status::OK() if the document can be used. If there are any error or + * conflicts along the way then those errors will be returned. + */ + Status populateDocumentWithQueryFields(const BSONObj& query, + const std::vector<FieldRef*>* immutablePaths, + mutablebson::Document& doc) const; + + Status populateDocumentWithQueryFields(const CanonicalQuery* query, + const std::vector<FieldRef*>* immutablePaths, + mutablebson::Document& doc) const; + + /** + * return a BSONObj with the _id field of the doc passed in, or the doc itself. + * If no _id and multi, error. + */ + BSONObj makeOplogEntryQuery(const BSONObj& doc, bool multi) const; + + /** + * Returns OK and executes '_mods' over 'doc', generating 'newObj'. If any mod is + * positional, use 'matchedField' (index of the array item matched). If doc allows + * mods to be applied in place and no index updating is involved, then the mods may + * be applied "in place" over 'doc'. + * + * If the driver's '_logOp' mode is turned on, and if 'logOpRec' is not NULL, fills in + * the latter with the oplog entry corresponding to the update. If '_mods' can't be + * applied, returns an error status with a corresponding description. + * + * If a non-NULL updatedField vector* is supplied, + * then all updated fields will be added to it. + */ + Status update(StringData matchedField, + mutablebson::Document* doc, + BSONObj* logOpRec = NULL, + FieldRefSet* updatedFields = NULL, + bool* docWasModified = NULL); + + // + // Accessors + // + + size_t numMods() const; + + bool isDocReplacement() const; + + bool modsAffectIndices() const; + void refreshIndexKeys(const UpdateIndexData* indexedFields); + + bool logOp() const; + void setLogOp(bool logOp); + + ModifierInterface::Options modOptions() const; + void setModOptions(ModifierInterface::Options modOpts); + + ModifierInterface::ExecInfo::UpdateContext context() const; + void setContext(ModifierInterface::ExecInfo::UpdateContext context); + + mutablebson::Document& getDocument() { + return _objDoc; + } + + const mutablebson::Document& getDocument() const { + return _objDoc; + } + + bool needMatchDetails() const { + return _positional; + } + +private: + /** Resets the state of the class associated with mods (not the error state) */ + void clear(); + + /** Create the modifier and add it to the back of the modifiers vector */ + inline Status addAndParse(const modifiertable::ModifierType type, const BSONElement& elem); + + // + // immutable properties after parsing + // + + // Is there a list of $mod's on '_mods' or is it just full object replacement? + bool _replacementMode; + + // Collection of update mod instances. Owned here. + std::vector<ModifierInterface*> _mods; + + // What are the list of fields in the collection over which the update is going to be + // applied that participate in indices? + // + // NOTE: Owned by the collection's info cache!. + const UpdateIndexData* _indexedFields; + + // + // mutable properties after parsing + // + + // Should this driver generate an oplog record when it applies the update? + bool _logOp; + + // The options to initiate the mods with + ModifierInterface::Options _modOptions; + + // Are any of the fields mentioned in the mods participating in any index? Is set anew + // at each call to update. + bool _affectIndices; - // Are any of the fields mentioned in the mods participating in any index? Is set anew - // at each call to update. - bool _affectIndices; + // Do any of the mods require positional match details when calling 'prepare'? + bool _positional; - // Do any of the mods require positional match details when calling 'prepare'? - bool _positional; + // Is this update going to be an upsert? + ModifierInterface::ExecInfo::UpdateContext _context; - // Is this update going to be an upsert? - ModifierInterface::ExecInfo::UpdateContext _context; + // The document used to represent or store the object being updated. + mutablebson::Document _objDoc; - // The document used to represent or store the object being updated. - mutablebson::Document _objDoc; + // The document used to build the oplog entry for the update. + mutablebson::Document _logDoc; +}; - // The document used to build the oplog entry for the update. - mutablebson::Document _logDoc; - }; +struct UpdateDriver::Options { + bool logOp; + ModifierInterface::Options modOptions; - struct UpdateDriver::Options { - bool logOp; - ModifierInterface::Options modOptions; + Options() : logOp(false), modOptions() {} +}; - Options() : logOp(false), modOptions() {} - }; - -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/update_driver_test.cpp b/src/mongo/db/ops/update_driver_test.cpp index 1fd93aa27c1..1a778b231a9 100644 --- a/src/mongo/db/ops/update_driver_test.cpp +++ b/src/mongo/db/ops/update_driver_test.cpp @@ -42,362 +42,351 @@ namespace { - using mongo::BSONObj; - using mongo::BSONElement; - using mongo::BSONObjIterator; - using mongo::FieldRef; - using mongo::fromjson; - using mongo::OwnedPointerVector; - using mongo::UpdateIndexData; - using mongo::mutablebson::Document; - using mongo::StringData; - using mongo::UpdateDriver; - using mongoutils::str::stream; - - TEST(Parse, Normal) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_OK(driver.parse(fromjson("{$set:{a:1}}"))); - ASSERT_EQUALS(driver.numMods(), 1U); - ASSERT_FALSE(driver.isDocReplacement()); - } - - TEST(Parse, MultiMods) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_OK(driver.parse(fromjson("{$set:{a:1, b:1}}"))); - ASSERT_EQUALS(driver.numMods(), 2U); - ASSERT_FALSE(driver.isDocReplacement()); - } - - TEST(Parse, MixingMods) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_OK(driver.parse(fromjson("{$set:{a:1}, $unset:{b:1}}"))); - ASSERT_EQUALS(driver.numMods(), 2U); - ASSERT_FALSE(driver.isDocReplacement()); - } - - TEST(Parse, ObjectReplacment) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_OK(driver.parse(fromjson("{obj: \"obj replacement\"}"))); - ASSERT_TRUE(driver.isDocReplacement()); - } - - TEST(Parse, EmptyMod) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_NOT_OK(driver.parse(fromjson("{$set:{}}"))); - } - - TEST(Parse, WrongMod) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_NOT_OK(driver.parse(fromjson("{$xyz:{a:1}}"))); - } - - TEST(Parse, WrongType) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_NOT_OK(driver.parse(fromjson("{$set:[{a:1}]}"))); - } - - TEST(Parse, ModsWithLaterObjReplacement) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_NOT_OK(driver.parse(fromjson("{$set:{a:1}, obj: \"obj replacement\"}"))); - } - - TEST(Parse, PushAll) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_OK(driver.parse(fromjson("{$pushAll:{a:[1,2,3]}}"))); - ASSERT_EQUALS(driver.numMods(), 1U); - ASSERT_FALSE(driver.isDocReplacement()); - } - - TEST(Parse, SetOnInsert) { - UpdateDriver::Options opts; - UpdateDriver driver(opts); - ASSERT_OK(driver.parse(fromjson("{$setOnInsert:{a:1}}"))); - ASSERT_EQUALS(driver.numMods(), 1U); - ASSERT_FALSE(driver.isDocReplacement()); - } - - // - // Tests of creating a base for an upsert from a query document - // $or, $and, $all get special handling, as does the _id field - // - // NONGOAL: Testing all query parsing and nesting combinations - // - - class CreateFromQueryFixture : public mongo::unittest::Test { - public: - - CreateFromQueryFixture() - : _driverOps(new UpdateDriver(UpdateDriver::Options())), - _driverRepl(new UpdateDriver(UpdateDriver::Options())) { - _driverOps->parse(fromjson("{$set:{'_':1}}")); - _driverRepl->parse(fromjson("{}")); - } - - Document& doc() { - return _doc; - } - - UpdateDriver& driverOps() { - return *_driverOps; - } +using mongo::BSONObj; +using mongo::BSONElement; +using mongo::BSONObjIterator; +using mongo::FieldRef; +using mongo::fromjson; +using mongo::OwnedPointerVector; +using mongo::UpdateIndexData; +using mongo::mutablebson::Document; +using mongo::StringData; +using mongo::UpdateDriver; +using mongoutils::str::stream; + +TEST(Parse, Normal) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_OK(driver.parse(fromjson("{$set:{a:1}}"))); + ASSERT_EQUALS(driver.numMods(), 1U); + ASSERT_FALSE(driver.isDocReplacement()); +} + +TEST(Parse, MultiMods) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_OK(driver.parse(fromjson("{$set:{a:1, b:1}}"))); + ASSERT_EQUALS(driver.numMods(), 2U); + ASSERT_FALSE(driver.isDocReplacement()); +} + +TEST(Parse, MixingMods) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_OK(driver.parse(fromjson("{$set:{a:1}, $unset:{b:1}}"))); + ASSERT_EQUALS(driver.numMods(), 2U); + ASSERT_FALSE(driver.isDocReplacement()); +} + +TEST(Parse, ObjectReplacment) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_OK(driver.parse(fromjson("{obj: \"obj replacement\"}"))); + ASSERT_TRUE(driver.isDocReplacement()); +} + +TEST(Parse, EmptyMod) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_NOT_OK(driver.parse(fromjson("{$set:{}}"))); +} + +TEST(Parse, WrongMod) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_NOT_OK(driver.parse(fromjson("{$xyz:{a:1}}"))); +} + +TEST(Parse, WrongType) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_NOT_OK(driver.parse(fromjson("{$set:[{a:1}]}"))); +} + +TEST(Parse, ModsWithLaterObjReplacement) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_NOT_OK(driver.parse(fromjson("{$set:{a:1}, obj: \"obj replacement\"}"))); +} + +TEST(Parse, PushAll) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_OK(driver.parse(fromjson("{$pushAll:{a:[1,2,3]}}"))); + ASSERT_EQUALS(driver.numMods(), 1U); + ASSERT_FALSE(driver.isDocReplacement()); +} + +TEST(Parse, SetOnInsert) { + UpdateDriver::Options opts; + UpdateDriver driver(opts); + ASSERT_OK(driver.parse(fromjson("{$setOnInsert:{a:1}}"))); + ASSERT_EQUALS(driver.numMods(), 1U); + ASSERT_FALSE(driver.isDocReplacement()); +} + +// +// Tests of creating a base for an upsert from a query document +// $or, $and, $all get special handling, as does the _id field +// +// NONGOAL: Testing all query parsing and nesting combinations +// + +class CreateFromQueryFixture : public mongo::unittest::Test { +public: + CreateFromQueryFixture() + : _driverOps(new UpdateDriver(UpdateDriver::Options())), + _driverRepl(new UpdateDriver(UpdateDriver::Options())) { + _driverOps->parse(fromjson("{$set:{'_':1}}")); + _driverRepl->parse(fromjson("{}")); + } + + Document& doc() { + return _doc; + } + + UpdateDriver& driverOps() { + return *_driverOps; + } + + UpdateDriver& driverRepl() { + return *_driverRepl; + } + +private: + std::unique_ptr<UpdateDriver> _driverOps; + std::unique_ptr<UpdateDriver> _driverRepl; + Document _doc; +}; + +// Make name nicer to report +typedef CreateFromQueryFixture CreateFromQuery; + +static void assertSameFields(const BSONObj& docA, const BSONObj& docB); - UpdateDriver& driverRepl() { - return *_driverRepl; - } - - private: - std::unique_ptr<UpdateDriver> _driverOps; - std::unique_ptr<UpdateDriver> _driverRepl; - Document _doc; - }; - - // Make name nicer to report - typedef CreateFromQueryFixture CreateFromQuery; - - static void assertSameFields(const BSONObj& docA, const BSONObj& docB); - - /** - * Recursively asserts that two BSONElements contain the same data or sub-elements, - * ignoring element order. - */ - static void assertSameElements(const BSONElement& elA, const BSONElement& elB) { - if (elA.type() != elB.type() || (!elA.isABSONObj() && !elA.valuesEqual(elB))) { +/** + * Recursively asserts that two BSONElements contain the same data or sub-elements, + * ignoring element order. + */ +static void assertSameElements(const BSONElement& elA, const BSONElement& elB) { + if (elA.type() != elB.type() || (!elA.isABSONObj() && !elA.valuesEqual(elB))) { + FAIL(stream() << "element " << elA << " not equal to " << elB); + } else if (elA.type() == mongo::Array) { + std::vector<BSONElement> elsA = elA.Array(); + std::vector<BSONElement> elsB = elB.Array(); + if (elsA.size() != elsB.size()) FAIL(stream() << "element " << elA << " not equal to " << elB); - } - else if (elA.type() == mongo::Array) { - std::vector<BSONElement> elsA = elA.Array(); - std::vector<BSONElement> elsB = elB.Array(); - if (elsA.size() != elsB.size()) - FAIL(stream() << "element " << elA << " not equal to " << elB); - - std::vector<BSONElement>::iterator arrItA = elsA.begin(); - std::vector<BSONElement>::iterator arrItB = elsB.begin(); - for (; arrItA != elsA.end(); ++arrItA, ++arrItB) { - assertSameElements(*arrItA, *arrItB); - } - } - else if (elA.type() == mongo::Object) { - assertSameFields(elA.Obj(), elB.Obj()); - } - } - - /** - * Recursively asserts that two BSONObjects contain the same elements, - * ignoring element order. - */ - static void assertSameFields(const BSONObj& docA, const BSONObj& docB) { - - if (docA.nFields() != docB.nFields()) - FAIL(stream() << "document " << docA << " has different fields than " << docB); - std::map<StringData, BSONElement> docAMap; - BSONObjIterator itA(docA); - while (itA.more()) { - BSONElement elA = itA.next(); - docAMap.insert(std::make_pair(elA.fieldNameStringData(), elA)); + std::vector<BSONElement>::iterator arrItA = elsA.begin(); + std::vector<BSONElement>::iterator arrItB = elsB.begin(); + for (; arrItA != elsA.end(); ++arrItA, ++arrItB) { + assertSameElements(*arrItA, *arrItB); } - - BSONObjIterator itB(docB); - while (itB.more()) { - BSONElement elB = itB.next(); - - std::map<StringData, BSONElement>::iterator seenIt = docAMap.find(elB - .fieldNameStringData()); - if (seenIt == docAMap.end()) - FAIL(stream() << "element " << elB << " not found in " << docA); - - BSONElement elA = seenIt->second; - assertSameElements(elA, elB); - } - } - - TEST_F(CreateFromQuery, BasicOp) { - BSONObj query = fromjson("{a:1,b:2}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(query, doc().getObject()); - } - - TEST_F(CreateFromQuery, BasicOpEq) { - BSONObj query = fromjson("{a:{$eq:1}}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{a:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, BasicOpWithId) { - BSONObj query = fromjson("{_id:1,a:1,b:2}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(query, doc().getObject()); - } - - TEST_F(CreateFromQuery, BasicRepl) { - BSONObj query = fromjson("{a:1,b:2}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{}"), doc().getObject()); + } else if (elA.type() == mongo::Object) { + assertSameFields(elA.Obj(), elB.Obj()); } +} - TEST_F(CreateFromQuery, BasicReplWithId) { - BSONObj query = fromjson("{_id:1,a:1,b:2}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{_id:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, BasicReplWithIdEq) { - BSONObj query = fromjson("{_id:{$eq:1},a:1,b:2}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{_id:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, NoRootIdOp) { - BSONObj query = fromjson("{'_id.a':1,'_id.b':2}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{_id:{a:1,b:2}}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, NoRootIdRepl) { - BSONObj query = fromjson("{'_id.a':1,'_id.b':2}"); - ASSERT_NOT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - } - - TEST_F(CreateFromQuery, NestedSharedRootOp) { - BSONObj query = fromjson("{'a.c':1,'a.b':{$eq:2}}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{a:{c:1,b:2}}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, OrQueryOp) { - BSONObj query = fromjson("{$or:[{a:1}]}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{a:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, OrQueryIdRepl) { - BSONObj query = fromjson("{$or:[{_id:1}]}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{_id:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, OrQueryNoExtractOps) { - BSONObj query = fromjson("{$or:[{a:1}, {b:2}]}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(BSONObj(), doc().getObject()); - } - - TEST_F(CreateFromQuery, OrQueryNoExtractIdRepl) { - BSONObj query = fromjson("{$or:[{_id:1}, {_id:2}]}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(BSONObj(), doc().getObject()); - } - - TEST_F(CreateFromQuery, AndQueryOp) { - BSONObj query = fromjson("{$and:[{'a.c':1},{'a.b':{$eq:2}}]}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{a:{c:1,b:2}}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, AndQueryIdRepl) { - BSONObj query = fromjson("{$and:[{_id:1},{a:{$eq:2}}]}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{_id:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, AllArrayOp) { - BSONObj query = fromjson("{a:{$all:[1]}}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{a:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, AllArrayIdRepl) { - BSONObj query = fromjson("{_id:{$all:[1]}, b:2}"); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(fromjson("{_id:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, ConflictFieldsFailOp) { - BSONObj query = fromjson("{a:1,'a.b':1}"); - ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - } - - TEST_F(CreateFromQuery, ConflictFieldsFailSameValueOp) { - BSONObj query = fromjson("{a:{b:1},'a.b':1}"); - ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - } - - TEST_F(CreateFromQuery, ConflictWithIdRepl) { - BSONObj query = fromjson("{_id:1,'_id.a':1}"); - ASSERT_NOT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); - } - - TEST_F(CreateFromQuery, ConflictAndQueryOp) { - BSONObj query = fromjson("{$and:[{a:{b:1}},{'a.b':{$eq:1}}]}"); - ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - } - - TEST_F(CreateFromQuery, ConflictAllMultipleValsOp) { - BSONObj query = fromjson("{a:{$all:[1, 2]}}"); - ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - } - - TEST_F(CreateFromQuery, NoConflictOrQueryOp) { - BSONObj query = fromjson("{$or:[{a:{b:1}},{'a.b':{$eq:1}}]}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(BSONObj(), doc().getObject()); - } - - TEST_F(CreateFromQuery, ImmutableFieldsOp) { - BSONObj query = fromjson("{$or:[{a:{b:1}},{'a.b':{$eq:1}}]}"); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); - assertSameFields(BSONObj(), doc().getObject()); - } - - TEST_F(CreateFromQuery, ShardKeyRepl) { - BSONObj query = fromjson("{a:{$eq:1}}, b:2}"); - OwnedPointerVector<FieldRef> immutablePaths; - immutablePaths.push_back(new FieldRef("a")); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, - &immutablePaths.vector(), - doc())); - assertSameFields(fromjson("{a:1}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, NestedShardKeyRepl) { - BSONObj query = fromjson("{a:{$eq:1},'b.c':2},d:2}"); - OwnedPointerVector<FieldRef> immutablePaths; - immutablePaths.push_back(new FieldRef("a")); - immutablePaths.push_back(new FieldRef("b.c")); - ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, - &immutablePaths.vector(), - doc())); - assertSameFields(fromjson("{a:1,b:{c:2}}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, NestedShardKeyOp) { - BSONObj query = fromjson("{a:{$eq:1},'b.c':2,d:{$all:[3]}},e:2}"); - OwnedPointerVector<FieldRef> immutablePaths; - immutablePaths.push_back(new FieldRef("a")); - immutablePaths.push_back(new FieldRef("b.c")); - ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, - &immutablePaths.vector(), - doc())); - assertSameFields(fromjson("{a:1,b:{c:2},d:3}"), doc().getObject()); - } - - TEST_F(CreateFromQuery, NotFullShardKeyRepl) { - BSONObj query = fromjson("{a:{$eq:1}, 'b.c':2}, d:2}"); - OwnedPointerVector<FieldRef> immutablePaths; - immutablePaths.push_back(new FieldRef("a")); - immutablePaths.push_back(new FieldRef("b")); - ASSERT_NOT_OK(driverRepl().populateDocumentWithQueryFields(query, - &immutablePaths.vector(), - doc())); - } - -} // unnamed namespace +/** + * Recursively asserts that two BSONObjects contain the same elements, + * ignoring element order. + */ +static void assertSameFields(const BSONObj& docA, const BSONObj& docB) { + if (docA.nFields() != docB.nFields()) + FAIL(stream() << "document " << docA << " has different fields than " << docB); + + std::map<StringData, BSONElement> docAMap; + BSONObjIterator itA(docA); + while (itA.more()) { + BSONElement elA = itA.next(); + docAMap.insert(std::make_pair(elA.fieldNameStringData(), elA)); + } + + BSONObjIterator itB(docB); + while (itB.more()) { + BSONElement elB = itB.next(); + + std::map<StringData, BSONElement>::iterator seenIt = + docAMap.find(elB.fieldNameStringData()); + if (seenIt == docAMap.end()) + FAIL(stream() << "element " << elB << " not found in " << docA); + + BSONElement elA = seenIt->second; + assertSameElements(elA, elB); + } +} + +TEST_F(CreateFromQuery, BasicOp) { + BSONObj query = fromjson("{a:1,b:2}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(query, doc().getObject()); +} + +TEST_F(CreateFromQuery, BasicOpEq) { + BSONObj query = fromjson("{a:{$eq:1}}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{a:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, BasicOpWithId) { + BSONObj query = fromjson("{_id:1,a:1,b:2}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(query, doc().getObject()); +} + +TEST_F(CreateFromQuery, BasicRepl) { + BSONObj query = fromjson("{a:1,b:2}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, BasicReplWithId) { + BSONObj query = fromjson("{_id:1,a:1,b:2}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{_id:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, BasicReplWithIdEq) { + BSONObj query = fromjson("{_id:{$eq:1},a:1,b:2}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{_id:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, NoRootIdOp) { + BSONObj query = fromjson("{'_id.a':1,'_id.b':2}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{_id:{a:1,b:2}}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, NoRootIdRepl) { + BSONObj query = fromjson("{'_id.a':1,'_id.b':2}"); + ASSERT_NOT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); +} + +TEST_F(CreateFromQuery, NestedSharedRootOp) { + BSONObj query = fromjson("{'a.c':1,'a.b':{$eq:2}}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{a:{c:1,b:2}}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, OrQueryOp) { + BSONObj query = fromjson("{$or:[{a:1}]}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{a:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, OrQueryIdRepl) { + BSONObj query = fromjson("{$or:[{_id:1}]}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{_id:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, OrQueryNoExtractOps) { + BSONObj query = fromjson("{$or:[{a:1}, {b:2}]}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(BSONObj(), doc().getObject()); +} + +TEST_F(CreateFromQuery, OrQueryNoExtractIdRepl) { + BSONObj query = fromjson("{$or:[{_id:1}, {_id:2}]}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(BSONObj(), doc().getObject()); +} + +TEST_F(CreateFromQuery, AndQueryOp) { + BSONObj query = fromjson("{$and:[{'a.c':1},{'a.b':{$eq:2}}]}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{a:{c:1,b:2}}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, AndQueryIdRepl) { + BSONObj query = fromjson("{$and:[{_id:1},{a:{$eq:2}}]}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{_id:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, AllArrayOp) { + BSONObj query = fromjson("{a:{$all:[1]}}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{a:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, AllArrayIdRepl) { + BSONObj query = fromjson("{_id:{$all:[1]}, b:2}"); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(fromjson("{_id:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, ConflictFieldsFailOp) { + BSONObj query = fromjson("{a:1,'a.b':1}"); + ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); +} + +TEST_F(CreateFromQuery, ConflictFieldsFailSameValueOp) { + BSONObj query = fromjson("{a:{b:1},'a.b':1}"); + ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); +} + +TEST_F(CreateFromQuery, ConflictWithIdRepl) { + BSONObj query = fromjson("{_id:1,'_id.a':1}"); + ASSERT_NOT_OK(driverRepl().populateDocumentWithQueryFields(query, NULL, doc())); +} + +TEST_F(CreateFromQuery, ConflictAndQueryOp) { + BSONObj query = fromjson("{$and:[{a:{b:1}},{'a.b':{$eq:1}}]}"); + ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); +} + +TEST_F(CreateFromQuery, ConflictAllMultipleValsOp) { + BSONObj query = fromjson("{a:{$all:[1, 2]}}"); + ASSERT_NOT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); +} + +TEST_F(CreateFromQuery, NoConflictOrQueryOp) { + BSONObj query = fromjson("{$or:[{a:{b:1}},{'a.b':{$eq:1}}]}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(BSONObj(), doc().getObject()); +} + +TEST_F(CreateFromQuery, ImmutableFieldsOp) { + BSONObj query = fromjson("{$or:[{a:{b:1}},{'a.b':{$eq:1}}]}"); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, NULL, doc())); + assertSameFields(BSONObj(), doc().getObject()); +} + +TEST_F(CreateFromQuery, ShardKeyRepl) { + BSONObj query = fromjson("{a:{$eq:1}}, b:2}"); + OwnedPointerVector<FieldRef> immutablePaths; + immutablePaths.push_back(new FieldRef("a")); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, &immutablePaths.vector(), doc())); + assertSameFields(fromjson("{a:1}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, NestedShardKeyRepl) { + BSONObj query = fromjson("{a:{$eq:1},'b.c':2},d:2}"); + OwnedPointerVector<FieldRef> immutablePaths; + immutablePaths.push_back(new FieldRef("a")); + immutablePaths.push_back(new FieldRef("b.c")); + ASSERT_OK(driverRepl().populateDocumentWithQueryFields(query, &immutablePaths.vector(), doc())); + assertSameFields(fromjson("{a:1,b:{c:2}}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, NestedShardKeyOp) { + BSONObj query = fromjson("{a:{$eq:1},'b.c':2,d:{$all:[3]}},e:2}"); + OwnedPointerVector<FieldRef> immutablePaths; + immutablePaths.push_back(new FieldRef("a")); + immutablePaths.push_back(new FieldRef("b.c")); + ASSERT_OK(driverOps().populateDocumentWithQueryFields(query, &immutablePaths.vector(), doc())); + assertSameFields(fromjson("{a:1,b:{c:2},d:3}"), doc().getObject()); +} + +TEST_F(CreateFromQuery, NotFullShardKeyRepl) { + BSONObj query = fromjson("{a:{$eq:1}, 'b.c':2}, d:2}"); + OwnedPointerVector<FieldRef> immutablePaths; + immutablePaths.push_back(new FieldRef("a")); + immutablePaths.push_back(new FieldRef("b")); + ASSERT_NOT_OK( + driverRepl().populateDocumentWithQueryFields(query, &immutablePaths.vector(), doc())); +} + +} // unnamed namespace diff --git a/src/mongo/db/ops/update_lifecycle.h b/src/mongo/db/ops/update_lifecycle.h index 93811f28cff..9454aef1c1e 100644 --- a/src/mongo/db/ops/update_lifecycle.h +++ b/src/mongo/db/ops/update_lifecycle.h @@ -32,42 +32,41 @@ namespace mongo { - class Collection; - class FieldRef; - class OperationContext; - class UpdateIndexData; +class Collection; +class FieldRef; +class OperationContext; +class UpdateIndexData; - class UpdateLifecycle { - public: +class UpdateLifecycle { +public: + virtual ~UpdateLifecycle() {} - virtual ~UpdateLifecycle() {} + /** + * Update the cached collection pointer that this lifecycle object uses. + */ + virtual void setCollection(Collection* collection) = 0; - /** - * Update the cached collection pointer that this lifecycle object uses. - */ - virtual void setCollection(Collection* collection) = 0; + /** + * Can the update continue? + * + * The (only) implementation will check the following: + * 1.) Collection still exists + * 2.) Shard version has not changed (indicating that the query/update is not valid + */ + virtual bool canContinue() const = 0; - /** - * Can the update continue? - * - * The (only) implementation will check the following: - * 1.) Collection still exists - * 2.) Shard version has not changed (indicating that the query/update is not valid - */ - virtual bool canContinue() const = 0; + /** + * Return a pointer to any indexes if there is a collection. + */ + virtual const UpdateIndexData* getIndexKeys(OperationContext* opCtx) const = 0; - /** - * Return a pointer to any indexes if there is a collection. - */ - virtual const UpdateIndexData* getIndexKeys( OperationContext* opCtx ) const = 0; + /** + * Returns the shard keys as immutable fields + * Immutable fields in this case mean that they are required to exist, cannot change values + * and must not be multi-valued (in an array, or an array) + */ + virtual const std::vector<FieldRef*>* getImmutableFields() const = 0; +}; - /** - * Returns the shard keys as immutable fields - * Immutable fields in this case mean that they are required to exist, cannot change values - * and must not be multi-valued (in an array, or an array) - */ - virtual const std::vector<FieldRef*>* getImmutableFields() const = 0; - }; - -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/update_lifecycle_impl.cpp b/src/mongo/db/ops/update_lifecycle_impl.cpp index fed352c4fa4..95c15f2d04b 100644 --- a/src/mongo/db/ops/update_lifecycle_impl.cpp +++ b/src/mongo/db/ops/update_lifecycle_impl.cpp @@ -36,46 +36,45 @@ #include "mongo/s/d_state.h" namespace mongo { - namespace { - CollectionMetadataPtr getMetadata(const NamespaceString& nsString) { - if (shardingState.enabled()) { - return shardingState.getCollectionMetadata(nsString.ns()); - } - - return CollectionMetadataPtr(); - } +namespace { +CollectionMetadataPtr getMetadata(const NamespaceString& nsString) { + if (shardingState.enabled()) { + return shardingState.getCollectionMetadata(nsString.ns()); } - UpdateLifecycleImpl::UpdateLifecycleImpl(bool ignoreVersion, const NamespaceString& nsStr) - : _nsString(nsStr) - , _shardVersion((!ignoreVersion && getMetadata(_nsString)) ? - getMetadata(_nsString)->getShardVersion() : - ChunkVersion::IGNORED()) { - } + return CollectionMetadataPtr(); +} +} - void UpdateLifecycleImpl::setCollection(Collection* collection) { - _collection = collection; - } +UpdateLifecycleImpl::UpdateLifecycleImpl(bool ignoreVersion, const NamespaceString& nsStr) + : _nsString(nsStr), + _shardVersion((!ignoreVersion && getMetadata(_nsString)) + ? getMetadata(_nsString)->getShardVersion() + : ChunkVersion::IGNORED()) {} - bool UpdateLifecycleImpl::canContinue() const { - // Collection needs to exist to continue - return _collection; - } +void UpdateLifecycleImpl::setCollection(Collection* collection) { + _collection = collection; +} - const UpdateIndexData* UpdateLifecycleImpl::getIndexKeys(OperationContext* opCtx) const { - if (_collection) - return &_collection->infoCache()->indexKeys(opCtx); - return NULL; - } +bool UpdateLifecycleImpl::canContinue() const { + // Collection needs to exist to continue + return _collection; +} + +const UpdateIndexData* UpdateLifecycleImpl::getIndexKeys(OperationContext* opCtx) const { + if (_collection) + return &_collection->infoCache()->indexKeys(opCtx); + return NULL; +} - const std::vector<FieldRef*>* UpdateLifecycleImpl::getImmutableFields() const { - CollectionMetadataPtr metadata = getMetadata(_nsString); - if (metadata) { - const std::vector<FieldRef*>& fields = metadata->getKeyPatternFields(); - // Return shard-keys as immutable for the update system. - return &fields; - } - return NULL; +const std::vector<FieldRef*>* UpdateLifecycleImpl::getImmutableFields() const { + CollectionMetadataPtr metadata = getMetadata(_nsString); + if (metadata) { + const std::vector<FieldRef*>& fields = metadata->getKeyPatternFields(); + // Return shard-keys as immutable for the update system. + return &fields; } + return NULL; +} -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/update_lifecycle_impl.h b/src/mongo/db/ops/update_lifecycle_impl.h index c321b43912d..114ebd72ba2 100644 --- a/src/mongo/db/ops/update_lifecycle_impl.h +++ b/src/mongo/db/ops/update_lifecycle_impl.h @@ -35,31 +35,30 @@ namespace mongo { - class UpdateLifecycleImpl : public UpdateLifecycle { - MONGO_DISALLOW_COPYING(UpdateLifecycleImpl); +class UpdateLifecycleImpl : public UpdateLifecycle { + MONGO_DISALLOW_COPYING(UpdateLifecycleImpl); - public: +public: + /** + * ignoreVersion is for shard version checking and + * means that version checks will not be done + * + * nsString represents the namespace for the + */ + UpdateLifecycleImpl(bool ignoreVersion, const NamespaceString& nsString); - /** - * ignoreVersion is for shard version checking and - * means that version checks will not be done - * - * nsString represents the namespace for the - */ - UpdateLifecycleImpl(bool ignoreVersion, const NamespaceString& nsString); + virtual void setCollection(Collection* collection); - virtual void setCollection(Collection* collection); + virtual bool canContinue() const; - virtual bool canContinue() const; + virtual const UpdateIndexData* getIndexKeys(OperationContext* opCtx) const; - virtual const UpdateIndexData* getIndexKeys(OperationContext* opCtx) const; + virtual const std::vector<FieldRef*>* getImmutableFields() const; - virtual const std::vector<FieldRef*>* getImmutableFields() const; - - private: - Collection* _collection; - const NamespaceString& _nsString; - ChunkVersion _shardVersion; - }; +private: + Collection* _collection; + const NamespaceString& _nsString; + ChunkVersion _shardVersion; +}; } /* namespace mongo */ diff --git a/src/mongo/db/ops/update_request.h b/src/mongo/db/ops/update_request.h index d300a8fc4f2..589be047046 100644 --- a/src/mongo/db/ops/update_request.h +++ b/src/mongo/db/ops/update_request.h @@ -36,212 +36,206 @@ namespace mongo { - namespace str = mongoutils::str; - - class FieldRef; - class UpdateLifecycle; - - class UpdateRequest { - public: - enum ReturnDocOption { - // Return no document. - RETURN_NONE, - - // Return the document as it was before the update. If the update results in an insert, - // no document will be returned. - RETURN_OLD, - - // Return the document as it is after the update. - RETURN_NEW - }; - inline UpdateRequest(const NamespaceString& nsString) - : _nsString(nsString) - , _god(false) - , _upsert(false) - , _multi(false) - , _fromMigration(false) - , _lifecycle(NULL) - , _isExplain(false) - , _returnDocs(ReturnDocOption::RETURN_NONE) - , _yieldPolicy(PlanExecutor::YIELD_MANUAL) {} - - const NamespaceString& getNamespaceString() const { - return _nsString; - } - - inline void setQuery(const BSONObj& query) { - _query = query; - } - - inline const BSONObj& getQuery() const { - return _query; - } - - inline void setProj(const BSONObj& proj) { - _proj = proj; - } - - inline const BSONObj& getProj() const { - return _proj; - } - - inline void setSort(const BSONObj& sort) { - _sort = sort; - } - - inline const BSONObj& getSort() const { - return _sort; - } - - inline void setUpdates(const BSONObj& updates) { - _updates = updates; - } - - inline const BSONObj& getUpdates() const { - return _updates; - } - - // Please see documentation on the private members matching these names for - // explanations of the following fields. - - inline void setGod(bool value = true) { - _god = value; - } - - bool isGod() const { - return _god; - } - - inline void setUpsert(bool value = true) { - _upsert = value; - } - - bool isUpsert() const { - return _upsert; - } - - inline void setMulti(bool value = true) { - _multi = value; - } - - bool isMulti() const { - return _multi; - } - - inline void setFromMigration(bool value = true) { - _fromMigration = value; - } - - bool isFromMigration() const { - return _fromMigration; - } - - inline void setLifecycle(UpdateLifecycle* value) { - _lifecycle = value; - } - - inline UpdateLifecycle* getLifecycle() const { - return _lifecycle; - } - - inline void setExplain(bool value = true) { - _isExplain = value; - } - - inline bool isExplain() const { - return _isExplain; - } - - inline void setReturnDocs(ReturnDocOption value) { - _returnDocs = value; - } - - inline bool shouldReturnOldDocs() const { - return _returnDocs == ReturnDocOption::RETURN_OLD; - } - - inline bool shouldReturnNewDocs() const { - return _returnDocs == ReturnDocOption::RETURN_NEW; - } +namespace str = mongoutils::str; - inline bool shouldReturnAnyDocs() const { - return shouldReturnOldDocs() || shouldReturnNewDocs(); - } +class FieldRef; +class UpdateLifecycle; - inline void setYieldPolicy(PlanExecutor::YieldPolicy yieldPolicy) { - _yieldPolicy = yieldPolicy; - } +class UpdateRequest { +public: + enum ReturnDocOption { + // Return no document. + RETURN_NONE, - inline PlanExecutor::YieldPolicy getYieldPolicy() const { - return _yieldPolicy; - } + // Return the document as it was before the update. If the update results in an insert, + // no document will be returned. + RETURN_OLD, - const std::string toString() const { - return str::stream() - << " query: " << _query - << " projection: " << _proj - << " sort: " << _sort - << " updated: " << _updates - << " god: " << _god - << " upsert: " << _upsert - << " multi: " << _multi - << " fromMigration: " << _fromMigration - << " isExplain: " << _isExplain; - } - private: - - const NamespaceString& _nsString; + // Return the document as it is after the update. + RETURN_NEW + }; + inline UpdateRequest(const NamespaceString& nsString) + : _nsString(nsString), + _god(false), + _upsert(false), + _multi(false), + _fromMigration(false), + _lifecycle(NULL), + _isExplain(false), + _returnDocs(ReturnDocOption::RETURN_NONE), + _yieldPolicy(PlanExecutor::YIELD_MANUAL) {} + + const NamespaceString& getNamespaceString() const { + return _nsString; + } + + inline void setQuery(const BSONObj& query) { + _query = query; + } + + inline const BSONObj& getQuery() const { + return _query; + } + + inline void setProj(const BSONObj& proj) { + _proj = proj; + } + + inline const BSONObj& getProj() const { + return _proj; + } + + inline void setSort(const BSONObj& sort) { + _sort = sort; + } + + inline const BSONObj& getSort() const { + return _sort; + } + + inline void setUpdates(const BSONObj& updates) { + _updates = updates; + } + + inline const BSONObj& getUpdates() const { + return _updates; + } - // Contains the query that selects documents to update. - BSONObj _query; - - // Contains the projection information. - BSONObj _proj; - - // Contains the sort order information. - BSONObj _sort; + // Please see documentation on the private members matching these names for + // explanations of the following fields. + + inline void setGod(bool value = true) { + _god = value; + } - // Contains the modifiers to apply to matched objects, or a replacement document. - BSONObj _updates; + bool isGod() const { + return _god; + } - // Flags controlling the update. - - // God bypasses _id checking and index generation. It is only used on behalf of system - // updates, never user updates. - bool _god; - - // True if this should insert if no matching document is found. - bool _upsert; - - // True if this update is allowed to affect more than one document. - bool _multi; + inline void setUpsert(bool value = true) { + _upsert = value; + } - // True if this update is on behalf of a chunk migration. - bool _fromMigration; - - // The lifecycle data, and events used during the update request. - UpdateLifecycle* _lifecycle; - - // Whether or not we are requesting an explained update. Explained updates are read-only. - bool _isExplain; - - // Specifies which version of the documents to return, if any. - // - // RETURN_NONE (default): Never return any documents, old or new. - // RETURN_OLD: Return ADVANCED when a matching document is encountered, and the value of - // the document before it was updated. If there were no matches, return - // IS_EOF instead (even in case of an upsert). - // RETURN_NEW: Return ADVANCED when a matching document is encountered, and the value of - // the document after being updated. If an upsert was specified and it - // resulted in an insert, return the inserted document. - // - // This allows findAndModify to execute an update and retrieve the resulting document - // without another query before or after the update. - ReturnDocOption _returnDocs; - - // Whether or not the update should yield. Defaults to YIELD_MANUAL. - PlanExecutor::YieldPolicy _yieldPolicy; + bool isUpsert() const { + return _upsert; + } - }; + inline void setMulti(bool value = true) { + _multi = value; + } + + bool isMulti() const { + return _multi; + } + + inline void setFromMigration(bool value = true) { + _fromMigration = value; + } + + bool isFromMigration() const { + return _fromMigration; + } + + inline void setLifecycle(UpdateLifecycle* value) { + _lifecycle = value; + } + + inline UpdateLifecycle* getLifecycle() const { + return _lifecycle; + } + + inline void setExplain(bool value = true) { + _isExplain = value; + } + + inline bool isExplain() const { + return _isExplain; + } + + inline void setReturnDocs(ReturnDocOption value) { + _returnDocs = value; + } + + inline bool shouldReturnOldDocs() const { + return _returnDocs == ReturnDocOption::RETURN_OLD; + } + + inline bool shouldReturnNewDocs() const { + return _returnDocs == ReturnDocOption::RETURN_NEW; + } + + inline bool shouldReturnAnyDocs() const { + return shouldReturnOldDocs() || shouldReturnNewDocs(); + } + + inline void setYieldPolicy(PlanExecutor::YieldPolicy yieldPolicy) { + _yieldPolicy = yieldPolicy; + } + + inline PlanExecutor::YieldPolicy getYieldPolicy() const { + return _yieldPolicy; + } + + const std::string toString() const { + return str::stream() << " query: " << _query << " projection: " << _proj + << " sort: " << _sort << " updated: " << _updates << " god: " << _god + << " upsert: " << _upsert << " multi: " << _multi + << " fromMigration: " << _fromMigration + << " isExplain: " << _isExplain; + } + +private: + const NamespaceString& _nsString; + + // Contains the query that selects documents to update. + BSONObj _query; + + // Contains the projection information. + BSONObj _proj; + + // Contains the sort order information. + BSONObj _sort; + + // Contains the modifiers to apply to matched objects, or a replacement document. + BSONObj _updates; + + // Flags controlling the update. + + // God bypasses _id checking and index generation. It is only used on behalf of system + // updates, never user updates. + bool _god; + + // True if this should insert if no matching document is found. + bool _upsert; + + // True if this update is allowed to affect more than one document. + bool _multi; + + // True if this update is on behalf of a chunk migration. + bool _fromMigration; + + // The lifecycle data, and events used during the update request. + UpdateLifecycle* _lifecycle; + + // Whether or not we are requesting an explained update. Explained updates are read-only. + bool _isExplain; + + // Specifies which version of the documents to return, if any. + // + // RETURN_NONE (default): Never return any documents, old or new. + // RETURN_OLD: Return ADVANCED when a matching document is encountered, and the value of + // the document before it was updated. If there were no matches, return + // IS_EOF instead (even in case of an upsert). + // RETURN_NEW: Return ADVANCED when a matching document is encountered, and the value of + // the document after being updated. If an upsert was specified and it + // resulted in an insert, return the inserted document. + // + // This allows findAndModify to execute an update and retrieve the resulting document + // without another query before or after the update. + ReturnDocOption _returnDocs; + + // Whether or not the update should yield. Defaults to YIELD_MANUAL. + PlanExecutor::YieldPolicy _yieldPolicy; +}; -} // namespace mongo +} // namespace mongo diff --git a/src/mongo/db/ops/update_result.cpp b/src/mongo/db/ops/update_result.cpp index 64b33243131..93f48fae668 100644 --- a/src/mongo/db/ops/update_result.cpp +++ b/src/mongo/db/ops/update_result.cpp @@ -39,21 +39,20 @@ namespace mongo { - UpdateResult::UpdateResult(bool existing_, - bool modifiers_, - unsigned long long numDocsModified_, - unsigned long long numMatched_, - const BSONObj& upsertedObject_) - : existing(existing_), - modifiers(modifiers_), - numDocsModified(numDocsModified_), - numMatched(numMatched_) { - - BSONElement id = upsertedObject_["_id"]; - if ( ! existing && numMatched == 1 && !id.eoo() ) { - upserted = id.wrap(kUpsertedFieldName); - } - LOG(4) << "UpdateResult -- " << toString(); +UpdateResult::UpdateResult(bool existing_, + bool modifiers_, + unsigned long long numDocsModified_, + unsigned long long numMatched_, + const BSONObj& upsertedObject_) + : existing(existing_), + modifiers(modifiers_), + numDocsModified(numDocsModified_), + numMatched(numMatched_) { + BSONElement id = upsertedObject_["_id"]; + if (!existing && numMatched == 1 && !id.eoo()) { + upserted = id.wrap(kUpsertedFieldName); } + LOG(4) << "UpdateResult -- " << toString(); +} } // namespace mongo diff --git a/src/mongo/db/ops/update_result.h b/src/mongo/db/ops/update_result.h index 568da353554..9c1c27c5a93 100644 --- a/src/mongo/db/ops/update_result.h +++ b/src/mongo/db/ops/update_result.h @@ -35,40 +35,36 @@ namespace mongo { - namespace str = mongoutils::str; +namespace str = mongoutils::str; - struct UpdateResult { +struct UpdateResult { + UpdateResult(bool existing_, + bool modifiers_, + unsigned long long numDocsModified_, + unsigned long long numMatched_, + const BSONObj& upsertedObject_); - UpdateResult( bool existing_, - bool modifiers_, - unsigned long long numDocsModified_, - unsigned long long numMatched_, - const BSONObj& upsertedObject_); + // if existing objects were modified + const bool existing; - // if existing objects were modified - const bool existing; + // was this a $ mod + const bool modifiers; - // was this a $ mod - const bool modifiers; + // how many docs updated + const long long numDocsModified; - // how many docs updated - const long long numDocsModified; + // how many docs seen by update + const long long numMatched; - // how many docs seen by update - const long long numMatched; + // if something was upserted, the new _id of the object + BSONObj upserted; - // if something was upserted, the new _id of the object - BSONObj upserted; + const std::string toString() const { + return str::stream() << " upserted: " << upserted << " modifiers: " << modifiers + << " existing: " << existing << " numDocsModified: " << numDocsModified + << " numMatched: " << numMatched; + } +}; - const std::string toString() const { - return str::stream() - << " upserted: " << upserted - << " modifiers: " << modifiers - << " existing: " << existing - << " numDocsModified: " << numDocsModified - << " numMatched: " << numMatched; - } - }; - -} // namespace mongo +} // namespace mongo |