Skip to content

Commit 00bfeda

Browse files
committed
Adding HappyBase batch delete().
This completes the Batch() implementation.
1 parent e225636 commit 00bfeda

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

gcloud/bigtable/happybase/batch.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,80 @@ def put(self, row, data, wal=_WAL_SENTINEL):
171171
self._mutation_count += len(data)
172172
self._try_send()
173173

174+
def _delete_columns(self, columns, row_object):
175+
"""Adds delete mutations for a list of columns and column families.
176+
177+
:type columns: list
178+
:param columns: Iterable containing column names (as
179+
strings). Each column name can be either
180+
181+
* an entire column family: ``fam`` or ``fam:``
182+
* an single column: ``fam:col``
183+
184+
:type row_object: :class:`Row <gcloud_bigtable.row.Row>`
185+
:param row_object: The row which will hold the delete mutations.
186+
187+
:raises: :class:`ValueError <exceptions.ValueError>` if the delete
188+
timestamp range is set on the current batch, but a
189+
column family delete is attempted.
190+
"""
191+
column_pairs = _get_column_pairs(columns)
192+
for column_family_id, column_qualifier in column_pairs:
193+
if column_qualifier is None:
194+
if self._delete_range is not None:
195+
raise ValueError('The Cloud Bigtable API does not support '
196+
'adding a timestamp to '
197+
'"DeleteFromFamily" ')
198+
row_object.delete_cells(column_family_id,
199+
columns=row_object.ALL_COLUMNS)
200+
else:
201+
row_object.delete_cell(column_family_id,
202+
column_qualifier,
203+
time_range=self._delete_range)
204+
205+
def delete(self, row, columns=None, wal=_WAL_SENTINEL):
206+
"""Delete data from a row in the table owned by this batch.
207+
208+
:type row: str
209+
:param row: The row key where the delete will occur.
210+
211+
:type columns: list
212+
:param columns: (Optional) Iterable containing column names (as
213+
strings). Each column name can be either
214+
215+
* an entire column family: ``fam`` or ``fam:``
216+
* an single column: ``fam:col``
217+
218+
If not used, will delete the entire row.
219+
220+
:type wal: object
221+
:param wal: Unused parameter (to over-ride the default on the
222+
instance). Provided for compatibility with HappyBase, but
223+
irrelevant for Cloud Bigtable since it does not have a
224+
Write Ahead Log.
225+
226+
:raises: If if the delete timestamp range is set on the
227+
current batch, but a full row delete is attempted.
228+
"""
229+
if wal is not _WAL_SENTINEL:
230+
_WARN(_WAL_WARNING)
231+
232+
row_object = self._get_row(row)
233+
234+
if columns is None:
235+
# Delete entire row.
236+
if self._delete_range is not None:
237+
raise ValueError('The Cloud Bigtable API does not support '
238+
'adding a timestamp to "DeleteFromRow" '
239+
'mutations')
240+
row_object.delete()
241+
self._mutation_count += 1
242+
else:
243+
self._delete_columns(columns, row_object)
244+
self._mutation_count += len(columns)
245+
246+
self._try_send()
247+
174248
def __enter__(self):
175249
"""Enter context manager, no set-up required."""
176250
return self

gcloud/bigtable/happybase/test_batch.py

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ def test_put_bad_wal(self):
212212

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

218219
table = object()
@@ -288,6 +289,139 @@ def _try_send(self):
288289
self.assertEqual(batch._mutation_count, 0)
289290
self.assertEqual(batch.try_send_calls, 1)
290291

292+
def _delete_columns_test_helper(self, time_range=None):
293+
table = object()
294+
batch = self._makeOne(table)
295+
batch._delete_range = time_range
296+
297+
col1_fam = 'cf1'
298+
col2_fam = 'cf2'
299+
col2_qual = 'col-name'
300+
columns = [col1_fam + ':', col2_fam + ':' + col2_qual]
301+
row_object = _MockRow()
302+
303+
batch._delete_columns(columns, row_object)
304+
self.assertEqual(row_object.commits, 0)
305+
306+
cell_deleted_args = (col2_fam, col2_qual)
307+
cell_deleted_kwargs = {'time_range': time_range}
308+
self.assertEqual(row_object.delete_cell_calls,
309+
[(cell_deleted_args, cell_deleted_kwargs)])
310+
fam_deleted_args = (col1_fam,)
311+
fam_deleted_kwargs = {'columns': row_object.ALL_COLUMNS}
312+
self.assertEqual(row_object.delete_cells_calls,
313+
[(fam_deleted_args, fam_deleted_kwargs)])
314+
315+
def test__delete_columns(self):
316+
self._delete_columns_test_helper()
317+
318+
def test__delete_columns_w_time_and_col_fam(self):
319+
time_range = object()
320+
with self.assertRaises(ValueError):
321+
self._delete_columns_test_helper(time_range=time_range)
322+
323+
def test_delete_bad_wal(self):
324+
from gcloud._testing import _Monkey
325+
from gcloud.bigtable.happybase import batch as MUT
326+
327+
warned = []
328+
329+
def mock_warn(message):
330+
warned.append(message)
331+
# Raise an exception so we don't have to mock the entire
332+
# environment needed for delete().
333+
raise RuntimeError('No need to execute the rest.')
334+
335+
table = object()
336+
batch = self._makeOne(table)
337+
338+
row = 'row-key'
339+
columns = []
340+
wal = None
341+
342+
self.assertNotEqual(wal, MUT._WAL_SENTINEL)
343+
with _Monkey(MUT, _WARN=mock_warn):
344+
with self.assertRaises(RuntimeError):
345+
batch.delete(row, columns=columns, wal=wal)
346+
347+
self.assertEqual(warned, [MUT._WAL_WARNING])
348+
349+
def test_delete_entire_row(self):
350+
table = object()
351+
batch = self._makeOne(table)
352+
353+
row_key = 'row-key'
354+
batch._row_map[row_key] = row = _MockRow()
355+
356+
self.assertEqual(row.deletes, 0)
357+
self.assertEqual(batch._mutation_count, 0)
358+
batch.delete(row_key, columns=None)
359+
self.assertEqual(row.deletes, 1)
360+
self.assertEqual(batch._mutation_count, 1)
361+
362+
def test_delete_entire_row_with_ts(self):
363+
table = object()
364+
batch = self._makeOne(table)
365+
batch._delete_range = object()
366+
367+
row_key = 'row-key'
368+
batch._row_map[row_key] = row = _MockRow()
369+
370+
self.assertEqual(row.deletes, 0)
371+
self.assertEqual(batch._mutation_count, 0)
372+
with self.assertRaises(ValueError):
373+
batch.delete(row_key, columns=None)
374+
self.assertEqual(row.deletes, 0)
375+
self.assertEqual(batch._mutation_count, 0)
376+
377+
def test_delete_call_try_send(self):
378+
klass = self._getTargetClass()
379+
380+
class CallTrySend(klass):
381+
382+
try_send_calls = 0
383+
384+
def _try_send(self):
385+
self.try_send_calls += 1
386+
387+
table = object()
388+
batch = CallTrySend(table)
389+
390+
row_key = 'row-key'
391+
batch._row_map[row_key] = _MockRow()
392+
393+
self.assertEqual(batch._mutation_count, 0)
394+
self.assertEqual(batch.try_send_calls, 0)
395+
# No columns so that nothing happens
396+
batch.delete(row_key, columns=[])
397+
self.assertEqual(batch._mutation_count, 0)
398+
self.assertEqual(batch.try_send_calls, 1)
399+
400+
def test_delete_some_columns(self):
401+
table = object()
402+
batch = self._makeOne(table)
403+
404+
row_key = 'row-key'
405+
batch._row_map[row_key] = row = _MockRow()
406+
407+
self.assertEqual(batch._mutation_count, 0)
408+
409+
col1_fam = 'cf1'
410+
col2_fam = 'cf2'
411+
col2_qual = 'col-name'
412+
columns = [col1_fam + ':', col2_fam + ':' + col2_qual]
413+
batch.delete(row_key, columns=columns)
414+
415+
self.assertEqual(batch._mutation_count, 2)
416+
cell_deleted_args = (col2_fam, col2_qual)
417+
cell_deleted_kwargs = {'time_range': None}
418+
self.assertEqual(row.delete_cell_calls,
419+
[(cell_deleted_args, cell_deleted_kwargs)])
420+
fam_deleted_args = (col1_fam,)
421+
fam_deleted_kwargs = {'columns': row.ALL_COLUMNS}
422+
self.assertEqual(row.delete_cells_calls,
423+
[(fam_deleted_args, fam_deleted_kwargs)])
424+
291425
def test_context_manager(self):
292426
klass = self._getTargetClass()
293427

@@ -390,16 +524,30 @@ def clear(self):
390524

391525
class _MockRow(object):
392526

527+
ALL_COLUMNS = object()
528+
393529
def __init__(self):
394530
self.commits = 0
531+
self.deletes = 0
395532
self.set_cell_calls = []
533+
self.delete_cell_calls = []
534+
self.delete_cells_calls = []
396535

397536
def commit(self):
398537
self.commits += 1
399538

539+
def delete(self):
540+
self.deletes += 1
541+
400542
def set_cell(self, *args, **kwargs):
401543
self.set_cell_calls.append((args, kwargs))
402544

545+
def delete_cell(self, *args, **kwargs):
546+
self.delete_cell_calls.append((args, kwargs))
547+
548+
def delete_cells(self, *args, **kwargs):
549+
self.delete_cells_calls.append((args, kwargs))
550+
403551

404552
class _MockTable(object):
405553

0 commit comments

Comments
 (0)