summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAaron <aaron@10gen.com>2010-09-29 10:10:10 -0700
committerAaron <aaron@10gen.com>2010-09-29 11:16:25 -0700
commit5e538ccb8ab0d99129c3b5c9316f2b25e877d3cf (patch)
tree8304b2ec277680d14c0a8c7b24ae270c965b3471
parent2a4c95a3f44b12968f819e77799c8b0af9434625 (diff)
downloadmongo-5e538ccb8ab0d99129c3b5c9316f2b25e877d3cf.tar.gz
SERVER-394 implement rename modifier
-rw-r--r--db/jsobjmanipulator.h4
-rw-r--r--db/update.cpp154
-rw-r--r--db/update.h41
-rw-r--r--dbtests/repltests.cpp54
-rw-r--r--jstests/rename4.js113
-rw-r--r--mongo.xcodeproj/project.pbxproj2
6 files changed, 354 insertions, 14 deletions
diff --git a/db/jsobjmanipulator.h b/db/jsobjmanipulator.h
index fed96762f28..bd4089b1410 100644
--- a/db/jsobjmanipulator.h
+++ b/db/jsobjmanipulator.h
@@ -103,6 +103,10 @@ namespace mongo {
}
}
}
+
+ void rewriteFieldName( const char *name ) {
+ strcpy( data() + 1, name );
+ }
private:
char *data() { return nonConst( _element.rawdata() ); }
char *value() { return nonConst( _element.value() ); }
diff --git a/db/update.cpp b/db/update.cpp
index 41cf9c35bd6..186440ce531 100644
--- a/db/update.cpp
+++ b/db/update.cpp
@@ -31,7 +31,7 @@
namespace mongo {
const char* Mod::modNames[] = { "$inc", "$set", "$push", "$pushAll", "$pull", "$pullAll" , "$pop", "$unset" ,
- "$bitand" , "$bitor" , "$bit" , "$addToSet" };
+ "$bitand" , "$bitor" , "$bit" , "$addToSet", "$rename", "$rename" };
unsigned Mod::modNamesNum = sizeof(Mod::modNames)/sizeof(char*);
bool Mod::_pullElementMatch( BSONElement& toMatch ) const {
@@ -85,6 +85,10 @@ namespace mongo {
template< class Builder >
void Mod::apply( Builder& b , BSONElement in , ModState& ms ) const {
+ if ( ms.dontApply ) {
+ return;
+ }
+
switch ( op ){
case INC: {
@@ -307,6 +311,15 @@ namespace mongo {
break;
}
+ case RENAME_FROM: {
+ break;
+ }
+
+ case RENAME_TO: {
+ b.appendAs( ms.newVal , shortFieldName, ms.newVal );
+ break;
+ }
+
default:
stringstream ss;
ss << "Mod::apply can't handle type: " << op;
@@ -314,6 +327,48 @@ namespace mongo {
}
}
+ // -1 inside a non-object (non-object could be array)
+ // 0 missing
+ // 1 found
+ int validRenamePath( BSONObj obj, const char *path ) {
+ while( const char *p = strchr( path, '.' ) ) {
+ string left( path, p - path );
+ BSONElement e = obj.getField( left );
+ if ( e.eoo() ) {
+ return 0;
+ }
+ if ( e.type() != Object ) {
+ return -1;
+ }
+ obj = e.embeddedObject();
+ path = p + 1;
+ }
+ return !obj.getField( path ).eoo();
+ }
+
+ bool nameRewritePossible( const char *a, const char *b ) {
+ const char *ar = strrchr( a, '.' );
+ const char *br = strrchr( b, '.' );
+ if ( ar && br ) {
+ if ( ( ar - a ) != ( br - b ) ) {
+ return false;
+ }
+ if ( strncmp( a, b, ar - a ) != 0 ) {
+ return false;
+ }
+ if ( strlen( ar ) != strlen( br ) ) {
+ return false;
+ }
+ } else if ( !ar && !br ) {
+ if ( strlen( a ) != strlen( b ) ) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ return true;
+ }
+
auto_ptr<ModSetState> ModSet::prepare(const BSONObj &obj) const {
DEBUGUPDATE( "\t start prepare" );
ModSetState * mss = new ModSetState( obj );
@@ -330,6 +385,28 @@ namespace mongo {
ms.m = &m;
ms.old = e;
+ if ( m.op == Mod::RENAME_FROM ) {
+ int source = validRenamePath( obj, m.fieldName );
+ uassert( 13489, "$rename source field invalid", source != -1 );
+ if ( source != 1 ) {
+ ms.dontApply = true;
+ }
+ continue;
+ }
+
+ if ( m.op == Mod::RENAME_TO ) {
+ int source = validRenamePath( obj, m.renameFrom() );
+ if ( source == 1 ) {
+ int target = validRenamePath( obj, m.fieldName );
+ uassert( 13490, "$rename target field invalid", target != -1 );
+ ms.newVal = obj.getFieldDotted( m.renameFrom() );
+ mss->amIInPlacePossible( target == 0 && nameRewritePossible( m.renameFrom(), m.fieldName ) );
+ } else {
+ ms.dontApply = true;
+ }
+ continue;
+ }
+
if ( e.eoo() ) {
mss->amIInPlacePossible( m.op == Mod::UNSET );
continue;
@@ -425,6 +502,10 @@ namespace mongo {
}
void ModState::appendForOpLog( BSONObjBuilder& b ) const {
+ if ( dontApply ) {
+ return;
+ }
+
if ( incType ){
DEBUGUPDATE( "\t\t\t\t\t appendForOpLog inc fieldname: " << m->fieldName << " short:" << m->shortFieldName );
BSONObjBuilder bb( b.subobjStart( "$set" ) );
@@ -432,16 +513,32 @@ namespace mongo {
bb.done();
return;
}
+
+ if ( m->op == Mod::RENAME_FROM ) {
+ DEBUGUPDATE( "\t\t\t\t\t appendForOpLog RENAME_FROM fielName:" << m->fieldName );
+ BSONObjBuilder bb( b.subobjStart( "$unset" ) );
+ bb.append( m->fieldName, 1 );
+ bb.done();
+ return;
+ }
+
+ if ( m->op == Mod::RENAME_TO ) {
+ DEBUGUPDATE( "\t\t\t\t\t appendForOpLog RENAME_TO fielName:" << m->fieldName );
+ BSONObjBuilder bb( b.subobjStart( "$set" ) );
+ bb.appendAs( newVal, m->fieldName );
+ return;
+ }
const char * name = fixedOpName ? fixedOpName : Mod::modNames[op()];
DEBUGUPDATE( "\t\t\t\t\t appendForOpLog name:" << name << " fixed: " << fixed << " fn: " << m->fieldName );
BSONObjBuilder bb( b.subobjStart( name ) );
- if ( fixed )
+ if ( fixed ) {
bb.appendAs( *fixed , m->fieldName );
- else
+ } else {
bb.appendAs( m->elt , m->fieldName );
+ }
bb.done();
}
@@ -456,15 +553,23 @@ namespace mongo {
void ModSetState::ApplyModsInPlace() {
for ( ModStateHolder::iterator i = _mods.begin(); i != _mods.end(); ++i ) {
- ModState& m = i->second;
+ ModState& m = i->second;
+ if ( m.dontApply ) {
+ continue;
+ }
+
switch ( m.m->op ){
case Mod::UNSET:
case Mod::PULL:
case Mod::PULL_ALL:
case Mod::ADDTOSET:
+ case Mod::RENAME_FROM:
// this should have been handled by prepare
break;
// [dm] the BSONElementManipulator statements below are for replication (correct?)
+ case Mod::RENAME_TO:
+ BSONElementManipulator( m.newVal ).rewriteFieldName( m.m->shortFieldName );
+ break;
case Mod::INC:
m.m->IncrementMe( m.old );
m.fixedOpName = "$set";
@@ -633,7 +738,7 @@ namespace mongo {
BSONObj ModSetState::createNewFromMods() {
BSONObjBuilder b( (int)(_obj.objsize() * 1.1) );
createNewFromMods( "" , b , _obj );
- return b.obj();
+ return _newFromMods = b.obj();
}
string ModSetState::toString() const {
@@ -717,17 +822,44 @@ namespace mongo {
uassert( 10152 , "Modifier $inc allowed for numbers only", f.isNumber() || op != Mod::INC );
uassert( 10153 , "Modifier $pushAll/pullAll allowed for arrays only", f.type() == Array || ( op != Mod::PUSH_ALL && op != Mod::PULL_ALL ) );
+ if ( op == Mod::RENAME_TO ) {
+ uassert( 13476, "$rename target must be a string", f.type() == String );
+ const char *target = f.valuestr();
+ uassert( 13477, "$rename source must differ from target", strcmp( fieldName, target ) != 0 );
+ uassert( 13478, "invalid mod field name, source may not be empty", fieldName[0] );
+ uassert( 13479, "invalid mod field name, target may not be empty", target[0] );
+ uassert( 13480, "invalid mod field name, source may not begin or end in period", fieldName[0] != '.' && fieldName[ strlen( fieldName ) - 1 ] != '.' );
+ uassert( 13481, "invalid mod field name, target may not begin or end in period", target[0] != '.' && target[ strlen( target ) - 1 ] != '.' );
+ uassert( 13482, "$rename affecting _id not allowed", !( fieldName[0] == '_' && fieldName[1] == 'i' && fieldName[2] == 'd' && ( !fieldName[3] || fieldName[3] == '.' ) ) );
+ uassert( 13483, "$rename affecting _id not allowed", !( target[0] == '_' && target[1] == 'i' && target[2] == 'd' && ( !target[3] || target[3] == '.' ) ) );
+ uassert( 13484, "field name duplication not allowed with $rename target", !haveModForField( target ) );
+ uassert( 13485, "conflicting mods not allowed with $rename target", !haveConflictingMod( target ) );
+ uassert( 13486, "$rename target may not be a parent of source", !( strncmp( fieldName, target, strlen( target ) ) == 0 && fieldName[ strlen( target ) ] == '.' ) );
+ uassert( 13487, "$rename source may not be dynamic array", strstr( fieldName , ".$" ) == 0 );
+ uassert( 13488, "$rename target may not be dynamic array", strstr( target , ".$" ) == 0 );
+
+ Mod from;
+ from.init( Mod::RENAME_FROM, f );
+ from.setFieldName( fieldName );
+ updateIsIndexed( from, idxKeys, backgroundKeys );
+ _mods[ from.fieldName ] = from;
+
+ Mod to;
+ to.init( Mod::RENAME_TO, f );
+ to.setFieldName( target );
+ updateIsIndexed( to, idxKeys, backgroundKeys );
+ _mods[ to.fieldName ] = to;
+
+ DEBUGUPDATE( "\t\t " << fieldName << "\t" << from.fieldName << "\t" << to.fieldName );
+ continue;
+ }
+
_hasDynamicArray = _hasDynamicArray || strstr( fieldName , ".$" ) > 0;
Mod m;
m.init( op , f );
m.setFieldName( f.fieldName() );
-
- if ( m.isIndexed( idxKeys ) ||
- (backgroundKeys && m.isIndexed(*backgroundKeys)) ) {
- _isIndexed++;
- }
-
+ updateIsIndexed( m, idxKeys, backgroundKeys );
_mods[m.fieldName] = m;
DEBUGUPDATE( "\t\t " << fieldName << "\t" << m.fieldName << "\t" << _hasDynamicArray );
diff --git a/db/update.h b/db/update.h
index 306166042b3..45234bed2b3 100644
--- a/db/update.h
+++ b/db/update.h
@@ -32,8 +32,8 @@ namespace mongo {
*/
struct Mod {
// See opFromStr below
- // 0 1 2 3 4 5 6 7 8 9 10 11
- enum Op { INC, SET, PUSH, PUSH_ALL, PULL, PULL_ALL , POP, UNSET, BITAND, BITOR , BIT , ADDTOSET } op;
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13
+ enum Op { INC, SET, PUSH, PUSH_ALL, PULL, PULL_ALL , POP, UNSET, BITAND, BITOR , BIT , ADDTOSET, RENAME_FROM, RENAME_TO } op;
static const char* modNames[];
static unsigned modNamesNum;
@@ -231,6 +231,10 @@ namespace mongo {
}
}
+ const char *renameFrom() const {
+ massert( 13492, "mod must be RENAME_TO type", op == Mod::RENAME_TO );
+ return elt.fieldName();
+ }
};
/**
@@ -327,6 +331,13 @@ namespace mongo {
return Mod::ADDTOSET;
}
+ break;
+ }
+ case 'r': {
+ if ( fn[2] == 'e' && fn[3] == 'n' && fn[4] == 'a' && fn[5] == 'm' && fn[6] =='e' ) {
+ return Mod::RENAME_TO; // with this return code we handle both RENAME_TO and RENAME_FROM
+ }
+ break;
}
default: break;
}
@@ -336,6 +347,13 @@ namespace mongo {
ModSet(){}
+ void updateIsIndexed( const Mod &m, const set<string> &idxKeys, const set<string> *backgroundKeys ) {
+ if ( m.isIndexed( idxKeys ) ||
+ (backgroundKeys && m.isIndexed(*backgroundKeys)) ) {
+ _isIndexed++;
+ }
+ }
+
public:
ModSet( const BSONObj &from ,
@@ -403,6 +421,7 @@ namespace mongo {
public:
const Mod * m;
BSONElement old;
+ BSONElement newVal;
const char * fixedOpName;
BSONElement * fixed;
@@ -413,11 +432,14 @@ namespace mongo {
double incdouble;
long long inclong;
+ bool dontApply;
+
ModState(){
fixedOpName = 0;
fixed = 0;
pushStartSize = -1;
incType = EOO;
+ dontApply = false;
}
Mod::Op op() const {
@@ -429,10 +451,16 @@ namespace mongo {
}
bool needOpLogRewrite() const {
+ if ( dontApply )
+ return false;
+
if ( fixed || fixedOpName || incType )
return true;
switch( op() ){
+ case Mod::RENAME_FROM:
+ case Mod::RENAME_TO:
+ return true;
case Mod::BIT:
case Mod::BITAND:
case Mod::BITOR:
@@ -483,6 +511,7 @@ namespace mongo {
const BSONObj& _obj;
ModStateHolder _mods;
bool _inPlacePossible;
+ BSONObj _newFromMods; // keep this data alive, as oplog generation may depend on it
ModSetState( const BSONObj& obj )
: _obj( obj ) , _inPlacePossible(true){
@@ -505,6 +534,10 @@ namespace mongo {
template< class Builder >
void appendNewFromMod( ModState& ms , Builder& b ){
+ if ( ms.dontApply ) {
+ return;
+ }
+
//const Mod& m = *(ms.m); // HACK
Mod& m = *((Mod*)(ms.m)); // HACK
@@ -541,6 +574,10 @@ namespace mongo {
b.appendAs( m.elt, m.shortFieldName );
break;
}
+ // shouldn't see RENAME_FROM here
+ case Mod::RENAME_TO:
+ b.appendAs( ms.newVal, m.shortFieldName, ms.newVal );
+ break;
default:
stringstream ss;
ss << "unknown mod in appendNewFromMod: " << m.op;
diff --git a/dbtests/repltests.cpp b/dbtests/repltests.cpp
index a190dc88b12..4ae78cd8a96 100644
--- a/dbtests/repltests.cpp
+++ b/dbtests/repltests.cpp
@@ -955,8 +955,57 @@ namespace ReplTests {
}
};
-
+ class Rename : public Base {
+ public:
+ void doIt() const {
+ client()->update( ns(), BSON( "_id" << 0 ), fromjson( "{$rename:{a:'b'}}" ) );
+ client()->update( ns(), BSON( "_id" << 0 ), fromjson( "{$set:{a:50}}" ) );
+ }
+ using ReplTests::Base::check;
+ void check() const {
+ ASSERT_EQUALS( 1, count() );
+ check( BSON( "_id" << 0 << "a" << 50 << "b" << 3 ) , one( fromjson( "{'_id':0}" ) ) );
+ }
+ void reset() const {
+ deleteAll( ns() );
+ insert( fromjson( "{'_id':0,a:3}" ) );
+ }
+ };
+ class RenameOverwrite : public Base {
+ public:
+ void doIt() const {
+ client()->update( ns(), BSON( "_id" << 0 ), fromjson( "{$rename:{a:'b'}}" ) );
+ client()->update( ns(), BSON( "_id" << 0 ), fromjson( "{$set:{a:50}}" ) );
+ }
+ using ReplTests::Base::check;
+ void check() const {
+ ASSERT_EQUALS( 1, count() );
+ check( BSON( "_id" << 0 << "a" << 50 << "b" << 3 ) , one( fromjson( "{'_id':0}" ) ) );
+ }
+ void reset() const {
+ deleteAll( ns() );
+ insert( fromjson( "{'_id':0,a:3,b:100}" ) );
+ }
+ };
+
+ class NoRename : public Base {
+ public:
+ void doIt() const {
+ client()->update( ns(), BSON( "_id" << 0 ), fromjson( "{$rename:{c:'b'},$set:{z:1}}" ) );
+ }
+ using ReplTests::Base::check;
+ void check() const {
+ ASSERT_EQUALS( 1, count() );
+ check( BSON( "_id" << 0 << "a" << 3 << "z" << 1 ) , one( fromjson( "{'_id':0}" ) ) );
+ }
+ void reset() const {
+ deleteAll( ns() );
+ insert( fromjson( "{'_id':0,a:3}" ) );
+ }
+ };
+
+
} // namespace Idempotence
class DeleteOpIsIdBased : public Base {
@@ -1140,6 +1189,9 @@ namespace ReplTests {
add< Idempotence::Pop >();
add< Idempotence::PopReverse >();
add< Idempotence::BitOp >();
+ add< Idempotence::Rename >();
+ add< Idempotence::RenameOverwrite >();
+ add< Idempotence::NoRename >();
add< DeleteOpIsIdBased >();
add< DbIdsTest >();
add< MemIdsTest >();
diff --git a/jstests/rename4.js b/jstests/rename4.js
new file mode 100644
index 00000000000..22c0fddfa59
--- /dev/null
+++ b/jstests/rename4.js
@@ -0,0 +1,113 @@
+t = db.jstests_rename4;
+t.drop();
+
+function c( f ) {
+ assert( !db.getLastError(), "error" );
+ eval( f );
+ assert( db.getLastError(), "no error" );
+ db.resetError();
+}
+
+c( "t.update( {}, {$rename:{'a':'a'}} )" );
+c( "t.update( {}, {$rename:{'':'a'}} )" );
+c( "t.update( {}, {$rename:{'a':''}} )" );
+c( "t.update( {}, {$rename:{'_id':'a'}} )" );
+c( "t.update( {}, {$rename:{'a':'_id'}} )" );
+c( "t.update( {}, {$rename:{'_id.a':'b'}} )" );
+c( "t.update( {}, {$rename:{'b':'_id.a'}} )" );
+c( "t.update( {}, {$rename:{'_id.a':'_id.b'}} )" );
+c( "t.update( {}, {$rename:{'_id.b':'_id.a'}} )" );
+c( "t.update( {}, {$rename:{'.a':'b'}} )" );
+c( "t.update( {}, {$rename:{'a':'.b'}} )" );
+c( "t.update( {}, {$rename:{'a.':'b'}} )" );
+c( "t.update( {}, {$rename:{'a':'b.'}} )" );
+c( "t.update( {}, {$rename:{'a.b':'a'}} )" );
+c( "t.update( {}, {$rename:{'a.$':'b'}} )" );
+c( "t.update( {}, {$rename:{'a':'b.$'}} )" );
+c( "t.update( {}, {$set:{b:1},$rename:{'a':'b'}} )" );
+c( "t.update( {}, {$rename:{'a':'b'},$set:{b:1}} )" );
+c( "t.update( {}, {$rename:{'a':'b'},$set:{a:1}} )" );
+c( "t.update( {}, {$set:{'b.c':1},$rename:{'a':'b'}} )" );
+c( "t.update( {}, {$set:{b:1},$rename:{'a':'b.c'}} )" );
+c( "t.update( {}, {$rename:{'a':'b'},$set:{'b.c':1}} )" );
+c( "t.update( {}, {$rename:{'a':'b.c'},$set:{b:1}} )" );
+
+t.save( {a:[1],b:{c:[1]},d:[{e:1}],f:1} );
+c( "t.update( {}, {$rename:{'a.0':'f'}} )" );
+c( "t.update( {}, {$rename:{'a.0':'g'}} )" );
+c( "t.update( {}, {$rename:{'f':'a.0'}} )" );
+c( "t.update( {}, {$rename:{'b.c.0':'f'}} )" );
+c( "t.update( {}, {$rename:{'f':'b.c.0'}} )" );
+c( "t.update( {}, {$rename:{'d.e':'d.f'}} )" );
+c( "t.update( {}, {$rename:{'d.e':'f'}} )" );
+c( "t.update( {}, {$rename:{'d.f':'d.e'}} )" );
+c( "t.update( {}, {$rename:{'f':'d.e'}} )" );
+c( "t.update( {}, {$rename:{'d.0.e':'d.f'}} )" );
+c( "t.update( {}, {$rename:{'d.0.e':'f'}} )" );
+c( "t.update( {}, {$rename:{'d.f':'d.0.e'}} )" );
+c( "t.update( {}, {$rename:{'f':'d.0.e'}} )" );
+c( "t.update( {}, {$rename:{'f.g':'a'}} )" );
+c( "t.update( {}, {$rename:{'a':'f.g'}} )" );
+
+function v( start, mod, expected ) {
+ t.remove();
+ t.save( start );
+ t.update( {}, mod );
+ assert( !db.getLastError() );
+ var got = t.findOne();
+ delete got._id;
+ assert.eq( expected, got );
+}
+
+v( {a:1}, {$rename:{a:'b'}}, {b:1} );
+v( {a:1}, {$rename:{a:'bb'}}, {bb:1} );
+v( {b:1}, {$rename:{b:'a'}}, {a:1} );
+v( {bb:1}, {$rename:{bb:'a'}}, {a:1} );
+v( {a:{y:1}}, {$rename:{'a.y':'a.z'}}, {a:{z:1}} );
+v( {a:{yy:1}}, {$rename:{'a.yy':'a.z'}}, {a:{z:1}} );
+v( {a:{z:1}}, {$rename:{'a.z':'a.y'}}, {a:{y:1}} );
+v( {a:{zz:1}}, {$rename:{'a.zz':'a.y'}}, {a:{y:1}} );
+v( {a:{c:1}}, {$rename:{a:'b'}}, {b:{c:1}} );
+v( {aa:{c:1}}, {$rename:{aa:'b'}}, {b:{c:1}} );
+v( {a:1,b:2}, {$rename:{a:'b'}}, {b:1} );
+v( {aa:1,b:2}, {$rename:{aa:'b'}}, {b:1} );
+v( {a:1,bb:2}, {$rename:{a:'bb'}}, {bb:1} );
+v( {a:1}, {$rename:{a:'b.c'}}, {b:{c:1}} );
+v( {aa:1}, {$rename:{aa:'b.c'}}, {b:{c:1}} );
+v( {a:1,b:{}}, {$rename:{a:'b.c'}}, {b:{c:1}} );
+v( {aa:1,b:{}}, {$rename:{aa:'b.c'}}, {b:{c:1}} );
+v( {a:1}, {$rename:{b:'c'}}, {a:1} );
+v( {aa:1}, {$rename:{b:'c'}}, {aa:1} );
+v( {}, {$rename:{b:'c'}}, {} );
+v( {a:{b:1,c:2}}, {$rename:{'a.b':'d'}}, {a:{c:2},d:1} );
+v( {a:{bb:1,c:2}}, {$rename:{'a.bb':'d'}}, {a:{c:2},d:1} );
+v( {a:{b:1}}, {$rename:{'a.b':'d'}}, {a:{},d:1} );
+v( {a:[5]}, {$rename:{a:'b'}}, {b:[5]} );
+v( {aa:[5]}, {$rename:{aa:'b'}}, {b:[5]} );
+v( {'0':1}, {$rename:{'0':'5'}}, {'5':1} );
+v( {a:1,b:2}, {$rename:{a:'c'},$set:{b:5}}, {c:1,b:5} );
+v( {aa:1,b:2}, {$rename:{aa:'c'},$set:{b:5}}, {b:5,c:1} );
+v( {a:1,b:2}, {$rename:{z:'c'},$set:{b:5}}, {a:1,b:5} );
+v( {aa:1,b:2}, {$rename:{z:'c'},$set:{b:5}}, {aa:1,b:5} );
+
+// invalid target, but missing source
+v( {a:1,c:4}, {$rename:{b:'c.d'}}, {a:1,c:4} );
+
+// check index
+t.drop();
+t.ensureIndex( {a:1} );
+
+function l( start, mod, expected ) {
+ t.remove();
+ t.save( start );
+ t.update( {}, mod );
+ assert( !db.getLastError() );
+ var got = t.find().hint( {a:1} ).next();
+ delete got._id;
+ assert.eq( expected, got );
+}
+
+l( {a:1}, {$rename:{a:'b'}}, {b:1} );
+l( {a:1}, {$rename:{a:'bb'}}, {bb:1} );
+l( {b:1}, {$rename:{b:'a'}}, {a:1} );
+l( {bb:1}, {$rename:{bb:'a'}}, {a:1} );
diff --git a/mongo.xcodeproj/project.pbxproj b/mongo.xcodeproj/project.pbxproj
index e84420fd8b4..e4f2896729e 100644
--- a/mongo.xcodeproj/project.pbxproj
+++ b/mongo.xcodeproj/project.pbxproj
@@ -445,6 +445,7 @@
936B895C0F4C899400934AF2 /* md5.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = md5.hpp; sourceTree = "<group>"; };
936B895E0F4C899400934AF2 /* message.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = message.cpp; sourceTree = "<group>"; };
936B895F0F4C899400934AF2 /* message.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = message.h; sourceTree = "<group>"; };
+ 936D5EC71251BA2A0015722C /* rename4.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = rename4.js; sourceTree = "<group>"; };
9378842D11C6C987007E85F5 /* indexh.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = indexh.js; sourceTree = "<group>"; };
937884E811C80B22007E85F5 /* or8.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = or8.js; sourceTree = "<group>"; };
937C493311C0358D00836543 /* or7.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = or7.js; sourceTree = "<group>"; };
@@ -840,6 +841,7 @@
934BEB9A10DFFA9600178102 /* jstests */ = {
isa = PBXGroup;
children = (
+ 936D5EC71251BA2A0015722C /* rename4.js */,
93B771FE124024A4007C8F0C /* evald.js */,
93B77135123F0BB2007C8F0C /* indexk.js */,
938D493D122C99A3001D83EE /* ne3.js */,