0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-11-30 17:10:48 +01:00
mongodb/db/update.cpp

787 lines
31 KiB
C++

// update.cpp
/**
* Copyright (C) 2008 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "stdafx.h"
#include "query.h"
#include "pdfile.h"
#include "jsobjmanipulator.h"
#include "queryoptimizer.h"
#include "repl.h"
namespace mongo {
// utility class for assembling hierarchical objects
class EmbeddedBuilder {
public:
EmbeddedBuilder( BSONObjBuilder *b ) {
_builders.push_back( make_pair( "", b ) );
}
// It is assumed that the calls to prepareContext will be made with the 'name'
// parameter in lex ascending order.
void prepareContext( string &name ) {
int i = 1, n = _builders.size();
while( i < n &&
name.substr( 0, _builders[ i ].first.length() ) == _builders[ i ].first &&
( name[ _builders[i].first.length() ] == '.' || name[ _builders[i].first.length() ] == 0 )
){
name = name.substr( _builders[ i ].first.length() + 1 );
++i;
}
for( int j = n - 1; j >= i; --j ) {
popBuilder();
}
for( string next = splitDot( name ); !next.empty(); next = splitDot( name ) ) {
addBuilder( next );
}
}
void appendAs( const BSONElement &e, string name ) {
if ( e.type() == Object && e.valuesize() == 5 ) { // empty object -- this way we can add to it later
string dummyName = name + ".foo";
prepareContext( dummyName );
return;
}
prepareContext( name );
back()->appendAs( e, name.c_str() );
}
BufBuilder &subarrayStartAs( string name ) {
prepareContext( name );
return back()->subarrayStart( name.c_str() );
}
void done() {
while( ! _builderStorage.empty() )
popBuilder();
}
static string splitDot( string & str ) {
size_t pos = str.find( '.' );
if ( pos == string::npos )
return "";
string ret = str.substr( 0, pos );
str = str.substr( pos + 1 );
return ret;
}
private:
void addBuilder( const string &name ) {
shared_ptr< BSONObjBuilder > newBuilder( new BSONObjBuilder( back()->subobjStart( name.c_str() ) ) );
_builders.push_back( make_pair( name, newBuilder.get() ) );
_builderStorage.push_back( newBuilder );
}
void popBuilder() {
back()->done();
_builders.pop_back();
_builderStorage.pop_back();
}
BSONObjBuilder *back() { return _builders.back().second; }
vector< pair< string, BSONObjBuilder * > > _builders;
vector< shared_ptr< BSONObjBuilder > > _builderStorage;
};
/* Used for modifiers such as $inc, $set, ... */
struct Mod {
enum Op { INC, SET, PUSH, PUSH_ALL, PULL, PULL_ALL , POP } op;
const char *fieldName;
// kind of lame; fix one day?
double *ndouble;
int *nint;
long long *nlong;
BSONElement elt;
int pushStartSize;
/* [dm] why is this const? (or rather, why was setn const?) i see why but think maybe clearer if were not. */
void inc(BSONElement& n) const {
uassert( "$inc value is not a number", n.isNumber() );
if( ndouble )
*ndouble += n.numberDouble();
else if( nint )
*nint += n.numberInt();
else
*nlong += n.numberLong();
}
void setElementToOurNumericValue(BSONElement& e) const {
BSONElementManipulator manip(e);
if( e.type() == NumberLong )
manip.setLong(_getlong());
else
manip.setNumber(_getn());
}
double _getn() const {
if( ndouble ) return *ndouble;
if( nint ) return *nint;
return (double) *nlong;
}
long long _getlong() const {
if( nlong ) return *nlong;
if( ndouble ) return (long long) *ndouble;
return *nint;
}
bool operator<( const Mod &other ) const {
return strcmp( fieldName, other.fieldName ) < 0;
}
bool arrayDep() const {
switch (op){
case PUSH:
case PUSH_ALL:
case POP:
return true;
default:
return false;
}
}
};
class ModSet {
vector< Mod > _mods;
void sortMods() {
sort( _mods.begin(), _mods.end() );
}
static void extractFields( map< string, BSONElement > &fields, const BSONElement &top, const string &base );
FieldCompareResult compare( const vector< Mod >::iterator &m, map< string, BSONElement >::iterator &p, const map< string, BSONElement >::iterator &pEnd ) const {
bool mDone = ( m == _mods.end() );
bool pDone = ( p == pEnd );
assert( ! mDone );
assert( ! pDone );
if ( mDone && pDone )
return SAME;
// If one iterator is done we want to read from the other one, so say the other one is lower.
if ( mDone )
return RIGHT_BEFORE;
if ( pDone )
return LEFT_BEFORE;
return compareDottedFieldNames( m->fieldName, p->first.c_str() );
}
void appendNewFromMod( Mod& m , EmbeddedBuilder& b ){
if ( m.op == Mod::PUSH ) {
BSONObjBuilder arr( b.subarrayStartAs( m.fieldName ) );
arr.appendAs( m.elt, "0" );
arr.done();
m.pushStartSize = -1;
}
else if ( m.op == Mod::PUSH_ALL ) {
b.appendAs( m.elt, m.fieldName );
m.pushStartSize = -1;
}
else if ( m.op != Mod::PULL && m.op != Mod::PULL_ALL ) {
b.appendAs( m.elt, m.fieldName );
}
}
bool mayAddEmbedded( map< string, BSONElement > &existing, string right ) {
for( string left = EmbeddedBuilder::splitDot( right );
left.length() > 0 && left[ left.length() - 1 ] != '.';
left += "." + EmbeddedBuilder::splitDot( right ) ) {
if ( existing.count( left ) > 0 && existing[ left ].type() != Object )
return false;
if ( modForField( left.c_str() ) )
return false;
}
return true;
}
static Mod::Op opFromStr( const char *fn ) {
const char *valid[] = { "$inc", "$set", "$push", "$pushAll", "$pull", "$pullAll" , "$pop" };
for( int i = 0; i < 7; ++i )
if ( strcmp( fn, valid[ i ] ) == 0 )
return Mod::Op( i );
uassert( "Invalid modifier specified " + string( fn ), false );
return Mod::INC;
}
public:
void getMods( const BSONObj &from );
bool applyModsInPlace( const BSONObj &obj ) const;
BSONObj createNewFromMods( const BSONObj &obj );
bool isIndexed( const set<string>& idxKeys ) const {
for ( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); i++ ) {
// check if there is an index key that is a parent of mod
for( const char *dot = strchr( i->fieldName, '.' ); dot; dot = strchr( dot + 1, '.' ) )
if ( idxKeys.count( string( i->fieldName, dot - i->fieldName ) ) )
return true;
string fullName = i->fieldName;
// check if there is an index key equal to mod
if ( idxKeys.count(fullName) )
return true;
// check if there is an index key that is a child of mod
set< string >::const_iterator j = idxKeys.upper_bound( fullName );
if ( j != idxKeys.end() && j->find( fullName ) == 0 && (*j)[fullName.size()] == '.' )
return true;
}
return false;
}
unsigned size() const { return _mods.size(); }
bool haveModForField( const char *fieldName ) const {
// Presumably the number of mods is small, so this loop isn't too expensive.
for( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); ++i ) {
if ( strlen( fieldName ) == strlen( i->fieldName ) && strcmp( fieldName, i->fieldName ) == 0 )
return true;
}
return false;
}
bool haveModForFieldOrSubfield( const char *fieldName ) const {
// Presumably the number of mods is small, so this loop isn't too expensive.
for( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); ++i ) {
const char *dot = strchr( i->fieldName, '.' );
size_t len = dot ? dot - i->fieldName : strlen( i->fieldName );
if ( len == strlen( fieldName ) && strncmp( fieldName, i->fieldName, len ) == 0 )
return true;
}
return false;
}
const Mod *modForField( const char *fieldName ) const {
// Presumably the number of mods is small, so this loop isn't too expensive.
for( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); ++i ) {
if ( strcmp( fieldName, i->fieldName ) == 0 )
return &*i;
}
return 0;
}
bool haveArrayDepMod() const {
for ( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); i++ )
if ( i->arrayDep() )
return true;
return false;
}
void appendSizeSpecForArrayDepMods( BSONObjBuilder &b ) const {
for ( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); i++ ) {
if ( i->arrayDep() ){
if ( i->pushStartSize == -1 )
b.appendNull( i->fieldName );
else
b << i->fieldName << BSON( "$size" << i->pushStartSize );
}
}
}
};
bool ModSet::applyModsInPlace(const BSONObj &obj) const {
bool inPlacePossible = true;
// Perform this check first, so that we don't leave a partially modified object
// on uassert.
for ( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); ++i ) {
const Mod& m = *i;
BSONElement e = obj.getFieldDotted(m.fieldName);
if ( e.eoo() ) {
inPlacePossible = false;
} else {
switch( m.op ) {
case Mod::INC:
uassert( "Cannot apply $inc modifier to non-number", e.isNumber() || e.eoo() );
if ( !e.isNumber() )
inPlacePossible = false;
break;
case Mod::SET:
if ( !( e.isNumber() && m.elt.isNumber() ) &&
m.elt.valuesize() != e.valuesize() )
inPlacePossible = false;
break;
case Mod::PUSH:
case Mod::PUSH_ALL:
uassert( "Cannot apply $push/$pushAll modifier to non-array", e.type() == Array || e.eoo() );
inPlacePossible = false;
break;
case Mod::PULL:
case Mod::PULL_ALL: {
uassert( "Cannot apply $pull/$pullAll modifier to non-array", e.type() == Array || e.eoo() );
BSONObjIterator i( e.embeddedObject() );
while( inPlacePossible && i.moreWithEOO() ) {
BSONElement arrI = i.next();
if ( arrI.eoo() )
break;
if ( m.op == Mod::PULL ) {
if ( arrI.woCompare( m.elt, false ) == 0 ) {
inPlacePossible = false;
}
} else if ( m.op == Mod::PULL_ALL ) {
BSONObjIterator j( m.elt.embeddedObject() );
while( inPlacePossible && j.moreWithEOO() ) {
BSONElement arrJ = j.next();
if ( arrJ.eoo() )
break;
if ( arrI.woCompare( arrJ, false ) == 0 ) {
inPlacePossible = false;
}
}
}
}
break;
}
case Mod::POP: {
uassert( "Cannot apply $pop modifier to non-array", e.type() == Array || e.eoo() );
if ( ! e.embeddedObject().isEmpty() )
inPlacePossible = false;
break;
}
}
}
}
if ( !inPlacePossible ) {
return false;
}
for ( vector<Mod>::const_iterator i = _mods.begin(); i != _mods.end(); ++i ) {
const Mod& m = *i;
BSONElement e = obj.getFieldDotted(m.fieldName);
switch ( m.op ){
case Mod::PULL:
case Mod::PULL_ALL:
break;
// [dm] the BSONElementManipulator statements below are for replication (correct?)
case Mod::INC:
m.inc(e);
m.setElementToOurNumericValue(e);
break;
case Mod::SET:
if ( e.isNumber() && m.elt.isNumber() ) {
// todo: handle NumberLong:
m.setElementToOurNumericValue(e);
}
else {
BSONElementManipulator( e ).replaceTypeAndValue( m.elt );
}
break;
default:
uassert( "can't handle mod" , 0 );
}
}
return true;
}
void ModSet::extractFields( map< string, BSONElement > &fields, const BSONElement &top, const string &base ) {
if ( top.type() != Object ) {
fields[ base + top.fieldName() ] = top;
return;
}
BSONObjIterator i( top.embeddedObject() );
bool empty = true;
while( i.moreWithEOO() ) {
BSONElement e = i.next();
if ( e.eoo() )
break;
extractFields( fields, e, base + top.fieldName() + "." );
empty = false;
}
if ( empty )
fields[ base + top.fieldName() ] = top;
}
BSONObj ModSet::createNewFromMods( const BSONObj &obj ) {
sortMods();
map< string, BSONElement > existing;
BSONObjBuilder b;
BSONObjIterator i( obj );
while( i.moreWithEOO() ) {
BSONElement e = i.next();
if ( e.eoo() )
break;
if ( !haveModForFieldOrSubfield( e.fieldName() ) ) {
b.append( e );
} else {
extractFields( existing, e, "" );
}
}
EmbeddedBuilder b2( &b );
vector< Mod >::iterator m = _mods.begin();
map< string, BSONElement >::iterator p = existing.begin();
while( m != _mods.end() || p != existing.end() ) {
if ( m == _mods.end() ){
// no more mods, just regular elements
assert( p != existing.end() );
if ( mayAddEmbedded( existing, p->first ) )
b2.appendAs( p->second, p->first );
p++;
continue;
}
if ( p == existing.end() ){
uassert( "Modifier spec implies existence of an encapsulating object with a name that already represents a non-object,"
" or is referenced in another $set clause",
mayAddEmbedded( existing, m->fieldName ) );
// $ modifier applied to missing field -- create field from scratch
appendNewFromMod( *m , b2 );
m++;
continue;
}
FieldCompareResult cmp = compareDottedFieldNames( m->fieldName , p->first );
if ( cmp <= 0 )
uassert( "Modifier spec implies existence of an encapsulating object with a name that already represents a non-object,"
" or is referenced in another $set clause",
mayAddEmbedded( existing, m->fieldName ) );
if ( cmp == 0 ) {
BSONElement e = p->second;
if ( m->op == Mod::INC ) {
m->inc(e);
//m->setn( m->getn() + e.number() );
b2.appendAs( m->elt, m->fieldName );
} else if ( m->op == Mod::SET ) {
b2.appendAs( m->elt, m->fieldName );
} else if ( m->op == Mod::PUSH || m->op == Mod::PUSH_ALL ) {
BSONObjBuilder arr( b2.subarrayStartAs( m->fieldName ) );
BSONObjIterator i( e.embeddedObject() );
int startCount = 0;
while( i.moreWithEOO() ) {
BSONElement arrI = i.next();
if ( arrI.eoo() )
break;
arr.append( arrI );
++startCount;
}
if ( m->op == Mod::PUSH ) {
stringstream ss;
ss << startCount;
string nextIndex = ss.str();
arr.appendAs( m->elt, nextIndex.c_str() );
} else {
BSONObjIterator i( m->elt.embeddedObject() );
int count = startCount;
while( i.moreWithEOO() ) {
BSONElement arrI = i.next();
if ( arrI.eoo() )
break;
stringstream ss;
ss << count++;
string nextIndex = ss.str();
arr.appendAs( arrI, nextIndex.c_str() );
}
}
arr.done();
m->pushStartSize = startCount;
} else if ( m->op == Mod::PULL || m->op == Mod::PULL_ALL ) {
BSONObjBuilder arr( b2.subarrayStartAs( m->fieldName ) );
BSONObjIterator i( e.embeddedObject() );
int count = 0;
while( i.moreWithEOO() ) {
BSONElement arrI = i.next();
if ( arrI.eoo() )
break;
bool allowed = true;
if ( m->op == Mod::PULL ) {
allowed = ( arrI.woCompare( m->elt, false ) != 0 );
} else {
BSONObjIterator j( m->elt.embeddedObject() );
while( allowed && j.moreWithEOO() ) {
BSONElement arrJ = j.next();
if ( arrJ.eoo() )
break;
allowed = ( arrI.woCompare( arrJ, false ) != 0 );
}
}
if ( allowed ) {
stringstream ss;
ss << count++;
string index = ss.str();
arr.appendAs( arrI, index.c_str() );
}
}
arr.done();
}
else if ( m->op == Mod::POP ){
int startCount = 0;
BSONObjBuilder arr( b2.subarrayStartAs( m->fieldName ) );
BSONObjIterator i( e.embeddedObject() );
if ( m->elt.isNumber() && m->elt.number() < 0 ){
if ( i.more() ) i.next();
int count = 0;
startCount++;
while( i.more() ) {
arr.appendAs( i.next() , arr.numStr( count++ ).c_str() );
startCount++;
}
}
else {
while( i.more() ) {
startCount++;
BSONElement arrI = i.next();
if ( i.more() ){
arr.append( arrI );
}
}
}
arr.done();
m->pushStartSize = startCount;
}
++m;
++p;
}
else if ( cmp < 0 ) {
// $ modifier applied to missing field -- create field from scratch
appendNewFromMod( *m , b2 );
m++;
}
else if ( cmp > 0 ) {
// No $ modifier
if ( mayAddEmbedded( existing, p->first ) )
b2.appendAs( p->second, p->first );
++p;
}
}
b2.done();
return b.obj();
}
/* get special operations like $inc
{ $inc: { a:1, b:1 } }
{ $set: { a:77 } }
{ $push: { a:55 } }
{ $pushAll: { a:[77,88] } }
{ $pull: { a:66 } }
{ $pullAll : { a:[99,1010] } }
NOTE: MODIFIES source from object!
*/
void ModSet::getMods(const BSONObj &from) {
BSONObjIterator it(from);
while ( it.more() ) {
BSONElement e = it.next();
const char *fn = e.fieldName();
uassert( "Invalid modifier specified" + string( fn ), e.type() == Object );
BSONObj j = e.embeddedObject();
BSONObjIterator jt(j);
Mod::Op op = opFromStr( fn );
if ( op == Mod::INC )
strcpy((char *) fn, "$set"); // rewrite for op log
while ( jt.more() ) {
BSONElement f = jt.next();
Mod m;
m.op = op;
m.fieldName = f.fieldName();
uassert( "Mod on _id not allowed", strcmp( m.fieldName, "_id" ) != 0 );
uassert( "Invalid mod field name, may not end in a period", m.fieldName[ strlen( m.fieldName ) - 1 ] != '.' );
for ( vector<Mod>::iterator i = _mods.begin(); i != _mods.end(); i++ ) {
uassert( "Field name duplication not allowed with modifiers",
strcmp( m.fieldName, i->fieldName ) != 0 );
}
uassert( "Modifier $inc allowed for numbers only", f.isNumber() || op != Mod::INC );
uassert( "Modifier $pushAll/pullAll allowed for arrays only", f.type() == Array || ( op != Mod::PUSH_ALL && op != Mod::PULL_ALL ) );
m.elt = f;
// horrible - to be cleaned up
if ( f.type() == NumberDouble ) {
m.ndouble = (double *) f.value();
m.nint = 0;
} else if ( f.type() == NumberInt ) {
m.ndouble = 0;
m.nint = (int *) f.value();
}
else if( f.type() == NumberLong ) {
m.ndouble = 0;
m.nint = 0;
m.nlong = (long long *) f.value();
}
_mods.push_back( m );
}
}
}
void checkNoMods( BSONObj o ) {
BSONObjIterator i( o );
while( i.moreWithEOO() ) {
BSONElement e = i.next();
if ( e.eoo() )
break;
massert( "Modifiers and non-modifiers cannot be mixed", e.fieldName()[ 0 ] != '$' );
}
}
class UpdateOp : public QueryOp {
public:
UpdateOp() : nscanned_() {}
virtual void init() {
BSONObj pattern = qp().query();
c_ = qp().newCursor();
if ( !c_->ok() )
setComplete();
else
matcher_.reset( new KeyValJSMatcher( pattern, qp().indexKey() ) );
}
virtual void next() {
if ( !c_->ok() ) {
setComplete();
return;
}
nscanned_++;
if ( matcher_->matches(c_->currKey(), c_->currLoc()) ) {
setComplete();
return;
}
c_->advance();
}
virtual bool mayRecordPlan() const { return false; }
virtual QueryOp *clone() const {
return new UpdateOp();
}
auto_ptr< Cursor > c() { return c_; }
long long nscanned() const { return nscanned_; }
private:
auto_ptr< Cursor > c_;
long long nscanned_;
auto_ptr< KeyValJSMatcher > matcher_;
};
UpdateResult updateObjects(const char *ns, BSONObj updateobjOrig, BSONObj patternOrig, bool upsert, bool multi, stringstream& ss, bool logop ) {
int profile = cc().database()->profile;
uassert("cannot update reserved $ collection", strchr(ns, '$') == 0 );
if ( strstr(ns, ".system.") ) {
/* dm: it's very important that system.indexes is never updated as IndexDetails has pointers into it */
uassert("cannot update system collection", legalClientSystemNS( ns , true ) );
}
QueryPlanSet qps( ns, patternOrig, BSONObj() );
UpdateOp original;
shared_ptr< UpdateOp > u = qps.runOp( original );
massert( u->exceptionMessage(), u->complete() );
auto_ptr< Cursor > c = u->c();
int numModded = 0;
while ( c->ok() ) {
Record *r = c->_current();
BSONObj js(r);
BSONObj pattern = patternOrig;
BSONObj updateobj = updateobjOrig;
if ( logop ) {
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 ( js.getObjectID( id ) ) {
idPattern.append( id );
pattern = idPattern.obj();
}
else {
uassert( "multi-update requires all modified objects to have an _id" , ! multi );
}
}
/* note: we only update one row and quit. if you do multiple later,
be careful or multikeys in arrays could break things badly. best
to only allow updating a single row with a multikey lookup.
*/
if ( profile )
ss << " nscanned:" << u->nscanned();
/* look for $inc etc. note as listed here, all fields to inc must be this type, you can't set some
regular ones at the moment. */
const char *firstField = updateobj.firstElement().fieldName();
if ( firstField[0] == '$' ) {
if ( multi )
updateobj = updateobj.copy();
ModSet mods;
mods.getMods(updateobj);
NamespaceDetailsTransient& ndt = NamespaceDetailsTransient::get(ns);
set<string>& idxKeys = ndt.indexKeys();
if ( ! mods.isIndexed( idxKeys ) && mods.applyModsInPlace( c->currLoc().obj() ) ) {
if ( profile )
ss << " fastmod ";
} else {
BSONObj newObj = mods.createNewFromMods( c->currLoc().obj() );
theDataFileMgr.update(ns, r, c->currLoc(), newObj.objdata(), newObj.objsize(), ss);
}
if ( logop ) {
if ( mods.size() ) {
if ( mods.haveArrayDepMod() ) {
BSONObjBuilder patternBuilder;
patternBuilder.appendElements( pattern );
mods.appendSizeSpecForArrayDepMods( patternBuilder );
pattern = patternBuilder.obj();
}
}
logOp("u", ns, updateobj, &pattern );
}
numModded++;
if ( ! multi )
break;
c->advance();
continue;
}
uassert( "multi update only works with $ operators" , ! multi );
BSONElementManipulator::lookForTimestamps( updateobj );
checkNoMods( updateobj );
theDataFileMgr.update(ns, r, c->currLoc(), updateobj.objdata(), updateobj.objsize(), ss);
if ( logop )
logOp("u", ns, updateobj, &pattern );
return UpdateResult( 1 , 0 , 1 );
}
if ( numModded )
return UpdateResult( 1 , 1 , numModded );
if ( profile )
ss << " nscanned:" << u->nscanned();
if ( upsert ) {
if ( updateobjOrig.firstElement().fieldName()[0] == '$' ) {
/* upsert of an $inc. build a default */
ModSet mods;
mods.getMods(updateobjOrig);
BSONObj newObj = patternOrig.copy();
if ( mods.applyModsInPlace( newObj ) ) {
//
} else {
newObj = mods.createNewFromMods( newObj );
}
if ( profile )
ss << " fastmodinsert ";
theDataFileMgr.insert(ns, newObj);
if ( profile )
ss << " fastmodinsert ";
if ( logop )
logOp( "i", ns, newObj );
return UpdateResult( 0 , 1 , 1 );
}
uassert( "multi update only works with $ operators" , ! multi );
checkNoMods( updateobjOrig );
if ( profile )
ss << " upsert ";
theDataFileMgr.insert(ns, updateobjOrig);
if ( logop )
logOp( "i", ns, updateobjOrig );
return UpdateResult( 0 , 0 , 1 );
}
return UpdateResult( 0 , 0 , 0 );
}
}