Skip to content

Commit 7126a54

Browse files
feat(bigtable): Rerouted DirectRow.commit to use mutate_row (#18191)
Migrating over @gkevinzheng PR from bigtable monorepo googleapis/python-bigtable#1276 ### Original description: > **Changes Made:** > - Use MutateRow instead of MutateRows for DirectRow.commit instead of Table.MutateRows > - Added system test for DirectRow.commit because of the decoupling of DirectRow.commit and Table.MutateRows > - Adjusted input error system test because of slight changes in error behavior > - Adjusted unit tests for DirectRow.commit Note to reviewers: This PR has already been reviewed and merged to a staging branch, with the intention of doing a single merge to main. We are now planning to slowly rollout these changes back to the main branch. Minimal re-review should be necessary --------- Co-authored-by: Kevin Zheng <147537668+gkevinzheng@users.noreply.github.com>
1 parent 227fecc commit 7126a54

3 files changed

Lines changed: 182 additions & 26 deletions

File tree

packages/google-cloud-bigtable/google/cloud/bigtable/row.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414

1515
"""User-friendly container for Google Cloud Bigtable Row."""
1616

17+
from google.api_core.exceptions import GoogleAPICallError
1718
from google.cloud._helpers import (
1819
_datetime_from_microseconds, # type: ignore
1920
_microseconds_from_datetime, # type: ignore
2021
_to_bytes, # type: ignore
2122
)
23+
from google.rpc import code_pb2, status_pb2
2224

2325
from google.cloud.bigtable.data import mutations
2426
from google.cloud.bigtable.data import read_modify_write_rules as rmw_rules
@@ -441,7 +443,8 @@ def delete_cells(self, column_family_id, columns, time_range=None):
441443
def commit(self):
442444
"""Makes a ``MutateRow`` API request.
443445
444-
If no mutations have been created in the row, no request is made.
446+
If no mutations have been created in the row, no request is made and a
447+
ValueError is raised instead.
445448
446449
Mutations are applied atomically and in order, meaning that earlier
447450
mutations can be masked / negated by later ones. Cells already present
@@ -460,14 +463,30 @@ def commit(self):
460463
:rtype: :class:`~google.rpc.status_pb2.Status`
461464
:returns: A response status (`google.rpc.status_pb2.Status`)
462465
representing success or failure of the row committed.
463-
:raises: :exc:`~.table.TooManyMutationsError` if the number of
464-
mutations is greater than 100,000.
466+
:raises: ValueError: if no mutations have been created in the row
467+
or if the number of mutations is greater than 100,000.
465468
"""
466-
response = self._table.mutate_rows([self])
467-
468-
self.clear()
469+
num_mutations = len(self._get_mutations())
470+
if num_mutations > MAX_MUTATIONS:
471+
raise ValueError(
472+
"Number of mutations exceeds the maximum "
473+
"allowable %d." % (MAX_MUTATIONS,)
474+
)
469475

470-
return response[0]
476+
try:
477+
self._table._table_impl.mutate_row(self.row_key, self._get_mutations())
478+
return status_pb2.Status(code=code_pb2.OK)
479+
except GoogleAPICallError as e:
480+
# If the RPC call returns an error, extract the error into a status object, if possible.
481+
return status_pb2.Status(
482+
code=e.grpc_status_code.value[0]
483+
if e.grpc_status_code is not None
484+
else code_pb2.UNKNOWN,
485+
message=e.message,
486+
details=getattr(e, "details", []),
487+
)
488+
finally:
489+
self.clear()
471490

472491
def clear(self):
473492
"""Removes all currently accumulated mutations on the current row.

packages/google-cloud-bigtable/tests/system/v2_client/test_data_api.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,37 @@ def test_table_read_rows_filter_millis(data_table):
215215
row_data.consume_all()
216216

217217

218+
def test_table_direct_row_commit(data_table, rows_to_delete):
219+
from google.rpc import code_pb2
220+
221+
row = data_table.direct_row(ROW_KEY)
222+
223+
# Test set cell
224+
row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, CELL_VAL1)
225+
row.set_cell(COLUMN_FAMILY_ID1, COL_NAME2, CELL_VAL1)
226+
status = row.commit()
227+
rows_to_delete.append(row)
228+
assert status.code == code_pb2.Code.OK
229+
row_data = data_table.read_row(ROW_KEY)
230+
assert row_data.cells[COLUMN_FAMILY_ID1][COL_NAME1][0].value == CELL_VAL1
231+
assert row_data.cells[COLUMN_FAMILY_ID1][COL_NAME2][0].value == CELL_VAL1
232+
233+
# Test delete cell
234+
row.delete_cell(COLUMN_FAMILY_ID1, COL_NAME1)
235+
status = row.commit()
236+
assert status.code == code_pb2.Code.OK
237+
row_data = data_table.read_row(ROW_KEY)
238+
assert COL_NAME1 not in row_data.cells[COLUMN_FAMILY_ID1]
239+
assert row_data.cells[COLUMN_FAMILY_ID1][COL_NAME2][0].value == CELL_VAL1
240+
241+
# Test delete row
242+
row.delete()
243+
status = row.commit()
244+
assert status.code == code_pb2.Code.OK
245+
row_data = data_table.read_row(ROW_KEY)
246+
assert row_data is None
247+
248+
218249
def test_table_mutate_rows(data_table, rows_to_delete):
219250
row1 = data_table.direct_row(ROW_KEY)
220251
row1.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, CELL_VAL1)
@@ -1033,8 +1064,6 @@ def test_table_sample_row_keys(data_table, skip_on_emulator):
10331064

10341065

10351066
def test_table_direct_row_input_errors(data_table, rows_to_delete):
1036-
from google.api_core.exceptions import InvalidArgument
1037-
10381067
from google.cloud.bigtable.row import MAX_MUTATIONS
10391068

10401069
row = data_table.direct_row(ROW_KEY)
@@ -1069,10 +1098,9 @@ def test_table_direct_row_input_errors(data_table, rows_to_delete):
10691098
with pytest.raises(ValueError):
10701099
row.commit()
10711100

1072-
# Not having any mutations gives a server error (InvalidArgument), not
1073-
# enforced on the client side.
1101+
# Not having any mutations raises a ValueError
10741102
row.clear()
1075-
with pytest.raises(InvalidArgument):
1103+
with pytest.raises(ValueError):
10761104
row.commit()
10771105

10781106

packages/google-cloud-bigtable/tests/unit/v2_client/test_row.py

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -370,44 +370,154 @@ def test_direct_row_delete_cells_with_string_columns():
370370

371371

372372
def test_direct_row_commit():
373+
from google.rpc import code_pb2, status_pb2
374+
375+
from google.cloud.bigtable_v2.services.bigtable import BigtableClient
376+
373377
project_id = "project-id"
374378
row_key = b"row_key"
375379
table_name = "projects/more-stuff"
380+
app_profile_id = "app_profile_id"
376381
column_family_id = "column_family_id"
377382
column = b"column"
378383

379384
credentials = _make_credentials()
380385
client = _make_client(project=project_id, credentials=credentials, admin=True)
381-
table = _Table(table_name, client=client)
386+
table = _Table(table_name, client=client, app_profile_id=app_profile_id)
382387
row = _make_direct_row(row_key, table)
383388
value = b"bytes-value"
384389

390+
# Set mock
391+
api = mock.create_autospec(BigtableClient)
392+
response_pb = _MutateRowResponsePB()
393+
api.mutate_row.side_effect = [response_pb]
394+
client.table_data_client
395+
client._table_data_client._gapic_client = api
396+
385397
# Perform the method and check the result.
386398
row.set_cell(column_family_id, column, value)
387-
row.commit()
388-
assert table.mutated_rows == [row]
399+
response = row.commit()
400+
assert row._mutations == []
401+
assert response == status_pb2.Status(code=code_pb2.OK)
402+
call_args = api.mutate_row.call_args
403+
assert app_profile_id == call_args.app_profile_id[0]
389404

390405

391406
def test_direct_row_commit_with_exception():
392-
from google.rpc import status_pb2
407+
from google.api_core.exceptions import InternalServerError
408+
from google.rpc import code_pb2, status_pb2
409+
410+
from google.cloud.bigtable_v2.services.bigtable import BigtableClient
393411

394412
project_id = "project-id"
395413
row_key = b"row_key"
396414
table_name = "projects/more-stuff"
415+
app_profile_id = "app_profile_id"
397416
column_family_id = "column_family_id"
398417
column = b"column"
399418

400419
credentials = _make_credentials()
401420
client = _make_client(project=project_id, credentials=credentials, admin=True)
402-
table = _Table(table_name, client=client)
421+
table = _Table(table_name, client=client, app_profile_id=app_profile_id)
422+
row = _make_direct_row(row_key, table)
423+
value = b"bytes-value"
424+
425+
# Set mock
426+
api = mock.create_autospec(BigtableClient)
427+
exception_message = "Boom!"
428+
exception = InternalServerError(exception_message)
429+
api.mutate_row.side_effect = [exception]
430+
client.table_data_client
431+
client._table_data_client._gapic_client = api
432+
433+
# Perform the method and check the result.
434+
row.set_cell(column_family_id, column, value)
435+
result = row.commit()
436+
assert row._mutations == []
437+
assert result == status_pb2.Status(
438+
code=code_pb2.Code.INTERNAL, message=exception_message
439+
)
440+
call_args = api.mutate_row.call_args
441+
assert app_profile_id == call_args.app_profile_id[0]
442+
443+
444+
def test_direct_row_commit_with_unknown_exception():
445+
from google.api_core.exceptions import GoogleAPICallError
446+
from google.rpc import code_pb2, status_pb2
447+
448+
from google.cloud.bigtable_v2.services.bigtable import BigtableClient
449+
450+
project_id = "project-id"
451+
row_key = b"row_key"
452+
table_name = "projects/more-stuff"
453+
app_profile_id = "app_profile_id"
454+
column_family_id = "column_family_id"
455+
column = b"column"
456+
457+
credentials = _make_credentials()
458+
client = _make_client(project=project_id, credentials=credentials, admin=True)
459+
table = _Table(table_name, client=client, app_profile_id=app_profile_id)
403460
row = _make_direct_row(row_key, table)
404461
value = b"bytes-value"
405462

463+
# Set mock
464+
api = mock.create_autospec(BigtableClient)
465+
exception_message = "Boom!"
466+
exception = GoogleAPICallError(message=exception_message)
467+
api.mutate_row.side_effect = [exception]
468+
client.table_data_client
469+
client._table_data_client._gapic_client = api
470+
406471
# Perform the method and check the result.
407472
row.set_cell(column_family_id, column, value)
408473
result = row.commit()
409-
expected = status_pb2.Status(code=0)
410-
assert result == expected
474+
assert row._mutations == []
475+
assert result == status_pb2.Status(
476+
code=code_pb2.Code.UNKNOWN, message=exception_message
477+
)
478+
call_args = api.mutate_row.call_args
479+
assert app_profile_id == call_args.app_profile_id[0]
480+
481+
482+
def test_direct_row_commit_with_invalid_argument():
483+
from google.cloud.bigtable_v2.services.bigtable import BigtableClient
484+
485+
project_id = "project-id"
486+
row_key = b"row_key"
487+
table_name = "projects/more-stuff"
488+
app_profile_id = "app_profile_id"
489+
490+
credentials = _make_credentials()
491+
client = _make_client(project=project_id, credentials=credentials, admin=True)
492+
table = _Table(table_name, client=client, app_profile_id=app_profile_id)
493+
row = _make_direct_row(row_key, table)
494+
495+
# Set mock
496+
api = mock.create_autospec(BigtableClient)
497+
client.table_data_client
498+
client._table_data_client._gapic_client = api
499+
500+
# Perform the method and check the result.
501+
with pytest.raises(ValueError, match="No mutations provided"):
502+
row.commit()
503+
api.mutate_row.assert_not_called()
504+
505+
506+
def test_direct_row_commit_too_many_mutations():
507+
from google.cloud._testing import _Monkey
508+
509+
from google.cloud.bigtable import row as MUT
510+
511+
row_key = b"row_key"
512+
table = object()
513+
row = _make_direct_row(row_key, table)
514+
row._mutations = [1, 2, 3]
515+
num_mutations = len(row._mutations)
516+
with _Monkey(MUT, MAX_MUTATIONS=num_mutations - 1):
517+
with pytest.raises(
518+
ValueError, match="Number of mutations exceeds the maximum allowable"
519+
):
520+
row.commit()
411521

412522

413523
def _make_conditional_row(*args, **kwargs):
@@ -734,6 +844,12 @@ def test__parse_rmw_row_response():
734844
assert expected_output == _parse_rmw_row_response(sample_input)
735845

736846

847+
def _MutateRowResponsePB():
848+
from google.cloud.bigtable_v2.types import bigtable as messages_v2_pb2
849+
850+
return messages_v2_pb2.MutateRowResponse()
851+
852+
737853
def _CheckAndMutateRowResponsePB(*args, **kw):
738854
from google.cloud.bigtable_v2.types import bigtable as messages_v2_pb2
739855

@@ -783,16 +899,9 @@ def __init__(self, name, client=None, app_profile_id=None):
783899
self._instance = _Instance(client)
784900
self._app_profile_id = app_profile_id
785901
self.client = client
786-
self.mutated_rows = []
787902

788903
self._table_impl = self._instance._client._veneer_data_client.get_table(
789904
_INSTANCE_ID,
790905
self.name,
791906
app_profile_id=self._app_profile_id,
792907
)
793-
794-
def mutate_rows(self, rows):
795-
from google.rpc import status_pb2
796-
797-
self.mutated_rows.extend(rows)
798-
return [status_pb2.Status(code=0)]

0 commit comments

Comments
 (0)