Repository: basho/riak-nodejs-client
Pull request #181 (2017-01-12 15:28:49)

GSet support / RIAK-2907




Patch:
diff --git a/lib/commands/commandbase.js b/lib/commands/commandbase.js
index f86453c..2f3a84b 100644
--- a/lib/commands/commandbase.js
+++ b/lib/commands/commandbase.js
@@ -150,7 +150,7 @@ CommandBase.prototype.getPbReqBuilder = function() {
  * @return {Object} a protocol buffer message builder
  */
 CommandBase.prototype.constructPbRequest = function() {
-    throw new Error('Not supported yet!');
+    throw new Error('Not Implemented');
 };
 
 /**
@@ -162,7 +162,7 @@ CommandBase.prototype.constructPbRequest = function() {
  * @return {Boolean} true if not streaming or the last response has been received, false otherwise.
  */
 CommandBase.prototype.onSuccess = function(pbResponseMessage) {
-    throw new Error('Not supported yet!');
+    throw new Error('Not Implemented');
 };
 
 /**
diff --git a/lib/commands/crdt.js b/lib/commands/crdt.js
index a0e0a69..995a37c 100644
--- a/lib/commands/crdt.js
+++ b/lib/commands/crdt.js
@@ -32,5 +32,6 @@ module.exports.FetchMap =  require('./crdt/fetchmap');
 module.exports.UpdateMap =  require('./crdt/updatemap');
 module.exports.FetchSet =  require('./crdt/fetchset');
 module.exports.UpdateSet =  require('./crdt/updateset');
+module.exports.UpdateGSet =  require('./crdt/updategset');
 module.exports.FetchHll =  require('./crdt/fetchhll');
 module.exports.UpdateHll =  require('./crdt/updatehll');
diff --git a/lib/commands/crdt/fetchset.js b/lib/commands/crdt/fetchset.js
index aec56ee..859da8d 100644
--- a/lib/commands/crdt/fetchset.js
+++ b/lib/commands/crdt/fetchset.js
@@ -20,7 +20,7 @@
 var CommandBase = require('../commandbase');
 var inherits = require('util').inherits;
 var Joi = require('joi');
-
+var DtFetchResp = require('../../protobuf/riakprotobuf').getProtoFor('DtFetchResp');
 var utils = require('../../utils');
 
 /**
@@ -95,14 +95,23 @@ FetchSet.prototype.constructPbRequest = function() {
 };
 
 FetchSet.prototype.onSuccess = function(dtFetchResp) {
-
     var response;
     var isNotFound = false;
     // on a "not found" the value is null
     var valuesToReturn = null;
     var context = null;
     if (dtFetchResp.getValue()) {
-        var valueBuffers = dtFetchResp.getValue().getSetValue();
+        var v = dtFetchResp.getValue();
+        var type = dtFetchResp.getType();
+        var valueBuffers;
+        switch(type) {
+            case DtFetchResp.DataType.SET:
+                valueBuffers = v.getSetValue();
+                break;
+            case DtFetchResp.DataType.GSET:
+                valueBuffers = v.getGsetValue();
+                break;
+        }
         valuesToReturn = new Array(valueBuffers.length);
         var valueCount = valueBuffers.length;
         for (var i = 0; i < valueCount; i++) {
@@ -121,7 +130,12 @@ FetchSet.prototype.onSuccess = function(dtFetchResp) {
     }
 
     // TODO 2.0 - remove notFound
-    response = { context: context, values: valuesToReturn, isNotFound: isNotFound, notFound: isNotFound };
+    response = {
+        context: context,
+        values: valuesToReturn,
+        isNotFound: isNotFound,
+        notFound: isNotFound
+    };
 
     this._callback(null, response);
     return true;
diff --git a/lib/commands/crdt/updategset.js b/lib/commands/crdt/updategset.js
new file mode 100644
index 0000000..7b49052
--- /dev/null
+++ b/lib/commands/crdt/updategset.js
@@ -0,0 +1,132 @@
+/**
+ *
+ * Copyright 2014-present 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.
+ */
+
+'use strict';
+
+var UpdateSetBase = require('./updatesetbase');
+var inherits = require('util').inherits;
+var Joi = require('joi');
+var ByteBuffer = require('bytebuffer');
+
+var utils = require('../../utils');
+var rpb = require('../../protobuf/riakprotobuf');
+var DtOp = rpb.getProtoFor('DtOp');
+var GSetOp = rpb.getProtoFor('GSetOp');
+
+/**
+ * Provides the Update GSet class, its builder, and its response.
+ * @module CRDT
+ */
+
+/**
+ * Command used tp update a gset in Riak
+ *
+ * As a convenience, a builder class is provided:
+ *
+ *        var update = new UpdateGSet.Builder()
+ *               .withBucketType('gsets')
+ *               .withBucket('myBucket')
+ *               .withKey('set_1')
+ *               .withAdditions(['this', 'that', 'other'])
+ *               .withCallback(callback)
+ *               .build();
+ *
+ * See {{#crossLink "UpdateSet.Builder"}}UpdateSet.Builder{{/crossLink}}
+ * @class UpdateGSet
+ * @constructor
+ * @param {String[]|Buffer[]} [options.additions] The values to be added to the set.
+ * @extends UpdateSetBase
+ */
+function UpdateGSet(options, callback) {
+    var gset_opts = {
+        additions: options.additions
+    };
+    delete options.additions;
+
+    UpdateSetBase.call(this, options, callback);
+
+    var self = this;
+    Joi.validate(gset_opts, schema, function(err, opts) {
+        if (err) {
+            throw err;
+        }
+        self.additions = opts.additions;
+    });
+}
+
+inherits(UpdateGSet, UpdateSetBase);
+
+UpdateGSet.prototype.constructDtOp = function() {
+    var dt_op = new DtOp();
+    var gset_op = new GSetOp();
+    dt_op.setGsetOp(gset_op);
+    gset_op.adds = UpdateSetBase.buflist(this.additions);
+    return dt_op;
+};
+
+UpdateGSet.prototype.getUpdateRespValues = function(dtUpdateResp) {
+    return dtUpdateResp.gset_value;
+};
+
+var schema = Joi.object().keys({
+    additions: Joi.array().default([]).optional(),
+});
+
+/**
+ * A builder for constructing UpdateGSet instances.
+ *
+ * Rather than having to manually construct the __options__ and instantiating
+ * a UpdateGSet directly, this builder may be used.
+ *
+ *     var update = new UpdateGSet.Builder()
+ *                       .withBucketType('myBucketType')
+ *                       .withBucket('myBucket')
+ *                       .withKey('myKey')
+ *                       .withAdditions(['this', 'that', 'other'])
+ *                       .withCallback(myCallback)
+ *                       .build();
+ *
+ * @class UpdateGSet.Builder
+ * @extends UpdateSetBase.Builder
+ */
+function Builder() {
+    UpdateSetBase.Builder.call(this);
+}
+
+inherits(Builder, UpdateSetBase.Builder);
+
+/**
+ * Construct an UpdateGSet instance.
+ * @method build
+ * @return {UpdateGSet}
+ */
+Builder.prototype.build = function() {
+    var cb = this.callback;
+    delete this.callback;
+    return new UpdateGSet(this, cb);
+};
+
+/**
+ * The values you wish to add to this gset.
+ * @method withAdditions
+ * @param {String[]|Buffer[]} additions The values to add.
+ * @chainable
+ */
+utils.bb(Builder, 'additions');
+
+module.exports = UpdateGSet;
+module.exports.Builder = Builder;
diff --git a/lib/commands/crdt/updateset.js b/lib/commands/crdt/updateset.js
index 8a3fa5e..9496bff 100644
--- a/lib/commands/crdt/updateset.js
+++ b/lib/commands/crdt/updateset.js
@@ -17,7 +17,7 @@
 
 'use strict';
 
-var CommandBase = require('../commandbase');
+var UpdateSetBase = require('./updatesetbase');
 var inherits = require('util').inherits;
 var Joi = require('joi');
 var ByteBuffer = require('bytebuffer');
@@ -48,145 +48,48 @@ var SetOp = rpb.getProtoFor('SetOp');
  * See {{#crossLink "UpdateSet.Builder"}}UpdateSet.Builder{{/crossLink}}
  * @class UpdateSet
  * @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[]} [options.additions] The values to be added to the set.
  * @param {String[]|Buffer[]} [options.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.returnBody=true] Return the set.
- * @param {Boolean} [options.setsAsBuffers=false] Return the set as an array 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 {Number} [options.timeout] Set a timeout for this command.
- * @param {Function} callback Rhe callback to be executed when the operation completes.
- * @param {String} callback.err An error message. Will ne null if no error.
- * @param {Object} callback.response The response from Riak. will be null if returnBody is not set.
- * @param {String} callback.response.generatedKey If no key was supplied, Riak will generate and return one here.
- * @param {Buffer} callback.response.context An opaque context to be used in any subsequent modification of the set.
- * @param {String[]|Buffer[]} callback.response.values An array holding the values in the set. String by default, Buffers if setsAsBuffers was used.
- * @param {Object} callback.data additional error data. Will be null if no error.
- * @extends CommandBase
+ * @extends UpdateSetBase
  */
 function UpdateSet(options, callback) {
-    CommandBase.call(this, 'DtUpdateReq', 'DtUpdateResp', callback);
-    this.validateOptions(options, schema);
-}
-
-inherits(UpdateSet, CommandBase);
-
-function pbuf(self, prop) {
-    return new Buffer(self.options[prop]);
-}
-
-function bufferize(thing) {
-    if (utils.isString(thing)) {
-        return new Buffer(thing);
-    }
-    else if (Buffer.isBuffer(thing)) {
-        return thing;
-    }
-    else if (ByteBuffer.isByteBuffer(thing)) {
-        return thing.toBuffer();
-    }
-    throw new Error("Couldn't bufferize " + (typeof thing) + " " + thing);
-}
-
-function buflist(list) {
-    var newList = [];
-    var len = list.length;
-    for (var i = 0; i < len; i++) {
-        newList[i] = bufferize(list[i]);
-    }
-    return newList;
+    var set_opts = {
+        additions: options.additions,
+        removals: options.removals
+    };
+    delete options.additions;
+    delete options.removals;
+
+    UpdateSetBase.call(this, options, callback);
+
+    var self = this;
+    Joi.validate(set_opts, schema, function(err, opts) {
+        if (err) {
+            throw err;
+        }
+        self.additions = opts.additions;
+        self.removals = opts.removals;
+    });
 }
 
-UpdateSet.prototype.constructPbRequest = function() {
-    var protobuf = this.getPbReqBuilder();
-
-    // namespace
-    protobuf.setType(pbuf(this, 'bucketType'));
-    protobuf.setBucket(pbuf(this, 'bucket'));
-
-    if (this.options.key) {
-        protobuf.setKey(pbuf(this, 'key'));
-    }
+inherits(UpdateSet, UpdateSetBase);
 
-    // operation
-    var op = new DtOp();
-    protobuf.setOp(op);
+UpdateSet.prototype.constructDtOp = function() {
+    var dt_op = new DtOp();
     var setOp = new SetOp();
-    op.setSetOp(setOp);
-    setOp.adds = buflist(this.options.additions);
-    setOp.removes = buflist(this.options.removals);
-
-    // quorum
-    protobuf.setW(this.options.w);
-    protobuf.setDw(this.options.dw);
-    protobuf.setPw(this.options.pw);
-
-    // options
-    protobuf.setReturnBody(this.options.returnBody);
-    protobuf.setTimeout(this.options.timeout);
-    protobuf.setContext(this.options.context);
-
-    return protobuf;
+    dt_op.setSetOp(setOp);
+    setOp.adds = UpdateSetBase.buflist(this.additions);
+    setOp.removes = UpdateSetBase.buflist(this.removals);
+    return dt_op;
 };
 
-UpdateSet.prototype.onSuccess = function(dtUpdateResp) {
-    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;
+UpdateSet.prototype.getUpdateRespValues = function(dtUpdateResp) {
+    return dtUpdateResp.set_value;
 };
 
 var schema = Joi.object().keys({
-    //namespace
-    bucket: Joi.string().required(),
-    // bucket type is required since default probably shouldn't have a
-    // datatype associated with it
-    bucketType: Joi.string().required(),
-    // key is optional because Riak can generate one
-    key: Joi.binary().default(null).optional(),
-
-    //operations
     additions: Joi.array().default([]).optional(),
-    removals: Joi.array().default([]).optional(),
-
-    context: Joi.default(null).optional(),
-
-    //quorum
-    w: Joi.number().default(null).optional(),
-    dw: Joi.number().default(null).optional(),
-    pw: Joi.number().default(null).optional(),
-
-    //options
-    returnBody: Joi.boolean().default(true).optional(),
-    timeout: Joi.number().default(null).optional(),
-    setsAsBuffers: Joi.boolean().default(false).optional(),
+    removals: Joi.array().default([]).optional()
 });
 
 /**
@@ -204,135 +107,24 @@ var schema = Joi.object().keys({
  *                       .build();
  *
  * @class UpdateSet.Builder
- * @constructor
+ * @extends UpdateSetBase.Builder
  */
-function Builder() {}
-
-Builder.prototype = {
-    /**
-     * Construct an UpdateSet instance.
-     * @method build
-     * @return {UpdateSet}
-     */
-    build: function() {
-        var cb = this.callback;
-        delete this.callback;
-        return new UpdateSet(this, cb);
-    }
-};
-
-/**
- * Set the callback.
- * @method withCallback
- * @param {Function} callback the callback to be executed when the operation completes.
- * @param {String} callback.err An error message. Will ne null if no error.
- * @param {Object} callback.response The response from Riak. will be null if returnBody is not set.
- * @param {String} callback.response.generatedKey If no key was supplied, Riak will generate and return one here.
- * @param {Buffer} callback.response.context An opaque context to be used in any subsequent modification of the set.
- * @param {String[]|Buffer[]} callback.response.values An array holding the values in the set. String by default, Buffers if setsAsBuffers was used.
- */
-utils.bb(Builder, 'callback');
-
-/**
-* Set the bucket type.
-* @method withBucketType
-* @param {String} bucketType the bucket type in riak
-* @chainable
-*/
-utils.bb(Builder, 'bucketType');
-
-/**
-* Set the bucket.
-* @method withBucket
-* @param {String} bucket the bucket in Riak
-* @chainable
-*/
-utils.bb(Builder, 'bucket');
+function Builder() {
+    UpdateSetBase.Builder.call(this);
+}
 
-/**
-* 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
-*/
-utils.bb(Builder, 'key');
+inherits(Builder, UpdateSetBase.Builder);
 
 /**
-* Set the causal context. The context is necessary for set removals. It is
-* an opaque field, and should not be parsed or modified.
-* @method withContext
-* @param {ByteBuffer} context the causal context
-* @chainable
+ * Construct an UpdateSet instance.
+ * @method build
+ * @return {UpdateSet}
  */
-utils.bb(Builder, '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
-*/
-utils.bb(Builder, 'setsAsBuffers');
-
-/**
-* Set the W value.
-* If not set the bucket default is used.
-* @method withW
-* @param {Number} w the W value.
-* @chainable
-*/
-utils.bb(Builder, 'w');
-
-/**
-* Set the DW value.
-* If not set the bucket default is used.
-* @method withDw
-* @param {Number} dw the DW value.
-* @chainable
-*/
-utils.bb(Builder, 'dw');
-
-/**
-* Set the PW value.
-* If not set the bucket default is used.
-* @method withPw
-* @param {Number} pw the PW value.
-* @chainable
-*/
-utils.bb(Builder, 'pw');
-
-/**
-* Set the return_body value.
-* If true, the callback is passed the contents of the set after the update.
-* @method withReturnBody
-* @param {Boolean} returnBody the return_body value.
-* @chainable
-*/
-utils.bb(Builder, 'returnBody');
-
-/**
-* 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
-*/
-utils.bb(Builder, 'useBasicQuorum');
-
-/**
-* Set a timeout for this operation.
-* @method withTimeout
-* @param {Number} timeout a timeout in milliseconds.
-* @chainable
-*/
-utils.bb(Builder, 'timeout');
+Builder.prototype.build = function() {
+    var cb = this.callback;
+    delete this.callback;
+    return new UpdateSet(this, cb);
+};
 
 /**
  * The values you wish to add to this set.
diff --git a/lib/commands/crdt/updatesetbase.js b/lib/commands/crdt/updatesetbase.js
new file mode 100644
index 0000000..70c44f1
--- /dev/null
+++ b/lib/commands/crdt/updatesetbase.js
@@ -0,0 +1,316 @@
+/**
+ *
+ * Copyright 2014-present 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.
+ */
+
+'use strict';
+
+var CommandBase = require('../commandbase');
+var inherits = require('util').inherits;
+var Joi = require('joi');
+var ByteBuffer = require('bytebuffer');
+
+var utils = require('../../utils');
+
+/**
+ * Provides the Update Set class, its builder, and its response.
+ * @module CRDT
+ */
+
+/**
+ * @class UpdateSetBase
+ * @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 {Buffer} [options.context] The context from a previous fetch. Required for remove operations.
+ * @param {Boolean} [options.returnBody=true] Return the set.
+ * @param {Boolean} [options.setsAsBuffers=false] Return the set as an array 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 {Number} [options.timeout] Set a timeout for this command.
+ * @param {Function} callback Rhe callback to be executed when the operation completes.
+ * @param {String} callback.err An error message. Will ne null if no error.
+ * @param {Object} callback.response The response from Riak. will be null if returnBody is not set.
+ * @param {String} callback.response.generatedKey If no key was supplied, Riak will generate and return one here.
+ * @param {Buffer} callback.response.context An opaque context to be used in any subsequent modification of the set.
+ * @param {String[]|Buffer[]} callback.response.values An array holding the values in the set. String by default, Buffers if setsAsBuffers was used.
+ * @param {Object} callback.data additional error data. Will be null if no error.
+ * @extends CommandBase
+ */
+function UpdateSetBase(options, callback) {
+    CommandBase.call(this, 'DtUpdateReq', 'DtUpdateResp', callback);
+    this.validateOptions(options, schema);
+}
+
+inherits(UpdateSetBase, CommandBase);
+
+function pbuf(self, prop) {
+    return new Buffer(self.options[prop]);
+}
+
+function bufferize(thing) {
+    if (utils.isString(thing)) {
+        return new Buffer(thing);
+    }
+    else if (Buffer.isBuffer(thing)) {
+        return thing;
+    }
+    else if (ByteBuffer.isByteBuffer(thing)) {
+        return thing.toBuffer();
+    }
+    throw new Error("Couldn't bufferize " + (typeof thing) + " " + thing);
+}
+
+function buflist(list) {
+    var newList = [];
+    var len = list.length;
+    for (var i = 0; i < len; i++) {
+        newList[i] = bufferize(list[i]);
+    }
+    return newList;
+}
+
+UpdateSetBase.prototype.constructDtOp = function() {
+    throw new Error('Not Implemented');
+};
+
+UpdateSetBase.prototype.constructPbRequest = function() {
+    var protobuf = this.getPbReqBuilder();
+
+    // namespace
+    protobuf.setType(pbuf(this, 'bucketType'));
+    protobuf.setBucket(pbuf(this, 'bucket'));
+
+    if (this.options.key) {
+        protobuf.setKey(pbuf(this, 'key'));
+    }
+
+    // operation
+    var dt_op = this.constructDtOp();
+    protobuf.setOp(dt_op);
+
+    // quorum
+    protobuf.setW(this.options.w);
+    protobuf.setDw(this.options.dw);
+    protobuf.setPw(this.options.pw);
+
+    // options
+    protobuf.setReturnBody(this.options.returnBody);
+    protobuf.setTimeout(this.options.timeout);
+    protobuf.setContext(this.options.context);
+
+    return protobuf;
+};
+
+UpdateSetBase.prototype.getUpdateRespValues = function(dtUpdateResp) {
+    throw new Error('Not Implemented');
+};
+
+UpdateSetBase.prototype.onSuccess = function(dtUpdateResp) {
+    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 values = this.getUpdateRespValues(dtUpdateResp);
+
+        var valuesToReturn = new Array(values.length);
+        var i;
+        for (i = 0; i < values.length; i++) {
+            if (this.options.setsAsBuffers) {
+                valuesToReturn[i] = values[i].toBuffer();
+            } else {
+                valuesToReturn[i] = values[i].toString('utf8');
+            }
+        }
+
+        response = {
+            generatedKey: key,
+            context: dtUpdateResp.getContext() ? dtUpdateResp.getContext().toBuffer() : null,
+            values: valuesToReturn
+        };
+    }
+    this._callback(null, response);
+    return true;
+};
+
+var schema = Joi.object().keys({
+    //namespace
+    bucket: Joi.string().required(),
+    // bucket type is required since default probably shouldn't have a
+    // datatype associated with it
+    bucketType: Joi.string().required(),
+    // key is optional because Riak can generate one
+    key: Joi.binary().default(null).optional(),
+
+    context: Joi.default(null).optional(),
+
+    //quorum
+    w: Joi.number().default(null).optional(),
+    dw: Joi.number().default(null).optional(),
+    pw: Joi.number().default(null).optional(),
+
+    //options
+    returnBody: Joi.boolean().default(true).optional(),
+    timeout: Joi.number().default(null).optional(),
+    setsAsBuffers: Joi.boolean().default(false).optional(),
+});
+
+/**
+ * @class UpdateSetBase.Builder
+ * @constructor
+ */
+function Builder() {}
+
+/**
+ * Set the callback.
+ * @method withCallback
+ * @param {Function} callback the callback to be executed when the operation completes.
+ * @param {String} callback.err An error message. Will ne null if no error.
+ * @param {Object} callback.response The response from Riak. will be null if returnBody is not set.
+ * @param {String} callback.response.generatedKey If no key was supplied, Riak will generate and return one here.
+ * @param {Buffer} callback.response.context An opaque context to be used in any subsequent modification of the set.
+ * @param {String[]|Buffer[]} callback.response.values An array holding the values in the set. String by default, Buffers if setsAsBuffers was used.
+ */
+utils.bb(Builder, 'callback');
+
+/**
+* Set the bucket type.
+* @method withBucketType
+* @param {String} bucketType the bucket type in riak
+* @chainable
+*/
+utils.bb(Builder, 'bucketType');
+
+/**
+* Set the bucket.
+* @method withBucket
+* @param {String} bucket the bucket in Riak
+* @chainable
+*/
+utils.bb(Builder, '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
+*/
+utils.bb(Builder, 'key');
+
+/**
+* Set the causal context. The context is necessary for set removals. It is
+* an opaque field, and should not be parsed or modified.
+* @method withContext
+* @param {ByteBuffer} context the causal context
+* @chainable
+ */
+utils.bb(Builder, '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
+*/
+utils.bb(Builder, 'setsAsBuffers');
+
+/**
+* Set the W value.
+* If not set the bucket default is used.
+* @method withW
+* @param {Number} w the W value.
+* @chainable
+*/
+utils.bb(Builder, 'w');
+
+/**
+* Set the DW value.
+* If not set the bucket default is used.
+* @method withDw
+* @param {Number} dw the DW value.
+* @chainable
+*/
+utils.bb(Builder, 'dw');
+
+/**
+* Set the PW value.
+* If not set the bucket default is used.
+* @method withPw
+* @param {Number} pw the PW value.
+* @chainable
+*/
+utils.bb(Builder, 'pw');
+
+/**
+* Set the return_body value.
+* If true, the callback is passed the contents of the set after the update.
+* @method withReturnBody
+* @param {Boolean} returnBody the return_body value.
+* @chainable
+*/
+utils.bb(Builder, 'returnBody');
+
+/**
+* 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
+*/
+utils.bb(Builder, 'useBasicQuorum');
+
+/**
+* Set a timeout for this operation.
+* @method withTimeout
+* @param {Number} timeout a timeout in milliseconds.
+* @chainable
+*/
+utils.bb(Builder, 'timeout');
+
+/**
+ * The values you wish to add to this set.
+ * @method withAdditions
+ * @param {String[]|Buffer[]} additions The values to add.
+ * @chainable
+ */
+utils.bb(Builder, '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
+ */
+utils.bb(Builder, 'removals');
+
+module.exports = UpdateSetBase;
+module.exports.Builder = Builder;
+module.exports.buflist = buflist;
diff --git a/lib/commands/kv/fetchpropsbase.js b/lib/commands/kv/fetchpropsbase.js
index 2ac1265..80b37ec 100644
--- a/lib/commands/kv/fetchpropsbase.js
+++ b/lib/commands/kv/fetchpropsbase.js
@@ -43,11 +43,9 @@ function FetchPropsBase(pbRequestName, pbResponseName, callback) {
 inherits(FetchPropsBase, CommandBase);
 
 FetchPropsBase.prototype.onSuccess = function(rpbGetBucketResp) {
-
     var rpbBucketProps = rpbGetBucketResp.getProps();
 
     var bucketProps  = {
-
         nVal : rpbBucketProps.getNVal(),
         allowMult : rpbBucketProps.getAllowMult(),
         lastWriteWins: rpbBucketProps.getLastWriteWins(),
diff --git a/tools b/tools
index 1fa9070..1f54803 160000
--- a/tools
+++ b/tools
@@ -1,1 +1,1 @@
-Subproject commit 1fa9070257c3443f04337e7e37ef083ed7ca2fe4
+Subproject commit 1f54803ca7912a41a0ec47c0028c259b97475e1f


Test patch:
diff --git a/test/integration/crdt/updateandfetchgset.js b/test/integration/crdt/updateandfetchgset.js
new file mode 100644
index 0000000..fb1adcc
--- /dev/null
+++ b/test/integration/crdt/updateandfetchgset.js
@@ -0,0 +1,120 @@
+/**
+ *
+ * Copyright 2014-present 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+
+var Test = require('../testparams');
+var UpdateGSet = require('../../../lib/commands/crdt/updategset');
+var FetchSet = require('../../../lib/commands/crdt/fetchset');
+var RiakCluster = require('../../../lib/core/riakcluster');
+
+var gset_supported = true;
+
+describe('Update and Fetch GSet - Integration', function() {
+    var context;
+    var cluster;
+    before(function(done) {
+        var suite = this;
+        cluster = Test.buildCluster(function (err, rslt) {
+            assert(!err, err);
+            var callback = function(err, resp) {
+                if (err) {
+                    gset_supported = false;
+                    suite.skip();
+                } else {
+                    assert.equal(resp.values.length, 2);
+                    assert.equal(resp.context, null);
+                    context = resp.context;
+                }
+                done();
+            };
+            var update = new UpdateGSet.Builder()
+                    .withBucketType(Test.gsetBucketType)
+                    .withBucket(Test.bucketName)
+                    .withKey('gset_1')
+                    .withAdditions(['this', 'that'])
+                    .withCallback(callback)
+                    .build();
+            cluster.execute(update);
+        });
+    });
+
+    after(function(done) {
+        if (gset_supported) {
+            Test.cleanBucket(cluster, Test.gsetBucketType, Test.bucketName, function() {
+                cluster.stop(function (err, state) {
+                    done();
+                });
+            });
+        } else {
+            done();
+        }
+    });
+
+    it('Should fetch a gset', function(done) {
+        var callback = function(err, resp) {
+            assert(!err, err);
+            assert.equal(resp.values.length, 2);
+            assert.equal(resp.context, null);
+            done();
+        };
+        var fetch = new FetchSet.Builder()
+                .withBucketType(Test.gsetBucketType)
+                .withBucket(Test.bucketName)
+                .withKey('gset_1')
+                .withCallback(callback)
+                .build();
+        cluster.execute(fetch);
+    });
+
+    it('Should report isNotFound if a gset does not exist', function(done) {
+        var callback = function(err, resp) {
+            assert(!err, err);
+            assert(resp.notFound);
+            assert(resp.isNotFound);
+            done();
+        };
+        var fetch = new FetchSet.Builder()
+                .withBucketType(Test.gsetBucketType)
+                .withBucket(Test.bucketName)
+                .withKey('gset_notFound')
+                .withCallback(callback)
+                .build();
+        cluster.execute(fetch);
+    });
+
+    it('Should add to a gset', function(done) {
+        var callback = function(err, resp) {
+            assert(!err, err);
+            assert.equal(resp.values.length, 3);
+            done();
+        };
+
+        var update = new UpdateGSet.Builder()
+                .withBucketType(Test.gsetBucketType)
+                .withBucket(Test.bucketName)
+                .withKey('gset_1')
+                .withAdditions(['those'])
+                .withContext(context)
+                .withCallback(callback)
+                .build();
+
+        cluster.execute(update);
+    });
+});
diff --git a/test/integration/testparams.js b/test/integration/testparams.js
index a00e369..7469fbf 100644
--- a/test/integration/testparams.js
+++ b/test/integration/testparams.js
@@ -97,11 +97,11 @@ module.exports.buildCluster = function(start_cb) {
  */
 module.exports.counterBucketType = 'counters';
 module.exports.setBucketType = 'sets';
+module.exports.gsetBucketType = 'gsets';
 module.exports.mapBucketType = 'maps';
 module.exports.hllBucketType = 'hlls';
 
 module.exports.cleanBucket = function(cluster, type, bucket, callback) {
-
     // Note this also acts as the integration test for ListKeys and
     // DeleteValue
     logger.debug('Clearing bucket: %s:%s', type, bucket);
@@ -129,17 +129,14 @@ module.exports.cleanBucket = function(cluster, type, bucket, callback) {
         };
 
         for (var i = 0; i < resp.keys.length; i++) {
-
             var del = new DeleteValue.Builder()
                     .withBucket(resp.bucket)
                     .withBucketType(resp.bucketType)
                     .withKey(resp.keys[i])
                     .withCallback(dCallback)
                     .build();
-
             cluster.execute(del);
         }
-
     };
 
     var list = new ListKeys.Builder()
@@ -148,7 +145,5 @@ module.exports.cleanBucket = function(cluster, type, bucket, callback) {
             .withCallback(lkCallback)
             .withStreaming(false)
             .build();
-
     cluster.execute(list);
-
 };
diff --git a/test/unit/client.js b/test/unit/client.js
index 59be5fb..d13e02b 100644
--- a/test/unit/client.js
+++ b/test/unit/client.js
@@ -200,31 +200,49 @@ describe('Client', function() {
             assert(typeofCrdtUpdateSet === 'function', "typeof(Riak.Commands.CRDT.UpdateSet): " + typeofCrdtUpdateSet);
             done();
         });
-        
+
+        it('Riak.Commands.CRDT.UpdateGSet', function(done) {
+            var typeofCrdtUpdateGSet = typeof(Riak.Commands.CRDT.UpdateGSet);
+            assert(typeofCrdtUpdateGSet === 'function', "typeof(Riak.Commands.CRDT.UpdateGSet): " + typeofCrdtUpdateGSet);
+            done();
+        });
+
         it('Riak.Commands.CRDT.FetchCounter', function(done) {
             var typeofCrdtFetchCounter = typeof(Riak.Commands.CRDT.FetchCounter);
             assert(typeofCrdtFetchCounter === 'function', "typeof(Riak.Commands.CRDT.FetchCounter): " + typeofCrdtFetchCounter);
             done();
         });
-        
+
         it('Riak.Commands.CRDT.UpdateCounter', function(done) {
             var typeofCrdtUpdateCounter = typeof(Riak.Commands.CRDT.UpdateCounter);
             assert(typeofCrdtUpdateCounter === 'function', "typeof(Riak.Commands.CRDT.UpdateCounter): " + typeofCrdtUpdateCounter);
             done();
         });
-        
+
         it('Riak.Commands.CRDT.FetchMap', function(done) {
             var typeofCrdtFetchMap = typeof(Riak.Commands.CRDT.FetchMap);
             assert(typeofCrdtFetchMap === 'function', "typeof(Riak.Commands.CRDT.FetchMap): " + typeofCrdtFetchMap);
             done();
         });
-        
+
         it('Riak.Commands.CRDT.UpdateMap', function(done) {
             var typeofCrdtUpdateMap = typeof(Riak.Commands.CRDT.UpdateMap);
             assert(typeofCrdtUpdateMap === 'function', "typeof(Riak.Commands.CRDT.UpdateMap): " + typeofCrdtUpdateMap);
             done();
         });
 
+        it('Riak.Commands.CRDT.FetchHll', function(done) {
+            var typeofCrdtFetchHll = typeof(Riak.Commands.CRDT.FetchHll);
+            assert(typeofCrdtFetchHll === 'function', "typeof(Riak.Commands.CRDT.FetchHll): " + typeofCrdtFetchHll);
+            done();
+        });
+
+        it('Riak.Commands.CRDT.UpdateHll', function(done) {
+            var typeofCrdtUpdateHll = typeof(Riak.Commands.CRDT.UpdateHll);
+            assert(typeofCrdtUpdateHll === 'function', "typeof(Riak.Commands.CRDT.UpdateHll): " + typeofCrdtUpdateHll);
+            done();
+        });
+
         it('Riak.Commands.YZ.DeleteIndex', function(done) {
             var typeofYzDeleteIndex = typeof(Riak.Commands.YZ.DeleteIndex);
             assert(typeofYzDeleteIndex === 'function', "typeof(Riak.Commands.YZ.DeleteIndex): " + typeofYzDeleteIndex);
@@ -255,7 +273,7 @@ describe('Client', function() {
             assert(typeofYzStoreSchema === 'function', "typeof(Riak.Commands.YZ.StoreSchema): " + typeofYzStoreSchema);
             done();
         });
-        
+
         it('Riak.Commands.MR.MapReduce', function(done) {
             var typeofMrMapReduce = typeof(Riak.Commands.MR.MapReduce);
             assert(typeofMrMapReduce === 'function', "typeof(Riak.Commands.MR.MapReduce): " + typeofMrMapReduce);
diff --git a/test/unit/crdt/fetchcounter.js b/test/unit/crdt/fetchcounter.js
index 3454e86..a532030 100644
--- a/test/unit/crdt/fetchcounter.js
+++ b/test/unit/crdt/fetchcounter.js
@@ -95,6 +95,5 @@ describe('FetchCounter', function() {
         
             fetch.onRiakError(rpbErrorResp);
         });
-        
     });
 });
diff --git a/test/unit/crdt/fetchmap.js b/test/unit/crdt/fetchmap.js
index db98d11..e0210e7 100644
--- a/test/unit/crdt/fetchmap.js
+++ b/test/unit/crdt/fetchmap.js
@@ -29,11 +29,8 @@ var RpbErrorResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('Rp
 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')
@@ -57,11 +54,9 @@ describe('FetchMap', function() {
             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);
@@ -103,7 +98,6 @@ describe('FetchMap', function() {
                 mapEntries.push(mapEntry);
                 
                 return mapEntries;
-
             };
             
             Array.prototype.push.apply(dtValue.map_value, createMapEntries());
@@ -134,7 +128,6 @@ describe('FetchMap', function() {
                 verifyMap(resp.map);
                 verifyMap(resp.map.maps.map_1);
                 done();
-            
             };
             
             var fetch = new FetchMap.Builder()
@@ -145,12 +138,6 @@ describe('FetchMap', function() {
                 .build();
         
             fetch.onSuccess(dtFetchResp);
-            
-            
         });
-        
     });
-    
-    
-    
 });
diff --git a/test/unit/crdt/fetchset.js b/test/unit/crdt/fetchset.js
index 093d401..fd6bc03 100644
--- a/test/unit/crdt/fetchset.js
+++ b/test/unit/crdt/fetchset.js
@@ -23,10 +23,10 @@ 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');
+var utils = require('../../utils');
 
 describe('FetchSet', function() {
     describe('Build', function() {
@@ -47,12 +47,9 @@ describe('FetchSet', function() {
 
             var protobuf = fetchSet.constructPbRequest();
 
-            assert.equal(protobuf.getType().toString('utf8'),
-                         'sets_type');
-            assert.equal(protobuf.getBucket().toString('utf8'),
-                         'set_bucket');
-            assert.equal(protobuf.getKey().toString('utf8'),
-                         'cool_set');
+            assert.equal(protobuf.getType().toString('utf8'), 'sets_type');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'set_bucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'cool_set');
             assert.equal(protobuf.getR(), 1);
             assert.equal(protobuf.getPr(), 2);
             assert.equal(protobuf.getNotfoundOk(), true);
@@ -73,25 +70,11 @@ describe('FetchSet', function() {
                                ByteBuffer.fromUTF8("piper"),
                                ByteBuffer.fromUTF8("little one")];
 
-            var includesBuffer = function(haystack, needle) {
-                var needleBuf = new Buffer(needle);
-                var len = haystack.length;
-                for (var i = 0; i < len; i++) {
-                    if (haystack[i].equals(needleBuf)) return true;
-                }
-                
-                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"));
-                
+                assert(utils.includesBuffer(response.values, "zedo"));
+                assert(utils.includesBuffer(response.values, "piper"));
+                assert(utils.includesBuffer(response.values, "little one"));
                 done();
             };
 
@@ -99,7 +82,6 @@ describe('FetchSet', function() {
                     withCallback(callback).
                     withSetsAsBuffers(true).
                     build();
-
             fetch.onSuccess(resp);
         });
         
@@ -114,25 +96,12 @@ describe('FetchSet', function() {
             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++) {
-                    if (haystack[i] === needle) return true;
-                }
-
-                return false;
-            };
             
             var callback = function(err, response) {
                 assert.equal(response.context.toString("utf8"), "asdf");
-
-                assert(includes(response.values, "zedo"));
-
-                assert(includes(response.values, "piper"));
-                
-                assert(includes(response.values, "little one"));
-                
+                assert(utils.includes(response.values, "zedo"));
+                assert(utils.includes(response.values, "piper"));
+                assert(utils.includes(response.values, "little one"));
                 done();
             };
 
diff --git a/test/unit/crdt/updatecounter.js b/test/unit/crdt/updatecounter.js
index e35d1b1..548e2e7 100644
--- a/test/unit/crdt/updatecounter.js
+++ b/test/unit/crdt/updatecounter.js
@@ -27,7 +27,6 @@ var assert = require('assert');
 describe('UpdateCounter', function() {
     describe('Build', function() {
         it('should build a DtUpdateReq correctly', function(done) {
-            
             var update = new UpdateCounter.Builder()
                 .withBucketType('counters')
                 .withBucket('myBucket')
@@ -53,8 +52,6 @@ describe('UpdateCounter', function() {
             assert.equal(protobuf.op.counter_op.increment, 100);
             assert.equal(protobuf.getTimeout(), 20000);
             done();
-
-            
         });
         
         it('should take a DtUpdateResp and call the users callback with the response', function(done) {
@@ -103,4 +100,3 @@ describe('UpdateCounter', function() {
         });
     });
 });
-
diff --git a/test/unit/crdt/updategset.js b/test/unit/crdt/updategset.js
new file mode 100644
index 0000000..b630a70
--- /dev/null
+++ b/test/unit/crdt/updategset.js
@@ -0,0 +1,170 @@
+/**
+ *
+ * Copyright 2014-present 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.
+ */
+
+'use strict';
+
+var UpdateGSet = require('../../../lib/commands/crdt/updategset');
+var Rpb = require('../../../lib/protobuf/riakprotobuf');
+var DtUpdateReq = Rpb.getProtoFor('DtUpdateReq');
+var DtUpdateResp = Rpb.getProtoFor('DtUpdateResp');
+var DtValue = Rpb.getProtoFor('DtValue');
+var RpbErrorResp = Rpb.getProtoFor('RpbErrorResp');
+
+var ByteBuffer = require('bytebuffer');
+var assert = require('assert');
+var utils = require('../../utils');
+
+describe('UpdateGSet', function() {
+    // ikea rugs
+    var hampenBuffer = ByteBuffer.fromUTF8("hampen");
+    var snabbfotadBuffer = ByteBuffer.fromUTF8("snabbfotad");
+
+    var someContext = ByteBuffer.fromUTF8("context");
+
+    var builder = new UpdateGSet.Builder().
+            withBucketType('gsets').
+            withBucket('gset_bucket').
+            withKey('cool_gset');
+
+    describe('Build', function() {
+        it('builds a DtUpdateSet correctly', function(done){
+            var update = builder.
+                    withContext(someContext).
+                    withAdditions(["gåser", hampenBuffer]).
+                    withCallback(function(){}).
+                    withW(1).
+                    withDw(2).
+                    withPw(3).
+                    withReturnBody(false).
+                    withTimeout(12345).
+                    build();
+
+            var protobuf = update.constructPbRequest();
+
+            assert.equal(protobuf.getType().toString('utf8'), 'gsets');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'gset_bucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'cool_gset');
+            assert.equal(protobuf.getW(), 1);
+            assert.equal(protobuf.getDw(), 2);
+            assert.equal(protobuf.getPw(), 3);
+            assert.equal(protobuf.getReturnBody(), false);
+            assert.equal(protobuf.getTimeout(), 12345);
+            assert(utils.includesBuffer(protobuf.op.gset_op.adds, "gåser"));
+            assert(utils.includesBuffer(protobuf.op.gset_op.adds, "hampen"));
+            assert.equal(protobuf.getContext().toString('utf8'), 'context');
+            done();
+        });
+    });
+
+    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"));
+            resp.setGsetValue([hampenBuffer, snabbfotadBuffer]);
+
+            var callback = function(err, response) {
+                assert(!err);
+                assert(response);
+
+                assert.equal(response.context.toString("utf8"), "asdf");
+                assert(utils.includesBuffer(response.values, "hampen"));
+                assert(utils.includesBuffer(response.values, "snabbfotad"));
+
+                assert.equal(response.generatedKey, null);
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([hampenBuffer]).
+                    withSetsAsBuffers(true).
+                    build();
+
+            update.onSuccess(resp);
+        });
+    });
+
+    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.setGsetValue([hampenBuffer, snabbfotadBuffer]);
+
+            var callback = function(err, response) {
+                assert(!err);
+                assert(response);
+
+                assert.equal(response.context.toString("utf8"), "asdf");
+                assert(utils.includes(response.values, "hampen"));
+                assert(utils.includes(response.values, "snabbfotad"));
+
+                assert.equal(response.generatedKey, null);
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([hampenBuffer]).
+                    withSetsAsBuffers(false).
+                    build();
+
+            update.onSuccess(resp);
+        });
+    });
+
+    describe('without body, with key', function() {
+        it('calls back with successful results', function(done) {
+            var resp = new DtUpdateResp();
+            resp.key = new Buffer("ikea_rugs");
+
+            var callback = function(err, response) {
+                assert.equal(response.generatedKey, "ikea_rugs");
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([hampenBuffer]).
+                    build();
+
+            update.onSuccess(resp);
+        });
+    });
+
+    describe('returning an error', function() {
+        it('calls back with an error message', function(done) {
+            var errorMessage = "couldn't crdt :(";
+            var errorResp = new RpbErrorResp();
+            errorResp.setErrmsg(new Buffer(errorMessage));
+
+            var callback = function(err, response) {
+                assert(err);
+                assert(!response);
+
+                assert.equal(err, errorMessage);
+
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    build();
+
+            update.onRiakError(errorResp);
+        });
+    });
+});
diff --git a/test/unit/crdt/updatehll.js b/test/unit/crdt/updatehll.js
index 9df2cfc..c858a1b 100644
--- a/test/unit/crdt/updatehll.js
+++ b/test/unit/crdt/updatehll.js
@@ -26,6 +26,7 @@ var RpbErrorResp = Rpb.getProtoFor('RpbErrorResp');
 
 var ByteBuffer = require('bytebuffer');
 var assert = require('assert');
+var utils = require('../../utils');
 
 describe('UpdateHll', function() {
 
@@ -38,17 +39,6 @@ describe('UpdateHll', function() {
             withKey('hll_1');
 
     describe('Build', function() {
-        var includesBuffer = function(haystack, needle) {
-            var len = haystack.length;
-            for (var i = 0; i < len; i++) {
-                if (haystack[i].toString('utf8') === needle) {
-                    return true;
-                }
-            }
-
-            return false;
-        };
-
         it('builds a DtUpdateHll correctly', function(done){
             var update = builder.
                     withAdditions(['Jokes', 'Are', 'Better', 'Explained']).
@@ -72,38 +62,15 @@ describe('UpdateHll', function() {
             assert.equal(protobuf.getReturnBody(), false);
             assert.equal(protobuf.getTimeout(), 12345);
 
-            assert(includesBuffer(protobuf.op.hll_op.adds,
-                                  "Jokes"));
-            assert(includesBuffer(protobuf.op.hll_op.adds,
-                                  "Are"));
-            assert(includesBuffer(protobuf.op.hll_op.adds,
-                                  "Better"));
-            assert(includesBuffer(protobuf.op.hll_op.adds,
-                                  "Explained"));
+            assert(utils.includesBuffer(protobuf.op.hll_op.adds, "Jokes"));
+            assert(utils.includesBuffer(protobuf.op.hll_op.adds, "Are"));
+            assert(utils.includesBuffer(protobuf.op.hll_op.adds, "Better"));
+            assert(utils.includesBuffer(protobuf.op.hll_op.adds, "Explained"));
 
             done();
         });
     });
 
-    var includesBuffer = function(haystack, needle) {
-        var needleBuf = new Buffer(needle);
-        var len = haystack.length;
-        for (var i = 0; i < len; i++) {
-            if (haystack[i].equals(needleBuf)) return true;
-        }
-
-        return false;
-    };
-
-    var includes = function(haystack, needle) {
-        var len = haystack.length;
-        for (var i = 0; i < len; i++) {
-            if (haystack[i] === needle) return true;
-        }
-
-        return false;
-    };
-
     describe('with body, without key, as buffers', function() {
         it('calls back with successful results', function(done) {
             var resp = new DtUpdateResp();
diff --git a/test/unit/crdt/updateset.js b/test/unit/crdt/updateset.js
index 55c7410..4f9de83 100644
--- a/test/unit/crdt/updateset.js
+++ b/test/unit/crdt/updateset.js
@@ -26,6 +26,7 @@ var RpbErrorResp = Rpb.getProtoFor('RpbErrorResp');
 
 var ByteBuffer = require('bytebuffer');
 var assert = require('assert');
+var utils = require('../../utils');
 
 describe('UpdateSet', function() {
     // ikea rugs
@@ -40,17 +41,6 @@ describe('UpdateSet', function() {
             withKey('cool_set');
 
     describe('Build', function() {
-        var includesBuffer = function(haystack, needle) {
-            var len = haystack.length;
-            for (var i = 0; i < len; i++) {
-                if (haystack[i].toString('utf8') === needle) {
-                    return true;
-                }
-            }
-
-            return false;
-        };
-
         it('builds a DtUpdateSet correctly', function(done){
             var update = builder.
                     withContext(someContext).
@@ -66,12 +56,9 @@ describe('UpdateSet', function() {
 
             var protobuf = update.constructPbRequest();
 
-            assert.equal(protobuf.getType().toString('utf8'),
-                         'sets_type');
-            assert.equal(protobuf.getBucket().toString('utf8'),
-                         'set_bucket');
-            assert.equal(protobuf.getKey().toString('utf8'),
-                         'cool_set');
+            assert.equal(protobuf.getType().toString('utf8'), 'sets_type');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'set_bucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'cool_set');
             assert.equal(protobuf.getW(), 1);
             assert.equal(protobuf.getDw(), 2);
             assert.equal(protobuf.getPw(), 3);
@@ -79,15 +66,11 @@ describe('UpdateSet', function() {
             assert.equal(protobuf.getReturnBody(), false);
             assert.equal(protobuf.getTimeout(), 12345);
 
-            assert(includesBuffer(protobuf.op.set_op.adds,
-                                  "gåser"));
-            assert(includesBuffer(protobuf.op.set_op.adds,
-                                  "hampen"));
+            assert(utils.includesBuffer(protobuf.op.set_op.adds, "gåser"));
+            assert(utils.includesBuffer(protobuf.op.set_op.adds, "hampen"));
 
-            assert(includesBuffer(protobuf.op.set_op.removes,
-                                  "snabbfotad"));
-            assert(includesBuffer(protobuf.op.set_op.removes,
-                                  "valby ruta"));
+            assert(utils.includesBuffer(protobuf.op.set_op.removes, "snabbfotad"));
+            assert(utils.includesBuffer(protobuf.op.set_op.removes, "valby ruta"));
                                   
             assert.equal(protobuf.getContext().toString('utf8'), 'context');
 
@@ -95,25 +78,6 @@ describe('UpdateSet', function() {
         });
     });
 
-    var includesBuffer = function(haystack, needle) {
-        var needleBuf = new Buffer(needle);
-        var len = haystack.length;
-        for (var i = 0; i < len; i++) {
-            if (haystack[i].equals(needleBuf)) return true;
-        }
-
-        return false;
-    };
-
-    var includes = function(haystack, needle) {
-        var len = haystack.length;
-        for (var i = 0; i < len; i++) {
-            if (haystack[i] === needle) return true;
-        }
-
-        return false;
-    };
-
     describe('with body, without key, as buffers', function() {
         it('calls back with successful results', function(done) {
             var resp = new DtUpdateResp();
@@ -125,8 +89,8 @@ describe('UpdateSet', function() {
                 assert(response);
 
                 assert.equal(response.context.toString("utf8"), "asdf");
-                assert(includesBuffer(response.values, "hampen"));
-                assert(includesBuffer(response.values, "snabbfotad"));
+                assert(utils.includesBuffer(response.values, "hampen"));
+                assert(utils.includesBuffer(response.values, "snabbfotad"));
 
                 assert.equal(response.generatedKey, null);
                 done();
@@ -153,8 +117,8 @@ describe('UpdateSet', function() {
                 assert(response);
 
                 assert.equal(response.context.toString("utf8"), "asdf");
-                assert(includes(response.values, "hampen"));
-                assert(includes(response.values, "snabbfotad"));
+                assert(utils.includes(response.values, "hampen"));
+                assert(utils.includes(response.values, "snabbfotad"));
 
                 assert.equal(response.generatedKey, null);
                 done();
diff --git a/test/utils.js b/test/utils.js
new file mode 100644
index 0000000..1b5af50
--- /dev/null
+++ b/test/utils.js
@@ -0,0 +1,40 @@
+/**
+ *
+ * Copyright 2014-present 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.
+ */
+
+'use strict';
+
+function includesBuffer(haystack, needle) {
+    var needleBuf = new Buffer(needle);
+    var len = haystack.length;
+    for (var i = 0; i < len; i++) {
+        if (haystack[i].equals(needleBuf)) return true;
+    }
+    
+    return false;
+}
+
+function includes(haystack, needle) {
+    var len = haystack.length;
+    for (var i = 0; i < len; i++) {
+        if (haystack[i] === needle) return true;
+    }
+
+    return false;
+}
+
+module.exports.includesBuffer = includesBuffer;
+module.exports.includes = includes;