Skip to content

Commit

Permalink
Merge pull request #1537 from dhermes/happybase-finish-batch
Browse files Browse the repository at this point in the history
Adding HappyBase batch delete().
  • Loading branch information
dhermes committed Feb 25, 2016
2 parents e225636 + 00bfeda commit c05e7bd
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 1 deletion.
74 changes: 74 additions & 0 deletions gcloud/bigtable/happybase/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,80 @@ def put(self, row, data, wal=_WAL_SENTINEL):
self._mutation_count += len(data)
self._try_send()

def _delete_columns(self, columns, row_object):
"""Adds delete mutations for a list of columns and column families.
:type columns: list
:param columns: Iterable containing column names (as
strings). Each column name can be either
* an entire column family: ``fam`` or ``fam:``
* an single column: ``fam:col``
:type row_object: :class:`Row <gcloud_bigtable.row.Row>`
:param row_object: The row which will hold the delete mutations.
:raises: :class:`ValueError <exceptions.ValueError>` if the delete
timestamp range is set on the current batch, but a
column family delete is attempted.
"""
column_pairs = _get_column_pairs(columns)
for column_family_id, column_qualifier in column_pairs:
if column_qualifier is None:
if self._delete_range is not None:
raise ValueError('The Cloud Bigtable API does not support '
'adding a timestamp to '
'"DeleteFromFamily" ')
row_object.delete_cells(column_family_id,
columns=row_object.ALL_COLUMNS)
else:
row_object.delete_cell(column_family_id,
column_qualifier,
time_range=self._delete_range)

def delete(self, row, columns=None, wal=_WAL_SENTINEL):
"""Delete data from a row in the table owned by this batch.
:type row: str
:param row: The row key where the delete will occur.
:type columns: list
:param columns: (Optional) Iterable containing column names (as
strings). Each column name can be either
* an entire column family: ``fam`` or ``fam:``
* an single column: ``fam:col``
If not used, will delete the entire row.
:type wal: object
:param wal: Unused parameter (to over-ride the default on the
instance). Provided for compatibility with HappyBase, but
irrelevant for Cloud Bigtable since it does not have a
Write Ahead Log.
:raises: If if the delete timestamp range is set on the
current batch, but a full row delete is attempted.
"""
if wal is not _WAL_SENTINEL:
_WARN(_WAL_WARNING)

row_object = self._get_row(row)

if columns is None:
# Delete entire row.
if self._delete_range is not None:
raise ValueError('The Cloud Bigtable API does not support '
'adding a timestamp to "DeleteFromRow" '
'mutations')
row_object.delete()
self._mutation_count += 1
else:
self._delete_columns(columns, row_object)
self._mutation_count += len(columns)

self._try_send()

def __enter__(self):
"""Enter context manager, no set-up required."""
return self
Expand Down
150 changes: 149 additions & 1 deletion gcloud/bigtable/happybase/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,8 @@ def test_put_bad_wal(self):

def mock_warn(message):
warned.append(message)
# Raise an exception so we don't
# Raise an exception so we don't have to mock the entire
# environment needed for put().
raise RuntimeError('No need to execute the rest.')

table = object()
Expand Down Expand Up @@ -288,6 +289,139 @@ def _try_send(self):
self.assertEqual(batch._mutation_count, 0)
self.assertEqual(batch.try_send_calls, 1)

def _delete_columns_test_helper(self, time_range=None):
table = object()
batch = self._makeOne(table)
batch._delete_range = time_range

col1_fam = 'cf1'
col2_fam = 'cf2'
col2_qual = 'col-name'
columns = [col1_fam + ':', col2_fam + ':' + col2_qual]
row_object = _MockRow()

batch._delete_columns(columns, row_object)
self.assertEqual(row_object.commits, 0)

cell_deleted_args = (col2_fam, col2_qual)
cell_deleted_kwargs = {'time_range': time_range}
self.assertEqual(row_object.delete_cell_calls,
[(cell_deleted_args, cell_deleted_kwargs)])
fam_deleted_args = (col1_fam,)
fam_deleted_kwargs = {'columns': row_object.ALL_COLUMNS}
self.assertEqual(row_object.delete_cells_calls,
[(fam_deleted_args, fam_deleted_kwargs)])

def test__delete_columns(self):
self._delete_columns_test_helper()

def test__delete_columns_w_time_and_col_fam(self):
time_range = object()
with self.assertRaises(ValueError):
self._delete_columns_test_helper(time_range=time_range)

def test_delete_bad_wal(self):
from gcloud._testing import _Monkey
from gcloud.bigtable.happybase import batch as MUT

warned = []

def mock_warn(message):
warned.append(message)
# Raise an exception so we don't have to mock the entire
# environment needed for delete().
raise RuntimeError('No need to execute the rest.')

table = object()
batch = self._makeOne(table)

row = 'row-key'
columns = []
wal = None

self.assertNotEqual(wal, MUT._WAL_SENTINEL)
with _Monkey(MUT, _WARN=mock_warn):
with self.assertRaises(RuntimeError):
batch.delete(row, columns=columns, wal=wal)

self.assertEqual(warned, [MUT._WAL_WARNING])

def test_delete_entire_row(self):
table = object()
batch = self._makeOne(table)

row_key = 'row-key'
batch._row_map[row_key] = row = _MockRow()

self.assertEqual(row.deletes, 0)
self.assertEqual(batch._mutation_count, 0)
batch.delete(row_key, columns=None)
self.assertEqual(row.deletes, 1)
self.assertEqual(batch._mutation_count, 1)

def test_delete_entire_row_with_ts(self):
table = object()
batch = self._makeOne(table)
batch._delete_range = object()

row_key = 'row-key'
batch._row_map[row_key] = row = _MockRow()

self.assertEqual(row.deletes, 0)
self.assertEqual(batch._mutation_count, 0)
with self.assertRaises(ValueError):
batch.delete(row_key, columns=None)
self.assertEqual(row.deletes, 0)
self.assertEqual(batch._mutation_count, 0)

def test_delete_call_try_send(self):
klass = self._getTargetClass()

class CallTrySend(klass):

try_send_calls = 0

def _try_send(self):
self.try_send_calls += 1

table = object()
batch = CallTrySend(table)

row_key = 'row-key'
batch._row_map[row_key] = _MockRow()

self.assertEqual(batch._mutation_count, 0)
self.assertEqual(batch.try_send_calls, 0)
# No columns so that nothing happens
batch.delete(row_key, columns=[])
self.assertEqual(batch._mutation_count, 0)
self.assertEqual(batch.try_send_calls, 1)

def test_delete_some_columns(self):
table = object()
batch = self._makeOne(table)

row_key = 'row-key'
batch._row_map[row_key] = row = _MockRow()

self.assertEqual(batch._mutation_count, 0)

col1_fam = 'cf1'
col2_fam = 'cf2'
col2_qual = 'col-name'
columns = [col1_fam + ':', col2_fam + ':' + col2_qual]
batch.delete(row_key, columns=columns)

self.assertEqual(batch._mutation_count, 2)
cell_deleted_args = (col2_fam, col2_qual)
cell_deleted_kwargs = {'time_range': None}
self.assertEqual(row.delete_cell_calls,
[(cell_deleted_args, cell_deleted_kwargs)])
fam_deleted_args = (col1_fam,)
fam_deleted_kwargs = {'columns': row.ALL_COLUMNS}
self.assertEqual(row.delete_cells_calls,
[(fam_deleted_args, fam_deleted_kwargs)])

def test_context_manager(self):
klass = self._getTargetClass()

Expand Down Expand Up @@ -390,16 +524,30 @@ def clear(self):

class _MockRow(object):

ALL_COLUMNS = object()

def __init__(self):
self.commits = 0
self.deletes = 0
self.set_cell_calls = []
self.delete_cell_calls = []
self.delete_cells_calls = []

def commit(self):
self.commits += 1

def delete(self):
self.deletes += 1

def set_cell(self, *args, **kwargs):
self.set_cell_calls.append((args, kwargs))

def delete_cell(self, *args, **kwargs):
self.delete_cell_calls.append((args, kwargs))

def delete_cells(self, *args, **kwargs):
self.delete_cells_calls.append((args, kwargs))


class _MockTable(object):

Expand Down

0 comments on commit c05e7bd

Please sign in to comment.