Repository: basho/riak-nodejs-client
Pull request #170 (2016-09-08 18:34:46)

Add HyperLogLog Data Type
Adds HLL data type + bucket properties.

Test with a devrel of this Riak branch: https://github.com/basho/riak_ee/tree/develop-2.2

Docs PR forthcoming.


Patch:
diff --git a/Gruntfile.js b/Gruntfile.js
index 04aa00b..f393744 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -49,7 +49,17 @@ module.exports = function(grunt) {
                 clearRequireCache: false,
                 colors: false
             },
-            src: ['test/integration/core/*.js', 'test/integration/crdt/*.js', 'test/integration/kv/*.js', 'test/integration/mapreduce/*.js', 'test/integration/yokozuna/*.js']
+            src: ['test/integration/**/*.js', '!test/integration/crdt/*hll.js', '!test/integration/ts/*.js']
+        },
+        integration_hll: {
+            options: {
+                reporter: 'spec',
+                captureFile: 'integration-test-results.txt',
+                quiet: false,
+                clearRequireCache: false,
+                colors: false
+            },
+            src: ['test/integration/crdt/updateandfetchhll.js']
         },
         timeseries: {
             options: {
@@ -70,7 +80,7 @@ module.exports = function(grunt) {
             },
             src: ['test/security/**/*.js']
         }
-    },   
+    },
     watch: {
       files: ['<%= jshint.files %>'],
       tasks: ['jshint']
@@ -105,10 +115,10 @@ module.exports = function(grunt) {
     grunt.task.run(['jshint', 'mochaTest:unit']);
   });
   grunt.registerTask('integration', ['jshint', 'mochaTest:integration']);
+  grunt.registerTask('integration-hll', ['jshint', 'mochaTest:integration_hll']);
   grunt.registerTask('timeseries', ['jshint', 'mochaTest:timeseries']);
   grunt.registerTask('security-test', ['mochaTest:security']);
   grunt.registerTask('security', ['jshint', 'mochaTest:security']);
   grunt.registerTask('default', ['jshint', 'mochaTest:unit', 'mochaTest:integration']);
   grunt.registerTask('docs', 'yuidoc');
-
 };
diff --git a/build/common.mk b/build/common.mk
index f8e2cb6..67bd30d 100644
--- a/build/common.mk
+++ b/build/common.mk
@@ -1,7 +1,7 @@
 VERBOSITY = normal
 GRUNT=$(PROJDIR)/node_modules/grunt-cli/bin/grunt
 
-.PHONY: all install-deps help test unit-test integration-test security-test publish
+.PHONY: all install-deps help test unit-test integration-test integration-test-hll security-test publish
 
 all: test
 
@@ -17,6 +17,9 @@ unit-test:
 integration-test:
 	$(GRUNT) integration
 
+integration-test-hll:
+	$(GRUNT) integration-hll
+
 security-test:
 	$(GRUNT) security
 
@@ -29,13 +32,14 @@ publish:
 help:
 	@echo ''
 	@echo ' Targets:'
-	@echo '--------------------------------------------------------------'
-	@echo ' all                          - Run everything                '
-	@echo ' lint                         - Run jshint                    '
-	@echo ' install-deps                 - Install required dependencies '
-	@echo ' test                         - Run unit & integration tests  '
-	@echo ' unit-test [SUITE=suite_name] - Run unit tests                '
-	@echo ' integration-test             - Run integration tests         '
-	@echo ' security-test                - Run security tests            '
-	@echo '--------------------------------------------------------------'
+	@echo '-----------------------------------------------------------------'
+	@echo ' all                          - Run everything                   '
+	@echo ' lint                         - Run jshint                       '
+	@echo ' install-deps                 - Install required dependencies    '
+	@echo ' test                         - Run unit & integration tests     '
+	@echo ' unit-test [SUITE=suite_name] - Run unit tests                   '
+	@echo ' integration-test             - Run integration tests            '
+	@echo ' integration-test-hll         - Run Hyperloglog integration tests'
+	@echo ' security-test                - Run security tests               '
+	@echo '-----------------------------------------------------------------'
 	@echo ''
diff --git a/lib/commands/crdt.js b/lib/commands/crdt.js
index f48c2e3..95617b6 100644
--- a/lib/commands/crdt.js
+++ b/lib/commands/crdt.js
@@ -15,4 +15,5 @@ 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.FetchHll =  require('./crdt/fetchhll');
+module.exports.UpdateHll =  require('./crdt/updatehll');
diff --git a/lib/commands/crdt/fetchhll.js b/lib/commands/crdt/fetchhll.js
new file mode 100644
index 0000000..2f8094d
--- /dev/null
+++ b/lib/commands/crdt/fetchhll.js
@@ -0,0 +1,228 @@
+'use strict';
+
+var CommandBase = require('../commandbase');
+var inherits = require('util').inherits;
+var Joi = require('joi');
+
+/**
+ * Provides the FetchHll class, its builder, and its response.
+ * @module CRDT
+ */
+
+/**
+ *  Command used to fetch a hyperloglog value from Riak.
+ *
+ *  As a convenience, a builder class is provided:
+ *
+ *      var fetch = new FetchHll.Builder()
+ *                      .withBucketType('myBucketType')
+ *                      .withBucket('myBucket')
+ *                      .withKey('myKey')
+ *                      .withCallback(myCallback)
+ *                      .build();
+ *
+ * See {{#crossLink "FetchHll.Builder"}}FetchHll.Builder{{/crossLink}}
+ *
+ * @class FetchHll
+ * @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 hyperloglog you want to fetch.
+ * @param {Number} [options.timeout] Set a timeout in Riak for this operation.
+ * @param {Number} [options.r] The R value to use for this fetch.
+ * @param {Number} [options.pr] The PR value to use for this fetch.
+ * @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. Will be null if no error.
+ * @param {Object} callback.response The response from Riak.
+ * @param {Number} callback.response.cardinality The estimated cardinality of the hyperloglog.
+ * @param {Boolean} callback.response.isNotFound True if there was no hyperloglog in Riak.
+ * @param {Object} callback.data additional error data. Will be null if no error.
+ * @extends CommandBase
+ */
+function FetchHll(options, callback) {
+    CommandBase.call(this, 'DtFetchReq', 'DtFetchResp', callback);
+    this.validateOptions(options, schema);
+}
+
+inherits(FetchHll, CommandBase);
+
+FetchHll.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;
+};
+
+FetchHll.prototype.onSuccess = function(dtFetchResp) {
+    var cardinality = null;
+    var isNotFound = false;
+    // on a "not found" the value is null
+
+    if (dtFetchResp.value) {
+      cardinality = dtFetchResp.value.hll_value.toNumber();
+    } else {
+      isNotFound = true;
+    }
+
+    this._callback(null, { cardinality: cardinality, isNotFound: isNotFound });
+
+    return true;
+};
+
+var schema = Joi.object().keys({
+   bucket: Joi.string().required(),
+   bucketType: Joi.string().required(),
+   key: Joi.binary().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()
+});
+
+/**
+ * A builder for constructing FetchHll instances.
+ *
+ * Rather than having to manually construct the __options__ and instantiating
+ * a FetchHll directly, this builder may be used.
+ *
+ *     var fetch = new FetchHll.Builder()
+ *                      .withBucketType('myBucketType')
+ *                      .withBucket('myBucket')
+ *                      .withKey('myKey')
+ *                      .withCallback(myCallback)
+ *                      .build();
+ *
+ * @class FetchHll.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.
+     * If not supplied, 'default' is used.
+     * @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;
+    },
+    /**
+     * Set the R value.
+     * If not asSet 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 asSet 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. Will be null if no error.
+     * @param {Object} callback.response The response from Riak
+     * @param {Number} callback.response.cardinality The estimated cardinality of the hyperloglog.
+     * @chainable
+     */
+    withCallback : function(callback) {
+        this.callback = callback;
+        return this;
+    },
+    /**
+     * Construct a FetchHll instance.
+     * @method build
+     * @return {FetchHll}
+     */
+    build : function() {
+        var cb = this.callback;
+        delete this.callback;
+        return new FetchHll(this, cb);
+    }
+};
+
+module.exports = FetchHll;
+module.exports.Builder = Builder;
diff --git a/lib/commands/crdt/fetchset.js b/lib/commands/crdt/fetchset.js
index 7bc78c4..b9a83e9 100644
--- a/lib/commands/crdt/fetchset.js
+++ b/lib/commands/crdt/fetchset.js
@@ -4,6 +4,8 @@ var CommandBase = require('../commandbase');
 var inherits = require('util').inherits;
 var Joi = require('joi');
 
+var utils = require('../../utils');
+
 /**
  * Provides the FetchCounter class, its builder, and its response.
  * @module CRDT
@@ -159,17 +161,6 @@ Builder.prototype = {
     }
 };
 
-function firstUc(str) {
-    return str.charAt(0).toUpperCase() + str.slice(1);
-}
-
-function bb(prop) {
-    Builder.prototype["with"+firstUc(prop)] = function(pv) {
-        this[prop] = pv;
-        return this;
-    };
-}
-
 /**
 * Set the callback to be executed when the operation completes.
 * @method withCallback
@@ -180,28 +171,32 @@ function bb(prop) {
 * @param {Boolean} callback.response.isNotFound True if there was no set in Riak.
 * @chainable
 */
-bb('callback');
+utils.bb(Builder, 'callback');
+
 /**
 * Set the bucket type.
 * @method withBucketType
 * @param {String} bucketType the bucket type in riak
 * @chainable
 */
-bb('bucketType');
+utils.bb(Builder, 'bucketType');
+
 /**
 * Set the bucket.
 * @method withBucket
 * @param {String} bucket the bucket in Riak
 * @chainable
 */
-bb('bucket');
+utils.bb(Builder, 'bucket');
+
 /**
 * Set the key.
 * @method withKey
 * @param {String} key the key in riak.
 * @chainable
 */
-bb('key');
+utils.bb(Builder, 'key');
+
 /**
 * Set the R value.
 * If not set the bucket default is used.
@@ -209,7 +204,8 @@ bb('key');
 * @param {Number} r the R value.
 * @chainable
 */
-bb('r');
+utils.bb(Builder, 'r');
+
 /**
 * Set the PR value.
 * If not set the bucket default is used.
@@ -217,7 +213,8 @@ bb('r');
 * @param {Number} pr the PR value.
 * @chainable
 */
-bb('pr');
+utils.bb(Builder, 'pr');
+
 /**
 * Return sets as arrays of Buffers rather than strings.
 * By default the contents of sets are converted to strings. Setting this
@@ -227,7 +224,8 @@ bb('pr');
 * @param {Boolean} setsAsBuffers true to not convert set contents to strings.
 * @chainable
 */
-bb('setsAsBuffers');
+utils.bb(Builder, 'setsAsBuffers');
+
 /**
 * Set the not_found_ok value.
 * If true a vnode returning notfound for a key increments the r tally.
@@ -237,7 +235,8 @@ bb('setsAsBuffers');
 * @param {Boolean} notFoundOk the not_found_ok value.
 * @chainable
 */
-bb('notFoundOk');
+utils.bb(Builder, 'notFoundOk');
+
 /**
 * Set the basic_quorum value.
 * The parameter controls whether a read request should return early in
@@ -248,14 +247,15 @@ bb('notFoundOk');
 * @param {Boolean} useBasicQuorum the basic_quorum value.
 * @chainable
 */
-bb('useBasicQuorum');
+utils.bb(Builder, 'useBasicQuorum');
+
 /**
 * Set a timeout for this operation.
 * @method withTimeout
 * @param {Number} timeout a timeout in milliseconds.
 * @chainable
 */
-bb('timeout');
+utils.bb(Builder, 'timeout');
 
 module.exports = FetchSet;
 module.exports.Builder = Builder;
diff --git a/lib/commands/crdt/updatehll.js b/lib/commands/crdt/updatehll.js
new file mode 100644
index 0000000..e42a097
--- /dev/null
+++ b/lib/commands/crdt/updatehll.js
@@ -0,0 +1,294 @@
+'use strict';
+
+var CommandBase = require('../commandbase');
+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 HllOp = rpb.getProtoFor('HllOp');
+
+/**
+ * Provides the Update Hyperloglog class, its builder, and its response.
+ * @module DT
+ */
+
+/**
+ * Command used tp update a hyperloglog in Riak
+ *
+ * As a convenience, a builder class is provided:
+ *
+ *        var update = new UpdateHll.Builder()
+ *               .withBucketType('hlls')
+ *               .withBucket('myBucket')
+ *               .withKey('hll_1')
+ *               .withAdditions(['this', 'that', 'other'])
+ *               .withCallback(callback)
+ *               .build();
+ *
+ * See {{#crossLink "UpdateHll.Builder"}}UpdateHll.Builder{{/crossLink}}
+ * @class UpdateHll
+ * @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 hyperloglog you want to store. Riak will generate one if not set.
+ * @param {String[]|Buffer[]} [options.additions] The values to be added to the hyperloglog.
+ * @param {Buffer} [options.context] The context from a previous fetch. Required for remove operations.
+ * @param {Boolean} [options.returnBody=true] Return the hyperloglog.
+ * @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 The callback to be executed when the operation completes.
+ * @param {String} callback.err An error message. Will be 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 {Number} callback.response.cardinality The estimated cardinality of the hyperloglog.
+ * @param {Object} callback.data additional error data. Will be null if no error.
+ * @extends CommandBase
+ */
+function UpdateHll(options, callback) {
+    CommandBase.call(this, 'DtUpdateReq', 'DtUpdateResp', callback);
+    this.validateOptions(options, schema);
+}
+
+inherits(UpdateHll, 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;
+}
+
+UpdateHll.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 op = new DtOp();
+    protobuf.setOp(op);
+    var hllOp = new HllOp();
+    op.setHllOp(hllOp);
+    hllOp.adds = buflist(this.options.additions);
+
+    // 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);
+
+    return protobuf;
+};
+
+UpdateHll.prototype.onSuccess = function(dtUpdateResp) {
+    var response = null;
+
+    // dtUpdateResp will be null if returnBody is not set
+    if (dtUpdateResp) {
+        var key = null;
+        var cardinality = 0;
+
+        if (dtUpdateResp.getKey()) {
+            key = dtUpdateResp.getKey().toString('utf8');
+        }
+
+        if (dtUpdateResp.hll_value) {
+            cardinality = dtUpdateResp.hll_value.toNumber();
+        }
+
+        response = { generatedKey: key,
+                     cardinality: cardinality };
+    }
+    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(),
+
+    //operations
+    additions: Joi.array().default([]).required(),
+
+    //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(),
+});
+
+/**
+ * A builder for constructing UpdateHll instances.
+ *
+ * Rather than having to manually construct the __options__ and instantiating
+ * a UpdateHll directly, this builder may be used.
+ *
+ *     var update = new UpdateHll.Builder()
+ *                       .withBucketType('myBucketType')
+ *                       .withBucket('myBucket')
+ *                       .withKey('myKey')
+ *                       .withAdditions(['this', 'that', 'other'])
+ *                       .withCallback(myCallback)
+ *                       .build();
+ *
+ * @class UpdateHll.Builder
+ * @constructor
+ */
+function Builder() {}
+
+Builder.prototype = {
+    /**
+     * Construct an UpdateHll instance.
+     * @method build
+     * @return {UpdateHll}
+     */
+    build: function() {
+        var cb = this.callback;
+        delete this.callback;
+        return new UpdateHll(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 {Number} callback.response.cardinality The estimated cardinality of the hyperloglog.
+ */
+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 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 hyperloglog.
+ * @method withAdditions
+ * @param {String[]|Buffer[]} additions The values to add.
+ * @chainable
+ */
+utils.bb(Builder, 'additions');
+
+module.exports = UpdateHll;
+module.exports.Builder = Builder;
diff --git a/lib/commands/crdt/updateset.js b/lib/commands/crdt/updateset.js
index 49c76e5..00dbb0c 100644
--- a/lib/commands/crdt/updateset.js
+++ b/lib/commands/crdt/updateset.js
@@ -204,16 +204,6 @@ Builder.prototype = {
     }
 };
 
-function firstUc(str) {
-    return str.charAt(0).toUpperCase() + str.slice(1);
-}
-
-function bb(prop) {
-    Builder.prototype["with"+firstUc(prop)] = function(pv) {
-        this[prop] = pv;
-        return this;
-    };
-}
 /**
  * Set the callback.
  * @method withCallback
@@ -224,21 +214,24 @@ function bb(prop) {
  * @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.
  */
-bb('callback');
+utils.bb(Builder, 'callback');
+
 /**
 * Set the bucket type.
 * @method withBucketType
 * @param {String} bucketType the bucket type in riak
 * @chainable
 */
-bb('bucketType');
+utils.bb(Builder, 'bucketType');
+
 /**
 * Set the bucket.
 * @method withBucket
 * @param {String} bucket the bucket in Riak
 * @chainable
 */
-bb('bucket');
+utils.bb(Builder, 'bucket');
+
 /**
 * Set the key.
 * If this is not set one will be generated by and returned from Riak
@@ -246,7 +239,8 @@ bb('bucket');
 * @param {String} key the key in riak.
 * @chainable
 */
-bb('key');
+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.
@@ -254,7 +248,8 @@ bb('key');
 * @param {ByteBuffer} context the causal context
 * @chainable
  */
-bb('context');
+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
@@ -264,7 +259,8 @@ bb('context');
 * @param {Boolean} setsAsBuffers true to not convert set contents to strings.
 * @chainable
 */
-bb('setsAsBuffers');
+utils.bb(Builder, 'setsAsBuffers');
+
 /**
 * Set the W value.
 * If not set the bucket default is used.
@@ -272,8 +268,8 @@ bb('setsAsBuffers');
 * @param {Number} w the W value.
 * @chainable
 */
+utils.bb(Builder, 'w');
 
-bb('w');
 /**
 * Set the DW value.
 * If not set the bucket default is used.
@@ -281,7 +277,8 @@ bb('w');
 * @param {Number} dw the DW value.
 * @chainable
 */
-bb('dw');
+utils.bb(Builder, 'dw');
+
 /**
 * Set the PW value.
 * If not set the bucket default is used.
@@ -289,7 +286,8 @@ bb('dw');
 * @param {Number} pw the PW value.
 * @chainable
 */
-bb('pw');
+utils.bb(Builder, 'pw');
+
 /**
 * Set the return_body value.
 * If true, the callback is passed the contents of the set after the update.
@@ -297,7 +295,8 @@ bb('pw');
 * @param {Boolean} returnBody the return_body value.
 * @chainable
 */
-bb('returnBody');
+utils.bb(Builder, 'returnBody');
+
 /**
 * Set the basic_quorum value.
 * The parameter controls whether a read request should return early in
@@ -308,21 +307,24 @@ bb('returnBody');
 * @param {Boolean} useBasicQuorum the basic_quorum value.
 * @chainable
 */
-bb('useBasicQuorum');
+utils.bb(Builder, 'useBasicQuorum');
+
 /**
 * Set a timeout for this operation.
 * @method withTimeout
 * @param {Number} timeout a timeout in milliseconds.
 * @chainable
 */
-bb('timeout');
+utils.bb(Builder, 'timeout');
+
 /**
  * The values you wish to add to this set.
  * @method withAdditions
  * @param {String[]|Buffer[]} additions The values to add.
  * @chainable
  */
-bb('additions');
+utils.bb(Builder, 'additions');
+
 /**
  * The values you wish to remove from this set.
  * __Note:__ when performing removals a context must be provided.
@@ -330,7 +332,7 @@ bb('additions');
  * @param {String[]|Buffer[]} removals The values to remove.
  * @chainable
  */
-bb('removals');
+utils.bb(Builder, 'removals');
 
 module.exports = UpdateSet;
 module.exports.Builder = Builder;
diff --git a/lib/commands/kv/fetchpreflist.js b/lib/commands/kv/fetchpreflist.js
index 2fc5d27..5a3b9b1 100644
--- a/lib/commands/kv/fetchpreflist.js
+++ b/lib/commands/kv/fetchpreflist.js
@@ -106,7 +106,6 @@ var schema = Joi.object().keys({
 function Builder() {}
 
 Builder.prototype = {
-
     /**
      * Set the bucket type.
      * If not supplied, 'default' is used.
@@ -161,7 +160,6 @@ Builder.prototype = {
         delete this.callback;
         return new FetchPreflist(this, cb);
     }
-
 };
 
 module.exports = FetchPreflist;
diff --git a/lib/commands/kv/fetchpropsbase.js b/lib/commands/kv/fetchpropsbase.js
index 2768205..953f360 100644
--- a/lib/commands/kv/fetchpropsbase.js
+++ b/lib/commands/kv/fetchpropsbase.js
@@ -50,7 +50,8 @@ FetchPropsBase.prototype.onSuccess = function(rpbGetBucketResp) {
         notFoundOk: rpbBucketProps.getNotfoundOk(),
         search: rpbBucketProps.getSearch(),
         consistent: rpbBucketProps.getConsistent(),
-        repl: rpbBucketProps.getRepl()
+        repl: rpbBucketProps.getRepl(),
+        hllPrecision: rpbBucketProps.getHllPrecision()
     };
 
     var backend;
@@ -139,4 +140,3 @@ Builder.prototype.withCallback = function(callback) {
 
 module.exports = FetchPropsBase;
 module.exports.Builder = Builder;
-
diff --git a/lib/commands/kv/storepropsbase.js b/lib/commands/kv/storepropsbase.js
index a29bcc1..76ede10 100644
--- a/lib/commands/kv/storepropsbase.js
+++ b/lib/commands/kv/storepropsbase.js
@@ -40,6 +40,7 @@ var RpbModFun = rpb.getProtoFor('RpbModFun');
  * @param {Object} [options.chashKeyfun] An object representing the Erlang func to use.
  * @param {Object[]} [options.precommit] Array of precommit hooks
  * @param {Object[]} [options.postcommit] Array of postcommit hooks
+ * @param {Number} [options.hllPrecision] The number of bits to use in the HyperLogLog precision.
  * @param {String} pbRequestName name of the Riak protocol buffer this command will send
  * @param {String} pbResponseName name of the Riak protocol buffer this command will receive
  * @param {Function} callback The callback to be executed when the operation completes.
@@ -85,6 +86,8 @@ StorePropsBase.prototype.constructPbRequest = function() {
     props.setBackend(this.options.backend ? new Buffer(this.options.backend) : null);
     props.setSearchIndex(this.options.searchIndex ? new Buffer(this.options.searchIndex) : null);
 
+    props.setHllPrecision(this.options.hllPrecision >= 0 ? this.options.hllPrecision : null);
+
     if (this.options.precommit) {
         props.setPrecommit(this._convertHooks(this.options.precommit));
     }
@@ -154,7 +157,8 @@ var schema = Joi.object().keys({
     basicQuorum : Joi.boolean().optional(),
     search : Joi.boolean().optional(),
     searchIndex : Joi.string().optional(),
-    chashKeyfun : Joi.object().optional()
+    chashKeyfun : Joi.object().optional(),
+    hllPrecision : Joi.number().min(4).max(16).optional()
 });
 
 function Builder() {}
@@ -430,6 +434,20 @@ Builder.prototype.withChashkeyFunction = function(func) {
     return this;
 };
 
+/**
+ * Set the Hyperloglog Precision value
+ * @method withHllPrecision
+ * @param {Number} precision the number of bits to use in the HyperLogLog precision.
+ *     Valid values are [4 - 16] inclusive, default is 14 on new buckets.
+ *     <b>NOTE:</b> When changing precision, it may only be reduced from
+ *     it's current value, and never increased.
+ * @chainable
+ */
+Builder.prototype.withHllPrecision = function(precision) {
+    this.hllPrecision = precision;
+    return this;
+};
+
 /**
  * Set the callback to be executed when the operation completes.
  * @method withCallback
diff --git a/lib/protobuf/riak_pb b/lib/protobuf/riak_pb
index 33e4d4d..f5af9ff 160000
--- a/lib/protobuf/riak_pb
+++ b/lib/protobuf/riak_pb
@@ -1,1 +1,1 @@
-Subproject commit 33e4d4dded18c42bb8d8a821d49f998453060760
+Subproject commit f5af9ffe04eb27689d483509de26574bdf70343f
diff --git a/lib/utils.js b/lib/utils.js
index 64a263d..76b3f5f 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -33,8 +33,20 @@ function isFloat(n) {
     return typeof n === 'number' && n % 1 !== 0;
 }
 
+function firstUc(str) {
+    return str.charAt(0).toUpperCase() + str.slice(1);
+}
+
+function bb(obj, prop) {
+    obj.prototype['with' + firstUc(prop)] = function(pv) {
+        this[prop] = pv;
+        return this;
+    };
+}
+
 module.exports.isString = isString;
 module.exports.isNullOrUndefined = isNullOrUndefined;
 module.exports.maybeIsNumber = maybeIsNumber;
 module.exports.isInteger = isInteger;
 module.exports.isFloat = isFloat;
+module.exports.bb = bb;
diff --git a/tools b/tools
index bbd2499..8d1b6e5 160000
--- a/tools
+++ b/tools
@@ -1,1 +1,1 @@
-Subproject commit bbd249958d4a23639876bf7b5c2933d902758c29
+Subproject commit 8d1b6e502a4082693e935774a77a73d4896e4bf5


Test patch:
diff --git a/test/integration/crdt/updateandfetchhll.js b/test/integration/crdt/updateandfetchhll.js
new file mode 100644
index 0000000..dd88238
--- /dev/null
+++ b/test/integration/crdt/updateandfetchhll.js
@@ -0,0 +1,74 @@
+'use strict';
+
+var assert = require('assert');
+
+var Test = require('../testparams');
+var UpdateHll = require('../../../lib/commands/crdt/updatehll');
+var FetchHll = require('../../../lib/commands/crdt/fetchhll');
+var RiakCluster = require('../../../lib/core/riakcluster');
+
+describe('Update and Fetch Hyperloglog - Integration', function() {
+    var context;
+    var cluster;
+    before(function(done) {
+        cluster = Test.buildCluster(function (err, rslt) {
+            assert(!err, err);
+            var callback = function(err, resp) {
+                assert(!err, err);
+                assert.equal(resp.cardinality, 4);
+
+                done();
+            };
+            var update = new UpdateHll.Builder()
+                    .withBucketType(Test.hllBucketType)
+                    .withBucket(Test.bucketName)
+                    .withKey('hll_1')
+                    .withAdditions(['Jokes', 'are', 'better', 'explained', 'Jokes'])
+                    .withCallback(callback)
+                    .build();
+            cluster.execute(update);
+        });
+    });
+
+    after(function(done) {
+        Test.cleanBucket(cluster, Test.hllBucketType, Test.bucketName, function() {
+            cluster.stop(function (err, state) {
+                done();
+            });
+        });
+    });
+
+    it('Should fetch a hyperloglog', function(done) {
+        var callback = function(err, resp) {
+            assert(!err, err);
+            assert(!resp.notFound);
+            assert(!resp.isNotFound);
+            assert.equal(resp.cardinality, 4);
+            done();
+        };
+        var fetch = new FetchHll.Builder()
+                .withBucketType(Test.hllBucketType)
+                .withBucket(Test.bucketName)
+                .withKey('hll_1')
+                .withCallback(callback)
+                .build();
+        cluster.execute(fetch);
+    });
+
+    it('Should report isNotFound if a hyperloglog does not exist', function(done) {
+        var callback = function(err, resp) {
+            assert(!err, err);
+            assert(resp.isNotFound);
+            assert.equal(resp.cardinality, null);
+            done();
+        };
+        var fetch = new FetchHll.Builder()
+                .withBucketType(Test.hllBucketType)
+                .withBucket(Test.bucketName)
+                .withKey('empty_hll')
+                .withCallback(callback)
+                .build();
+        cluster.execute(fetch);
+    });
+
+});
diff --git a/test/integration/testparams.js b/test/integration/testparams.js
index 6057c69..a015157 100644
--- a/test/integration/testparams.js
+++ b/test/integration/testparams.js
@@ -19,7 +19,7 @@ module.exports.bucketName = 'riak-nodejs-client';
 
 /**
 * Bucket type
-* 
+*
 * you must create the type 'leveldb_type' to use this:
 *
 * riak-admin bucket-type create leveldb_type '{"props":{"backend":"leveldb_backend"}}'
@@ -75,23 +75,24 @@ module.exports.buildCluster = function(start_cb) {
 };
 
 /**
- * 
- * CRDTs - need to create these types 
+ *
+ * CRDTs - need to create these types
  */
 module.exports.counterBucketType = 'counters';
 module.exports.setBucketType = 'sets';
 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 
+
+    // Note this also acts as the integration test for ListKeys and
     // DeleteValue
     logger.debug('Clearing bucket: %s:%s', type, bucket);
     var numKeys = 0;
     var count = 0;
     var lkCallback = function(err, resp) {
         assert(!err, err);
-        
+
         numKeys += resp.keys.length;
 
         if (numKeys > 0) {
@@ -100,7 +101,7 @@ module.exports.cleanBucket = function(cluster, type, bucket, callback) {
             logger.debug('DONE clearing bucket: %s:%s', type, bucket);
             callback();
         }
-        
+
         var dCallback = function(err, resp) {
             assert(!err, err);
             count++;
@@ -109,28 +110,28 @@ module.exports.cleanBucket = function(cluster, type, bucket, callback) {
                 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()
             .withBucket(bucket)
             .withBucketType(type)
             .withCallback(lkCallback)
             .withStreaming(false)
             .build();
-    
+
     cluster.execute(list);
-    
+
 };
diff --git a/test/unit/crdt/fetchhll.js b/test/unit/crdt/fetchhll.js
new file mode 100644
index 0000000..c2bf046
--- /dev/null
+++ b/test/unit/crdt/fetchhll.js
@@ -0,0 +1,82 @@
+'use strict';
+
+var FetchHll = require('../../../lib/commands/crdt/fetchhll');
+var DtFetchResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('DtFetchResp');
+var DtValue = require('../../../lib/protobuf/riakprotobuf').getProtoFor('DtValue');
+var RpbErrorResp = require('../../../lib/protobuf/riakprotobuf').getProtoFor('RpbErrorResp');
+
+var assert = require('assert');
+
+describe('FetchHll', function() {
+    describe('Build', function() {
+        it('should build a DtFetchReq correctly', function(done) {
+            var fetch = new FetchHll.Builder()
+                .withBucketType('hlls')
+                .withBucket('myBucket')
+                .withKey('hll_1')
+                .withCallback(function(){})
+                .withR(1)
+                .withPr(2)
+                .withNotFoundOk(true)
+                .withBasicQuorum(true)
+                .withTimeout(20000)
+                .build();
+
+            var protobuf = fetch.constructPbRequest();
+
+            assert.equal(protobuf.getType().toString('utf8'), 'hlls');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'myBucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'hll_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();
+            var dtValue = new DtValue();
+            dtValue.setHllValue(42);
+            dtFetchResp.value = dtValue;
+
+            var callback = function(err, response) {
+                if (response) {
+                    assert.equal(response.cardinality, 42);
+                    done();
+                }
+            };
+
+            var fetch = new FetchHll.Builder()
+                .withBucketType('hlls')
+                .withBucket('myBucket')
+                .withKey('hll_1')
+                .withCallback(callback)
+                .build();
+
+            fetch.onSuccess(dtFetchResp);
+        });
+
+        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 fetch = new FetchHll.Builder()
+                .withBucketType('hlls')
+                .withBucket('myBucket')
+                .withKey('hll_1')
+                .withCallback(callback)
+                .build();
+
+            fetch.onRiakError(rpbErrorResp);
+        });
+    });
+});
diff --git a/test/unit/crdt/updatehll.js b/test/unit/crdt/updatehll.js
new file mode 100644
index 0000000..7c9d296
--- /dev/null
+++ b/test/unit/crdt/updatehll.js
@@ -0,0 +1,201 @@
+'use strict';
+
+var UpdateHll = require('../../../lib/commands/crdt/updatehll');
+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');
+
+describe('UpdateHll', function() {
+
+  var someBuffer = ByteBuffer.fromUTF8("Some");
+  var jokesBuffer = ByteBuffer.fromUTF8("Jokes");
+
+    var builder = new UpdateHll.Builder().
+            withBucketType('hlls').
+            withBucket('myBucket').
+            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']).
+                    withCallback(function(){}).
+                    withW(1).
+                    withDw(2).
+                    withPw(3).
+                    withReturnBody(false).
+                    withTimeout(12345).
+                    build();
+
+            var protobuf = update.constructPbRequest();
+
+            assert.equal(protobuf.getType().toString('utf8'), 'hlls');
+            assert.equal(protobuf.getBucket().toString('utf8'), 'myBucket');
+            assert.equal(protobuf.getKey().toString('utf8'), 'hll_1');
+            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(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"));
+
+            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();
+            resp.setHllValue(42);
+
+            var callback = function(err, response) {
+                assert(!err);
+                assert(response);
+
+                assert.equal(response.generatedKey, null);
+                assert.equal(response.cardinality, 42);
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([jokesBuffer]).
+                    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.setHllValue(42);
+
+            var callback = function(err, response) {
+                assert(!err);
+                assert(response);
+
+                assert.equal(response.generatedKey, null);
+                assert.equal(response.cardinality, 42);
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([someBuffer]).
+                    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("hll_2");
+
+            var callback = function(err, response) {
+                assert.equal(response.generatedKey, "hll_2");
+                done();
+            };
+
+            var update = builder.
+                    withCallback(callback).
+                    withAdditions([jokesBuffer]).
+                    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);
+        });
+    });
+
+    describe('additions are required', function() {
+        it('throws an error if an empty array of additions are made', function(done) {
+            var validationError = null;
+            try {
+              builder.withAdditions([]).build();
+            } catch(err) {
+                validationError = err;
+            }
+            assert.notEqual(validationError, null);
+            done();
+        });
+
+        it('throws an error if no additions are made', function(done) {
+            var validationError = null;
+            try {
+              builder.build();
+            } catch(err) {
+                validationError = err;
+            }
+            assert.notEqual(validationError, null);
+            done();
+        });
+    });
+});
diff --git a/test/unit/kv/fetchbucketprops.js b/test/unit/kv/fetchbucketprops.js
index 9c8beaf..a0a7613 100644
--- a/test/unit/kv/fetchbucketprops.js
+++ b/test/unit/kv/fetchbucketprops.js
@@ -39,10 +39,10 @@ describe('FetchBucketProps', function() {
             assert.equal(protobuf.getBucket().toString('utf8'), bucketName);
             done();
         });
-        
+
         it('should take a RpbGetBucketResp and call the users callback with the response', function(done) {
             var props = new RpbBucketProps();
-            
+
             props.setNVal(3);
             props.setAllowMult(true);
             props.setLastWriteWins(true);
@@ -66,21 +66,22 @@ describe('FetchBucketProps', function() {
             props.setBackend(new Buffer('backend'));
             props.setSearchIndex(new Buffer('index'));
             props.setDatatype(new Buffer('dataType'));
-            
+            props.setHllPrecision(14);
+
             var hook = new RpbCommitHook();
             var modfun = new RpbModFun();
             modfun.module = new Buffer('module_name');
             modfun.function = new Buffer('function_name');
             hook.modfun = modfun;
-            
+
             props.precommit.push(hook);
             props.postcommit.push(hook);
             props.setChashKeyfun(modfun);
             props.setLinkfun(modfun);
-            
+
             var protobuf = new RpbGetBucketResp();
             protobuf.setProps(props);
-            
+
             var callback = function(err, response) {
                 assert.equal(response.nVal, 3);
                 assert.equal(response.allowMult, true);
@@ -113,9 +114,10 @@ describe('FetchBucketProps', function() {
                 assert.equal(response.chashKeyfun.fun, 'function_name');
                 assert.equal(response.linkFun.mod, 'module_name');
                 assert.equal(response.linkFun.fun, 'function_name');
+                assert.equal(response.hllPrecision, 14);
                 done();
             };
-            
+
             var fetchProps = new FetchBucketProps.Builder()
                     .withBucketType('bucket_type')
                     .withBucket('bucket_name')
@@ -123,18 +125,18 @@ describe('FetchBucketProps', function() {
                     .build();
             fetchProps.onSuccess(protobuf);
         });
-        
+
         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 fetchCommand = new FetchBucketProps.Builder()
                .withBucketType('bucket_type')
                .withBucket('bucket_name')
diff --git a/test/unit/kv/fetchbuckettypeprops.js b/test/unit/kv/fetchbuckettypeprops.js
index d50c2d1..4ccb0b4 100644
--- a/test/unit/kv/fetchbuckettypeprops.js
+++ b/test/unit/kv/fetchbuckettypeprops.js
@@ -62,21 +62,22 @@ describe('FetchBucketTypeProps', function() {
             props.setBackend(new Buffer('backend'));
             props.setSearchIndex(new Buffer('index'));
             props.setDatatype(new Buffer('dataType'));
-            
+            props.setHllPrecision(14);
+
             var hook = new RpbCommitHook();
             var modfun = new RpbModFun();
             modfun.module = new Buffer('module_name');
             modfun.function = new Buffer('function_name');
             hook.modfun = modfun;
-            
+
             props.precommit.push(hook);
             props.postcommit.push(hook);
             props.setChashKeyfun(modfun);
             props.setLinkfun(modfun);
-            
+
             var protobuf = new RpbGetBucketResp();
             protobuf.setProps(props);
-            
+
             var callback = function(err, response) {
                 assert.equal(response.nVal, 3);
                 assert.equal(response.allowMult, true);
@@ -109,27 +110,28 @@ describe('FetchBucketTypeProps', function() {
                 assert.equal(response.chashKeyfun.fun, 'function_name');
                 assert.equal(response.linkFun.mod, 'module_name');
                 assert.equal(response.linkFun.fun, 'function_name');
+                assert.equal(response.hllPrecision, 14);
                 done();
             };
-            
+
             var fetchProps = new FetchBucketTypeProps.Builder()
                     .withBucketType('bucket_type')
                     .withCallback(callback)
                     .build();
             fetchProps.onSuccess(protobuf);
         });
-        
+
         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 fetchCommand = new FetchBucketTypeProps.Builder()
                .withBucketType('bucket_type')
                .withCallback(callback)
diff --git a/test/unit/kv/storebucketprops.js b/test/unit/kv/storebucketprops.js
index 4c58dc2..1c2bbdf 100644
--- a/test/unit/kv/storebucketprops.js
+++ b/test/unit/kv/storebucketprops.js
@@ -10,9 +10,9 @@ var assert = require('assert');
 describe('StoreBucketProps', function() {
     describe('Build', function() {
         it('should build a RpbSetBucketProps correctly', function(done) {
-            
+
             var hook = { mod: 'module_name', fun: 'function_name' };
-            
+
             var storeProps = new StoreBucketProps.Builder()
                 .withBucketType('bucket_type')
                 .withBucket('bucket_name')
@@ -38,11 +38,12 @@ describe('StoreBucketProps', function() {
                 .addPostcommitHook(hook)
                 .withChashkeyFunction(hook)
                 .withCallback(function(){})
+                .withHllPrecision(14)
                 .build();
-        
+
             var protobuf = storeProps.constructPbRequest();
             var props = protobuf.getProps();
-            
+
             assert.equal(protobuf.bucket.toString('utf8'), 'bucket_name');
             assert.equal(protobuf.type.toString('utf8'), 'bucket_type');
             assert.equal(props.getNVal(), 3);
@@ -63,62 +64,62 @@ describe('StoreBucketProps', function() {
             assert.equal(props.getSearch(), true);
             assert.equal(props.getBackend().toString('utf8'), 'backend');
             assert.equal(props.getSearchIndex().toString('utf8'), 'indexName');
-            
+
             assert.equal(props.precommit.length, 1);
             assert.equal(props.precommit[0].modfun.module.toString('utf8'), 'module_name');
             assert.equal(props.precommit[0].modfun.function.toString('utf8'), 'function_name');
-            
+
             assert.equal(props.postcommit.length, 1);
             assert.equal(props.postcommit[0].modfun.module.toString('utf8'), 'module_name');
             assert.equal(props.postcommit[0].modfun.function.toString('utf8'), 'function_name');
-            
+
             assert.equal(props.getChashKeyfun().module.toString('utf8'), 'module_name');
             assert.equal(props.getChashKeyfun().function.toString('utf8'), 'function_name');
+            assert.equal(props.getHllPrecision(), 14);
             done();
-            
-                
+
+
         });
-        
+
         it('should take a RpbSetBucketResp and call the users callback with the response', function(done) {
-           
+
             // RpbSetBucketResp has no body. Riak just sends back the code so we supply null
             // to the command on success and a simple boolean true is sent to the user callback
-            
+
             var callback = function(err, response) {
                 assert.equal(response, true);
                 done();
             };
-            
+
             var storeProps = new StoreBucketProps.Builder()
                 .withBucketType('bucket_type')
                 .withBucket('bucket_name')
                 .withCallback(callback)
                 .build();
-        
+
             storeProps.onSuccess(null);
-            
+
         });
-        
+
         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 storeProps = new StoreBucketProps.Builder()
                 .withBucketType('bucket_type')
                 .withBucket('bucket_name')
                 .withCallback(callback)
                 .build();
-        
+
             storeProps.onRiakError(rpbErrorResp);
-           
+
         });
     });
 });
-        
diff --git a/test/unit/kv/storebuckettypeprops.js b/test/unit/kv/storebuckettypeprops.js
index f285d25..8cd3dbd 100644
--- a/test/unit/kv/storebuckettypeprops.js
+++ b/test/unit/kv/storebuckettypeprops.js
@@ -34,12 +34,13 @@ describe('StoreBucketTypeProps', function() {
                 .addPrecommitHook(hook)
                 .addPostcommitHook(hook)
                 .withChashkeyFunction(hook)
+                .withHllPrecision(14)
                 .withCallback(function(){})
                 .build();
-        
+
             var protobuf = storeProps.constructPbRequest();
             var props = protobuf.getProps();
-            
+
             assert.equal(protobuf.type.toString('utf8'), 'bucket_type');
             assert.equal(props.getNVal(), 3);
             assert.equal(props.getAllowMult(), true);
@@ -59,20 +60,21 @@ describe('StoreBucketTypeProps', function() {
             assert.equal(props.getSearch(), true);
             assert.equal(props.getBackend().toString('utf8'), 'backend');
             assert.equal(props.getSearchIndex().toString('utf8'), 'indexName');
-            
+
             assert.equal(props.precommit.length, 1);
             assert.equal(props.precommit[0].modfun.module.toString('utf8'), 'module_name');
             assert.equal(props.precommit[0].modfun.function.toString('utf8'), 'function_name');
-            
+
             assert.equal(props.postcommit.length, 1);
             assert.equal(props.postcommit[0].modfun.module.toString('utf8'), 'module_name');
             assert.equal(props.postcommit[0].modfun.function.toString('utf8'), 'function_name');
-            
+
             assert.equal(props.getChashKeyfun().module.toString('utf8'), 'module_name');
             assert.equal(props.getChashKeyfun().function.toString('utf8'), 'function_name');
+            assert.equal(props.getHllPrecision(), 14);
             done();
         });
-        
+
         it('should take a RpbSetBucketResp and call the users callback with the response', function(done) {
             // RpbSetBucketResp has no body. Riak just sends back the code so we supply null
             // to the command on success and a simple boolean true is sent to the user callback
@@ -86,7 +88,7 @@ describe('StoreBucketTypeProps', function() {
                 .build();
             storeProps.onSuccess(null);
         });
-        
+
         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'));