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 ab493cd98 [FIP-37] Add bitmap aggregate functions: rb_build_agg,
rb_or_agg, rb_and_agg and register via FlussCatalog (#3398)
ab493cd98 is described below
commit ab493cd987ea1eef91a33b7e43a532eb27a71ca8
Author: Prajwal Banakar <[email protected]>
AuthorDate: Mon Jun 8 21:21:44 2026 +0530
[FIP-37] Add bitmap aggregate functions: rb_build_agg, rb_or_agg,
rb_and_agg and register via FlussCatalog (#3398)
* Add bitmap aggregate functions: rb_build_agg, rb_or_agg, rb_and_agg and
register via FlussCatalog
* Improve coverage for RbAndAggFunction inner classes
* add RbFunctionsCatalogITCase and address review comments
* add some small improvements
---------
Co-authored-by: ipolyzos <[email protected]>
---
.../apache/fluss/flink/catalog/FlinkCatalog.java | 37 ++-
.../flink/functions/bitmap/RbAndAggFunction.java | 310 +++++++++++++++++++
.../flink/functions/bitmap/RbBuildAggFunction.java | 58 ++++
.../flink/functions/bitmap/RbOrAggFunction.java | 58 ++++
.../fluss/flink/catalog/FlinkCatalogTest.java | 18 +-
.../flink/functions/bitmap/RbAggFunctionsTest.java | 343 +++++++++++++++++++++
.../functions/bitmap/RbFunctionsCatalogITCase.java | 197 ++++++++++++
fluss-test-coverage/pom.xml | 3 +
8 files changed, 1016 insertions(+), 8 deletions(-)
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/catalog/FlinkCatalog.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/catalog/FlinkCatalog.java
index 8781cd674..103bf311b 100644
---
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/catalog/FlinkCatalog.java
+++
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/catalog/FlinkCatalog.java
@@ -25,6 +25,9 @@ import org.apache.fluss.config.Configuration;
import org.apache.fluss.exception.InvalidTableException;
import org.apache.fluss.flink.FlinkConnectorOptions;
import org.apache.fluss.flink.adapter.CatalogTableAdapter;
+import org.apache.fluss.flink.functions.bitmap.RbAndAggFunction;
+import org.apache.fluss.flink.functions.bitmap.RbBuildAggFunction;
+import org.apache.fluss.flink.functions.bitmap.RbOrAggFunction;
import org.apache.fluss.flink.lake.LakeFlinkCatalog;
import org.apache.fluss.flink.procedure.ProcedureManager;
import org.apache.fluss.flink.utils.CatalogExceptionUtils;
@@ -48,11 +51,13 @@ import org.apache.flink.table.catalog.CatalogBaseTable;
import org.apache.flink.table.catalog.CatalogDatabase;
import org.apache.flink.table.catalog.CatalogDatabaseImpl;
import org.apache.flink.table.catalog.CatalogFunction;
+import org.apache.flink.table.catalog.CatalogFunctionImpl;
import org.apache.flink.table.catalog.CatalogMaterializedTable;
import org.apache.flink.table.catalog.CatalogPartition;
import org.apache.flink.table.catalog.CatalogPartitionSpec;
import org.apache.flink.table.catalog.CatalogTable;
import org.apache.flink.table.catalog.CatalogView;
+import org.apache.flink.table.catalog.FunctionLanguage;
import org.apache.flink.table.catalog.ObjectPath;
import org.apache.flink.table.catalog.ResolvedCatalogBaseTable;
import org.apache.flink.table.catalog.ResolvedCatalogMaterializedTable;
@@ -83,6 +88,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -134,6 +140,16 @@ public class FlinkCatalog extends AbstractCatalog {
protected Connection connection;
protected Admin admin;
+ private static final Map<String, String> BUILTIN_BITMAP_FUNCTIONS;
+
+ static {
+ Map<String, String> map = new HashMap<>();
+ map.put("rb_build_agg", RbBuildAggFunction.class.getName());
+ map.put("rb_or_agg", RbOrAggFunction.class.getName());
+ map.put("rb_and_agg", RbAndAggFunction.class.getName());
+ BUILTIN_BITMAP_FUNCTIONS = Collections.unmodifiableMap(map);
+ }
+
public FlinkCatalog(
String name,
String defaultDatabase,
@@ -746,19 +762,26 @@ public class FlinkCatalog extends AbstractCatalog {
}
@Override
- public List<String> listFunctions(String s) throws
DatabaseNotExistException, CatalogException {
- return Collections.emptyList();
+ public List<String> listFunctions(String dbName)
+ throws DatabaseNotExistException, CatalogException {
+ return new ArrayList<>(BUILTIN_BITMAP_FUNCTIONS.keySet());
}
@Override
- public CatalogFunction getFunction(ObjectPath functionPath)
- throws FunctionNotExistException, CatalogException {
- throw new FunctionNotExistException(getName(), functionPath);
+ public boolean functionExists(ObjectPath objectPath) throws
CatalogException {
+ return BUILTIN_BITMAP_FUNCTIONS.containsKey(
+ objectPath.getObjectName().toLowerCase(Locale.ROOT));
}
@Override
- public boolean functionExists(ObjectPath objectPath) throws
CatalogException {
- return false;
+ public CatalogFunction getFunction(ObjectPath functionPath)
+ throws FunctionNotExistException, CatalogException {
+ String className =
+
BUILTIN_BITMAP_FUNCTIONS.get(functionPath.getObjectName().toLowerCase(Locale.ROOT));
+ if (className == null) {
+ throw new FunctionNotExistException(getName(), functionPath);
+ }
+ return new CatalogFunctionImpl(className, FunctionLanguage.JAVA);
}
@Override
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbAndAggFunction.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbAndAggFunction.java
new file mode 100644
index 000000000..4564955b8
--- /dev/null
+++
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbAndAggFunction.java
@@ -0,0 +1,310 @@
+/*
+ * 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.ExecutionConfig;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.common.typeutils.SimpleTypeSerializerSnapshot;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+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.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 javax.annotation.concurrent.ThreadSafe;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * {@code rb_and_agg(bitmap BYTES) -> BYTES}
+ *
+ * <p>Intersects multiple serialized {@link RoaringBitmap} values using
bitwise AND across rows.
+ *
+ * <p>Unlike {@link RbOrAggFunction}, this function requires a custom {@link
Accumulator} that
+ * carries an {@code initialized} flag alongside the bitmap. This is necessary
because an empty
+ * bitmap cannot be used as a sentinel for "no input received yet": once the
AND result becomes the
+ * empty set, it must stay empty even if further inputs arrive, whereas a
truly uninitialized
+ * accumulator must be seeded with the first input via OR before AND can
proceed.
+ *
+ * <p>Note: there is no server-side {@code FieldRoaringBitmapAndAgg}
counterpart. This function
+ * executes entirely in Flink. Users should be aware that combining it with
{@code
+ * table.merge-engine=aggregation} may produce unexpected results during
server-side compaction.
+ *
+ * <p>Returns {@code null} when all inputs are null or the input set is empty.
+ */
+@FunctionHint(
+ accumulator = @DataTypeHint(value = "RAW", bridgedTo =
RbAndAggFunction.Accumulator.class))
+public class RbAndAggFunction extends AggregateFunction<byte[],
RbAndAggFunction.Accumulator> {
+
+ //
-------------------------------------------------------------------------
+ // Accumulator
+ //
-------------------------------------------------------------------------
+
+ /** Mutable accumulator that tracks initialization state for AND
aggregation. */
+ public static final class Accumulator {
+
+ /** True after the first non-null input has been accumulated. */
+ public boolean initialized = false;
+
+ /** Current AND result; meaningless if {@code initialized} is false. */
+ public RoaringBitmap value = new RoaringBitmap();
+ }
+
+ //
-------------------------------------------------------------------------
+ // AggregateFunction implementation
+ //
-------------------------------------------------------------------------
+
+ @Override
+ public Accumulator createAccumulator() {
+ return new Accumulator();
+ }
+
+ /**
+ * Intersects the input bitmap into the accumulator.
+ *
+ * @param acc the running accumulator
+ * @param bitmapBytes serialized RoaringBitmap bytes; null and empty
arrays are ignored
+ */
+ public void accumulate(Accumulator acc, @Nullable byte[] bitmapBytes)
throws IOException {
+ if (bitmapBytes == null || bitmapBytes.length == 0) {
+ return;
+ }
+ RoaringBitmap input = BitmapUtils.fromBytes(bitmapBytes);
+ if (!acc.initialized) {
+ acc.value.or(input);
+ acc.initialized = true;
+ } else {
+ acc.value.and(input);
+ }
+ }
+
+ /** Merges partial accumulators, required for two-phase aggregation in the
Flink Table API. */
+ public void merge(Accumulator acc, Iterable<Accumulator> it) {
+ for (Accumulator other : it) {
+ if (!other.initialized) {
+ continue;
+ }
+ if (!acc.initialized) {
+ acc.value.or(other.value);
+ acc.initialized = true;
+ } else {
+ acc.value.and(other.value);
+ }
+ }
+ }
+
+ public void resetAccumulator(Accumulator acc) {
+ acc.initialized = false;
+ acc.value.clear();
+ }
+
+ public void retract(Accumulator acc, byte[] bitmapBytes) {
+ throw new UnsupportedOperationException(
+ "rb_and_agg does not support retraction. " + "Use it only on
append-only streams.");
+ }
+
+ @Override
+ @Nullable
+ public byte[] getValue(Accumulator acc) {
+ if (!acc.initialized || acc.value.isEmpty()) {
+ return null;
+ }
+ try {
+ return BitmapUtils.toBytes(acc.value);
+ } catch (IOException e) {
+ throw new FlussRuntimeException("Failed to serialize rb_and_agg
accumulator.", e);
+ }
+ }
+
+ @Override
+ public TypeInformation<Accumulator> getAccumulatorType() {
+ return AccumulatorTypeInfo.INSTANCE;
+ }
+
+ //
-------------------------------------------------------------------------
+ // TypeSerializer and TypeInformation for Accumulator
+ //
-------------------------------------------------------------------------
+
+ /** {@link TypeInformation} for {@link Accumulator}. */
+ @ThreadSafe
+ public static final class AccumulatorTypeInfo extends
TypeInformation<Accumulator> {
+
+ public static final AccumulatorTypeInfo INSTANCE = new
AccumulatorTypeInfo();
+
+ private static final long serialVersionUID = 1L;
+
+ private AccumulatorTypeInfo() {}
+
+ @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<Accumulator> getTypeClass() {
+ return Accumulator.class;
+ }
+
+ @Override
+ public boolean isKeyType() {
+ return false;
+ }
+
+ @Override
+ public TypeSerializer<Accumulator> createSerializer(ExecutionConfig
config) {
+ return AccumulatorSerializer.INSTANCE;
+ }
+
+ @Override
+ public String toString() {
+ return "RbAndAccumulatorTypeInfo";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof AccumulatorTypeInfo &&
((AccumulatorTypeInfo) obj).canEqual(this);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getTypeClass());
+ }
+
+ @Override
+ public boolean canEqual(Object obj) {
+ return obj instanceof AccumulatorTypeInfo;
+ }
+ }
+
+ /** {@link TypeSerializer} for {@link Accumulator}. */
+ @ThreadSafe
+ public static final class AccumulatorSerializer extends
TypeSerializerSingleton<Accumulator> {
+
+ public static final AccumulatorSerializer INSTANCE = new
AccumulatorSerializer();
+
+ private static final long serialVersionUID = 1L;
+
+ private AccumulatorSerializer() {}
+
+ @Override
+ public boolean isImmutableType() {
+ return false;
+ }
+
+ @Override
+ public Accumulator createInstance() {
+ return new Accumulator();
+ }
+
+ @Override
+ public Accumulator copy(Accumulator from) {
+ Accumulator copy = new Accumulator();
+ copy.initialized = from.initialized;
+ copy.value = from.value.clone();
+ return copy;
+ }
+
+ @Override
+ public Accumulator copy(Accumulator from, Accumulator reuse) {
+ return copy(from);
+ }
+
+ @Override
+ public int getLength() {
+ return -1;
+ }
+
+ @Override
+ public void serialize(Accumulator record, DataOutputView target)
throws IOException {
+ target.writeBoolean(record.initialized);
+ if (record.initialized) {
+ byte[] bytes = BitmapUtils.toBytes(record.value);
+ target.writeInt(bytes.length);
+ target.write(bytes);
+ }
+ }
+
+ @Override
+ public Accumulator deserialize(DataInputView source) throws
IOException {
+ Accumulator acc = new Accumulator();
+ acc.initialized = source.readBoolean();
+ if (acc.initialized) {
+ int size = source.readInt();
+ byte[] bytes = new byte[size];
+ source.readFully(bytes);
+ acc.value = BitmapUtils.fromBytes(bytes);
+ }
+ return acc;
+ }
+
+ @Override
+ public Accumulator deserialize(Accumulator reuse, DataInputView
source) throws IOException {
+ return deserialize(source);
+ }
+
+ @Override
+ public void copy(DataInputView source, DataOutputView target) throws
IOException {
+ boolean initialized = source.readBoolean();
+ target.writeBoolean(initialized);
+ if (initialized) {
+ int size = source.readInt();
+ target.writeInt(size);
+ byte[] buffer = new byte[size];
+ source.readFully(buffer);
+ target.write(buffer);
+ }
+ }
+
+ @Override
+ public TypeSerializerSnapshot<Accumulator> snapshotConfiguration() {
+ return new AccumulatorSerializerSnapshot();
+ }
+
+ /** Snapshot for {@link AccumulatorSerializer}. */
+ public static final class AccumulatorSerializerSnapshot
+ extends SimpleTypeSerializerSnapshot<Accumulator> {
+
+ public AccumulatorSerializerSnapshot() {
+ super(() -> INSTANCE);
+ }
+ }
+ }
+}
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbBuildAggFunction.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbBuildAggFunction.java
new file mode 100644
index 000000000..547290986
--- /dev/null
+++
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbBuildAggFunction.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * {@code rb_build_agg(value INT) -> BYTES}
+ *
+ * <p>Aggregates a column of 32-bit integer values into a single serialized
{@link RoaringBitmap}.
+ * Each non-null integer is added to the bitmap during accumulation. Duplicate
values are silently
+ * ignored — the bitmap stores each integer at most once.
+ *
+ * <p>Returns {@code null} when the input set is empty.
+ */
+public class RbBuildAggFunction extends AbstractRbAggFunction {
+
+ /**
+ * Adds a single integer value to the accumulator bitmap.
+ *
+ * @param acc the running bitmap accumulator
+ * @param value the integer value to add; null values are ignored
+ */
+ public void accumulate(RoaringBitmap acc, @Nullable Integer value) {
+ if (value == null) {
+ return;
+ }
+ acc.add(value);
+ }
+
+ /**
+ * Retraction is not supported for bitmap build — integer addition is not
reversible.
+ *
+ * @throws UnsupportedOperationException always
+ */
+ public void retract(RoaringBitmap acc, @Nullable Integer value) {
+ throw new UnsupportedOperationException(
+ "rb_build_agg does not support retraction. "
+ + "Use it only on append-only streams.");
+ }
+}
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbOrAggFunction.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbOrAggFunction.java
new file mode 100644
index 000000000..87b4d9851
--- /dev/null
+++
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/functions/bitmap/RbOrAggFunction.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+/**
+ * {@code rb_or_agg(bitmap BYTES) -> BYTES}
+ *
+ * <p>Aggregates multiple serialized {@link RoaringBitmap} values using
bitwise OR (union). Used in
+ * roll-up aggregation where per-group bitmaps need to be merged into a
coarser granularity.
+ *
+ * <p>Null and empty inputs are ignored. Returns {@code null} when all inputs
are null.
+ */
+public class RbOrAggFunction extends AbstractRbAggFunction {
+
+ /**
+ * Unions the input bitmap into the accumulator.
+ *
+ * @param acc the running bitmap accumulator
+ * @param bitmapBytes serialized RoaringBitmap bytes; null and empty
arrays are ignored
+ */
+ public void accumulate(RoaringBitmap acc, @Nullable byte[] bitmapBytes)
throws IOException {
+ if (bitmapBytes == null || bitmapBytes.length == 0) {
+ return;
+ }
+ acc.or(BitmapUtils.fromBytes(bitmapBytes));
+ }
+
+ /**
+ * Retraction is not supported for bitmap OR — union is not reversible.
+ *
+ * @throws UnsupportedOperationException always
+ */
+ public void retract(RoaringBitmap acc, @Nullable byte[] bitmapBytes) {
+ throw new UnsupportedOperationException(
+ "rb_or_agg does not support retraction. " + "Use it only on
append-only streams.");
+ }
+}
diff --git
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/catalog/FlinkCatalogTest.java
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/catalog/FlinkCatalogTest.java
index b941b04f3..8354fbf20 100644
---
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/catalog/FlinkCatalogTest.java
+++
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/catalog/FlinkCatalogTest.java
@@ -1007,7 +1007,7 @@ class FlinkCatalogTest {
// Test functions operations
List<String> functions = catalog.listFunctions(DEFAULT_DB);
- assertThat(functions).isEmpty();
+ assertThat(functions).contains("rb_build_agg", "rb_or_agg",
"rb_and_agg");
ObjectPath functionPath = new ObjectPath(DEFAULT_DB, "testFunction");
assertThat(catalog.functionExists(functionPath)).isFalse();
@@ -1102,4 +1102,20 @@ class FlinkCatalogTest {
catalog.createTable(tablePath, table, false);
}
}
+
+ @Test
+ void testBitmapFunctionsRegistered() throws Exception {
+
+ List<String> functions = catalog.listFunctions(DEFAULT_DB);
+
+ assertThat(functions).contains("rb_build_agg", "rb_or_agg",
"rb_and_agg");
+
+ assertThat(catalog.functionExists(new ObjectPath(DEFAULT_DB,
"rb_build_agg"))).isTrue();
+
+ assertThat(catalog.functionExists(new ObjectPath(DEFAULT_DB,
"rb_or_agg"))).isTrue();
+
+ assertThat(catalog.functionExists(new ObjectPath(DEFAULT_DB,
"rb_and_agg"))).isTrue();
+
+ assertThat(catalog.functionExists(new ObjectPath(DEFAULT_DB,
"unknown_fn"))).isFalse();
+ }
}
diff --git
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RbAggFunctionsTest.java
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RbAggFunctionsTest.java
new file mode 100644
index 000000000..1f9e91496
--- /dev/null
+++
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RbAggFunctionsTest.java
@@ -0,0 +1,343 @@
+/*
+ * 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.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Unit tests for {@link RbBuildAggFunction}, {@link RbOrAggFunction}, {@link
RbAndAggFunction}. */
+class RbAggFunctionsTest {
+
+ //
-------------------------------------------------------------------------
+ // RbBuildAggFunction
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testBuildAggBasic() throws IOException {
+ RbBuildAggFunction fn = new RbBuildAggFunction();
+ RoaringBitmap acc = fn.createAccumulator();
+
+ fn.accumulate(acc, 1);
+ fn.accumulate(acc, 2);
+ fn.accumulate(acc, 1); // duplicate — ignored
+ fn.accumulate(acc, null); // null — ignored
+
+ byte[] result = fn.getValue(acc);
+ assertThat(result).isNotNull();
+ RoaringBitmap restored = BitmapUtils.fromBytes(result);
+ assertThat(restored.getLongCardinality()).isEqualTo(2L);
+ assertThat(restored.contains(1)).isTrue();
+ assertThat(restored.contains(2)).isTrue();
+ }
+
+ @Test
+ void testBuildAggEmptyReturnsNull() {
+ RbBuildAggFunction fn = new RbBuildAggFunction();
+ RoaringBitmap acc = fn.createAccumulator();
+ assertThat(fn.getValue(acc)).isNull();
+ }
+
+ @Test
+ void testBuildAggMerge() throws IOException {
+ RbBuildAggFunction fn = new RbBuildAggFunction();
+ RoaringBitmap acc1 = fn.createAccumulator();
+ fn.accumulate(acc1, 1);
+ fn.accumulate(acc1, 2);
+
+ RoaringBitmap acc2 = fn.createAccumulator();
+ fn.accumulate(acc2, 3);
+
+ fn.merge(acc1, Collections.singletonList(acc2));
+ assertThat(acc1.getLongCardinality()).isEqualTo(3L);
+ assertThat(acc1.contains(3)).isTrue();
+ }
+
+ @Test
+ void testBuildAggRetractThrows() {
+ RbBuildAggFunction fn = new RbBuildAggFunction();
+ assertThatThrownBy(() -> fn.retract(fn.createAccumulator(), 1))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ //
-------------------------------------------------------------------------
+ // RbOrAggFunction
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testOrAggBasic() throws IOException {
+ RbOrAggFunction fn = new RbOrAggFunction();
+ RoaringBitmap acc = fn.createAccumulator();
+
+ byte[] bitmap1 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2, 3));
+ byte[] bitmap2 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(3, 4, 5));
+
+ fn.accumulate(acc, bitmap1);
+ fn.accumulate(acc, bitmap2);
+ fn.accumulate(acc, null); // ignored
+ fn.accumulate(acc, new byte[0]); // ignored
+
+ byte[] result = fn.getValue(acc);
+ assertThat(result).isNotNull();
+ RoaringBitmap restored = BitmapUtils.fromBytes(result);
+ assertThat(restored.getLongCardinality()).isEqualTo(5L);
+ assertThat(restored.contains(1)).isTrue();
+ assertThat(restored.contains(5)).isTrue();
+ }
+
+ @Test
+ void testOrAggEmptyReturnsNull() {
+ RbOrAggFunction fn = new RbOrAggFunction();
+ assertThat(fn.getValue(fn.createAccumulator())).isNull();
+ }
+
+ @Test
+ void testOrAggMerge() throws IOException {
+ RbOrAggFunction fn = new RbOrAggFunction();
+ RoaringBitmap acc1 = fn.createAccumulator();
+ fn.accumulate(acc1, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2)));
+
+ RoaringBitmap acc2 = fn.createAccumulator();
+ fn.accumulate(acc2, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(3, 4)));
+
+ fn.merge(acc1, Collections.singletonList(acc2));
+ assertThat(acc1.getLongCardinality()).isEqualTo(4L);
+ }
+
+ //
-------------------------------------------------------------------------
+ // RbAndAggFunction
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testAndAggBasic() throws IOException {
+ RbAndAggFunction fn = new RbAndAggFunction();
+ RbAndAggFunction.Accumulator acc = fn.createAccumulator();
+
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2,
3)));
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(2, 3,
4)));
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(3, 5,
6)));
+
+ byte[] result = fn.getValue(acc);
+ assertThat(result).isNotNull();
+ RoaringBitmap restored = BitmapUtils.fromBytes(result);
+ assertThat(restored.getLongCardinality()).isEqualTo(1L);
+ assertThat(restored.contains(3)).isTrue();
+ }
+
+ @Test
+ void testAndAggNullInputsIgnored() throws IOException {
+ RbAndAggFunction fn = new RbAndAggFunction();
+ RbAndAggFunction.Accumulator acc = fn.createAccumulator();
+
+ fn.accumulate(acc, null);
+ fn.accumulate(acc, new byte[0]);
+ assertThat(fn.getValue(acc)).isNull(); // no real input
+
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2)));
+ assertThat(fn.getValue(acc)).isNotNull();
+ }
+
+ @Test
+ void testAndAggEmptyIntersectionReturnsNull() throws IOException {
+ RbAndAggFunction fn = new RbAndAggFunction();
+ RbAndAggFunction.Accumulator acc = fn.createAccumulator();
+
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2)));
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(3, 4)));
+ // Intersection of {1,2} and {3,4} is empty
+ assertThat(fn.getValue(acc)).isNull();
+ }
+
+ @Test
+ void testAndAggMerge() throws IOException {
+ RbAndAggFunction fn = new RbAndAggFunction();
+
+ RbAndAggFunction.Accumulator acc1 = fn.createAccumulator();
+ fn.accumulate(acc1, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2,
3)));
+
+ RbAndAggFunction.Accumulator acc2 = fn.createAccumulator();
+ fn.accumulate(acc2, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(2, 3,
4)));
+
+ fn.merge(acc1, Collections.singletonList(acc2));
+ byte[] result = fn.getValue(acc1);
+ assertThat(result).isNotNull();
+ RoaringBitmap restored = BitmapUtils.fromBytes(result);
+ assertThat(restored.getLongCardinality()).isEqualTo(2L);
+ assertThat(restored.contains(2)).isTrue();
+ assertThat(restored.contains(3)).isTrue();
+ }
+
+ @Test
+ void testAndAggResetAccumulator() throws IOException {
+ RbAndAggFunction fn = new RbAndAggFunction();
+ RbAndAggFunction.Accumulator acc = fn.createAccumulator();
+
+ fn.accumulate(acc, BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2)));
+ fn.resetAccumulator(acc);
+ assertThat(acc.initialized).isFalse();
+ assertThat(fn.getValue(acc)).isNull();
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerRoundTrip() throws Exception {
+ RbAndAggFunction.AccumulatorSerializer ser =
+ RbAndAggFunction.AccumulatorSerializer.INSTANCE;
+
+ RbAndAggFunction.Accumulator original = new
RbAndAggFunction.Accumulator();
+ original.initialized = true;
+ original.value = RoaringBitmap.bitmapOf(10, 20, 30);
+
+ DataOutputSerializer out = new DataOutputSerializer(256);
+ ser.serialize(original, out);
+
+ DataInputDeserializer in = new
DataInputDeserializer(out.getCopyOfBuffer());
+ RbAndAggFunction.Accumulator restored = ser.deserialize(in);
+
+ assertThat(restored.initialized).isTrue();
+ assertThat(restored.value).isEqualTo(original.value);
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerUninitializedRoundTrip() throws
Exception {
+ RbAndAggFunction.AccumulatorSerializer ser =
+ RbAndAggFunction.AccumulatorSerializer.INSTANCE;
+
+ RbAndAggFunction.Accumulator original = new
RbAndAggFunction.Accumulator();
+
+ DataOutputSerializer out = new DataOutputSerializer(64);
+ ser.serialize(original, out);
+
+ DataInputDeserializer in = new
DataInputDeserializer(out.getCopyOfBuffer());
+ RbAndAggFunction.Accumulator restored = ser.deserialize(in);
+ assertThat(restored.initialized).isFalse();
+ }
+
+ //
-------------------------------------------------------------------------
+ // RbAndAggFunction inner class coverage
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testAndAggAccumulatorTypeInfoProperties() {
+ RbAndAggFunction.AccumulatorTypeInfo info =
RbAndAggFunction.AccumulatorTypeInfo.INSTANCE;
+
assertThat(info.getTypeClass()).isEqualTo(RbAndAggFunction.Accumulator.class);
+ assertThat(info.isBasicType()).isFalse();
+ assertThat(info.isTupleType()).isFalse();
+ assertThat(info.getArity()).isEqualTo(1);
+ assertThat(info.getTotalFields()).isEqualTo(1);
+ assertThat(info.isKeyType()).isFalse();
+ assertThat(info.toString()).isEqualTo("RbAndAccumulatorTypeInfo");
+ assertThat(info.hashCode()).isNotZero();
+ assertThat(info.equals(info)).isTrue();
+ assertThat(info.equals("other")).isFalse();
+ assertThat(info.canEqual(info)).isTrue();
+ assertThat(info.canEqual("other")).isFalse();
+ }
+
+ @Test
+ void testAndAggAccumulatorTypeInfoCreateSerializer() {
+ TypeSerializer<RbAndAggFunction.Accumulator> s =
+ RbAndAggFunction.AccumulatorTypeInfo.INSTANCE.createSerializer(
+ new ExecutionConfig());
+
assertThat(s).isInstanceOf(RbAndAggFunction.AccumulatorSerializer.class);
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerCreateInstance() {
+ RbAndAggFunction.AccumulatorSerializer ser =
+ RbAndAggFunction.AccumulatorSerializer.INSTANCE;
+ RbAndAggFunction.Accumulator acc = ser.createInstance();
+ assertThat(acc.initialized).isFalse();
+ assertThat(acc.value.isEmpty()).isTrue();
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerIsNotImmutable() {
+
assertThat(RbAndAggFunction.AccumulatorSerializer.INSTANCE.isImmutableType()).isFalse();
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerGetLength() {
+
assertThat(RbAndAggFunction.AccumulatorSerializer.INSTANCE.getLength()).isEqualTo(-1);
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerCopy() {
+ RbAndAggFunction.AccumulatorSerializer ser =
+ RbAndAggFunction.AccumulatorSerializer.INSTANCE;
+ RbAndAggFunction.Accumulator original = new
RbAndAggFunction.Accumulator();
+ original.initialized = true;
+ original.value = RoaringBitmap.bitmapOf(1, 2, 3);
+
+ RbAndAggFunction.Accumulator copy = ser.copy(original);
+ assertThat(copy.initialized).isTrue();
+ assertThat(copy.value).isEqualTo(original.value);
+ // Verify deep copy
+ copy.value.add(999);
+ assertThat(original.value.contains(999)).isFalse();
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerCopyWithReuse() {
+ RbAndAggFunction.AccumulatorSerializer ser =
+ RbAndAggFunction.AccumulatorSerializer.INSTANCE;
+ RbAndAggFunction.Accumulator original = new
RbAndAggFunction.Accumulator();
+ original.initialized = true;
+ original.value = RoaringBitmap.bitmapOf(10, 20);
+
+ RbAndAggFunction.Accumulator reuse = new
RbAndAggFunction.Accumulator();
+ RbAndAggFunction.Accumulator copy = ser.copy(original, reuse);
+ assertThat(copy.initialized).isTrue();
+ assertThat(copy.value).isEqualTo(original.value);
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerCopyStream() throws Exception {
+ RbAndAggFunction.AccumulatorSerializer ser =
+ RbAndAggFunction.AccumulatorSerializer.INSTANCE;
+ RbAndAggFunction.Accumulator original = new
RbAndAggFunction.Accumulator();
+ original.initialized = true;
+ original.value = RoaringBitmap.bitmapOf(5, 10, 15);
+
+ DataOutputSerializer out = new DataOutputSerializer(256);
+ ser.serialize(original, out);
+
+ DataInputDeserializer in = new
DataInputDeserializer(out.getCopyOfBuffer());
+ DataOutputSerializer copied = new DataOutputSerializer(256);
+ ser.copy(in, copied);
+
+ DataInputDeserializer copiedIn = new
DataInputDeserializer(copied.getCopyOfBuffer());
+ RbAndAggFunction.Accumulator restored = ser.deserialize(copiedIn);
+ assertThat(restored.initialized).isTrue();
+ assertThat(restored.value).isEqualTo(original.value);
+ }
+
+ @Test
+ void testAndAggAccumulatorSerializerSnapshotNotNull() {
+
assertThat(RbAndAggFunction.AccumulatorSerializer.INSTANCE.snapshotConfiguration())
+ .isNotNull();
+ }
+}
diff --git
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RbFunctionsCatalogITCase.java
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RbFunctionsCatalogITCase.java
new file mode 100644
index 000000000..251d3a255
--- /dev/null
+++
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/functions/bitmap/RbFunctionsCatalogITCase.java
@@ -0,0 +1,197 @@
+/*
+ * 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.flink.catalog.FlinkCatalog;
+import org.apache.fluss.server.testutils.FlussClusterExtension;
+
+import org.apache.flink.table.api.DataTypes;
+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.table.api.TableResult;
+import org.apache.flink.types.Row;
+import org.apache.flink.util.CollectionUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.roaringbitmap.RoaringBitmap;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.apache.fluss.config.ConfigOptions.BOOTSTRAP_SERVERS;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * End-to-end integration test that validates bitmap aggregate functions
registered via {@link
+ * FlinkCatalog} are correctly resolved by the Flink Table planner.
+ *
+ * <p>Unlike {@link AbstractRbAggFunctionITCase}, which registers functions
directly via {@link
+ * TableEnvironment#createTemporarySystemFunction}, this test exercises the
full catalog
+ * registration path: functions are looked up through {@code USE CATALOG
fluss_catalog}, validating
+ * that {@link FlinkCatalog#getFunction} returns the correct {@code
CatalogFunctionImpl} and that
+ * the Flink planner correctly instantiates and runs the UDF through real
codegen.
+ */
+class RbFunctionsCatalogITCase {
+
+ @RegisterExtension
+ static final FlussClusterExtension FLUSS_CLUSTER =
+ FlussClusterExtension.builder().setNumOfTabletServers(1).build();
+
+ private static final String CATALOG_NAME = "fluss_catalog";
+ private static final String DATABASE_NAME = "fluss";
+
+ private TableEnvironment tEnv;
+
+ @BeforeEach
+ void beforeEach() throws Exception {
+ String bootstrapServers =
+ String.join(",",
FLUSS_CLUSTER.getClientConfig().get(BOOTSTRAP_SERVERS));
+
+ FlinkCatalog catalog =
+ new FlinkCatalog(
+ CATALOG_NAME,
+ DATABASE_NAME,
+ bootstrapServers,
+ Thread.currentThread().getContextClassLoader(),
+ Collections.emptyMap(),
+ Collections::emptyMap);
+
+ tEnv = TableEnvironment.create(EnvironmentSettings.inBatchMode());
+ tEnv.registerCatalog(CATALOG_NAME, catalog);
+ tEnv.useCatalog(CATALOG_NAME);
+ }
+
+ @Test
+ void testRbBuildAggResolvedFromCatalog() throws Exception {
+ // Use fromValues so no Fluss table is needed for data.
+ // The key assertion: rb_build_agg is resolved from FlinkCatalog, not
manually registered.
+ Table source =
+ tEnv.fromValues(
+ DataTypes.ROW(
+ DataTypes.FIELD("k", DataTypes.INT()),
+ DataTypes.FIELD("v", DataTypes.INT())),
+ Row.of(1, 10),
+ Row.of(1, 20),
+ Row.of(1, 10), // duplicate — should be deduplicated
+ Row.of(2, 30));
+ tEnv.createTemporaryView("events", source);
+
+ TableResult result = tEnv.executeSql("SELECT k, rb_build_agg(v) FROM
events GROUP BY k");
+ List<Row> rows = CollectionUtil.iteratorToList(result.collect());
+
+ assertThat(rows).hasSize(2);
+
+ // Sort by k for deterministic assertion
+ rows.sort((a, b) -> Integer.compare((int) a.getField(0), (int)
b.getField(0)));
+
+ // k=1: bitmap of {10, 20}
+ byte[] group1Bytes = (byte[]) rows.get(0).getField(1);
+ assertThat(group1Bytes).isNotNull();
+ RoaringBitmap group1 = BitmapUtils.fromBytes(group1Bytes);
+ assertThat(group1.getLongCardinality()).isEqualTo(2L);
+ assertThat(group1.contains(10)).isTrue();
+ assertThat(group1.contains(20)).isTrue();
+
+ // k=2: bitmap of {30}
+ byte[] group2Bytes = (byte[]) rows.get(1).getField(1);
+ RoaringBitmap group2 = BitmapUtils.fromBytes(group2Bytes);
+ assertThat(group2.getLongCardinality()).isEqualTo(1L);
+ assertThat(group2.contains(30)).isTrue();
+ }
+
+ @Test
+ void testRbOrAggResolvedFromCatalog() throws Exception {
+ byte[] bitmap1 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2, 3));
+ byte[] bitmap2 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(3, 4, 5));
+
+ Table source =
+ tEnv.fromValues(
+ DataTypes.ROW(
+ DataTypes.FIELD("k", DataTypes.INT()),
+ DataTypes.FIELD("bmap", DataTypes.BYTES())),
+ Row.of(1, bitmap1),
+ Row.of(1, bitmap2));
+ tEnv.createTemporaryView("bitmaps", source);
+
+ TableResult result = tEnv.executeSql("SELECT k, rb_or_agg(bmap) FROM
bitmaps GROUP BY k");
+ List<Row> rows = CollectionUtil.iteratorToList(result.collect());
+
+ assertThat(rows).hasSize(1);
+ byte[] resultBytes = (byte[]) rows.get(0).getField(1);
+ RoaringBitmap union = BitmapUtils.fromBytes(resultBytes);
+ assertThat(union.getLongCardinality()).isEqualTo(5L);
+ assertThat(union.contains(1)).isTrue();
+ assertThat(union.contains(5)).isTrue();
+ }
+
+ @Test
+ void testRbAndAggResolvedFromCatalog() throws Exception {
+ byte[] bitmap1 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2, 3,
4));
+ byte[] bitmap2 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(2, 3, 5));
+
+ Table source =
+ tEnv.fromValues(
+ DataTypes.ROW(
+ DataTypes.FIELD("k", DataTypes.INT()),
+ DataTypes.FIELD("bmap", DataTypes.BYTES())),
+ Row.of(1, bitmap1),
+ Row.of(1, bitmap2));
+ tEnv.createTemporaryView("bitmaps", source);
+
+ TableResult result = tEnv.executeSql("SELECT k, rb_and_agg(bmap) FROM
bitmaps GROUP BY k");
+ List<Row> rows = CollectionUtil.iteratorToList(result.collect());
+
+ assertThat(rows).hasSize(1);
+ byte[] resultBytes = (byte[]) rows.get(0).getField(1);
+ RoaringBitmap intersection = BitmapUtils.fromBytes(resultBytes);
+ assertThat(intersection.getLongCardinality()).isEqualTo(2L);
+ assertThat(intersection.contains(2)).isTrue();
+ assertThat(intersection.contains(3)).isTrue();
+ }
+
+ @Test
+ void testAllThreeFunctionsInSingleQuery() throws Exception {
+ // Validates the PR title claim: all three functions callable after
USE CATALOG
+ // fluss_catalog
+ byte[] bmap1 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(1, 2, 3));
+ byte[] bmap2 = BitmapUtils.toBytes(RoaringBitmap.bitmapOf(2, 3, 4));
+
+ Table source =
+ tEnv.fromValues(
+ DataTypes.ROW(
+ DataTypes.FIELD("k", DataTypes.INT()),
+ DataTypes.FIELD("id", DataTypes.INT()),
+ DataTypes.FIELD("bmap", DataTypes.BYTES())),
+ Row.of(1, 10, bmap1),
+ Row.of(1, 20, bmap2));
+ tEnv.createTemporaryView("events", source);
+
+ TableResult result =
+ tEnv.executeSql(
+ "SELECT k, rb_build_agg(id), rb_or_agg(bmap),
rb_and_agg(bmap) "
+ + "FROM events GROUP BY k");
+ List<Row> rows = CollectionUtil.iteratorToList(result.collect());
+
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getField(1)).isNotNull(); // rb_build_agg
+ assertThat(rows.get(0).getField(2)).isNotNull(); // rb_or_agg
+ assertThat(rows.get(0).getField(3)).isNotNull(); // rb_and_agg
+ }
+}
diff --git a/fluss-test-coverage/pom.xml b/fluss-test-coverage/pom.xml
index 38aeaa989..db770f56c 100644
--- a/fluss-test-coverage/pom.xml
+++ b/fluss-test-coverage/pom.xml
@@ -497,6 +497,9 @@
<exclude>org.apache.fluss.flink.tiering.FlussLakeTieringEntrypoint</exclude>
<exclude>org.apache.fluss.flink.tiering.FlussLakeTiering</exclude>
<!-- end exclude for flink tiering
service -->
+ <exclude>
+
org.apache.fluss.flink.functions.bitmap.RbAndAggFunction.AccumulatorSerializer.AccumulatorSerializerSnapshot
+ </exclude>
<!-- exclude flink compatibility class
for catalogs -->
<exclude>org.apache.flink.table.catalog.*</exclude>
</excludes>