Repository: basho/riak-nodejs-client
Pull request #27 (2015-03-27 21:13:06)

CLIENTS-241-239: FetchMap and UpdateMap commands
Also 2 small logging changes in RiakNode.


Patch:
diff --git a/lib/commands/crdt/fetchmap.js b/lib/commands/crdt/fetchmap.js
new file mode 100644
index 0000000..8be6cf3
--- /dev/null
+++ b/lib/commands/crdt/fetchmap.js
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2015 Basho Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var CommandBase = require('../commandbase');
+var inherits = require('util').inherits;
+var Joi = require('joi');
+var DtFetchResp = require('../../protobuf/riakprotobuf').getProtoFor('DtFetchResp');
+var MapField = require('../../protobuf/riakprotobuf').getProtoFor('MapField');
+
+/**
+ * Provides the FetchMap class, its builder, and its response.
+ * @module FetchMap
+ */
+
+/**
+ * Command for fetching a map from Riak.
+ * 
+ * As a convenience, a builder class is provided:
+ * 
+ *     var fetch = new FetchMap.Builder()
+ *                      .withBucketType('myBucketType')
+ *                      .withBucket('myBucket')
+ *                      .withKey('myKey')
+ *                      .withCallback(myCallback)
+ *                      .build();
+ *              
+ * @class FetchMap
+ * @constructor
+ * @param {Object} options the options to use for this command.
+ * @param {String} options.bucketType the bucket type in riak.
+ * @param {String} options.bucket the bucket in riak.
+ * @param {String} options.key the key for the map you want to fetch.
+ * @param {Boolean} [options.setsAsBuffers=false] return sets as arrays of Buffers rather than strings.
+ * @param {Number} [options.timeout] set a timeout for this operation.
+ * @param {Number} [options.r] the R value to use for this operation.
+ * @param {Number} [options.pr] the PR value to use for this operation.
+ * @param {Boolean} [options.notFoundOk] if true a vnode returning notfound for a key increments the r tally.
+ * @param {Boolean} [options.useBasicQuorum] controls whether a read request should return early in some fail cases.
+ * @param {Function} callback the callback to be executed when the operation completes.
+ * @param {String} callback.err An error message
+ * @param {Number} callback.response the response from Riak.
+ * @extends CommandBase 
+ * @constructor
+ */
+function FetchMap(options, callback) {
+    CommandBase.call(this, 'DtFetchReq', 'DtFetchResp', callback);
+    var self = this;
+    Joi.validate(options, schema, function(err, options) {
+       
+        if (err) {
+            throw err;
+        }
+        
+        self.options = options;
+    
+    });
+    
+    this.remainingTries = 1;
+}
+
+inherits(FetchMap, CommandBase);
+
+FetchMap.prototype.constructPbRequest = function() {
+    
+    var protobuf = this.getPbReqBuilder();
+    
+    protobuf.setBucket(new Buffer(this.options.bucket));
+    protobuf.setType(new Buffer(this.options.bucketType));
+    protobuf.setKey(new Buffer(this.options.key));
+    protobuf.setR(this.options.r);
+    protobuf.setPr(this.options.pr);
+    protobuf.setBasicQuorum(this.options.basicQuorum);
+    protobuf.setNotfoundOk(this.options.notFoundOk);
+    protobuf.setTimeout(this.options.timeout);
+    
+    return protobuf;
+    
+};
+
+FetchMap.prototype.onSuccess = function(dtFetchResp) {
+    var response = null;
+    // on "not found" dtFetchResp.value will be null
+    if (dtFetchResp.getValue()) {
+        if (dtFetchResp.getType() !== DtFetchResp.DataType.MAP) {
+            this.onError('Requested map, received ' + dtType[dtFetchResp.getType()]);
+        } else {
+            response = { context: dtFetchResp.getContext().toBuffer(),
+                map: FetchMap.parsePbResponse(dtFetchResp.value.map_value, this.options.setsAsBuffers) };
+        }
+    }
+    this._callback(null, response);
+    return true;
+};
+
+var schema = Joi.object().keys({
+   bucket: Joi.string().required(),
+   bucketType: Joi.string().required(),
+   key: Joi.string().required(),
+   r: Joi.number().default(null).optional(),
+   pr: Joi.number().default(null).optional(),
+   notFoundOk: Joi.boolean().default(null).optional(),
+   basicQuorum: Joi.boolean().default(null).optional(),
+   timeout: Joi.number().default(null).optional(),
+   callback: Joi.func().strip().optional(),
+   setsAsBuffers: Joi.boolean().default(false).optional()
+});
+
+var dtType = {
+    1: 'counter',
+    2: 'set',
+    3: 'map'
+};
+
+
+/**
+ * A builder for constructing FetchMap instances.
+ * * Rather than having to manually construct the __options__ and instantiating
+ * a FetchMap directly, this builder may be used.
+ * 
+ *      var fetch = new FetchMap.Builder()
+ *                      .withBucketType('myBucketType')
+ *                      .withBucket('myBucket')
+ *                      .withKey('myKey')
+ *                      .withCallback(myCallback)
+ *                      .build();
+ *       
+ * @namespace FetchMap
+ * @class Builder
+ * @constructor
+ */
+function Builder() {}
+
+Builder.prototype = {
+  
+    /**
+     * Set the bucket.
+     * @method withBucket
+     * @param {String} bucket the bucket in Riak
+     * @chainable
+     */
+    withBucket : function(bucket) {
+        this.bucket = bucket;
+        return this;
+    },
+    /**
+     * Set the bucket type.
+     * @method withBucketType
+     * @param {String} bucketType the bucket type in riak
+     * @chainable
+     */
+    withBucketType : function(bucketType) {
+        this.bucketType = bucketType;
+        return this;
+    },
+    /**
+     * Set the key.
+     * @method withKey
+     * @param {String} key the key in riak.
+     * @chainable
+     */
+    withKey : function(key) {
+        this.key = key;
+        return this;
+    },
+    /**
+     * Return sets as arrays of Buffers rather than strings.
+     * By default the contents of sets are converted to strings. Setting this
+     * to true will cause this not to occur and the raw bytes returned 
+     * as Buffer objects.
+     * @method withSetsAsBuffers
+     * @param {Boolean} setsAsBuffers true to not convert set contents to strings.
+     * @chainable
+     */
+    withSetsAsBuffers : function(setsAsBuffers) {
+        this.setsAsBuffers = setsAsBuffers;
+        return this;
+    },
+    /**
+     * Set the R value.
+     * If not set the bucket default is used.
+     * @method withR
+     * @param {Number} r the R value.
+     * @chainable
+     */
+    withR : function(r) {
+        this.r = r;
+        return this;
+    },
+    /**
+    * Set the PR value.
+    * If not set the bucket default is used.
+    * @method withPr
+    * @param {Number} pr the PR value.
+    * @chainable
+    */
+    withPr : function(pr) {
+        this.pr = pr;
+        return this;
+    },/**
+    * Set the not_found_ok value.
+    * If true a vnode returning notfound for a key increments the r tally.
+    * False is higher consistency, true is higher availability.
+    * If not asSet the bucket default is used.
+    * @method withNotFoundOk
+    * @param {Boolean} notFoundOk the not_found_ok value.
+    * @chainable
+    */
+    withNotFoundOk : function(notFoundOk) {
+        this.notFoundOk = notFoundOk;
+        return this;
+    },
+    /**
+    * Set the basic_quorum value.
+    * The parameter controls whether a read request should return early in
+    * some fail cases. 
+    * E.g. If a quorum of nodes has already
+    * returned notfound/error, don't wait around for the rest.
+    * @method withBasicQuorum
+    * @param {Boolean} useBasicQuorum the basic_quorum value.
+    * @chainable
+    */
+    withBasicQuorum : function(useBasicQuorum) {
+        this.basicQuorum = useBasicQuorum;
+        return this;
+    },
+    /**
+    * Set a timeout for this operation.
+    * @method withTimeout
+    * @param {Number} timeout a timeout in milliseconds.
+    * @chainable
+    */
+    withTimeout : function(timeout) {
+        this.timeout = timeout;
+        return this;
+    },
+    /**
+     * Set the callback to be executed when the operation completes.
+     * @method withCallback
+     * @param {Function} callback - the callback to execute
+     * @param {String} callback.err An error message
+     * @param {Number} callback.response - the response from Riak
+     * @chainable
+     */
+    withCallback : function(callback) {
+        this.callback = callback;
+        return this;
+    },
+    /**
+     * Construct a FetchMap instance.
+     * @return {FetchMap}
+     */
+    build : function() {
+        return new FetchMap(this, this.callback);
+    }
+    
+};
+
+module.exports = FetchMap;
+module.exports.Builder = Builder;
+
+module.exports.parsePbResponse = function(pbMapEntries, setsAsBuffers) {
+    
+    var map = { counters: {}, sets: {}, registers: {}, flags: {}, maps: {} };
+    var i, j;
+    for (i = 0; i < pbMapEntries.length; i++) {
+        
+        var mapEntry = pbMapEntries[i];
+        var mapField = mapEntry.getField();
+        var key = mapField.name.toString('utf8');
+        switch(mapField.type) {
+            case MapField.MapFieldType.COUNTER:
+                map.counters[key] = mapEntry.getCounterValue().toNumber();
+                break;
+            case MapField.MapFieldType.SET:
+                var setToReturn = new Array(mapEntry.set_value.length);
+                for (j = 0; j < mapEntry.set_value.length; j++) {
+                    if (setsAsBuffers) {
+                        setToReturn[j] = mapEntry.set_value[j].toBuffer();
+                    } else {
+                        setToReturn[j] = mapEntry.set_value[j].toString('utf8');
+                    }
+                }
+                map.sets[key] = setToReturn;
+                break;
+            case MapField.MapFieldType.REGISTER:
+                map.registers[key] = mapEntry.getRegisterValue().toBuffer();
+                break;
+            case MapField.MapFieldType.FLAG:
+                map.flags[key] = mapEntry.getFlagValue();
+                break;
+            case MapField.MapFieldType.MAP:
+                map.maps[key] = FetchMap.parsePbResponse(mapEntry.map_value);
+                break;
+            default:
+                break;
+                
+        }
+        
+    }
+    
+    return map;
+};
\ No newline at end of file
diff --git a/lib/commands/crdt/fetchset.js b/lib/commands/crdt/fetchset.js
index 998b8ff..0ae3932 100644
--- a/lib/commands/crdt/fetchset.js
+++ b/lib/commands/crdt/fetchset.js
@@ -98,17 +98,19 @@ FetchSet.prototype.onSuccess = function(dtFetchResp) {
     if (dtFetchResp.getValue())
     {
         var valueBuffers = dtFetchResp.getValue().getSetValue();
-        var valueStrings = new Array(valueBuffers.length);
+        var valuesToReturn = new Array(valueBuffers.length);
         var valueCount = valueBuffers.length;
         for (var i = 0; i < valueCount; i++) {
-            valueStrings[i] = valueBuffers[i].toString('utf8');
+            if (this.options.setsAsBuffers) {
+                valuesToReturn[i] = valueBuffers[i].toBuffer();
+            } else {
+                valuesToReturn[i] = valueBuffers[i].toString('utf8');
+            }
         }
         response = {
             // treat context as opaque, don't string-ify
-            context: dtFetchResp.getContext(),
-            dataType: dtFetchResp.getType(),
-            valueBuffers: valueBuffers,
-            value: valueStrings
+            context: dtFetchResp.getContext() ? dtFetchResp.getContext().toBuffer() : null,
+            values: valuesToReturn
         };
     }
     this._callback(null, response);
@@ -132,6 +134,7 @@ var schema = Joi.object().keys({
     notFoundOk: Joi.boolean().default(null).optional(),
     useBasicQuorum: Joi.boolean().default(null).optional(),
 
+    setsAsBuffers: Joi.boolean().default(false).optional(),
     timeout: Joi.number().default(null).optional()
 
 });
@@ -216,6 +219,16 @@ bb('r');
 */
 bb('pr');
 /**
+* Return sets as arrays of Buffers rather than strings.
+* By default the contents of sets are converted to strings. Setting this
+* to true will cause this not to occur and the raw bytes returned 
+* as Buffer objects.
+* @method withSetsAsBuffers
+* @param {Boolean} setsAsBuffers true to not convert set contents to strings.
+* @chainable
+*/
+bb('setsAsBuffers');
+/**
 * Set the not_found_ok value.
 * If true a vnode returning notfound for a key increments the r tally.
 * False is higher consistency, true is higher availability.
diff --git a/lib/commands/crdt/updatemap.js b/lib/commands/crdt/updatemap.js
new file mode 100644
index 0000000..d2f0908
--- /dev/null
+++ b/lib/commands/crdt/updatemap.js
@@ -0,0 +1,695 @@
+/*
+ * Copyright 2015 Basho Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var CommandBase = require('../commandbase');
+var inherits = require('util').inherits;
+var Joi = require('joi');
+var DtOp = require('../../protobuf/riakprotobuf').getProtoFor('DtOp');
+var MapOp = require('../../protobuf/riakprotobuf').getProtoFor('MapOp');
+var MapField = require('../../protobuf/riakprotobuf').getProtoFor('MapField');
+var MapUpdate = require('../../protobuf/riakprotobuf').getProtoFor('MapUpdate');
+var CounterOp = require('../../protobuf/riakprotobuf').getProtoFor('CounterOp');
+var SetOp = require('../../protobuf/riakprotobuf').getProtoFor('SetOp');
+var FlagOp = require('../../protobuf/riakprotobuf').getProtoFor('FlagOp');
+var FetchMap = require('./fetchmap');
+
+/**
+ * Provides the UpdateMap class, its builders, and its response.
+ * @module UpdateMap
+ */
+
+/**
+ * Command used to update a Map in Riak.
+ * 
+ * As a convenience, a builder method is provided as well as an object with 
+ * a fluent API for constructing the update.
+ * 
+ *     var mapOp = new UpdateMap.MapOperation();
+ *     mapOp.incrementCounter('counter_1', 50)
+ *         .addToSet('set_1', 'set_value_1')
+ *         .setRegister('register_1', new Buffer('register_value_1'))
+ *         .setFlag('flag_1', true)
+ *         .map('inner_map')
+ *             .incrementCounter('counter_1', 50)
+ *             .addToSet('set_2', 'set_value_2');
+ *         
+ *     var update = new UpdateMap.Builder()
+ *               .withBucketType('maps')
+ *               .withBucket('myBucket')
+ *               .withKey('map_1')
+ *               .withMapOperation(mapOp)
+ *               .withCallback(myCallback)
+ *               .withReturnBody(true)
+ *               .build();
+ *
+ * @class UpdateMap
+ * @constructor
+ * @param {Object} options the options to use for this command.
+ * @param {String} options.bucketType the bucket type in riak.
+ * @param {String} options.bucket the bucket in riak.
+ * @param {MapOperation} op the set of modifications to make to this map.
+ * @param {String} [options.key] the key for the counter you want to store.
+ * @param {Buffer} [options.context] the context from a previous fetch. Required for remove operations.
+ * @param {Number} [options.w] the W value to use.
+ * @param {Number} [options.dw] the DW value to use.
+ * @param {Number} [options.pw] the PW value to use.
+ * @param {Boolean} [options.returnBody=false] return the map.
+ * @param {Boolean} [options.setsAsBuffers=false] return sets as arrays of Buffers rather than strings.
+ * @param {Number} [options.timeout] set a timeout for this command.
+ * @param {Function} callback the callback to be executed when the operation completes.
+ * @param {String} callback.err An error message
+ * @param {Number} callback.response the map from Riak if returnbody is set.
+ * @extends CommandBase
+ */
+function UpdateMap(options, callback) {
+    CommandBase.call(this, 'DtUpdateReq', 'DtUpdateResp', callback);
+    var self = this;
+    Joi.validate(options, schema, function(err, options) {
+       
+        if (err) {
+            throw err;
+        }
+        
+        if (options.op._hasRemoves() && options.context === null) {
+            throw 'When doing any removes a context must be provided.';
+        }
+    
+        self.options = options;
+    
+    });
+    
+    this.remainingTries = 1;
+}
+
+inherits(UpdateMap, CommandBase);
+
+UpdateMap.prototype.constructPbRequest = function() {
+  
+    var protobuf = this.getPbReqBuilder();
+    
+    protobuf.setBucket(new Buffer(this.options.bucket));
+    protobuf.setType(new Buffer(this.options.bucketType));
+    // key can be null to have Riak generate it.
+    if (this.options.key !== null) {
+        protobuf.setKey(new Buffer(this.options.key));
+    }
+    
+    protobuf.setTimeout(this.options.timeout);
+    protobuf.setW(this.options.w);
+    protobuf.setPw(this.options.pw);
+    protobuf.setDw(this.options.dw);
+    protobuf.setReturnBody(this.options.returnBody);
+    
+    protobuf.setContext(this.options.context);
+    
+    var dtOp = new DtOp();
+    
+    var pbMapOp = new MapOp();
+    
+    this._populate(this.options.op, pbMapOp);
+    
+    dtOp.setMapOp(pbMapOp);
+    
+    protobuf.setOp(dtOp);
+    
+    return protobuf;
+    
+};
+
+UpdateMap.prototype.onSuccess = function(dtUpdateResp) {
+    // dtUpdateResp will be null if returnBody wasn't specified
+    var response = null;
+    // on "not found" dtFetchResp will be null
+    if (dtUpdateResp) {
+        var key = null;
+        if (dtUpdateResp.key) {
+            key = dtUpdateResp.key.toString('utf8');
+        }
+        response = { generatedKey: key,
+                context: dtUpdateResp.getContext().toBuffer(),
+                map: FetchMap.parsePbResponse(dtUpdateResp.map_value, this.options.setsAsBuffers) };
+        
+    }
+    this._callback(null, response);
+    return true;
+    
+};
+
+UpdateMap.prototype._populate = function(mapOp, pbMapOp) {
+    var i;
+    var field;
+    var update;
+    if (mapOp._hasRemoves()) {
+        for (i = 0; i < mapOp.counters.remove.length; i++) {
+            field = new MapField();
+            field.setName(new Buffer(mapOp.counters.remove[i]));
+            field.setType(MapField.MapFieldType.COUNTER);
+            pbMapOp.removes.push(field);
+        }
+        for (i = 0; i < mapOp.sets.remove.length; i++) {
+            field = new MapField();
+            field.setName(new Buffer(mapOp.sets.remove[i]));
+            field.setType(MapField.MapFieldType.SET);
+            pbMapOp.removes.push(field);
+        }
+        for (i = 0; i < mapOp.maps.remove.length; i++) {
+            field = new MapField();
+            field.setName(new Buffer(mapOp.maps.remove[i]));
+            field.setType(MapField.MapFieldType.MAP);
+            pbMapOp.removes.push(field);
+        }
+        for (i = 0; i < mapOp.registers.remove.length; i++) {
+            field = new MapField();
+            field.setName(new Buffer(mapOp.registers.remove[i]));
+            field.setType(MapField.MapFieldType.REGISTER);
+            pbMapOp.removes.push(field);
+        }
+        for (i = 0; i < mapOp.flags.remove.length; i++) {
+            field = new MapField();
+            field.setName(new Buffer(mapOp.flags.remove[i]));
+            field.setType(MapField.MapFieldType.FLAG);
+            pbMapOp.removes.push(field);
+        }
+    }
+    
+    for (i = 0; i < mapOp.counters.increment.length; i++) {
+        update = new MapUpdate();
+        field = new MapField();
+        field.setName(new Buffer(mapOp.counters.increment[i].key));
+        field.setType(MapField.MapFieldType.COUNTER);
+        var counterOp = new CounterOp();
+        counterOp.setIncrement(mapOp.counters.increment[i].increment);
+        update.setField(field);
+        update.setCounterOp(counterOp);
+        pbMapOp.updates.push(update);
+    }
+    var j,v;
+    for (i = 0; i < mapOp.sets.adds.length; i++) {
+        update = new MapUpdate();
+        field = new MapField();
+        field.setName(new Buffer(mapOp.sets.adds[i].key));
+        field.setType(MapField.MapFieldType.SET);
+        var setOp = new SetOp();
+        for (j = 0; j < mapOp.sets.adds[i].add.length; j++) {
+            v = mapOp.sets.adds[i].add[j];
+            if (typeof v === 'string') {
+                v = new Buffer(v);
+            }
+            setOp.adds.push(v);
+        }
+        update.setField(field);
+        update.setSetOp(setOp);
+        pbMapOp.updates.push(update);
+    }
+    for (i = 0; i < mapOp.sets.removes.length; i++) {
+        update = new MapUpdate();
+        field = new MapField();
+        field.setName(new Buffer(mapOp.sets.removes[i].key));
+        field.setType(MapField.MapFieldType.SET);
+        var setOp = new SetOp();
+        for (j = 0; j < mapOp.sets.removes[i].remove.length; j++) {
+            v = mapOp.sets.removes[i].remove[j];
+            if (typeof v === 'string') {
+                v = new Buffer(v);
+            }
+            setOp.removes.push(v);
+        }
+        update.setField(field);
+        update.setSetOp(setOp);
+        pbMapOp.updates.push(update);
+    }
+    for (i = 0; i < mapOp.registers.set.length; i++) {
+        update = new MapUpdate();
+        field = new MapField();
+        field.setName(new Buffer(mapOp.registers.set[i].key));
+        field.setType(MapField.MapFieldType.REGISTER);
+        update.setField(field);
+        update.setRegisterOp(mapOp.registers.set[i].value);
+        pbMapOp.updates.push(update);
+    } 
+    for (i = 0; i < mapOp.flags.set.length; i++) {
+        update = new MapUpdate();
+        field = new MapField();
+        field.setName(new Buffer(mapOp.flags.set[i].key));
+        field.setType(MapField.MapFieldType.FLAG);
+        update.setField(field);
+        if (mapOp.flags.set[i].state) {
+            update.setFlagOp(MapUpdate.FlagOp.ENABLE);
+        } else {
+            update.setFlagOp(MapUpdate.FlagOp.DISABLE);
+        }
+        pbMapOp.updates.push(update);
+    } 
+    for (i = 0; i < mapOp.maps.modify.length; i++) {
+        update = new MapUpdate();
+        field = new MapField();
+        field.setName(new Buffer(mapOp.maps.modify[i].key));
+        field.setType(MapField.MapFieldType.MAP);
+        var nestedMapOp = new MapOp();
+        this._populate(mapOp.maps.modify[i].map, nestedMapOp);
+        update.setMapOp(nestedMapOp);
+        update.setField(field);
+        pbMapOp.updates.push(update);
+    }
+};
+
+var schema = Joi.object().keys({
+    bucket: Joi.string().required(),
+    bucketType: Joi.string().required(),
+    key: Joi.string().optional(),
+    callback: Joi.func().required(),
+    op: Joi.object().type(MapOperation).required(),
+    w: Joi.number().default(null).optional(),
+    dw: Joi.number().default(null).optional(),
+    pw: Joi.number().default(null).optional(),
+    returnBody: Joi.boolean().default(false).optional(),
+    setsAsBuffers: Joi.boolean().default(false).optional(),
+    timeout: Joi.number().default(null).optional(),
+    context: Joi.binary().default(null).optional()    
+});
+
+
+/**
+ * A builder for constructing UpdateMap instances.
+ * * Rather than having to manually construct the __options__ and instantiating
+ * a UpdateMap directly, this builder may be used.
+ * 
+ *      var UpdateMap = require('./lib/commands/datatype/updateMap');
+ *      var update = new UpdateMap.Builder()
+ *               .withBucketType('counters')
+ *               .withBucket('myBucket')
+ *               .withKey('counter_1')
+ *               .withMapOperation(mapOp)
+ *               .withCallback(callback)
+ *               .build();
+ *       
+ * @namespace UpdateCounter
+ * @class Builder
+ * @constructor
+ */
+function Builder() {}
+
+Builder.prototype = {
+  
+    /**
+     * Set the bucket.
+     * @method withBucket
+     * @param {String} bucket the bucket in Riak
+     * @chainable
+     */
+    withBucket : function(bucket) {
+        this.bucket = bucket;
+        return this;
+    },
+    /**
+     * Set the bucket type.
+     * @method withBucketType
+     * @param {String} bucketType the bucket type in riak
+     * @chainable
+     */
+    withBucketType : function(bucketType) {
+        this.bucketType = bucketType;
+        return this;
+    },
+    /**
+     * Set the key.
+     * If not set, riak will generate and return a key.
+     * @method withKey
+     * @param {String} key the key in riak.
+     * @chainable
+     */
+    withKey : function(key) {
+        this.key = key;
+        return this;
+    },
+    /**
+     * Set the modifications to make to this map.
+     * @method withMapOperation
+     * @param {MapOperation} mapOp the modifications
+     * @chainable
+     */
+    withMapOperation : function(mapOp) {
+        this.op = mapOp;
+        return this;
+    },
+    /**
+     * The context returned from a previous fetch of this map.
+     * __Note:__ this is required when performing any removes.
+     * @method withContext
+     * @param {Buffer} context the contect from a previous fetch.
+     * @chainable
+     */
+    withContext : function(context) {
+        this.context = context;
+        return this;
+    },
+    /**
+     * Set the callback to be executed when the operation completes.
+     * @method withCallback
+     * @param {Function} callback - the callback to execute
+     * @param {String} callback.err An error message
+     * @param {Object} callback.response - the response from Riak if returnbody is set to true.
+     * @chainable
+     */
+    withCallback : function(callback) {
+        this.callback = callback;
+        return this;
+    },
+     /**
+    * Set the W value.
+    * How many replicas to write to before returning a successful response.
+    * If not set the bucket default is used.
+    * @method withW
+    * @param {number} w the W value.
+    * @chainable
+    */
+    withW : function(w) {
+        this.w = w ;
+        return this;
+    },
+    /**
+     * Set the DW value.
+     * How many replicas to commit to durable storage before returning a successful response.
+     * If not set the bucket default is used.
+     * @method withDw
+     * @param {number} dw the DW value.
+     * @chainable
+     */
+    withDw : function(dw) {
+        this.dw = dw;
+        return this;
+    },
+    /**
+     * Set the PW value.
+     * How many primary nodes must be up when the write is attempted.
+     * If not set the bucket default is used.
+     * @method withPw
+     * @param {number} pw the PW value.
+     * @chainable
+     */
+    withPw : function(pw) {
+        this.pw = pw;
+        return this;
+    },
+    /**
+    * Return the counter after updating.
+    * @method withReturnBody
+    * @param {boolean} returnBody true to return the counter. 
+    * @chainable
+    */
+    withReturnBody: function(returnBody) {
+        this.returnBody = returnBody;
+        return this;
+    },
+    /**
+     * Return sets as arrays of Buffers rather than strings.
+     * By default the contents of sets are converted to strings. Setting this
+     * to true will cause this not to occur and the raw bytes returned 
+     * as Buffer objects. Note this is only used with the returnBody option.
+     * @method withSetsAsBuffers
+     * @param {Boolean} setsAsBuffers true to not convert set contents to strings.
+     * @chainable
+     */
+    withSetsAsBuffers : function(setsAsBuffers) {
+        this.setsAsBuffers = setsAsBuffers;
+        return this;
+    },
+    /**
+    * Set a timeout for this operation.
+    * @method withTimeout
+    * @param {number} timeout a timeout in milliseconds.
+    * @chainable
+    */
+    withTimeout : function(timeout) {
+        this.timeout = timeout;
+        return this;
+    },
+    /**
+     * 
+     * @return {UpdateMap}
+     */
+    build : function() {
+        return new UpdateMap(this, this.callback);
+    }
+    
+};
+
+/**
+ * Class that encapsulates modifications to a Map in Riak.
+ * 
+ * Rather than manually constructing this yourself, a fluent API is provided.
+ * 
+ * var mapOp = new UpdateMap.MapOperation();
+ *     mapOp.incrementCounter('counter_1', 50)
+ *         .addToSet('set_1', 'set_value_1')
+ *         .setRegister('register_1', new Buffer('register_value_1'))
+ *         .setFlag('flag_1', true)
+ *         .map('inner_map')
+ *             .incrementCounter('counter_1', 50)
+ *             .addToSet('set_2', 'set_value_2');
+ *             
+ * @namespace UpdateMap
+ * @class MapOperation
+ * @constructor
+ */
+function MapOperation() {
+    
+    this.counters = { increment: [], remove : []};
+    this.maps = { modify: [], remove: []};
+    this.sets = { adds: [], removes: [], remove: []};
+    this.registers = { set: [], remove: [] };
+    this.flags = { set: [], remove: [] };
+}
+
+MapOperation.prototype = {
+  
+    /**
+     * Increment (and/or create) a counter inside the map. 
+     * @param {String} key the key in the map for this counter.
+     * @param {Number} increment the amount to increment (or decrement if negative) 
+     * @chainable
+     */
+    incrementCounter : function(key, increment) {
+        this._removeRemove(this.counters.remove, key);
+        var op = this._getOp(this.counters.increment, key);
+        if (op) {
+            op.increment += increment;
+        } else { 
+            this.counters.increment.push({key: key, increment: increment});
+        }
+        return this;
+    },
+    /**
+     * Remove a counter from a map.
+     * @method removeCounter
+     * @param {String} key the key in the map for this counter.
+     * @chainable
+     */
+    removeCounter : function(key) {
+        this._removeAddsOrRemoves(this.counters.increment, key);
+        if (this.counters.remove.indexOf(key) === -1) {
+            this.counters.remove.push(key);
+        }
+        return this;
+    },
+    /**
+     * Add a value to a set (and create is necessary) in a map.
+     * @method addToSet
+     * @param {String} key the key for the set in the map.
+     * @param {String|Buffer} value the value to add to the set.
+     * @chainable
+     */
+    addToSet : function(key, value) {
+        this._removeRemove(this.sets.remove, key);
+        var op = this._getOp(this.sets.adds, key);
+        if (op) {
+            op.add.push(value);
+        } else {
+            this.sets.adds.push({key: key, add: [value]});
+        }
+        return this;
+    },
+    /**
+     * Remove a value from a set in a map.
+     * @method removeFromSet
+     * @param {String} key the key for the set in the map.
+     * @param {String|Buffer} value the value to add to the set.
+     * @chainable
+     */
+    removeFromSet : function(key, value) {
+        this._removeRemove(this.sets.remove, key);
+        var op = this._getOp(this.sets.removes, key);
+        if (op) {
+            op.remove.push(value);
+        } else {
+            this.sets.removes.push({key: key, remove: [value]});
+        }
+        return this;
+    },
+    /**
+     * Remove a set from a map.
+     * @method removeSet
+     * @param {String} key the key for the set in the map.
+     * @chainable
+     */
+    removeSet : function(key) {
+        this._removeAddsOrRemoves(this.sets.adds, key);
+        this._removeAddsOrRemoves(this.sets.removes, key);
+        if (this.sets.remove.indexOf(key) === -1) {
+            this.sets.remove.push(key);
+        }
+        return this;
+    },
+    /**
+     * Set a register in a map.
+     * @method setRegister
+     * @param {String} key the key for the register in the map.
+     * @param {Buffer} value the value for the register.
+     * @chainable}
+     */
+    setRegister : function(key, value) {
+        this._removeRemove(this.registers.remove, key);
+        var op = this._getOp(this.registers.set, key);
+        if (op) {
+            op.value = value;
+        } else {
+            this.registers.set.push({key: key, value: value});
+        }
+        return this;
+    },
+    /**
+     * Remove a register from a map.
+     * @method removeRegister
+     * @param {String} key the key for the register in the map.
+     * @chainable
+     */
+    removeRegister : function(key) {
+        this._removeAddsOrRemoves(this.registers.set, key);
+        if (this.registers.remove.indexOf(key) === -1) {
+            this.registers.remove.push(key);
+        }
+        return this;
+    },
+    /**
+     * Set a flag in a map.
+     * @method setFlag
+     * @param {String} key the key for the set in the map.
+     * @param {Boolean} value the value for the flag.
+     * @chainable}
+     */
+    setFlag : function(key, state) {
+        this._removeRemove(this.flags.remove, key);
+        var op = this._getOp(this.flags.set, key);
+        if (op) {
+            op.state = state;
+        } else {
+            this.flags.set.push({key: key, state: state});
+        }
+        return this;
+    },
+    /**
+     * Remove a flag from a map.
+     * @method removeFlag
+     * @param {String} key the key for the flag in the map.
+     * @chainable
+     */
+    removeFlag : function(key) {
+        this._removeAddsOrRemoves(this.flags.set, key);
+        if (this.flags.remove.indexOf(key) === -1) {
+            this.flags.remove.push(key);
+        }
+        return this;
+    },
+    /**
+     * Access/create a map inside the current one.
+     * This adds/accesses a nested map and returns a reference to another
+     * MapOperation that applies to it. You can then modify the componets of 
+     * that map;
+     * 
+     *     mapOp.map('inner_map')
+     *         .incrementCounter('counter_1', 50)
+     *         .addToSet('set_2', 'set_value_2');
+     *
+     * @param {String} key the key for the nested map in the current map.
+     * @return @chainable
+     */
+    map : function(key) {
+        this._removeRemove(this.maps.remove, key);
+        var map = this._getOp(this.maps.modify);
+        if (map) {
+            return map;
+        } else {
+            map = new MapOperation(this);
+            this.maps.modify.push({key: key, map: map});
+            return map;
+        }
+    },
+    /**
+     * Remove a map from a map.
+     * @method removeMap
+     * @param {String} key the key for the map in the map.
+     * @chainable
+     */
+    removeMap : function(key) {
+        this._removeAddsOrRemoves(this.maps.modify, key);
+        if (this.maps.remove.indexOf(key) === -1) {
+            this.maps.remove.push(key);
+        }
+        return this;
+    },
+    _removeRemove : function(array, key) {
+        for (var i = 0; i < array.length; i++) {
+            if (array[i] === key) {
+                array.splice(i,1);
+            }
+        }
+    },
+    _removeAddsOrRemoves : function(array, key) {
+        for (var i = 0; i < array.length; i++) {
+            if (array[i].key === key) {
+                array.splice(i,1);
+            }
+        }
+    },
+    _getOp : function(array, key) {
+        for (var i = 0; i < array.length; i++) {
+            if (array[i].key === key) {
+                return array[i];
+            }
+        }
+        return null;
+    },
+    _hasRemoves : function() {
+        
+        var nestedHaveRemoves = false;
+        for (var i = 0; i < this.maps.modify.length; i++) {
+            nestedHaveRemoves |= this.maps.modify[i].map._hasRemoves();
+        }
+        
+        return nestedHaveRemoves ||
+                this.counters.remove.length ||
+                this.maps.remove.length ||
+                this.sets.remove.length ||
+                this.sets.removes.length ||
+                this.registers.remove.length ||
+                this.flags.remove.length;
+    }
+    
+};
+
+module.exports = UpdateMap;
+module.exports.MapOperation = MapOperation;
+module.exports.Builder = Builder;
\ No newline at end of file
diff --git a/lib/commands/crdt/updateset.js b/lib/commands/crdt/updateset.js
index 4bdd9c9..a6d167b 100644
--- a/lib/commands/crdt/updateset.js
+++ b/lib/commands/crdt/updateset.js
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 Basho Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 var CommandBase = require('../commandbase');
 var inherits = require('util').inherits;
 var Joi = require('joi');
@@ -7,6 +23,45 @@ var Rpb = require('../../protobuf/riakprotobuf');
 var DtOp = Rpb.getProtoFor('DtOp');
 var SetOp = Rpb.getProtoFor('SetOp');
 
+
+/**
+ * Provides the Update Set class, its builder, and its response.
+ * @module UpdateSet
+ */
+
+/**
+ * Command used tp update a set in Riak
+ * 
+ * As a convenience, a builder class is provided:
+ * 
+ *        var update = new UpdateSet.Builder()
+ *               .withBucketType('sets')
+ *               .withBucket('myBucket')
+ *               .withKey('set_1')
+ *               .withAdditions(['this', 'that', 'other'])
+ *               .withCallback(callback)
+ *               .build();
+ * 
+ * @class UpdateCounter
+ * @constructor
+ * @param {Object} options the options to use for this command.
+ * @param {String} options.bucketType the bucket type in riak.
+ * @param {String} options.bucket the bucket in riak.
+ * @param {String} [options.key] the key for the set you want to store. Riak will generate one if not set.
+ * @param {String[]|Buffer[]} additions the values to be added to the set.
+ * @param {String[]|Buffer[]} removals the values to remove from the set. Note that a context is required.
+ * @param {Buffer} [options.context] the context from a previous fetch. Required for remove operations.
+ * @param {Boolean} [options.setsAsBuffers=false] return sets as arrays of Buffers rather than strings.
+ * @param {Number} [options.w] the W value to use.
+ * @param {Number} [options.dw] the DW value to use.
+ * @param {Number} [options.pw] the PW value to use.
+ * @param {Boolean} [options.returnBody] return the set.
+ * @param {Number} [options.timeout] set a timeout for this command.
+ * @param {Function} callback the callback to be executed when the operation completes.
+ * @param {String} callback.err An error message
+ * @param {Number} callback.response the set from Riak if returnBody is set or a key is generated.
+ * @extends CommandBase
+ */
 function UpdateSet(options, callback) {
     CommandBase.call(this, 'DtUpdateReq', 'DtUpdateResp', callback);
 
@@ -43,23 +98,17 @@ function buflist(list) {
     return newList;
 }
 
-function stringlist(bufferList) {
-    var newList = [];
-    var len = bufferList.length;
-    for (var i = 0; i < len; i++) {
-        newList[i] = bufferList[i].toString();
-    }
-    return newList;
-}
-
 UpdateSet.prototype.constructPbRequest = function() {
     var protobuf = this.getPbReqBuilder();
 
     // namespace
     protobuf.setType(pbuf(this, 'bucketType'));
     protobuf.setBucket(pbuf(this, 'bucket'));
-    protobuf.setKey(pbuf(this, 'key'));
-
+    
+    if (this.options.key) {
+        protobuf.setKey(pbuf(this, 'key'));
+    }
+    
     // operation
     var op = new DtOp();
     protobuf.setOp(op);
@@ -76,30 +125,35 @@ UpdateSet.prototype.constructPbRequest = function() {
     // options
     protobuf.setReturnBody(this.options.returnBody);
     protobuf.setTimeout(this.options.timeout);
-    protobuf.setIncludeContext(this.options.includeContext);
+    protobuf.setContext(this.options.context);
 
     return protobuf;
 };
 
 UpdateSet.prototype.onSuccess = function(dtUpdateResp) {
-    var response = {};
-
-    if (dtUpdateResp.getKey()) {
-        response.key = dtUpdateResp.getKey();
-        response.keyString = response.key.toString("utf8");
-    }
-
-    if (dtUpdateResp.getContext()) {
-        response.context = dtUpdateResp.getContext();
-    }
-
-    if (dtUpdateResp.getSetValue()) {
-        // set a dataType field so we look like a fetchset response
-        response.dataType = 2;
-        response.valueBuffers = buflist(dtUpdateResp.getSetValue());
-        response.value = stringlist(response.valueBuffers);
+    var response = null;
+    
+    // dtUpdateResp will be null if returnBody is not set
+    if (dtUpdateResp) {
+        var key = null;
+        if (dtUpdateResp.getKey()) {
+            key = dtUpdateResp.getKey().toString('utf8');
+        }
+        
+        var valuesToReturn = new Array(dtUpdateResp.set_value.length);
+        var i;
+        for (i = 0; i < dtUpdateResp.set_value.length; i++) {
+            if (this.options.setsAsBuffers) {
+                valuesToReturn[i] = dtUpdateResp.set_value[i].toBuffer();
+            } else {
+                valuesToReturn[i] = dtUpdateResp.set_value[i].toString('utf8');
+            }
+        }
+        
+        response = { generatedKey: key,
+                context: dtUpdateResp.getContext() ? dtUpdateResp.getContext().toBuffer() : null,
+                values: valuesToReturn };
     }
-
     this._callback(null, response);
     return true;
 };
@@ -112,7 +166,8 @@ var schema = Joi.object().keys({
     // bucket type is required since default probably shouldn't have a
     // datatype associated with it
     bucketType: Joi.string().required(),
-    key: Joi.string().required(),
+    // key is optional because Riak can generate one
+    key: Joi.string().optional(),
 
     //operations
     additions: Joi.array().default([]).optional(),
@@ -128,7 +183,7 @@ var schema = Joi.object().keys({
     //options
     returnBody: Joi.boolean().default(true).optional(),
     timeout: Joi.number().default(null).optional(),
-    includeContext: Joi.boolean().default(true).optional()
+    setsAsBuffers: Joi.boolean().default(false).optional(),
 });
 
 /**
@@ -136,11 +191,11 @@ var schema = Joi.object().keys({
  * * Rather than having to manually construct the __options__ and instantiating
  * a UpdateSet directly, this builder may be used.
  *
- *      var UpdateSet = require('./lib/commands/datatype/updateset');
  *      var update = new UpdateSet.Builder()
  *                       .withBucketType('myBucketType')
  *                       .withBucket('myBucket')
  *                       .withKey('myKey')
+ *                       .withAdditions(['this', 'that', 'other'])
  *                       .withCallback(myCallback)
  *                       .build();
  *
@@ -152,9 +207,9 @@ function Builder() {}
 
 Builder.prototype = {
     /**
-     * Construct a SetchSet instance.
+     * Construct an UpdateSet instance.
      * @method build
-     * @return {FetchSet}
+     * @return {UpdateSet}
      */
     build: function() {
         return new UpdateSet(this, this.callback);
@@ -189,6 +244,7 @@ bb('bucketType');
 bb('bucket');
 /**
 * Set the key.
+* If this is not set one will be generated by and returned from Riak
 * @method withKey
 * @param {String} key the key in riak.
 * @chainable
@@ -203,12 +259,23 @@ bb('key');
  */
 bb('context');
 /**
+* Return sets as arrays of Buffers rather than strings.
+* By default the contents of sets are converted to strings. Setting this
+* to true will cause this not to occur and the raw bytes returned 
+* as Buffer objects.
+* @method withSetsAsBuffers
+* @param {Boolean} setsAsBuffers true to not convert set contents to strings.
+* @chainable
+*/
+bb('setsAsBuffers');
+/**
 * Set the W value.
 * If not set the bucket default is used.
 * @method withW
 * @param {Number} w the W value.
 * @chainable
 */
+
 bb('w');
 /**
 * Set the DW value.
@@ -252,9 +319,22 @@ bb('useBasicQuorum');
 * @chainable
 */
 bb('timeout');
-
+/**
+ * The values you wish to add to this set.
+ * @method withAdditions
+ * @param {String[]|Buffer[]} additions The values to add. 
+ * @chainable
+ */
 bb('additions');
+/**
+ * The values you wish to remove from this set.
+ * __Note:__ when performing removals a context must be provided.
+ * @method withRemovals
+ * @param {String[]|Buffer[]} removals The values to remove.
+ * @chainable
+ */
 bb('removals');
 
+
 module.exports = UpdateSet;
 module.exports.Builder = Builder;
diff --git a/lib/core/riaknode.js b/lib/core/riaknode.js
index 42e368a..2e43732 100644
--- a/lib/core/riaknode.js
+++ b/lib/core/riaknode.js
@@ -219,6 +219,7 @@ RiakNode.prototype.execute = function(command) {
             return false;
         }
     } else {
+        logger.info("Executing command on RiakNode (%s:%d)", this.remoteAddress, this.remotePort);
         conn.execute(command);
         return true;
     }
@@ -230,7 +231,7 @@ RiakNode.prototype._responseReceived = function(conn, command, code, decoded) {
     command.remainingTries--;
     if (code === RiakProtobuf.getCodeFor('RpbErrorResp')) {
         this._returnConnectionToPool(conn);
-        logger.info('RiakNode (%s:%d): recevied RpbErrorResp for command; %s', this.remoteAddress, this.remotePort, decoded.getErrmsg().toString());
+        logger.info('RiakNode (%s:%d): recevied RpbErrorResp for command; %s', this.remoteAddress, this.remotePort, decoded.getErrmsg().toString('utf8'));
         if (command.remainingTries) {
             this.emit('retryCommand', command, this);
         } else {
diff --git a/tools b/tools
deleted file mode 160000
index 5eb97e8..0000000
--- a/tools
+++ /dev/null
@@ -1,1 +0,0 @@
-Subproject commit 5eb97e800f082e90d2047891042d11f46df4d835


Test patch:
diff --git a/test/unit/crdt/fetchmap.js b/test/unit/crdt/fetchmap.js
new file mode 100644
index 0000000..a11997f
--- /dev/null
+++ b/test/unit/crdt/fetchmap.js
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2015 Basho Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var FetchMap = require('../../../lib/commands/crdt/fetchmap');
+var MapField = require('../../../lib/protobuf/riakprotobuf').getProtoFor('MapField');
+var MapUpdate = require('../../../lib/protobuf/riakprotobuf').getProtoFor('MapUpdate');
+var DtFetchResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('DtFetchResp');
+var MapEntry = require('../../../lib/protobuf/riakprotobuf').getProtoFor('MapEntry');
+var DtValue = require('../../../lib/protobuf/riakprotobuf').getProtoFor('DtValue');
+var ByteBuffer = require('bytebuffer');
+var RpbErrorResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('RpbErrorResp');
+
+var assert = require('assert');
+
+describe('FetchMap', function() {
+    
+    describe('Build', function() {
+    
+        it('should build a DtFetchReq correctly', function(done) {
+            
+            var fetch = new FetchMap.Builder()
+                .withBucketType('maps')
+                .withBucket('myBucket')
+                .withKey('map_1')
+                .withCallback(function(){})
+                .withR(1)
+                .withPr(2)
+                .withNotFoundOk(true)
+                .withBasicQuorum(true)
+                .withTimeout(20000)
+                .build();
+        
+            var protobuf = fetch.constructPbRequest();
+            
+            assert.equal(protobuf.getType().toString('utf8'), 'maps');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'myBucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'map_1');
+            assert.equal(protobuf.getR(), 1);
+            assert.equal(protobuf.getPr(), 2);
+            assert.equal(protobuf.getNotfoundOk(), true);
+            assert.equal(protobuf.getBasicQuorum(), true);
+            assert.equal(protobuf.getTimeout(), 20000);
+            done();
+            
+        });
+        
+        it('should take a DtFetchResp and call the users callback with the response', function(done) {
+           
+            var dtFetchResp = new DtFetchResp();
+            
+            dtFetchResp.setType(DtFetchResp.DataType.MAP);
+            dtFetchResp.setContext(new Buffer('1234'));
+            var dtValue = new DtValue();
+            var createMapEntries = function() {
+                var mapEntries = [];
+                
+                var mapEntry = new MapEntry();
+                var mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.COUNTER);
+                mapField.setName(new Buffer('counter_1'));
+                mapEntry.setField(mapField);
+                mapEntry.setCounterValue(50);
+                mapEntries.push(mapEntry);
+                
+                mapEntry = new MapEntry();
+                mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.SET);
+                mapField.setName(new Buffer('set_1'));
+                mapEntry.setField(mapField);
+                mapEntry.set_value.push.apply(mapEntry.set_value, [ByteBuffer.fromUTF8('value_1'), ByteBuffer.fromUTF8('value_2')]);
+                mapEntries.push(mapEntry);
+                
+                var mapEntry = new MapEntry();
+                var mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.REGISTER);
+                mapField.setName(new Buffer('register_1'));
+                mapEntry.setField(mapField);
+                mapEntry.setRegisterValue(ByteBuffer.fromUTF8('1234'));
+                mapEntries.push(mapEntry);
+                
+                var mapEntry = new MapEntry();
+                var mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.FLAG);
+                mapField.setName(new Buffer('flag_1'));
+                mapEntry.setField(mapField);
+                mapEntry.setFlagValue(true);
+                mapEntries.push(mapEntry);
+                
+                return mapEntries;
+
+            };
+            
+            dtValue.map_value.push.apply(dtValue.map_value, createMapEntries());
+            
+            var mapEntry = new MapEntry();
+            var mapField = new MapField();
+            mapField.setType(MapField.MapFieldType.MAP);
+            mapField.setName(new Buffer('map_1'));
+            mapEntry.setField(mapField);
+            mapEntry.map_value.push.apply(mapEntry.map_value, createMapEntries());
+            
+            dtValue.map_value.push(mapEntry);
+            
+            dtFetchResp.setValue(dtValue);
+            
+            var callback = function(err, resp) {
+                assert(resp !== null);
+                assert.equal(resp.context.toString('utf8'), '1234');
+                
+                var verifyMap = function(map) {
+                    assert.equal(map.counters['counter_1'], 50);
+                    assert.equal(map.sets['set_1'][0], 'value_1');
+                    assert.equal(map.sets['set_1'][1], 'value_2');
+                    assert.equal(map.registers['register_1'].toString('utf8'), '1234');
+                    assert.equal(map.flags['flag_1'], true);
+                };
+            
+                verifyMap(resp.map);
+                verifyMap(resp.map.maps['map_1']);
+                done();
+            
+            };
+            
+            var fetch = new FetchMap.Builder()
+                .withBucketType('maps')
+                .withBucket('myBucket')
+                .withKey('map_1')
+                .withCallback(callback)
+                .build();
+        
+            fetch.onSuccess(dtFetchResp);
+            
+            
+        });
+        
+    });
+    
+    
+    
+});
\ No newline at end of file
diff --git a/test/unit/crdt/fetchset.js b/test/unit/crdt/fetchset.js
index 6d72342..7c239a1 100644
--- a/test/unit/crdt/fetchset.js
+++ b/test/unit/crdt/fetchset.js
@@ -20,6 +20,7 @@ var DtFetchReq = Rpb.getProtoFor('DtFetchReq');
 var DtFetchResp = Rpb.getProtoFor('DtFetchResp');
 var DtValue = Rpb.getProtoFor('DtValue');
 var RpbErrorResp = Rpb.getProtoFor('RpbErrorResp');
+var ByteBuffer = require('bytebuffer');
 
 var ByteBuffer = require('bytebuffer');
 var assert = require('assert');
@@ -57,17 +58,17 @@ describe('FetchSet', function() {
             done();
         });
 
-        it('calls back with successful results', function(done){
+        it('calls back with successful results as buffers', function(done){
             var resp = new DtFetchResp();
             resp.type = 2;
-            resp.context = new Buffer("asdf");
+            resp.context = ByteBuffer.fromUTF8("asdf");
 
             var value = new DtValue();
             resp.setValue(value);
 
-            value.set_value = [new Buffer("zedo"),
-                               new Buffer("piper"),
-                               new Buffer("little one")];
+            value.set_value = [ByteBuffer.fromUTF8("zedo"),
+                               ByteBuffer.fromUTF8("piper"),
+                               ByteBuffer.fromUTF8("little one")];
 
             var includesBuffer = function(haystack, needle) {
                 var needleBuf = new Buffer(needle);
@@ -79,6 +80,38 @@ describe('FetchSet', function() {
                 return false;
             };
 
+            var callback = function(err, response) {
+                assert.equal(response.context.toString("utf8"), "asdf");
+
+                assert(includesBuffer(response.values, "zedo"));
+
+                assert(includesBuffer(response.values, "piper"));
+                
+                assert(includesBuffer(response.values, "little one"));
+                
+                done();
+            };
+
+            var fetch = builder.
+                    withCallback(callback).
+                    withSetsAsBuffers(true).
+                    build();
+
+            fetch.onSuccess(resp);
+        });
+        
+        it('calls back with successful results as strings', function(done){
+            var resp = new DtFetchResp();
+            resp.type = 2;
+            resp.context = ByteBuffer.fromUTF8("asdf");
+
+            var value = new DtValue();
+            resp.setValue(value);
+
+            value.set_value = [ByteBuffer.fromUTF8("zedo"),
+                               ByteBuffer.fromUTF8("piper"),
+                               ByteBuffer.fromUTF8("little one")];
+
             var includes = function(haystack, needle) {
                 var len = haystack.length;
                 for (var i = 0; i < len; i++) {
@@ -90,22 +123,19 @@ describe('FetchSet', function() {
             
             var callback = function(err, response) {
                 assert.equal(response.context.toString("utf8"), "asdf");
-                assert.equal(response.dataType, 2);
 
-                assert(includesBuffer(response.valueBuffers, "zedo"));
-                assert(includes(response.value, "zedo"));
+                assert(includes(response.values, "zedo"));
 
-                assert(includesBuffer(response.valueBuffers, "piper"));
-                assert(includes(response.value, "piper"));
+                assert(includes(response.values, "piper"));
                 
-                assert(includesBuffer(response.valueBuffers, "little one"));
-                assert(includes(response.value, "little one"));
+                assert(includes(response.values, "little one"));
                 
                 done();
             };
 
             var fetch = builder.
                     withCallback(callback).
+                    withSetsAsBuffers(false).
                     build();
 
             fetch.onSuccess(resp);
diff --git a/test/unit/crdt/updatemap.js b/test/unit/crdt/updatemap.js
new file mode 100644
index 0000000..e44f7c8
--- /dev/null
+++ b/test/unit/crdt/updatemap.js
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2015 Basho Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var UpdateMap = require('../../../lib/commands/crdt/updatemap');
+var MapField = require('../../../lib/protobuf/riakprotobuf').getProtoFor('MapField');
+var MapUpdate = require('../../../lib/protobuf/riakprotobuf').getProtoFor('MapUpdate');
+var DtUpdateResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('DtUpdateResp');
+var MapEntry = require('../../../lib/protobuf/riakprotobuf').getProtoFor('MapEntry');
+var ByteBuffer = require('bytebuffer');
+var RpbErrorResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('RpbErrorResp');
+
+var assert = require('assert');
+
+describe('UpdateMap', function() {
+    
+    describe('UpdateMap', function() {
+    
+        it('should build a DtUpdateReq correctly', function(done) {
+           
+           var mapOp = new UpdateMap.MapOperation();
+           
+            mapOp.incrementCounter('counter_1', 50)
+                .removeCounter('counter_2')
+                .addToSet('set_1', 'set_value_1')
+                .removeFromSet('set_2', 'set_value_2')
+                .removeSet('set_3')
+                .setRegister('register_1', new Buffer('register_value_1'))
+                .removeRegister('register_2')
+                .setFlag('flag_1', true)
+                .removeFlag('flag_2')
+                .removeMap('map_3');
+        
+            mapOp.map('map_2').incrementCounter('counter_1', 50)
+                .removeCounter('counter_2')
+                .addToSet('set_1', new Buffer('set_value_1'))
+                .removeFromSet('set_2', new Buffer('set_value_2'))
+                .removeSet('set_3')
+                .setRegister('register_1', new Buffer('register_value_1'))
+                .removeRegister('register_2')
+                .setFlag('flag_1', true)
+                .removeFlag('flag_2')
+                .removeMap('map_3')
+                .map('map_2');
+        
+           
+            var update = new UpdateMap.Builder()
+                .withBucketType('maps')
+                .withBucket('myBucket')
+                .withKey('map_1')
+                .withMapOperation(mapOp)
+                .withContext(new Buffer('1234'))
+                .withCallback(function(){})
+                .withW(3)
+                .withPw(1)
+                .withDw(2)
+                .withReturnBody(true)
+                .withTimeout(20000)
+                .build();
+        
+            var protobuf = update.constructPbRequest();
+                        
+            assert.equal(protobuf.getType().toString('utf8'), 'maps');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'myBucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'map_1');
+            assert.equal(protobuf.getW(), 3);
+            assert.equal(protobuf.getPw(), 1);
+            assert.equal(protobuf.getDw(), 2);
+            assert.equal(protobuf.getReturnBody(), true);
+            assert.equal(protobuf.getTimeout(), 20000);
+            assert.equal(protobuf.getContext().toString('utf8'), '1234');
+            
+            mapOp = protobuf.op.map_op;
+            
+            var verifyRemoves = function(removes) {
+                assert.equal(removes.length, 5);
+                var i;
+                var counterRemoved = false;
+                var setRemoved = false;
+                var registerRemoved = false;
+                var flagRemoved = false;
+                var mapRemoved = false;
+                for (i = 0; i < mapOp.removes.length; i++ ) {
+                    switch(mapOp.removes[i].type) {
+                        case MapField.MapFieldType.COUNTER:
+                            assert.equal(mapOp.removes[i].name.toString('utf8'), 'counter_2');
+                            counterRemoved = true;
+                            break;
+                        case MapField.MapFieldType.SET:
+                            assert.equal(mapOp.removes[i].name.toString('utf8'), 'set_3');
+                            setRemoved = true;
+                            break;
+                        case MapField.MapFieldType.MAP:
+                            assert.equal(mapOp.removes[i].name.toString('utf8'), 'map_3');
+                            mapRemoved = true;
+                            break;
+                        case MapField.MapFieldType.REGISTER:
+                            assert.equal(mapOp.removes[i].name.toString('utf8'), 'register_2');
+                            registerRemoved = true;
+                            break;
+                        case MapField.MapFieldType.FLAG:
+                            assert.equal(mapOp.removes[i].name.toString('utf8'), 'flag_2');
+                            flagRemoved = true;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+                
+                assert(counterRemoved);
+                assert(setRemoved);
+                assert(registerRemoved);
+                assert(flagRemoved);
+                assert(mapRemoved);
+                
+            };
+            
+            verifyRemoves(mapOp.removes);
+            
+            var verifyUpdates = function(updates) {
+              
+                var i;
+                var counterIncremented = false;
+                var setAddedTo = false;
+                var setRemovedFrom = false;
+                var registerSet = false;
+                var flagSet = false;
+                var mapAdded = false;
+                var mapUpdate = null;
+                for (i = 0; i < updates.length; i++) {
+                    switch(updates[i].field.type) {
+                        case MapField.MapFieldType.COUNTER:
+                            assert.equal(updates[i].field.name.toString('utf8'), 'counter_1');
+                            assert.equal(updates[i].counter_op.getIncrement(), 50);
+                            counterIncremented = true;
+                            break;
+                        case MapField.MapFieldType.SET:
+                            if (updates[i].set_op.adds.length) {
+                                assert.equal(updates[i].field.name.toString('utf8'), 'set_1');
+                                assert.equal(updates[i].set_op.adds[0].toString('utf8'),'set_value_1');
+                                setAddedTo = true;
+                                
+                            } else {
+                                assert.equal(updates[i].field.name.toString('utf8'), 'set_2');
+                                assert.equal(updates[i].set_op.removes[0].toString('utf8'),'set_value_2');
+                                setRemovedFrom = true;
+                            }   
+                            break;
+                        case MapField.MapFieldType.MAP:
+                            assert.equal(updates[i].field.name.toString('utf8'), 'map_2');
+                            mapAdded = true;
+                            mapUpdate = updates[i];
+                            break;
+                        case MapField.MapFieldType.REGISTER:
+                            assert.equal(updates[i].field.name.toString('utf8'), 'register_1');
+                            assert.equal(updates[i].register_op.toString('utf8'), 'register_value_1');
+                            registerSet = true;
+                            break;
+                        case MapField.MapFieldType.FLAG:
+                            assert.equal(updates[i].field.name.toString('utf8'), 'flag_1');
+                            assert.equal(updates[i].flag_op, MapUpdate.FlagOp.ENABLE);
+                            flagSet = true;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+                
+                assert(counterIncremented);
+                assert(setAddedTo);
+                assert(setRemovedFrom);
+                assert(registerSet);
+                assert(flagSet);
+                assert(mapAdded);
+                
+                return mapUpdate;
+                
+            };
+            
+            verifyRemoves(mapOp.removes);
+            var innerMapUpdate = verifyUpdates(mapOp.updates);
+            verifyUpdates(innerMapUpdate.map_op.updates);
+            verifyRemoves(innerMapUpdate.map_op.removes);
+            
+            // perform happy dance
+            
+            done();
+            
+        });
+        
+        it('should take a DtUpdateResp and call the users callback with the response', function(done) {
+           
+            var dtUpdateResp = new DtUpdateResp();
+            dtUpdateResp.setKey(new Buffer('riak_generated_key'));
+            dtUpdateResp.setContext(new Buffer('1234'));
+            
+            var createMapEntries = function() {
+                var mapEntries = [];
+                
+                var mapEntry = new MapEntry();
+                var mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.COUNTER);
+                mapField.setName(new Buffer('counter_1'));
+                mapEntry.setField(mapField);
+                mapEntry.setCounterValue(50);
+                mapEntries.push(mapEntry);
+                
+                mapEntry = new MapEntry();
+                mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.SET);
+                mapField.setName(new Buffer('set_1'));
+                mapEntry.setField(mapField);
+                mapEntry.set_value.push.apply(mapEntry.set_value, [ByteBuffer.fromUTF8('value_1'), ByteBuffer.fromUTF8('value_2')]);
+                mapEntries.push(mapEntry);
+                
+                var mapEntry = new MapEntry();
+                var mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.REGISTER);
+                mapField.setName(new Buffer('register_1'));
+                mapEntry.setField(mapField);
+                mapEntry.setRegisterValue(ByteBuffer.fromUTF8('1234'));
+                mapEntries.push(mapEntry);
+                
+                var mapEntry = new MapEntry();
+                var mapField = new MapField();
+                mapField.setType(MapField.MapFieldType.FLAG);
+                mapField.setName(new Buffer('flag_1'));
+                mapEntry.setField(mapField);
+                mapEntry.setFlagValue(true);
+                mapEntries.push(mapEntry);
+                
+                return mapEntries;
+
+            };
+            
+            dtUpdateResp.map_value.push.apply(dtUpdateResp.map_value, createMapEntries());
+            
+            var mapEntry = new MapEntry();
+            var mapField = new MapField();
+            mapField.setType(MapField.MapFieldType.MAP);
+            mapField.setName(new Buffer('map_1'));
+            mapEntry.setField(mapField);
+            mapEntry.map_value.push.apply(mapEntry.map_value, createMapEntries());
+            
+            dtUpdateResp.map_value.push(mapEntry);
+            
+            var callback = function(err, resp) {
+                assert(resp !== null);
+                assert.equal(resp.generatedKey.toString('utf8'), 'riak_generated_key');
+                assert.equal(resp.context.toString('utf8'), '1234');
+                
+                var verifyMap = function(map) {
+                    assert.equal(map.counters['counter_1'], 50);
+                    assert.equal(map.sets['set_1'][0], 'value_1');
+                    assert.equal(map.sets['set_1'][1], 'value_2');
+                    assert.equal(map.registers['register_1'].toString('utf8'), '1234');
+                    assert.equal(map.flags['flag_1'], true);
+                };
+            
+                verifyMap(resp.map);
+                verifyMap(resp.map.maps['map_1']);
+                done();
+            
+            };
+            
+            var mapOp = new UpdateMap.MapOperation();
+            
+            
+            var update = new UpdateMap.Builder()
+                .withBucketType('maps')
+                .withBucket('myBucket')
+                .withKey('map_1')
+                .withMapOperation(mapOp)
+                .withCallback(callback)
+                .build();
+        
+            update.onSuccess(dtUpdateResp);
+            
+        });
+        
+        it ('should take a RpbErrorResp and call the users callback with the error message', function(done) {
+            var rpbErrorResp = new RpbErrorResp();
+            rpbErrorResp.setErrmsg(new Buffer('this is an error'));
+           
+            var callback = function(err, response) {
+                if (err) {
+                    assert.equal(err,'this is an error');
+                    done();
+                }
+            };
+            
+            var mapOp = new UpdateMap.MapOperation();
+            var update = new UpdateMap.Builder()
+                .withBucketType('maps')
+                .withBucket('myBucket')
+                .withKey('map_1')
+                .withMapOperation(mapOp)
+                .withCallback(callback)
+                .build();
+        
+            update.onRiakError(rpbErrorResp);
+        });
+        
+    });
+    
+    describe('MapOperation', function() {
+        
+        it('should add counter increments', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            
+            op.incrementCounter('counter_1', 5);
+            op.incrementCounter('counter_2', 5);
+            op.incrementCounter('counter_2', -10);
+            
+            assert.equal(op.counters.increment.length, 2);
+            assert.equal(op.counters.increment[0].key, 'counter_1');
+            assert.equal(op.counters.increment[0].increment, 5);
+            assert.equal(op.counters.increment[1].key, 'counter_2');
+            assert.equal(op.counters.increment[1].increment, -5);
+            done();
+        });
+        
+        it('should invalidate counter increment on remove', function(done) {
+           
+            var op = new UpdateMap.MapOperation();
+            
+            op.incrementCounter('counter_1', 5);
+            op.removeCounter('counter_1');
+            
+            assert.equal(op.counters.increment.length, 0);
+            assert.equal(op.counters.remove.length, 1);
+            assert.equal(op.counters.remove[0], 'counter_1');
+            assert(op._hasRemoves());
+            done();
+            
+        });
+        
+        it('should invalidate counter remove on increment', function(done) {
+            var op = new UpdateMap.MapOperation();
+            
+            op.removeCounter('counter_1');
+            op.incrementCounter('counter_1', 5);
+            assert.equal(op.counters.remove.length, 0);
+            assert.equal(op.counters.increment[0].key, 'counter_1');
+            assert.equal(op.counters.increment[0].increment, 5);
+            done();
+            
+        });
+        
+        it('should add set adds', function(done) {
+           
+            var op = new UpdateMap.MapOperation();
+            
+            op.addToSet('set_1', 'value_1');
+            op.addToSet('set_1', 'value_2');
+            op.addToSet('set_2', 'value_1');
+            assert.equal(op.sets.adds.length, 2);
+            assert.equal(op.sets.adds[0].add.length, 2);
+            assert.equal(op.sets.adds[0].key, 'set_1');
+            assert.equal(op.sets.adds[0].add[0], 'value_1');
+            assert.equal(op.sets.adds[0].add[1], 'value_2');
+            assert.equal(op.sets.adds[1].add.length, 1);
+            assert.equal(op.sets.adds[1].key, 'set_2');
+            assert.equal(op.sets.adds[1].add[0], 'value_1');
+            done();
+            
+        });
+        
+        it('should add set removes', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            
+            op.removeFromSet('set_1', 'value_1');
+            op.removeFromSet('set_1', 'value_2');
+            op.removeFromSet('set_2', 'value_1');
+            assert.equal(op.sets.removes.length, 2);
+            assert.equal(op.sets.removes[0].remove.length, 2);
+            assert.equal(op.sets.removes[0].key, 'set_1');
+            assert.equal(op.sets.removes[0].remove[0], 'value_1');
+            assert.equal(op.sets.removes[0].remove[1], 'value_2');
+            assert.equal(op.sets.removes[1].remove.length, 1);
+            assert.equal(op.sets.removes[1].key, 'set_2');
+            assert.equal(op.sets.removes[1].remove[0], 'value_1');
+            assert(op._hasRemoves());
+            done();
+            
+        });
+        
+        it('should invalidate adding to a set on a remove set', function(done) {
+           
+           var op = new UpdateMap.MapOperation();
+           
+           op.addToSet('set_1', 'value_1');
+           op.addToSet('set_2', 'value_1');
+           op.removeSet('set_1');
+           op.removeSet('set_3');
+           
+           assert.equal(op.sets.adds.length, 1);
+           assert.equal(op.sets.adds[0].key, 'set_2');
+           assert.equal(op.sets.remove.length, 2);
+           assert.equal(op.sets.remove[0], 'set_1');
+           assert.equal(op.sets.remove[1], 'set_3');
+           assert(op._hasRemoves());
+           done();
+           
+        });
+        
+        it('should invalidate removing from a set on a remove set', function(done) {
+           
+           var op = new UpdateMap.MapOperation();
+           
+           op.removeFromSet('set_1', 'value_1');
+           op.removeFromSet('set_2', 'value_1');
+           op.removeSet('set_1');
+           op.removeSet('set_3');
+           
+           assert.equal(op.sets.removes.length, 1);
+           assert.equal(op.sets.removes[0].key, 'set_2');
+           assert.equal(op.sets.remove.length, 2);
+           assert.equal(op.sets.remove[0], 'set_1');
+           assert.equal(op.sets.remove[1], 'set_3');
+           assert(op._hasRemoves());
+           done();
+            
+        });
+        
+        it('should invalidate removing a set on an add to set', function(done) {
+           
+            var op = new UpdateMap.MapOperation();
+            
+            op.removeSet('set_1');
+            op.removeSet('set_3');
+            assert.equal(op.sets.remove.length, 2);
+            op.addToSet('set_1', 'value_1');
+            assert.equal(op.sets.remove.length, 1);
+            assert.equal(op.sets.remove[0], 'set_3');
+            assert.equal(op.sets.adds.length, 1);
+            assert(op._hasRemoves());
+
+            done();
+            
+        });
+        
+        it('should invalidate removing a set on a remove from set', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            
+            op.removeSet('set_1');
+            op.removeSet('set_3');
+            assert.equal(op.sets.remove.length, 2);
+            op.removeFromSet('set_1', 'value_1');
+            assert.equal(op.sets.remove.length, 1);
+            assert.equal(op.sets.remove[0], 'set_3');
+            assert.equal(op.sets.removes.length, 1);
+            assert(op._hasRemoves());
+
+            done();
+            
+        });
+        
+        it('should add registers', function(done) {
+           
+           var op = new UpdateMap.MapOperation();
+           
+           op.setRegister('register_1', new Buffer('value_1'));
+           op.setRegister('register_2', new Buffer('value_2'));
+           
+           assert.equal(op.registers.set.length, 2);
+           assert.equal(op.registers.set[0].key, 'register_1');
+           assert.equal(op.registers.set[0].value.toString('utf8'), 'value_1');
+           assert.equal(op.registers.set[1].key, 'register_2');
+           assert.equal(op.registers.set[1].value.toString('utf8'), 'value_2');
+           
+           op.setRegister('register_1', new Buffer('value_3'));
+           assert.equal(op.registers.set[0].key, 'register_1');
+           assert.equal(op.registers.set[0].value.toString('utf8'), 'value_3');
+           done();
+           
+            
+        });
+        
+        it('should invalidate register sets on removes', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            
+            op.setRegister('register_1', new Buffer('value_1'));
+            op.setRegister('register_2', new Buffer('value_2'));
+            
+            op.removeRegister('register_1');
+            
+            assert.equal(op.registers.set.length, 1);
+            assert.equal(op.registers.set[0].key, 'register_2');
+            assert.equal(op.registers.set[0].value.toString('utf8'), 'value_2');
+            assert.equal(op.registers.remove.length, 1);
+            assert.equal(op.registers.remove[0], 'register_1');
+            assert(op._hasRemoves());
+            done();
+        });
+        
+        if('should invalidate register removes on register sets', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            op.removeRegister('register_1');
+            op.removeRegister('register_2');
+            assert.equal(op.registers.remove.length, 2);
+            assert.equal(op.registers.remove[0], 'register_1');
+            assert.equal(op.registers.remove[1], 'register_2');
+            
+            op.setRegister('register_1', new Buffer('value_1'));
+            assert.equal(op.registers.set.length, 1);
+            assert.equal(op.registers.remove.length, 1);
+            assert.equal(op.registers.remove[0], 'register_2');
+            assert(op._hasRemoves());
+            done();
+        });
+        
+        it('should set flags', function(done) {
+           
+            var op = new UpdateMap.MapOperation();
+            
+            op.setFlag('flag_1', true);
+            op.setFlag('flag_2', false);
+            
+            assert.equal(op.flags.set.length, 2);
+            assert.equal(op.flags.set[0].key, 'flag_1');
+            assert.equal(op.flags.set[1].key, 'flag_2');
+            assert.equal(op.flags.set[0].state, true);
+            assert.equal(op.flags.set[1].state, false);
+            
+            op.setFlag('flag_1', false);
+            assert.equal(op.flags.set.length, 2);
+            assert.equal(op.flags.set[0].state, false);
+            done();
+            
+        });
+        
+        it('should invalidate a flag set on a flag remove', function(done) {
+           
+            var op = new UpdateMap.MapOperation();
+            
+            op.setFlag('flag_1', true);
+            op.setFlag('flag_2', false);
+            op.removeFlag('flag_2');
+            assert.equal(op.flags.set.length, 1);
+            assert.equal(op.flags.set[0].key, 'flag_1');
+            assert.equal(op.flags.remove.length, 1);
+            assert.equal(op.flags.remove[0], 'flag_2');
+            assert(op._hasRemoves());
+            done();
+            
+        });
+        
+        it('should invalidate a remove flag on a set flag', function(done) {
+           
+            var op = new UpdateMap.MapOperation();
+            
+            op.removeFlag('flag_1');
+            op.removeFlag('flag_2');
+            assert.equal(op.flags.remove.length, 2);
+            op.setFlag('flag_1', true);
+            assert.equal(op.flags.remove.length, 1);
+            assert.equal(op.flags.remove[0], 'flag_2');
+            assert.equal(op.flags.set.length, 1);
+            assert(op._hasRemoves());
+            done();
+            
+        });
+        
+        it('should nest maps', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            op.map('map_1')
+                    .setFlag('some_flag', true)
+                    .incrementCounter('counter_1', 5)
+                    .removeRegister('register_1');
+            
+            assert.equal(op.counters.increment.length, 0);
+            assert.equal(op.flags.set.length, 0);
+            assert.equal(op.registers.remove.length, 0);
+            assert.equal(op.maps.modify.length, 1);
+            
+            assert.equal(op.maps.modify[0].map.counters.increment.length, 1);
+            assert.equal(op.maps.modify[0].map.flags.set.length, 1);
+            assert.equal(op.maps.modify[0].map.registers.remove.length, 1);
+            assert(op._hasRemoves());
+            done();
+            
+        });
+    
+        it('should invalidate a map modify on a map remove', function(done) {
+            var op = new UpdateMap.MapOperation();
+            op.map('map_1').setFlag('some_flag', true);
+            
+            op.removeMap('map_1');
+            assert.equal(op.maps.modify.length, 0);
+            assert.equal(op.maps.remove.length, 1);
+            assert(op._hasRemoves());
+            assert.equal(op.maps.remove[0], 'map_1');
+            done();
+            
+        });
+        
+        it('should invalidate a map remove on a map modify', function(done) {
+            
+            var op = new UpdateMap.MapOperation();
+            op.removeMap('map_1');
+            op.removeMap('map_2');
+            assert.equal(op.maps.remove.length, 2);
+            op.map('map_1').setFlag('some_flag', true);
+            assert.equal(op.maps.remove.length, 1);
+            assert.equal(op.maps.modify.length, 1);
+            assert(op._hasRemoves());
+            done();
+            
+        });
+    
+    });
+});
+
diff --git a/test/unit/crdt/updateset.js b/test/unit/crdt/updateset.js
index 2ce6227..fad5870 100644
--- a/test/unit/crdt/updateset.js
+++ b/test/unit/crdt/updateset.js
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2015 Basho Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 var UpdateSet = require('../../../lib/commands/crdt/updateset');
 var Rpb = require('../../../lib/protobuf/riakprotobuf');
 var DtUpdateReq = Rpb.getProtoFor('DtUpdateReq');
@@ -69,6 +85,8 @@ describe('UpdateSet', function() {
                                   "snabbfotad"));
             assert(includesBuffer(protobuf.op.set_op.removes,
                                   "valby ruta"));
+                                  
+            assert.equal(protobuf.getContext().toString('utf8'), 'context');
 
             done();
         });
@@ -93,7 +111,7 @@ describe('UpdateSet', function() {
         return false;
     };
 
-    describe('with body, without key', function() {
+    describe('with body, without key, as buffers', function() {
         it('calls back with successful results', function(done) {
             var resp = new DtUpdateResp;
             resp.setContext(new Buffer("asdf"));
@@ -104,20 +122,45 @@ describe('UpdateSet', function() {
                 assert(response);
 
                 assert.equal(response.context.toString("utf8"), "asdf");
-                assert.equal(response.dataType, 2);
+                assert(includesBuffer(response.values, "hampen"));
+                assert(includesBuffer(response.values, "snabbfotad"));
+
+                assert.equal(response.generatedKey, null);
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([hampenBuffer]).
+                    withSetsAsBuffers(true).
+                    build();
+
+            update.onSuccess(resp);
+        });
+    });
 
-                assert(includesBuffer(response.valueBuffers, "hampen"));
-                assert(includes(response.value, "hampen"));
+    describe('with body, without key, as strings', function() {
+        it('calls back with successful results', function(done) {
+            var resp = new DtUpdateResp;
+            resp.setContext(new Buffer("asdf"));
+            resp.setSetValue([hampenBuffer, snabbfotadBuffer]);
 
-                assert(includesBuffer(response.valueBuffers, "snabbfotad"));
-                assert(includes(response.value, "snabbfotad"));
+            var callback = function(err, response) {
+                assert(!err);
+                assert(response);
+
+                assert.equal(response.context.toString("utf8"), "asdf");
+                assert(includes(response.values, "hampen"));
+                assert(includes(response.values, "snabbfotad"));
 
+                assert.equal(response.generatedKey, null);
                 done();
             };
 
             var update = builder.
                     withCallback(callback).
                     withAdditions([hampenBuffer]).
+                    withSetsAsBuffers(false).
                     build();
 
             update.onSuccess(resp);
@@ -130,8 +173,7 @@ describe('UpdateSet', function() {
             resp.key = new Buffer("ikea_rugs");
 
             var callback = function(err, response) {
-                assert.equal(response.key.toString("utf8"), "ikea_rugs");
-                assert.equal(response.keyString, "ikea_rugs");
+                assert.equal(response.generatedKey, "ikea_rugs");
                 done();
             };