This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 125d4e9b88 [python] Introduce rbm32 aggregator function (#8724)
125d4e9b88 is described below

commit 125d4e9b88b07179101c7d90960313e0df8bff53
Author: AuroraVoyage <[email protected]>
AuthorDate: Tue Jul 28 21:19:34 2026 +0800

    [python] Introduce rbm32 aggregator function (#8724)
---
 .../pypaimon/read/merge_engine_support.py          |  1 +
 .../pypaimon/read/reader/aggregate/aggregators.py  | 39 ++++++++++++++++++++
 .../pypaimon/tests/test_field_aggregators.py       | 41 ++++++++++++++++++++++
 3 files changed, 81 insertions(+)

diff --git a/paimon-python/pypaimon/read/merge_engine_support.py 
b/paimon-python/pypaimon/read/merge_engine_support.py
index 0624bf1e65..f1c477ed88 100644
--- a/paimon-python/pypaimon/read/merge_engine_support.py
+++ b/paimon-python/pypaimon/read/merge_engine_support.py
@@ -69,6 +69,7 @@ _AGGREGATION_SUPPORTED_AGG_FUNCS = frozenset([
     "merge_map_with_keytime",
     "merge_map",
     "theta_sketch",
+    "rbm32",
 ])
 _FIELDS_PREFIX = "fields."
 _FIELD_SEQUENCE_GROUP_SUFFIX = ".sequence-group"
diff --git a/paimon-python/pypaimon/read/reader/aggregate/aggregators.py 
b/paimon-python/pypaimon/read/reader/aggregate/aggregators.py
index 0f7c4d4c31..4d1a960db9 100644
--- a/paimon-python/pypaimon/read/reader/aggregate/aggregators.py
+++ b/paimon-python/pypaimon/read/reader/aggregate/aggregators.py
@@ -44,6 +44,7 @@ from pypaimon.read.reader.aggregate.field_aggregator import 
FieldAggregator
 from pypaimon.schema.data_types import AtomicType, DataType, ArrayType, 
RowType, MapType
 from pypaimon.table.row.generic_row import GenericRow
 from pypaimon.table.row.internal_row import InternalRow
+from pypaimon.utils.roaring_bitmap import RoaringBitmap
 
 # aggregator input type hints variables
 Record = Union[InternalRow, Dict[str, Any]]
@@ -69,6 +70,7 @@ NAME_COLLECT = "collect"
 NAME_MERGE_MAP_WITH_KEYTIME = "merge_map_with_keytime"
 NAME_MERGE_MAP = "merge_map"
 NAME_THETA_SKETCH = "theta_sketch"
+NAME_RBM32 = "rbm32"
 
 
 # Integer range limits used for overflow checking.
@@ -149,6 +151,18 @@ def _check_array_row(name: str, field_type: DataType) -> 
ArrayType:
     return field_type
 
 
+def _check_roaring_bitmap(name: str, field_type: DataType):
+    """Check field_type is VarBinaryType and return the VarBinaryType."""
+
+    base = _atomic_base_name(field_type)
+    if base not in ("VARBINARY", "BYTES"):
+        raise ValueError(
+            "Data type for {} column must be 'VARBINARY' or 'BYTES' but was "
+            "'{}'.".format(name, field_type)
+        )
+    return field_type
+
+
 def is_blank(s: str) -> bool:
     if s is None:
         return True
@@ -1410,6 +1424,21 @@ class FieldThetaSketchAgg(FieldAggregator):
         return union.get_result().serialize()
 
 
+class FieldRoaringBitmap32Agg(FieldAggregator):
+    """roaring bitmap 32 aggregate a field of a row."""
+
+    def agg(self, accumulator: Any, input_field: Any) -> Any:
+        if accumulator is None or input_field is None:
+            return input_field if accumulator is None else accumulator
+
+        try:
+            acc = RoaringBitmap.deserialize(accumulator)
+            input_bitmap = RoaringBitmap.deserialize(input_field)
+            return RoaringBitmap.or_(acc, input_bitmap).serialize()
+        except Exception as ex:
+            raise RuntimeError("Unable to se/deserialize roaring bitmap.") 
from ex
+
+
 # ---------------------------------------------------------------------------
 # Registration. Each builder binds an identifier to a factory that
 # optionally validates the column DataType before constructing the
@@ -1451,6 +1480,13 @@ def _build_field_options(cls, identifier: str):
     return _factory
 
 
+def _build_roaring_bitmap(cls, identifier: str):
+    def _factory(field_type, field_name, options):
+        _check_roaring_bitmap(identifier, field_type)
+        return cls(identifier, field_type)
+    return _factory
+
+
 register_aggregator(
     NAME_PRIMARY_KEY,
     _build_no_type_check(FieldPrimaryKeyAgg, NAME_PRIMARY_KEY),
@@ -1502,3 +1538,6 @@ register_aggregator(
 register_aggregator(
     NAME_THETA_SKETCH, _build_no_type_check(FieldThetaSketchAgg, 
NAME_THETA_SKETCH)
 )
+register_aggregator(
+    NAME_RBM32, _build_roaring_bitmap(FieldRoaringBitmap32Agg, NAME_RBM32)
+)
diff --git a/paimon-python/pypaimon/tests/test_field_aggregators.py 
b/paimon-python/pypaimon/tests/test_field_aggregators.py
index 6d548499ca..7ce1e013bb 100644
--- a/paimon-python/pypaimon/tests/test_field_aggregators.py
+++ b/paimon-python/pypaimon/tests/test_field_aggregators.py
@@ -55,10 +55,12 @@ from pypaimon.read.reader.aggregate.aggregators import (
     FieldMergeMapWithKeyTimeAgg,
     FieldMergeMapAgg,
     FieldThetaSketchAgg,
+    FieldRoaringBitmap32Agg,
 )
 from pypaimon.schema.data_types import AtomicType, DataField, RowType, 
ArrayType, MapType
 from pypaimon.table.row.generic_row import GenericRow
 from pypaimon.table.row.internal_row import InternalRow
+from pypaimon.utils.roaring_bitmap import RoaringBitmap
 
 
 def _make(identifier, sql_type, options: CoreOptions = None):
@@ -2472,6 +2474,45 @@ class FieldThetaSketchAggTest(unittest.TestCase):
         self.assertEqual(agg.agg(acc2, input_val), acc2)
 
 
+class FieldRoaringBitmap32AggTest(unittest.TestCase):
+
+    def test_field_roaring_bitmap32_agg(self):
+        agg = _make("rbm32", "VARBINARY(20)")
+        self.assertIsInstance(agg, FieldRoaringBitmap32Agg)
+
+        input_rbm = RoaringBitmap()
+        acc1_rbm = RoaringBitmap()
+        acc2_rbm = RoaringBitmap()
+
+        input_rbm.add(1)
+        acc1_rbm.add_range(2, 3)
+        acc2_rbm.add_range(1, 3)
+
+        input_val = input_rbm.serialize()
+        acc1 = acc1_rbm.serialize()
+        acc2 = acc2_rbm.serialize()
+
+        self.assertIsNone(agg.agg(None, None))
+
+        result1 = agg.agg(None, input_val)
+        self.assertEqual(result1, input_val)
+
+        result2 = agg.agg(acc1, None)
+        self.assertEqual(result2, acc1)
+
+        result3 = agg.agg(acc1, input_val)
+        self.assertEqual(result3, acc2)
+
+        result4 = agg.agg(acc2, input_val)
+        self.assertEqual(result4, acc2)
+
+    def test_field_roaring_bitmap32_requires_varbinary(self):
+        with self.assertRaises(ValueError) as ctx:
+            _make("rbm32", "VARCHAR(20)")
+
+        self.assertIn("VARBINARY", str(ctx.exception))
+
+
 class RegistrationTest(unittest.TestCase):
     """Sanity check that all 10 expected aggregators (the primary-key
     placeholder plus 9 value aggregators) are registered when the

Reply via email to