diff --git a/packages/bigtable/src/table.js b/packages/bigtable/src/table.js index 7cc541e5435..b9988d808e9 100644 --- a/packages/bigtable/src/table.js +++ b/packages/bigtable/src/table.js @@ -222,6 +222,45 @@ Table.formatName_ = function(instanceName, name) { return instanceName + '/tables/' + name; }; +/** + * Creates a range based off of a key prefix. + * + * @private + * + * @param {string} start - The key prefix/starting bound. + * @return {object} range + * + * @example + * Table.createPrefixRange_('start'); + * // => { + * // start: 'start', + * // end: { + * // value: 'staru', + * // inclusive: false + * // } + * // } + */ +Table.createPrefixRange_ = function(start) { + var prefix = start.replace(new RegExp('[\xff]+$'), ''); + var endKey = ''; + + if (prefix) { + var position = prefix.length - 1; + var charCode = prefix.charCodeAt(position); + var nextChar = String.fromCharCode(charCode + 1); + + endKey = prefix.substring(0, position) + nextChar; + } + + return { + start: start, + end: { + value: endKey, + inclusive: !endKey + } + }; +}; + /** * Create a column family. * @@ -321,13 +360,14 @@ Table.prototype.createFamily = function(name, rule, callback) { * @param {options=} options - Configuration object. * @param {boolean} options.decode - If set to `false` it will not decode Buffer * values returned from Bigtable. Default: true. - * @param {string[]} options.keys - A list of row keys. - * @param {string} options.start - Start value for key range. * @param {string} options.end - End value for key range. - * @param {object[]} options.ranges - A list of key ranges. - * @param {module:bigtable/filter} options.filter - Row filters allow you to +* @param {module:bigtable/filter} options.filter - Row filters allow you to * both make advanced queries and format how the data is returned. + * @param {string[]} options.keys - A list of row keys. * @param {number} options.limit - Maximum number of rows to be returned. + * @param {string} options.prefix - Prefix that the row key must match. + * @param {object[]} options.ranges - A list of key ranges. + * @param {string} options.start - Start value for key range. * @return {stream} * * @example @@ -361,6 +401,13 @@ Table.prototype.createFamily = function(name, rule, callback) { * }); * * //- + * // Scan for row keys that contain a specific prefix. + * //- + * table.createReadStream({ + * prefix: 'gwash' + * }); + * + * //- * // Specify a contiguous range of rows to read by supplying `start` and `end` * // keys. * // @@ -421,6 +468,10 @@ Table.prototype.createReadStream = function(options) { }); } + if (options.prefix) { + options.ranges.push(Table.createPrefixRange_(options.prefix)); + } + if (options.keys || options.ranges.length) { reqOpts.rows = {}; diff --git a/packages/bigtable/system-test/bigtable.js b/packages/bigtable/system-test/bigtable.js index 5c6308dd7a5..832f87da83e 100644 --- a/packages/bigtable/system-test/bigtable.js +++ b/packages/bigtable/system-test/bigtable.js @@ -563,6 +563,19 @@ describe('Bigtable', function() { }); }); + it('should fetch a range of rows via prefix', function(done) { + var options = { + prefix: 'g' + }; + + TABLE.getRows(options, function(err, rows) { + assert.ifError(err); + assert.strictEqual(rows.length, 1); + assert.strictEqual(rows[0].id, 'gwashington'); + done(); + }); + }); + it('should fetch individual cells of a row', function(done) { var row = TABLE.row('alincoln'); diff --git a/packages/bigtable/test/table.js b/packages/bigtable/test/table.js index b00e4b1a14c..105f66f54d1 100644 --- a/packages/bigtable/test/table.js +++ b/packages/bigtable/test/table.js @@ -202,6 +202,68 @@ describe('Bigtable/Table', function() { }); }); + describe('createPrefixRange_', function() { + it('should create a range from the prefix', function() { + assert.deepEqual(Table.createPrefixRange_('start'), { + start: 'start', + end: { + value: 'staru', + inclusive: false + } + }); + + assert.deepEqual(Table.createPrefixRange_('X\xff'), { + start: 'X\xff', + end: { + value: 'Y', + inclusive: false + } + }); + + assert.deepEqual(Table.createPrefixRange_('xoo\xff'), { + start: 'xoo\xff', + end: { + value: 'xop', + inclusive: false + } + }); + + assert.deepEqual(Table.createPrefixRange_('a\xffb'), { + start: 'a\xffb', + end: { + value: 'a\xffc', + inclusive: false + } + }); + + assert.deepEqual(Table.createPrefixRange_('com.google.'), { + start: 'com.google.', + end: { + value: 'com.google/', + inclusive: false + } + }); + }); + + it('should create an inclusive bound when the prefix is empty', function() { + assert.deepEqual(Table.createPrefixRange_('\xff'), { + start: '\xff', + end: { + value: '', + inclusive: true + } + }); + + assert.deepEqual(Table.createPrefixRange_(''), { + start: '', + end: { + value: '', + inclusive: true + } + }); + }); + }); + describe('createFamily', function() { var COLUMN_ID = 'my-column'; @@ -454,6 +516,39 @@ describe('Bigtable/Table', function() { table.createReadStream(options); }); + + it('should transform the prefix into a range', function(done) { + var fakeRange = {}; + var fakePrefixRange = { + start: 'a', + end: 'b' + }; + + var fakePrefix = 'abc'; + + var prefixSpy = Table.createPrefixRange_ = sinon.spy(function() { + return fakePrefixRange; + }); + + var rangeSpy = FakeFilter.createRange = sinon.spy(function() { + return fakeRange; + }); + + table.requestStream = function(g, reqOpts) { + assert.strictEqual(prefixSpy.getCall(0).args[0], fakePrefix); + assert.deepEqual(reqOpts.rows.rowRanges, [fakeRange]); + + assert.deepEqual(rangeSpy.getCall(0).args, [ + fakePrefixRange.start, + fakePrefixRange.end, + 'Key' + ]); + + done(); + }; + + table.createReadStream({ prefix: fakePrefix }); + }); }); describe('success', function() {