diff --git a/NEWS.rst b/NEWS.rst index 2c86998..383b01f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -12,6 +12,7 @@ Release date: *not yet released* * Add :py:meth:`ConnectionPool.close` and support using the pool as a context manager (`issue 229 `_) +* Add :py:meth:`Table.append` HappyBase 1.3.0 diff --git a/happybase/table.py b/happybase/table.py index f1ea350..cfb6fd5 100644 --- a/happybase/table.py +++ b/happybase/table.py @@ -8,7 +8,7 @@ from six import iteritems -from Hbase_thrift import TScan +from Hbase_thrift import TAppend, TScan from .util import thrift_type_to_dict, bytes_increment, OrderedDict from .batch import Batch @@ -494,6 +494,36 @@ def delete(self, row, columns=None, timestamp=None, wal=True): with self.batch(timestamp=timestamp, wal=wal) as batch: batch.delete(row, columns) + def append(self, row, data, include_timestamp=False): + """Atomically append values to one or more columns in a row. + + The `data` argument is a dictionary that maps columns to the values + to append. If a column did not exist, it is created with the given + value. + + :param str row: the row key + :param dict data: the values to append + :param bool include_timestamp: whether timestamps are returned + + :return: mapping of columns to their values after appending + :rtype: dict + """ + # The server returns the cells without their names, sorted by + # family and qualifier, so send the columns in that same order. + columns = sorted( + data, key=lambda c: c.split(b':' if isinstance(c, bytes) else ':')) + cells = self.connection.client.append(TAppend( + table=self.name, + row=row, + columns=columns, + values=[data[column] for column in columns], + )) + return { + column: (cell.value, cell.timestamp) if include_timestamp + else cell.value + for column, cell in zip(columns, cells) + } + def batch(self, timestamp=None, batch_size=None, transaction=False, wal=True): """Create a new batch operation for this table. diff --git a/tests/test_api.py b/tests/test_api.py index 9e1f797..484aab5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -175,6 +175,20 @@ def test_atomic_counters(): assert 10 == table.counter_dec(row, column, -7) +def test_append(): + row = b'row-with-append' + + assert {b'cf1:c1': b'a'} == table.append(row, {b'cf1:c1': b'a'}) + assert {b'cf1:c1': b'ab', b'cf2:c2': b'x'} == table.append( + row, {b'cf2:c2': b'x', b'cf1:c1': b'b'}) + assert {b'cf1:c1': b'ab', b'cf2:c2': b'x'} == table.row(row) + + value, timestamp = table.append( + row, {b'cf1:c1': b'c'}, include_timestamp=True)[b'cf1:c1'] + assert b'abc' == value + assert isinstance(timestamp, int) + + def test_batch(): with pytest.raises(TypeError): table.batch(timestamp='invalid')