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

polyzos pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 68ad16c79 [FIP-37] Add bitmap infrastructure: BitmapUtils, 
RoaringBitmapSerializer, AbstractRbAggFunction (#3319)
68ad16c79 is described below

commit 68ad16c799a4bbf19be9582c1f5b2209beca2503
Author: Prajwal Banakar <[email protected]>
AuthorDate: Tue May 26 14:48:58 2026 +0530

    [FIP-37] Add bitmap infrastructure: BitmapUtils, RoaringBitmapSerializer, 
AbstractRbAggFunction (#3319)
    
    * [FIP-37] Add bitmap infrastructure: BitmapUtils, RoaringBitmapSerializer, 
AbstractRbAggFunction
    
    * Add RoaringBitmapSerializer tests and fix jacoco coverage
    
    * Add missing RoaringBitmapTypeInfo coverage tests
    
    * Address review comments for bitmap infrastructure
    
    * small improvements
    
    ---------
    
    Co-authored-by: ipolyzos <[email protected]>
---
 fluss-flink/fluss-flink-common/pom.xml             |   7 +
 .../functions/bitmap/AbstractRbAggFunction.java    |  79 ++++++
 .../fluss/flink/functions/bitmap/BitmapUtils.java  |  70 +++++
 .../functions/bitmap/RoaringBitmapSerializer.java  | 114 +++++++++
 .../functions/bitmap/RoaringBitmapTypeInfo.java    |  98 +++++++
 .../bitmap/AbstractRbAggFunctionITCase.java        |  91 +++++++
 .../flink/functions/bitmap/BitmapUtilsTest.java    | 100 ++++++++
 .../bitmap/RoaringBitmapSerializerTest.java        | 281 +++++++++++++++++++++
 8 files changed, 840 insertions(+)

diff --git a/fluss-flink/fluss-flink-common/pom.xml 
b/fluss-flink/fluss-flink-common/pom.xml
index 026649f54..c1835466f 100644
--- a/fluss-flink/fluss-flink-common/pom.xml
+++ b/fluss-flink/fluss-flink-common/pom.xml
@@ -95,6 +95,13 @@
             <scope>provided</scope>
         </dependency>
 
+        <!-- RoaringBitmap for bitmap SQL functions (FIP-37) -->
+        <dependency>
+            <groupId>org.roaringbitmap</groupId>
+            <artifactId>RoaringBitmap</artifactId>
+            <version>${roaringbitmap.version}</version>
+        </dependency>
+
         <!-- test dependency -->
         <dependency>
             <groupId>org.apache.fluss</groupId>
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/AbstractRbAggFunction.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/AbstractRbAggFunction.java
new file mode 100644
index 000000000..1690a757f
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/AbstractRbAggFunction.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.apache.fluss.exception.FlussRuntimeException;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.table.annotation.DataTypeHint;
+import org.apache.flink.table.annotation.FunctionHint;
+import org.apache.flink.table.functions.AggregateFunction;
+import org.roaringbitmap.RoaringBitmap;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * Shared base for bitmap aggregate UDFs that use {@link RoaringBitmap} as the 
accumulator.
+ *
+ * <p>The {@code @FunctionHint} annotation with {@code accumulator = 
@DataTypeHint("RAW")} tells
+ * Flink's Table planner to skip reflection-based POJO extraction and instead 
use the {@link
+ * TypeInformation} returned by {@link #getAccumulatorType()}, which provides 
the custom {@link
+ * RoaringBitmapSerializer}. Without this annotation, Flink attempts POJO 
field extraction on
+ * RoaringBitmap and fails.
+ */
+@FunctionHint(accumulator = @DataTypeHint(value = "RAW", bridgedTo = 
RoaringBitmap.class))
+abstract class AbstractRbAggFunction extends AggregateFunction<byte[], 
RoaringBitmap> {
+
+    @Override
+    public RoaringBitmap createAccumulator() {
+        return new RoaringBitmap();
+    }
+
+    /** Merges partial accumulators, required for two-phase aggregation in the 
Flink Table API. */
+    public void merge(RoaringBitmap acc, Iterable<RoaringBitmap> it) {
+        for (RoaringBitmap other : it) {
+            if (other != null) {
+                acc.or(other);
+            }
+        }
+    }
+
+    public void resetAccumulator(RoaringBitmap acc) {
+        acc.clear();
+    }
+
+    @Override
+    @Nullable
+    public byte[] getValue(RoaringBitmap acc) {
+        if (acc == null || acc.isEmpty()) {
+            return null;
+        }
+        try {
+            return BitmapUtils.toBytes(acc);
+        } catch (IOException e) {
+            throw new FlussRuntimeException("Failed to serialize bitmap 
accumulator.", e);
+        }
+    }
+
+    @Override
+    public TypeInformation<RoaringBitmap> getAccumulatorType() {
+        return RoaringBitmapTypeInfo.INSTANCE;
+    }
+}
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/BitmapUtils.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/BitmapUtils.java
new file mode 100644
index 000000000..48c249d64
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/BitmapUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.roaringbitmap.RoaringBitmap;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for serializing and deserializing {@link RoaringBitmap}.
+ *
+ * <p>Uses the ByteBuffer-based serialization approach, which is the preferred 
method recommended by
+ * the RoaringBitmap library. This format is compatible with the server-side 
{@code
+ * RoaringBitmapUtils.serializeRoaringBitmap32} used by {@code 
FieldRoaringBitmap32Agg}.
+ */
+public final class BitmapUtils {
+
+    private BitmapUtils() {}
+
+    /**
+     * Serializes a {@link RoaringBitmap} to a byte array.
+     *
+     * @param bitmap the bitmap to serialize; null returns null
+     * @return serialized byte array, or null if input is null
+     */
+    @Nullable
+    public static byte[] toBytes(@Nullable RoaringBitmap bitmap) throws 
IOException {
+        if (bitmap == null) {
+            return null;
+        }
+        bitmap.runOptimize();
+        ByteBuffer buffer = 
ByteBuffer.allocate(bitmap.serializedSizeInBytes());
+        bitmap.serialize(buffer);
+        return buffer.array();
+    }
+
+    /**
+     * Deserializes a {@link RoaringBitmap} from a byte array.
+     *
+     * @param bytes the serialized bitmap bytes; null returns null
+     * @return deserialized RoaringBitmap, or null if input is null
+     */
+    @Nullable
+    public static RoaringBitmap fromBytes(@Nullable byte[] bytes) throws 
IOException {
+        if (bytes == null) {
+            return null;
+        }
+        RoaringBitmap bitmap = new RoaringBitmap();
+        bitmap.deserialize(ByteBuffer.wrap(bytes));
+        return bitmap;
+    }
+}
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapSerializer.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapSerializer.java
new file mode 100644
index 000000000..27ad9e927
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapSerializer.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.apache.flink.api.common.typeutils.SimpleTypeSerializerSnapshot;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.common.typeutils.base.TypeSerializerSingleton;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+import org.roaringbitmap.RoaringBitmap;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import java.io.IOException;
+
+/**
+ * Flink {@link org.apache.flink.api.common.typeutils.TypeSerializer} for 
{@link RoaringBitmap}.
+ *
+ * <p>Used as the accumulator serializer for bitmap aggregate functions to 
ensure correct
+ * checkpoint/savepoint behavior. Without a custom serializer, Flink falls 
back to Kryo which is
+ * sensitive to internal class layout changes across RoaringBitmap library 
versions.
+ */
+@ThreadSafe
+public final class RoaringBitmapSerializer extends 
TypeSerializerSingleton<RoaringBitmap> {
+
+    public static final RoaringBitmapSerializer INSTANCE = new 
RoaringBitmapSerializer();
+
+    private static final long serialVersionUID = 1L;
+
+    private RoaringBitmapSerializer() {}
+
+    @Override
+    public boolean isImmutableType() {
+        return false;
+    }
+
+    @Override
+    public RoaringBitmap createInstance() {
+        return new RoaringBitmap();
+    }
+
+    @Override
+    public RoaringBitmap copy(RoaringBitmap from) {
+        return from.clone();
+    }
+
+    @Override
+    public RoaringBitmap copy(RoaringBitmap from, RoaringBitmap reuse) {
+        return from.clone();
+    }
+
+    @Override
+    public int getLength() {
+        return -1;
+    }
+
+    @Override
+    public void serialize(RoaringBitmap record, DataOutputView target) throws 
IOException {
+        byte[] bytes = BitmapUtils.toBytes(record);
+        target.writeInt(bytes.length);
+        target.write(bytes);
+    }
+
+    @Override
+    public RoaringBitmap deserialize(DataInputView source) throws IOException {
+        int size = source.readInt();
+        byte[] bytes = new byte[size];
+        source.readFully(bytes);
+        return BitmapUtils.fromBytes(bytes);
+    }
+
+    @Override
+    public RoaringBitmap deserialize(RoaringBitmap reuse, DataInputView 
source) throws IOException {
+        return deserialize(source);
+    }
+
+    @Override
+    public void copy(DataInputView source, DataOutputView target) throws 
IOException {
+        int size = source.readInt();
+        target.writeInt(size);
+        byte[] buffer = new byte[size];
+        source.readFully(buffer);
+        target.write(buffer);
+    }
+
+    @Override
+    public TypeSerializerSnapshot<RoaringBitmap> snapshotConfiguration() {
+        return new RoaringBitmapSerializerSnapshot();
+    }
+
+    /** Snapshot for {@link RoaringBitmapSerializer}. */
+    public static final class RoaringBitmapSerializerSnapshot
+            extends SimpleTypeSerializerSnapshot<RoaringBitmap> {
+
+        public RoaringBitmapSerializerSnapshot() {
+            super(() -> INSTANCE);
+        }
+    }
+}
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapTypeInfo.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapTypeInfo.java
new file mode 100644
index 000000000..fe0905523
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapTypeInfo.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.apache.flink.api.common.ExecutionConfig;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.roaringbitmap.RoaringBitmap;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import java.util.Objects;
+
+/**
+ * {@link TypeInformation} for {@link RoaringBitmap}.
+ *
+ * <p>Provides the custom {@link RoaringBitmapSerializer} to Flink's type 
system, ensuring correct
+ * checkpoint and savepoint behavior for bitmap aggregate function 
accumulators.
+ */
+@ThreadSafe
+public final class RoaringBitmapTypeInfo extends 
TypeInformation<RoaringBitmap> {
+
+    public static final RoaringBitmapTypeInfo INSTANCE = new 
RoaringBitmapTypeInfo();
+
+    private static final long serialVersionUID = 1L;
+
+    private RoaringBitmapTypeInfo() {}
+
+    @Override
+    public boolean isBasicType() {
+        return false;
+    }
+
+    @Override
+    public boolean isTupleType() {
+        return false;
+    }
+
+    @Override
+    public int getArity() {
+        return 1;
+    }
+
+    @Override
+    public int getTotalFields() {
+        return 1;
+    }
+
+    @Override
+    public Class<RoaringBitmap> getTypeClass() {
+        return RoaringBitmap.class;
+    }
+
+    @Override
+    public boolean isKeyType() {
+        return false;
+    }
+
+    @Override
+    public TypeSerializer<RoaringBitmap> createSerializer(ExecutionConfig 
config) {
+        return RoaringBitmapSerializer.INSTANCE;
+    }
+
+    @Override
+    public String toString() {
+        return "RoaringBitmapTypeInfo";
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof RoaringBitmapTypeInfo && 
((RoaringBitmapTypeInfo) obj).canEqual(this);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getTypeClass());
+    }
+
+    @Override
+    public boolean canEqual(Object obj) {
+        return obj instanceof RoaringBitmapTypeInfo;
+    }
+}
diff --git 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/AbstractRbAggFunctionITCase.java
 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/AbstractRbAggFunctionITCase.java
new file mode 100644
index 000000000..51a3e2a50
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/AbstractRbAggFunctionITCase.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.apache.flink.table.api.EnvironmentSettings;
+import org.apache.flink.table.api.Table;
+import org.apache.flink.table.api.TableEnvironment;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.CollectionUtil;
+import org.junit.jupiter.api.Test;
+import org.roaringbitmap.RoaringBitmap;
+
+import java.util.List;
+
+import static org.apache.flink.table.api.Expressions.$;
+import static org.apache.flink.table.api.Expressions.call;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * End-to-end smoke test that validates the Flink Table planner accepts the 
{@link
+ * AbstractRbAggFunction} contract — namely the {@code 
@FunctionHint(accumulator
+ * = @DataTypeHint(value = "RAW", bridgedTo = RoaringBitmap.class))} hint 
together with the custom
+ * {@link RoaringBitmapTypeInfo} returned from {@link 
AbstractRbAggFunction#getAccumulatorType()}.
+ *
+ * <p>Direct method-level invocation of the aggregate function does not 
exercise the Flink planner's
+ * type-extraction path, so this IT test wires a minimal concrete subclass 
through a batch {@link
+ * TableEnvironment} to verify the planner accepts and runs it.
+ */
+class AbstractRbAggFunctionITCase {
+
+    /** Minimal concrete subclass used to exercise the planner. */
+    public static final class TestRbAggFunction extends AbstractRbAggFunction {
+        public void accumulate(RoaringBitmap acc, Integer value) {
+            if (value != null) {
+                acc.add(value);
+            }
+        }
+    }
+
+    @Test
+    void testPlannerAcceptsRawAccumulatorHintAndProducesCorrectResult() throws 
Exception {
+        TableEnvironment tEnv = 
TableEnvironment.create(EnvironmentSettings.inBatchMode());
+        tEnv.createTemporarySystemFunction("test_rb_agg", new 
TestRbAggFunction());
+
+        Table source = tEnv.fromValues(1, 2, 3, 1, 2).as("v");
+        Table result = source.select(call("test_rb_agg", $("v")));
+
+        List<Row> rows = 
CollectionUtil.iteratorToList(result.execute().collect());
+        assertThat(rows).hasSize(1);
+
+        byte[] bytes = (byte[]) rows.get(0).getField(0);
+        assertThat(bytes).isNotNull();
+
+        RoaringBitmap restored = BitmapUtils.fromBytes(bytes);
+        assertThat(restored).isNotNull();
+        assertThat(restored.getLongCardinality()).isEqualTo(3L);
+        assertThat(restored.contains(1)).isTrue();
+        assertThat(restored.contains(2)).isTrue();
+        assertThat(restored.contains(3)).isTrue();
+    }
+
+    @Test
+    void testPlannerReturnsNullForEmptyInput() throws Exception {
+        TableEnvironment tEnv = 
TableEnvironment.create(EnvironmentSettings.inBatchMode());
+        tEnv.createTemporarySystemFunction("test_rb_agg", new 
TestRbAggFunction());
+
+        // Filter eliminates all rows; non-grouped aggregation over zero rows 
must emit
+        // one row whose value is the result of getValue() on an empty 
accumulator (null).
+        Table source = tEnv.fromValues(1, 2, 
3).as("v").filter($("v").isGreater(100));
+        Table result = source.select(call("test_rb_agg", $("v")));
+
+        List<Row> rows = 
CollectionUtil.iteratorToList(result.execute().collect());
+        assertThat(rows).hasSize(1);
+        assertThat(rows.get(0).getField(0)).isNull();
+    }
+}
diff --git 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/BitmapUtilsTest.java
 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/BitmapUtilsTest.java
new file mode 100644
index 000000000..fa0a42ab6
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/BitmapUtilsTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.junit.jupiter.api.Test;
+import org.roaringbitmap.RoaringBitmap;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Unit tests for {@link BitmapUtils}. */
+class BitmapUtilsTest {
+
+    @Test
+    void testNullInputToBytes() throws IOException {
+        assertThat(BitmapUtils.toBytes(null)).isNull();
+    }
+
+    @Test
+    void testNullInputFromBytes() throws IOException {
+        assertThat(BitmapUtils.fromBytes(null)).isNull();
+    }
+
+    @Test
+    void testEmptyBitmapRoundTrip() throws IOException {
+        RoaringBitmap bitmap = new RoaringBitmap();
+        byte[] bytes = BitmapUtils.toBytes(bitmap);
+        assertThat(bytes).isNotNull();
+        RoaringBitmap result = BitmapUtils.fromBytes(bytes);
+        assertThat(result).isNotNull();
+        assertThat(result.isEmpty()).isTrue();
+    }
+
+    @Test
+    void testKnownValuesRoundTrip() throws IOException {
+        RoaringBitmap bitmap = new RoaringBitmap();
+        bitmap.add(1);
+        bitmap.add(100);
+        bitmap.add(1000);
+        bitmap.add(Integer.MAX_VALUE);
+
+        byte[] bytes = BitmapUtils.toBytes(bitmap);
+        assertThat(bytes).isNotNull();
+
+        RoaringBitmap result = BitmapUtils.fromBytes(bytes);
+        assertThat(result).isNotNull();
+        assertThat(result.getLongCardinality()).isEqualTo(4L);
+        assertThat(result.contains(1)).isTrue();
+        assertThat(result.contains(100)).isTrue();
+        assertThat(result.contains(1000)).isTrue();
+        assertThat(result.contains(Integer.MAX_VALUE)).isTrue();
+        assertThat(result.contains(2)).isFalse();
+    }
+
+    @Test
+    void testLargeCardinality() throws IOException {
+        RoaringBitmap bitmap = new RoaringBitmap();
+        for (int i = 0; i < 100_000; i++) {
+            bitmap.add(i);
+        }
+        byte[] bytes = BitmapUtils.toBytes(bitmap);
+        RoaringBitmap result = BitmapUtils.fromBytes(bytes);
+        assertThat(result.getLongCardinality()).isEqualTo(100_000L);
+    }
+
+    @Test
+    void testProducesSameBytesAsServerSerializationRecipe() throws IOException 
{
+        // Use a bitmap that benefits from runOptimize so the test would catch
+        // removal of runOptimize from one side but not the other.
+        RoaringBitmap original = new RoaringBitmap();
+        original.add(0L, 50_000L);
+
+        byte[] clientBytes = BitmapUtils.toBytes(original.clone());
+
+        RoaringBitmap forServer = original.clone();
+        forServer.runOptimize();
+        ByteBuffer buffer = 
ByteBuffer.allocate(forServer.serializedSizeInBytes());
+        forServer.serialize(buffer);
+        byte[] serverRecipeBytes = buffer.array();
+
+        assertThat(clientBytes).isEqualTo(serverRecipeBytes);
+    }
+}
diff --git 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapSerializerTest.java
 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapSerializerTest.java
new file mode 100644
index 000000000..75e0309aa
--- /dev/null
+++ 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RoaringBitmapSerializerTest.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.flink.functions.bitmap;
+
+import org.apache.flink.api.common.ExecutionConfig;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputSerializer;
+import org.junit.jupiter.api.Test;
+import org.roaringbitmap.RoaringBitmap;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Unit tests for {@link RoaringBitmapSerializer} and {@link 
RoaringBitmapTypeInfo}. */
+class RoaringBitmapSerializerTest {
+
+    private final RoaringBitmapSerializer serializer = 
RoaringBitmapSerializer.INSTANCE;
+
+    /** Minimal concrete implementation used only for testing 
AbstractRbAggFunction. */
+    private static final class TestRbAggFunction extends AbstractRbAggFunction 
{
+
+        public void accumulate(RoaringBitmap acc, Integer value) {
+            if (value != null) {
+                acc.add(value);
+            }
+        }
+    }
+
+    private final TestRbAggFunction aggFunction = new TestRbAggFunction();
+
+    @Test
+    void testCreateInstance() {
+        RoaringBitmap instance = serializer.createInstance();
+        assertThat(instance).isNotNull();
+        assertThat(instance.isEmpty()).isTrue();
+    }
+
+    @Test
+    void testIsNotImmutable() {
+        assertThat(serializer.isImmutableType()).isFalse();
+    }
+
+    @Test
+    void testGetLengthIsMinusOne() {
+        assertThat(serializer.getLength()).isEqualTo(-1);
+    }
+
+    @Test
+    void testSerializeDeserializeRoundTrip() throws Exception {
+        RoaringBitmap original = new RoaringBitmap();
+        original.add(1);
+        original.add(100);
+        original.add(100_000);
+
+        DataOutputSerializer out = new DataOutputSerializer(256);
+        serializer.serialize(original, out);
+
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RoaringBitmap restored = serializer.deserialize(in);
+
+        assertThat(restored).isEqualTo(original);
+        assertThat(restored.getLongCardinality()).isEqualTo(3L);
+    }
+
+    @Test
+    void testDeserializeWithReuse() throws Exception {
+        RoaringBitmap original = RoaringBitmap.bitmapOf(42, 99, 1000);
+
+        DataOutputSerializer out = new DataOutputSerializer(256);
+        serializer.serialize(original, out);
+
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RoaringBitmap reuse = new RoaringBitmap();
+        RoaringBitmap restored = serializer.deserialize(reuse, in);
+
+        assertThat(restored).isEqualTo(original);
+    }
+
+    @Test
+    void testCopy() {
+        RoaringBitmap original = RoaringBitmap.bitmapOf(1, 2, 3);
+        RoaringBitmap copy = serializer.copy(original);
+
+        assertThat(copy).isEqualTo(original);
+        copy.add(999);
+        assertThat(original.contains(999)).isFalse();
+    }
+
+    @Test
+    void testCopyWithReuse() {
+        RoaringBitmap original = RoaringBitmap.bitmapOf(10, 20, 30);
+        RoaringBitmap reuse = new RoaringBitmap();
+        RoaringBitmap copy = serializer.copy(original, reuse);
+
+        assertThat(copy).isEqualTo(original);
+    }
+
+    @Test
+    void testSerializeWithRunOptimizableBitmap() throws Exception {
+        RoaringBitmap original = new RoaringBitmap();
+        original.add(0L, 50_000L);
+
+        DataOutputSerializer out = new DataOutputSerializer(64 * 1024);
+        serializer.serialize(original, out);
+
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RoaringBitmap restored = serializer.deserialize(in);
+
+        assertThat(restored).isEqualTo(original);
+        assertThat(restored.getLongCardinality()).isEqualTo(50_000L);
+        // The deserializer must have consumed exactly the bytes we wrote.
+        assertThat(in.available()).isZero();
+    }
+
+    @Test
+    void testCopyStreamWithRunOptimizableBitmap() throws Exception {
+        RoaringBitmap original = new RoaringBitmap();
+        original.add(0L, 50_000L);
+
+        DataOutputSerializer out = new DataOutputSerializer(64 * 1024);
+        serializer.serialize(original, out);
+
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        DataOutputSerializer copied = new DataOutputSerializer(64 * 1024);
+        serializer.copy(in, copied);
+
+        RoaringBitmap restored =
+                serializer.deserialize(new 
DataInputDeserializer(copied.getCopyOfBuffer()));
+        assertThat(restored).isEqualTo(original);
+    }
+
+    @Test
+    void testEmptyBitmapRoundTrip() throws Exception {
+        RoaringBitmap empty = new RoaringBitmap();
+
+        DataOutputSerializer out = new DataOutputSerializer(64);
+        serializer.serialize(empty, out);
+
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RoaringBitmap restored = serializer.deserialize(in);
+
+        assertThat(restored.isEmpty()).isTrue();
+    }
+
+    @Test
+    void testSnapshotConfiguration() {
+        assertThat(serializer.snapshotConfiguration()).isNotNull();
+    }
+
+    @Test
+    void testTypeInfoGetTypeClass() {
+        
assertThat(RoaringBitmapTypeInfo.INSTANCE.getTypeClass()).isEqualTo(RoaringBitmap.class);
+    }
+
+    @Test
+    void testTypeInfoCreateSerializer() {
+        TypeSerializer<RoaringBitmap> s =
+                RoaringBitmapTypeInfo.INSTANCE.createSerializer(new 
ExecutionConfig());
+        assertThat(s).isInstanceOf(RoaringBitmapSerializer.class);
+    }
+
+    @Test
+    void testTypeInfoEquality() {
+        
assertThat(RoaringBitmapTypeInfo.INSTANCE.equals(RoaringBitmapTypeInfo.INSTANCE)).isTrue();
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.equals("other")).isFalse();
+    }
+
+    @Test
+    void testTypeInfoIsNotKeyType() {
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.isKeyType()).isFalse();
+    }
+
+    @Test
+    void testTypeInfoArity() {
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.getArity()).isEqualTo(1);
+        
assertThat(RoaringBitmapTypeInfo.INSTANCE.getTotalFields()).isEqualTo(1);
+    }
+
+    @Test
+    void testTypeInfoIsNotBasicType() {
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.isBasicType()).isFalse();
+    }
+
+    @Test
+    void testTypeInfoIsNotTupleType() {
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.isTupleType()).isFalse();
+    }
+
+    @Test
+    void testTypeInfoToString() {
+        
assertThat(RoaringBitmapTypeInfo.INSTANCE.toString()).isEqualTo("RoaringBitmapTypeInfo");
+    }
+
+    @Test
+    void testTypeInfoHashCode() {
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.hashCode()).isNotZero();
+    }
+
+    @Test
+    void testTypeInfoCanEqual() {
+        
assertThat(RoaringBitmapTypeInfo.INSTANCE.canEqual(RoaringBitmapTypeInfo.INSTANCE))
+                .isTrue();
+        assertThat(RoaringBitmapTypeInfo.INSTANCE.canEqual("other")).isFalse();
+    }
+
+    @Test
+    void testAggCreateAccumulator() {
+        RoaringBitmap acc = aggFunction.createAccumulator();
+        assertThat(acc).isNotNull();
+        assertThat(acc.isEmpty()).isTrue();
+    }
+
+    @Test
+    void testAggGetValue() throws Exception {
+        RoaringBitmap acc = aggFunction.createAccumulator();
+        aggFunction.accumulate(acc, 1);
+        aggFunction.accumulate(acc, 2);
+
+        byte[] result = aggFunction.getValue(acc);
+
+        assertThat(result).isNotNull();
+        RoaringBitmap restored = BitmapUtils.fromBytes(result);
+        assertThat(restored).isNotNull();
+        assertThat(restored.getLongCardinality()).isEqualTo(2L);
+        assertThat(restored.contains(1)).isTrue();
+        assertThat(restored.contains(2)).isTrue();
+    }
+
+    @Test
+    void testAggGetValueNullOnEmpty() {
+        RoaringBitmap acc = aggFunction.createAccumulator();
+        assertThat(aggFunction.getValue(acc)).isNull();
+    }
+
+    @Test
+    void testAggMerge() {
+        RoaringBitmap acc1 = aggFunction.createAccumulator();
+        aggFunction.accumulate(acc1, 1);
+        aggFunction.accumulate(acc1, 2);
+
+        RoaringBitmap acc2 = aggFunction.createAccumulator();
+        aggFunction.accumulate(acc2, 3);
+
+        aggFunction.merge(acc1, Collections.singletonList(acc2));
+        assertThat(acc1.getLongCardinality()).isEqualTo(3L);
+        assertThat(acc1.contains(3)).isTrue();
+    }
+
+    @Test
+    void testAggResetAccumulator() {
+        RoaringBitmap acc = aggFunction.createAccumulator();
+        acc.add(1);
+        acc.add(2);
+
+        aggFunction.resetAccumulator(acc);
+
+        assertThat(acc.isEmpty()).isTrue();
+    }
+
+    @Test
+    void testAggGetAccumulatorType() {
+        
assertThat(aggFunction.getAccumulatorType()).isInstanceOf(RoaringBitmapTypeInfo.class);
+    }
+}


Reply via email to