From b56cabf42e0b130ac1a74bca68ec2e356b63247c Mon Sep 17 00:00:00 2001 From: Akshay Joshi Date: Tue, 26 May 2026 11:23:07 -0400 Subject: [PATCH 1/9] bigtable: add ValueBitmaskFilter for data client --- .../google/cloud/bigtable/data/row_filters.py | 31 +++++++++++++++++++ .../tests/unit/data/test_row_filters.py | 22 +++++++++++++ 2 files changed, 53 insertions(+) diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py index 007a09f5f830..6ae59875e94e 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py @@ -484,6 +484,37 @@ def _to_dict(self) -> dict[str, bytes]: return {"value_regex_filter": self.regex} +class ValueBitmaskFilter(RowFilter): + """Row filter for a value bitmask. + + Matches only cells with values that satisfy the condition + ``(value & mask) == mask``. The mask length must exactly match the value + length, otherwise the cell is not considered a match. + + :type mask: bytes or str + :param mask: A bitmask to match against cell values. String values + will be encoded as ASCII. + """ + + def __init__(self, mask: bytes | str): + self.mask: bytes = _to_bytes(mask) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return other.mask == self.mask + + def __ne__(self, other): + return not self == other + + def _to_dict(self) -> dict[str, Any]: + """Converts the row filter to a dict representation.""" + return {"value_bitmask_filter": data_v2_pb2.ValueBitmask(mask=self.mask)} + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(mask={self.mask!r})" + + class LiteralValueFilter(ValueRegexFilter): """Row filter for an exact value. diff --git a/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py b/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py index 6be9b4a2b252..db518a56137e 100644 --- a/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py +++ b/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py @@ -1987,6 +1987,28 @@ def test_literal_value__write_literal_regex(input_arg, expected_bytes): assert filter_.regex == expected_bytes +class TestValueBitmaskFilter: + @staticmethod + def _target_class(): + from google.cloud.bigtable.data.row_filters import ValueBitmaskFilter + + return ValueBitmaskFilter + + def test_to_dict(self): + from google.cloud.bigtable_v2.types import data as data_v2_pb2 + + mask = b"\xaa" * 8 + row_filter = self._target_class()(mask) + expected = {"value_bitmask_filter": data_v2_pb2.ValueBitmask(mask=mask)} + assert row_filter._to_dict() == expected + + def test_to_pb(self): + mask = b"\xaa" * 8 + row_filter = self._target_class()(mask) + pb = row_filter._to_pb() + assert pb.value_bitmask_filter.mask == mask + + def _ColumnRangePB(*args, **kw): from google.cloud.bigtable_v2.types import data as data_v2_pb2 From 6310b72e993ff12d8b7c3f1af462079470d132ef Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 14:20:51 -0400 Subject: [PATCH 2/9] fix: return plain dict representation in ValueBitmaskFilter._to_dict --- .../google/cloud/bigtable/data/row_filters.py | 2 +- .../google-cloud-bigtable/tests/unit/data/test_row_filters.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py index 6ae59875e94e..ddcc5450a76c 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py @@ -509,7 +509,7 @@ def __ne__(self, other): def _to_dict(self) -> dict[str, Any]: """Converts the row filter to a dict representation.""" - return {"value_bitmask_filter": data_v2_pb2.ValueBitmask(mask=self.mask)} + return {"value_bitmask_filter": {"mask": self.mask}} def __repr__(self) -> str: return f"{self.__class__.__name__}(mask={self.mask!r})" diff --git a/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py b/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py index db518a56137e..6c7bd84bed80 100644 --- a/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py +++ b/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py @@ -1995,11 +1995,9 @@ def _target_class(): return ValueBitmaskFilter def test_to_dict(self): - from google.cloud.bigtable_v2.types import data as data_v2_pb2 - mask = b"\xaa" * 8 row_filter = self._target_class()(mask) - expected = {"value_bitmask_filter": data_v2_pb2.ValueBitmask(mask=mask)} + expected = {"value_bitmask_filter": {"mask": mask}} assert row_filter._to_dict() == expected def test_to_pb(self): From f929dc0b80010f66f09a7a7b14ca5f6b10b39b2d Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 15:20:52 -0400 Subject: [PATCH 3/9] test: add integration tests for ValueBitmaskFilter --- .../tests/system/data/test_system_async.py | 32 +++++++++++++++++++ .../tests/system/data/test_system_autogen.py | 26 +++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py index b65f05e4bd17..b6f923aa94cb 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py @@ -1116,6 +1116,38 @@ async def test_literal_value_filter( f"row {type(cell_value)}({cell_value}) not found with {type(filter_input)}({filter_input}) filter" ) + @pytest.mark.usefixtures("target") + @CrossSync.Retry( + predicate=retry.if_exception_type(ClientError), initial=1, maximum=5 + ) + @pytest.mark.parametrize( + "cell_value,mask,expect_match", + [ + (b"\x01\x02\x03", b"\x01\x02\x03", True), + (b"\x01\x02\x03", b"\x01\x00\x00", True), + (b"\x00\x02\x03", b"\x01\x00\x00", False), + ], + ) + @CrossSync.pytest + async def test_value_bitmask_filter( + self, target, temp_rows, cell_value, mask, expect_match + ): + """ + ValueBitmaskFilter matches cells where (value & mask) == mask. + Make sure inputs are properly interpreted by the server. + """ + from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data.row_filters import ValueBitmaskFilter + + f = ValueBitmaskFilter(mask) + await temp_rows.add_row(b"row_key_1", value=cell_value) + query = ReadRowsQuery(row_filter=f) + row_list = await target.read_rows(query) + assert len(row_list) == bool(expect_match), ( + f"row {cell_value!r} not matched as {expect_match} with {mask!r} bitmask filter" + ) + + @pytest.mark.skipif( bool(os.environ.get(BIGTABLE_EMULATOR)), reason="emulator doesn't support SQL", diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py index c31b2c20a4b8..f3acda28dbed 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py @@ -903,6 +903,32 @@ def test_literal_value_filter( f"row {type(cell_value)}({cell_value}) not found with {type(filter_input)}({filter_input}) filter" ) + @pytest.mark.usefixtures("target") + @pytest.mark.parametrize( + "cell_value,mask,expect_match", + [ + (b"\x01\x02\x03", b"\x01\x02\x03", True), + (b"\x01\x02\x03", b"\x01\x00\x00", True), + (b"\x00\x02\x03", b"\x01\x00\x00", False), + ], + ) + def test_value_bitmask_filter( + self, target, temp_rows, cell_value, mask, expect_match + ): + """ValueBitmaskFilter matches cells where (value & mask) == mask. + Make sure inputs are properly interpreted by the server.""" + from google.cloud.bigtable.data import ReadRowsQuery + from google.cloud.bigtable.data.row_filters import ValueBitmaskFilter + + f = ValueBitmaskFilter(mask) + temp_rows.add_row(b"row_key_1", value=cell_value) + query = ReadRowsQuery(row_filter=f) + row_list = target.read_rows(query) + assert len(row_list) == bool(expect_match), ( + f"row {cell_value!r} not matched as {expect_match} with {mask!r} bitmask filter" + ) + + @pytest.mark.skipif( bool(os.environ.get(BIGTABLE_EMULATOR)), reason="emulator doesn't support SQL" ) From 27c4e734d7161cd7d92cec47cc09e641d89af941 Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 15:45:25 -0400 Subject: [PATCH 4/9] reformat --- .../google-cloud-bigtable/tests/system/data/test_system_async.py | 1 - .../tests/system/data/test_system_autogen.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py index b6f923aa94cb..01ea891ef425 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py @@ -1147,7 +1147,6 @@ async def test_value_bitmask_filter( f"row {cell_value!r} not matched as {expect_match} with {mask!r} bitmask filter" ) - @pytest.mark.skipif( bool(os.environ.get(BIGTABLE_EMULATOR)), reason="emulator doesn't support SQL", diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py index f3acda28dbed..1fb3f5642529 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py @@ -928,7 +928,6 @@ def test_value_bitmask_filter( f"row {cell_value!r} not matched as {expect_match} with {mask!r} bitmask filter" ) - @pytest.mark.skipif( bool(os.environ.get(BIGTABLE_EMULATOR)), reason="emulator doesn't support SQL" ) From d7501480879a4f1a4cc607cde39464fb340699db Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 16:08:43 -0400 Subject: [PATCH 5/9] fix(test): add CrossSync._Sync_Impl.Retry to test_value_bitmask_filter in test_system_autogen.py --- .../tests/system/data/test_system_autogen.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py index 1fb3f5642529..86317d3ce648 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py @@ -904,6 +904,9 @@ def test_literal_value_filter( ) @pytest.mark.usefixtures("target") + @CrossSync._Sync_Impl.Retry( + predicate=retry.if_exception_type(ClientError), initial=1, maximum=5 + ) @pytest.mark.parametrize( "cell_value,mask,expect_match", [ From ca4c2d8a48e43ac2f3d78b17fabaa2351e8ea812 Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 16:36:33 -0400 Subject: [PATCH 6/9] Update packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../google/cloud/bigtable/data/row_filters.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py index ddcc5450a76c..9a6511c40818 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py @@ -500,13 +500,10 @@ def __init__(self, mask: bytes | str): self.mask: bytes = _to_bytes(mask) def __eq__(self, other): - if not isinstance(other, self.__class__): + if not isinstance(other, ValueBitmaskFilter): return NotImplemented return other.mask == self.mask - def __ne__(self, other): - return not self == other - def _to_dict(self) -> dict[str, Any]: """Converts the row filter to a dict representation.""" return {"value_bitmask_filter": {"mask": self.mask}} From 100f049dc1b150b525cb4805ceb39b95b9692a6d Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 16:36:43 -0400 Subject: [PATCH 7/9] Update packages/google-cloud-bigtable/tests/system/data/test_system_async.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../tests/system/data/test_system_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py index 01ea891ef425..9c5d6a861485 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py @@ -1141,7 +1141,7 @@ async def test_value_bitmask_filter( f = ValueBitmaskFilter(mask) await temp_rows.add_row(b"row_key_1", value=cell_value) - query = ReadRowsQuery(row_filter=f) + query = ReadRowsQuery(row_keys=[b"row_key_1"], row_filter=f) row_list = await target.read_rows(query) assert len(row_list) == bool(expect_match), ( f"row {cell_value!r} not matched as {expect_match} with {mask!r} bitmask filter" From 66bdec91e74746162e43539e29f7442d451aa3fd Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 17:09:34 -0400 Subject: [PATCH 8/9] fix(test): add row_keys to ReadRowsQuery in test_value_bitmask_filter --- .../tests/system/data/test_system_autogen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py index 86317d3ce648..a7eb93dca751 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py @@ -925,7 +925,7 @@ def test_value_bitmask_filter( f = ValueBitmaskFilter(mask) temp_rows.add_row(b"row_key_1", value=cell_value) - query = ReadRowsQuery(row_filter=f) + query = ReadRowsQuery(row_keys=[b"row_key_1"], row_filter=f) row_list = target.read_rows(query) assert len(row_list) == bool(expect_match), ( f"row {cell_value!r} not matched as {expect_match} with {mask!r} bitmask filter" From bb3d168cc810725037a567c5cfa78f87e4b9229f Mon Sep 17 00:00:00 2001 From: Mattie Fu Date: Wed, 24 Jun 2026 17:12:14 -0400 Subject: [PATCH 9/9] fix(test): skip test_value_bitmask_filter on Bigtable emulator --- .../tests/system/data/test_system_async.py | 4 ++++ .../tests/system/data/test_system_autogen.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py index 9c5d6a861485..13674995411e 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_async.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_async.py @@ -1128,6 +1128,10 @@ async def test_literal_value_filter( (b"\x00\x02\x03", b"\x01\x00\x00", False), ], ) + @pytest.mark.skipif( + bool(os.environ.get(BIGTABLE_EMULATOR)), + reason="value_bitmask_filter not supported by emulator", + ) @CrossSync.pytest async def test_value_bitmask_filter( self, target, temp_rows, cell_value, mask, expect_match diff --git a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py index a7eb93dca751..48474925edb7 100644 --- a/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py +++ b/packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py @@ -915,6 +915,10 @@ def test_literal_value_filter( (b"\x00\x02\x03", b"\x01\x00\x00", False), ], ) + @pytest.mark.skipif( + bool(os.environ.get(BIGTABLE_EMULATOR)), + reason="value_bitmask_filter not supported by emulator", + ) def test_value_bitmask_filter( self, target, temp_rows, cell_value, mask, expect_match ):