Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 8cb8af0

Browse files
committed
feat: Rerouted CheckAndMutateRows and ReadModifyWriteRows
1 parent f6b90f7 commit 8cb8af0

5 files changed

Lines changed: 171 additions & 272 deletions

File tree

google/cloud/bigtable/row.py

Lines changed: 57 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,13 @@
1414

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

17-
18-
import struct
19-
2017
from google.cloud._helpers import _datetime_from_microseconds # type: ignore
2118
from google.cloud._helpers import _microseconds_from_datetime # type: ignore
2219
from google.cloud._helpers import _to_bytes # type: ignore
23-
from google.cloud.bigtable_v2.types import data as data_v2_pb2
2420

21+
from google.cloud.bigtable.data import mutations
22+
from google.cloud.bigtable.data import read_modify_write_rules as rmw_rules
2523

26-
_PACK_I64 = struct.Struct(">q").pack
2724

2825
MAX_MUTATIONS = 100000
2926
"""The maximum number of mutations that a row can accumulate."""
@@ -157,26 +154,21 @@ def _set_cell(self, column_family_id, column, value, timestamp=None, state=None)
157154
:param state: (Optional) The state that is passed along to
158155
:meth:`_get_mutations`.
159156
"""
160-
column = _to_bytes(column)
161-
if isinstance(value, int):
162-
value = _PACK_I64(value)
163-
value = _to_bytes(value)
164157
if timestamp is None:
165-
# Use -1 for current Bigtable server time.
166-
timestamp_micros = -1
158+
# Use current Bigtable server time.
159+
timestamp_micros = mutations._SERVER_SIDE_TIMESTAMP
167160
else:
168161
timestamp_micros = _microseconds_from_datetime(timestamp)
169162
# Truncate to millisecond granularity.
170163
timestamp_micros -= timestamp_micros % 1000
171164

172-
mutation_val = data_v2_pb2.Mutation.SetCell(
173-
family_name=column_family_id,
174-
column_qualifier=column,
165+
mutation = mutations.SetCell(
166+
family=column_family_id,
167+
qualifier=column,
168+
new_value=value,
175169
timestamp_micros=timestamp_micros,
176-
value=value,
177170
)
178-
mutation_pb = data_v2_pb2.Mutation(set_cell=mutation_val)
179-
self._get_mutations(state).append(mutation_pb)
171+
self._get_mutations(state).append(mutation)
180172

181173
def _delete(self, state=None):
182174
"""Helper for :meth:`delete`
@@ -191,9 +183,7 @@ def _delete(self, state=None):
191183
:param state: (Optional) The state that is passed along to
192184
:meth:`_get_mutations`.
193185
"""
194-
mutation_val = data_v2_pb2.Mutation.DeleteFromRow()
195-
mutation_pb = data_v2_pb2.Mutation(delete_from_row=mutation_val)
196-
self._get_mutations(state).append(mutation_pb)
186+
self._get_mutations(state).append(mutations.DeleteAllFromRow())
197187

198188
def _delete_cells(self, column_family_id, columns, time_range=None, state=None):
199189
"""Helper for :meth:`delete_cell` and :meth:`delete_cells`.
@@ -222,27 +212,28 @@ def _delete_cells(self, column_family_id, columns, time_range=None, state=None):
222212
"""
223213
mutations_list = self._get_mutations(state)
224214
if columns is self.ALL_COLUMNS:
225-
mutation_val = data_v2_pb2.Mutation.DeleteFromFamily(
226-
family_name=column_family_id
215+
self._get_mutations(state).append(
216+
mutations.DeleteAllFromFamily(family_to_delete=column_family_id)
227217
)
228-
mutation_pb = data_v2_pb2.Mutation(delete_from_family=mutation_val)
229-
mutations_list.append(mutation_pb)
230218
else:
231-
delete_kwargs = {}
219+
start_timestamp_micros = None
220+
end_timestamp_micros = None
232221
if time_range is not None:
233-
delete_kwargs["time_range"] = time_range._to_pb()
222+
timestamps = time_range._to_dict()
223+
start_timestamp_micros = timestamps.get("start_timestamp_micros")
224+
end_timestamp_micros = timestamps.get("end_timestamp_micros")
234225

235226
to_append = []
236227
for column in columns:
237228
column = _to_bytes(column)
238-
# time_range will never change if present, but the rest of
239-
# delete_kwargs will
240-
delete_kwargs.update(
241-
family_name=column_family_id, column_qualifier=column
229+
to_append.append(
230+
mutations.DeleteRangeFromColumn(
231+
family=column_family_id,
232+
qualifier=column,
233+
start_timestamp_micros=start_timestamp_micros,
234+
end_timestamp_micros=end_timestamp_micros,
235+
)
242236
)
243-
mutation_val = data_v2_pb2.Mutation.DeleteFromColumn(**delete_kwargs)
244-
mutation_pb = data_v2_pb2.Mutation(delete_from_column=mutation_val)
245-
to_append.append(mutation_pb)
246237

247238
# We don't add the mutations until all columns have been
248239
# processed without error.
@@ -284,7 +275,7 @@ class DirectRow(_SetDeleteRow):
284275

285276
def __init__(self, row_key, table=None):
286277
super(DirectRow, self).__init__(row_key, table)
287-
self._pb_mutations = []
278+
self._mutations = []
288279

289280
def _get_mutations(self, state=None): # pylint: disable=unused-argument
290281
"""Gets the list of mutations for a given state.
@@ -299,7 +290,12 @@ def _get_mutations(self, state=None): # pylint: disable=unused-argument
299290
:rtype: list
300291
:returns: The list to add new mutations to (for the current state).
301292
"""
302-
return self._pb_mutations
293+
return self._mutations
294+
295+
def _get_mutation_pbs(self):
296+
"""Gets the list of mutation protos."""
297+
298+
return [mut._to_pb() for mut in self._get_mutations()]
303299

304300
def get_mutations_size(self):
305301
"""Gets the total mutations size for current row
@@ -313,7 +309,7 @@ def get_mutations_size(self):
313309
"""
314310

315311
mutation_size = 0
316-
for mutation in self._get_mutations():
312+
for mutation in self._get_mutation_pbs():
317313
mutation_size += mutation._pb.ByteSize()
318314

319315
return mutation_size
@@ -486,7 +482,7 @@ def clear(self):
486482
:end-before: [END bigtable_api_row_clear]
487483
:dedent: 4
488484
"""
489-
del self._pb_mutations[:]
485+
del self._mutations[:]
490486

491487

492488
class ConditionalRow(_SetDeleteRow):
@@ -597,17 +593,15 @@ def commit(self):
597593
% (MAX_MUTATIONS, num_true_mutations, num_false_mutations)
598594
)
599595

600-
data_client = self._table._instance._client.table_data_client
601-
resp = data_client.check_and_mutate_row(
602-
table_name=self._table.name,
596+
table = self._table._table_impl
597+
resp = table.check_and_mutate_row(
603598
row_key=self._row_key,
604-
predicate_filter=self._filter._to_pb(),
605-
app_profile_id=self._table._app_profile_id,
606-
true_mutations=true_mutations,
607-
false_mutations=false_mutations,
599+
predicate=self._filter,
600+
true_case_mutations=true_mutations,
601+
false_case_mutations=false_mutations,
608602
)
609603
self.clear()
610-
return resp.predicate_matched
604+
return resp
611605

612606
# pylint: disable=arguments-differ
613607
def set_cell(self, column_family_id, column, value, timestamp=None, state=True):
@@ -797,7 +791,7 @@ class AppendRow(Row):
797791

798792
def __init__(self, row_key, table):
799793
super(AppendRow, self).__init__(row_key, table)
800-
self._rule_pb_list = []
794+
self._rule_list = []
801795

802796
def clear(self):
803797
"""Removes all currently accumulated modifications on current row.
@@ -809,7 +803,7 @@ def clear(self):
809803
:end-before: [END bigtable_api_row_clear]
810804
:dedent: 4
811805
"""
812-
del self._rule_pb_list[:]
806+
del self._rule_list[:]
813807

814808
def append_cell_value(self, column_family_id, column, value):
815809
"""Appends a value to an existing cell.
@@ -842,12 +836,11 @@ def append_cell_value(self, column_family_id, column, value):
842836
the targeted cell is unset, it will be treated as
843837
containing the empty string.
844838
"""
845-
column = _to_bytes(column)
846-
value = _to_bytes(value)
847-
rule_pb = data_v2_pb2.ReadModifyWriteRule(
848-
family_name=column_family_id, column_qualifier=column, append_value=value
839+
self._rule_list.append(
840+
rmw_rules.AppendValueRule(
841+
family=column_family_id, qualifier=column, append_value=value
842+
)
849843
)
850-
self._rule_pb_list.append(rule_pb)
851844

852845
def increment_cell_value(self, column_family_id, column, int_value):
853846
"""Increments a value in an existing cell.
@@ -886,13 +879,11 @@ def increment_cell_value(self, column_family_id, column, int_value):
886879
big-endian signed integer), or the entire request
887880
will fail.
888881
"""
889-
column = _to_bytes(column)
890-
rule_pb = data_v2_pb2.ReadModifyWriteRule(
891-
family_name=column_family_id,
892-
column_qualifier=column,
893-
increment_amount=int_value,
882+
self._rule_list.append(
883+
rmw_rules.IncrementRule(
884+
family=column_family_id, qualifier=column, increment_amount=int_value
885+
)
894886
)
895-
self._rule_pb_list.append(rule_pb)
896887

897888
def commit(self):
898889
"""Makes a ``ReadModifyWriteRow`` API request.
@@ -925,7 +916,7 @@ def commit(self):
925916
:raises: :class:`ValueError <exceptions.ValueError>` if the number of
926917
mutations exceeds the :data:`MAX_MUTATIONS`.
927918
"""
928-
num_mutations = len(self._rule_pb_list)
919+
num_mutations = len(self._rule_list)
929920
if num_mutations == 0:
930921
return {}
931922
if num_mutations > MAX_MUTATIONS:
@@ -934,12 +925,10 @@ def commit(self):
934925
"allowable %d." % (num_mutations, MAX_MUTATIONS)
935926
)
936927

937-
data_client = self._table._instance._client.table_data_client
938-
row_response = data_client.read_modify_write_row(
939-
table_name=self._table.name,
928+
table = self._table._table_impl
929+
row_response = table.read_modify_write_row(
940930
row_key=self._row_key,
941-
rules=self._rule_pb_list,
942-
app_profile_id=self._table._app_profile_id,
931+
rules=self._rule_list,
943932
)
944933

945934
# Reset modifications after commit-ing request.
@@ -983,47 +972,13 @@ def _parse_rmw_row_response(row_response):
983972
}
984973
"""
985974
result = {}
986-
for column_family in row_response.row.families:
987-
column_family_id, curr_family = _parse_family_pb(column_family)
988-
result[column_family_id] = curr_family
975+
for cell in row_response.cells:
976+
result.setdefault(cell.family, {}).setdefault(cell.qualifier, []).append(
977+
(cell.value, _datetime_from_microseconds(cell.timestamp_micros))
978+
)
989979
return result
990980

991981

992-
def _parse_family_pb(family_pb):
993-
"""Parses a Family protobuf into a dictionary.
994-
995-
:type family_pb: :class:`._generated.data_pb2.Family`
996-
:param family_pb: A protobuf
997-
998-
:rtype: tuple
999-
:returns: A string and dictionary. The string is the name of the
1000-
column family and the dictionary has column names (within the
1001-
family) as keys and cell lists as values. Each cell is
1002-
represented with a two-tuple with the value (in bytes) and the
1003-
timestamp for the cell. For example:
1004-
1005-
.. code:: python
1006-
1007-
{
1008-
b'col-name1': [
1009-
(b'cell-val', datetime.datetime(...)),
1010-
(b'cell-val-newer', datetime.datetime(...)),
1011-
],
1012-
b'col-name2': [
1013-
(b'altcol-cell-val', datetime.datetime(...)),
1014-
],
1015-
}
1016-
"""
1017-
result = {}
1018-
for column in family_pb.columns:
1019-
result[column.qualifier] = cells = []
1020-
for cell in column.cells:
1021-
val_pair = (cell.value, _datetime_from_microseconds(cell.timestamp_micros))
1022-
cells.append(val_pair)
1023-
1024-
return family_pb.name, result
1025-
1026-
1027982
class PartialRowData(object):
1028983
"""Representation of partial row in a Google Cloud Bigtable Table.
1029984

google/cloud/bigtable/table.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1368,7 +1368,7 @@ def _compile_mutation_entries(table_name, rows):
13681368
for row in rows:
13691369
_check_row_table_name(table_name, row)
13701370
_check_row_type(row)
1371-
mutations = row._get_mutations()
1371+
mutations = row._get_mutation_pbs()
13721372
entries.append(entry_klass(row_key=row.row_key, mutations=mutations))
13731373
mutations_count += len(mutations)
13741374

tests/system/v2_client/test_data_api.py

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import datetime
1616
import operator
17-
import struct
1817

1918
import pytest
2019

@@ -50,6 +49,8 @@
5049
INITIAL_ROW_SPLITS = [b"row_split_1", b"row_split_2", b"row_split_3"]
5150
JOY_EMOJI = "\N{FACE WITH TEARS OF JOY}"
5251

52+
GAP_MARGIN_OF_ERROR = 0.05
53+
5354
PASS_ALL_FILTER = row_filters.PassAllFilter(True)
5455
BLOCK_ALL_FILTER = row_filters.BlockAllFilter(True)
5556

@@ -266,7 +267,7 @@ def test_error_handler(exc):
266267
retry._initial * retry._multiplier**times_triggered,
267268
retry._maximum,
268269
)
269-
assert gap <= max_gap
270+
assert gap <= max_gap + GAP_MARGIN_OF_ERROR
270271
times_triggered += 1
271272
curr_time = next_time
272273

@@ -1041,22 +1042,11 @@ def test_table_direct_row_input_errors(data_table, rows_to_delete):
10411042
with pytest.raises(TypeError):
10421043
row.delete_cell(COLUMN_FAMILY_ID1, INT_COL_NAME)
10431044

1044-
# Unicode for column name and value does not get converted to bytes because
1045-
# internally we use to_bytes in ascii mode.
1046-
with pytest.raises(UnicodeEncodeError):
1047-
row.set_cell(COLUMN_FAMILY_ID1, JOY_EMOJI, CELL_VAL1)
1048-
1049-
with pytest.raises(UnicodeEncodeError):
1050-
row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, JOY_EMOJI)
1051-
1052-
with pytest.raises(UnicodeEncodeError):
1053-
row.delete_cell(COLUMN_FAMILY_ID1, JOY_EMOJI)
1054-
1055-
# Various non int64s, we use struct to pack a Python int to bytes.
1056-
with pytest.raises(struct.error):
1045+
# Various non int64s
1046+
with pytest.raises(ValueError):
10571047
row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, OVERFLOW_INT_CELL_VAL)
10581048

1059-
with pytest.raises(struct.error):
1049+
with pytest.raises(ValueError):
10601050
row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, OVERFLOW_INT_CELL_VAL2)
10611051

10621052
# Since floats aren't ints, they aren't converted to bytes via struct.pack,
@@ -1101,22 +1091,11 @@ def test_table_conditional_row_input_errors(data_table, rows_to_delete):
11011091
with pytest.raises(TypeError):
11021092
true_row.delete_cell(COLUMN_FAMILY_ID1, INT_COL_NAME)
11031093

1104-
# Unicode for column name and value does not get converted to bytes because
1105-
# internally we use to_bytes in ascii mode.
1106-
with pytest.raises(UnicodeEncodeError):
1107-
true_row.set_cell(COLUMN_FAMILY_ID1, JOY_EMOJI, CELL_VAL1)
1108-
1109-
with pytest.raises(UnicodeEncodeError):
1110-
true_row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, JOY_EMOJI)
1111-
1112-
with pytest.raises(UnicodeEncodeError):
1113-
true_row.delete_cell(COLUMN_FAMILY_ID1, JOY_EMOJI)
1114-
1115-
# Various non int64s, we use struct to pack a Python int to bytes.
1116-
with pytest.raises(struct.error):
1094+
# Various non int64s
1095+
with pytest.raises(ValueError):
11171096
true_row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, OVERFLOW_INT_CELL_VAL)
11181097

1119-
with pytest.raises(struct.error):
1098+
with pytest.raises(ValueError):
11201099
true_row.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, OVERFLOW_INT_CELL_VAL2)
11211100

11221101
# Since floats aren't ints, they aren't converted to bytes via struct.pack,

0 commit comments

Comments
 (0)