Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,34 @@ 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, ValueBitmaskFilter):
return NotImplemented
return other.mask == self.mask

def _to_dict(self) -> dict[str, Any]:
"""Converts the row filter to a dict representation."""
return {"value_bitmask_filter": {"mask": self.mask}}
Comment on lines +507 to +509

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In the Bigtable V2 API, value_bitmask_filter is a bytes field, not a message with a mask field. Therefore, _to_dict should return {"value_bitmask_filter": self.mask} directly rather than nesting it under a "mask" key.

Suggested change
def _to_dict(self) -> dict[str, Any]:
"""Converts the row filter to a dict representation."""
return {"value_bitmask_filter": {"mask": self.mask}}
def _to_dict(self) -> dict[str, bytes]:
"""Converts the row filter to a dict representation."""
return {"value_bitmask_filter": self.mask}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is right. The proto is a ValueBitMask message with a mask field.


def __repr__(self) -> str:
return f"{self.__class__.__name__}(mask={self.mask!r})"


class LiteralValueFilter(ValueRegexFilter):
"""Row filter for an exact value.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,41 @@ 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),
],
)
@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
):
"""
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_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"
)

@pytest.mark.skipif(
bool(os.environ.get(BIGTABLE_EMULATOR)),
reason="emulator doesn't support SQL",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,38 @@ 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._Sync_Impl.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),
],
)
@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
):
"""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_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"
)

@pytest.mark.skipif(
bool(os.environ.get(BIGTABLE_EMULATOR)), reason="emulator doesn't support SQL"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1987,6 +1987,26 @@ 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):
mask = b"\xaa" * 8
row_filter = self._target_class()(mask)
expected = {"value_bitmask_filter": {"mask": mask}}
assert row_filter._to_dict() == expected
Comment on lines +1997 to +2001

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update the expected dictionary format to match the corrected _to_dict implementation where value_bitmask_filter maps directly to the bytes mask.

Suggested change
def test_to_dict(self):
mask = b"\xaa" * 8
row_filter = self._target_class()(mask)
expected = {"value_bitmask_filter": {"mask": mask}}
assert row_filter._to_dict() == expected
def test_to_dict(self):
mask = b"\xaa" * 8
row_filter = self._target_class()(mask)
expected = {"value_bitmask_filter": 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
Comment on lines +2003 to +2007

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since value_bitmask_filter is a bytes field in the protobuf definition, accessing .mask on it will raise an AttributeError. Assert directly against pb.value_bitmask_filter.

Suggested change
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 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



def _ColumnRangePB(*args, **kw):
from google.cloud.bigtable_v2.types import data as data_v2_pb2

Expand Down
Loading