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

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


The following commit(s) were added to refs/heads/main by this push:
     new c421195d16 [VL][Delta] Add native Delta bitmap aggregation support 
(#12214)
c421195d16 is described below

commit c421195d162776b00454a93c72318a9a2972b40a
Author: Mohammad Linjawi <[email protected]>
AuthorDate: Wed Jun 17 18:17:32 2026 +0300

    [VL][Delta] Add native Delta bitmap aggregation support (#12214)
---
 cpp/velox/CMakeLists.txt                           |   1 +
 cpp/velox/benchmarks/CMakeLists.txt                |   2 +
 cpp/velox/benchmarks/DeltaBitmapBenchmark.cc       | 279 +++++++++++++++++++++
 cpp/velox/compute/delta/RoaringBitmapArray.cpp     | 170 ++++++++++++-
 cpp/velox/compute/delta/RoaringBitmapArray.h       |  27 +-
 cpp/velox/compute/delta/tests/CMakeLists.txt       |   2 +-
 .../delta/tests/DeltaBitmapAggregatorTest.cpp      | 140 +++++++++++
 .../compute/delta/tests/RoaringBitmapArrayTest.cpp | 149 ++++++++++-
 .../functions/RegistrationAllFunctions.cc          |   2 +
 .../functions/delta/DeltaBitmapAggregator.cc       | 128 ++++++++++
 .../functions/delta/DeltaBitmapAggregator.h        |  29 +++
 .../substrait/SubstraitToVeloxPlanValidator.cc     |   3 +-
 .../expression/AggregateFunctionsBuilder.scala     |  16 ++
 .../apache/gluten/expression/ExpressionNames.scala |   1 +
 14 files changed, 934 insertions(+), 15 deletions(-)

diff --git a/cpp/velox/CMakeLists.txt b/cpp/velox/CMakeLists.txt
index 2fcc6b9e51..2c98d9c78c 100644
--- a/cpp/velox/CMakeLists.txt
+++ b/cpp/velox/CMakeLists.txt
@@ -175,6 +175,7 @@ set(VELOX_SRCS
     memory/VeloxColumnarBatch.cc
     memory/VeloxMemoryManager.cc
     operators/functions/RegistrationAllFunctions.cc
+    operators/functions/delta/DeltaBitmapAggregator.cc
     operators/functions/RowConstructorWithNull.cc
     operators/functions/SparkExprToSubfieldFilterParser.cc
     operators/plannodes/RowVectorStream.cc
diff --git a/cpp/velox/benchmarks/CMakeLists.txt 
b/cpp/velox/benchmarks/CMakeLists.txt
index fdfd7bda25..43f5763902 100644
--- a/cpp/velox/benchmarks/CMakeLists.txt
+++ b/cpp/velox/benchmarks/CMakeLists.txt
@@ -37,3 +37,5 @@ add_velox_benchmark(parquet_write_benchmark 
ParquetWriteBenchmark.cc)
 add_velox_benchmark(plan_validator_util PlanValidatorUtil.cc)
 
 add_velox_benchmark(velox_batch_resizer_benchmark 
VeloxBatchResizerBenchmark.cc)
+
+add_velox_benchmark(delta_bitmap_benchmark DeltaBitmapBenchmark.cc)
diff --git a/cpp/velox/benchmarks/DeltaBitmapBenchmark.cc 
b/cpp/velox/benchmarks/DeltaBitmapBenchmark.cc
new file mode 100644
index 0000000000..b84d103ef9
--- /dev/null
+++ b/cpp/velox/benchmarks/DeltaBitmapBenchmark.cc
@@ -0,0 +1,279 @@
+/*
+ * 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.
+ */
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <set>
+#include <string>
+#include <vector>
+
+#include <benchmark/benchmark.h>
+
+#include "compute/delta/RoaringBitmapArray.h"
+#include "velox/common/base/Exceptions.h"
+
+using gluten::delta::RoaringBitmapArray;
+
+namespace {
+
+enum class RowIndexPattern {
+  kContiguous,
+  kSparse,
+  kClustered,
+  kMultiBucket,
+};
+
+enum class PartialDistribution {
+  kContiguous,
+  kRoundRobin,
+};
+
+struct RowIndexSummary {
+  uint64_t rowSpan{0};
+  size_t bucketCount{0};
+  double densityPercent{0};
+};
+
+std::vector<uint64_t> makeRowIndexes(size_t rowCount, RowIndexPattern pattern) 
{
+  std::vector<uint64_t> rows;
+  rows.reserve(rowCount);
+  for (size_t i = 0; i < rowCount; ++i) {
+    switch (pattern) {
+      case RowIndexPattern::kContiguous:
+        rows.push_back(i);
+        break;
+      case RowIndexPattern::kSparse:
+        rows.push_back(i * 97);
+        break;
+      case RowIndexPattern::kClustered:
+        rows.push_back((i / 64) * 4096 + (i % 64));
+        break;
+      case RowIndexPattern::kMultiBucket:
+        rows.push_back((static_cast<uint64_t>(i % 4) << 32) + (i / 4));
+        break;
+    }
+  }
+  return rows;
+}
+
+RowIndexSummary summarizeRowIndexes(const std::vector<uint64_t>& rows) {
+  if (rows.empty()) {
+    return {};
+  }
+
+  const auto [minIt, maxIt] = std::minmax_element(rows.begin(), rows.end());
+  std::set<uint32_t> buckets;
+  for (const auto row : rows) {
+    buckets.insert(static_cast<uint32_t>(row >> 32));
+  }
+
+  const auto rowSpan = *maxIt - *minIt + 1;
+  return RowIndexSummary{
+      rowSpan, buckets.size(), static_cast<double>(rows.size()) * 100.0 / 
static_cast<double>(rowSpan)};
+}
+
+std::string buildPayload(const std::vector<uint64_t>& rows, bool optimize) {
+  RoaringBitmapArray bitmap;
+  for (const auto row : rows) {
+    bitmap.addSafe(row);
+  }
+  return bitmap.serializeToString(optimize);
+}
+
+std::vector<std::string> buildPartialPayloads(
+    const std::vector<uint64_t>& rows,
+    size_t partialCount,
+    bool optimize,
+    PartialDistribution distribution) {
+  std::vector<RoaringBitmapArray> partials(partialCount);
+  for (size_t i = 0; i < rows.size(); ++i) {
+    const auto partialIndex = distribution == PartialDistribution::kRoundRobin
+        ? i % partialCount
+        : std::min(i * partialCount / rows.size(), partialCount - 1);
+    partials[partialIndex].addSafe(rows[i]);
+  }
+
+  std::vector<std::string> payloads;
+  payloads.reserve(partialCount);
+  for (const auto& partial : partials) {
+    payloads.push_back(partial.serializeToString(optimize));
+  }
+  return payloads;
+}
+
+std::vector<uint64_t> makeProbeRows(const std::vector<uint64_t>& rows) {
+  const auto hitProbeCount = std::min<size_t>(rows.size(), 4096);
+  std::vector<uint64_t> probes;
+  probes.reserve(hitProbeCount * 2);
+  if (hitProbeCount == 0) {
+    return probes;
+  }
+
+  const auto stride = std::max<size_t>(rows.size() / hitProbeCount, 1);
+  for (size_t i = 0; i < rows.size() && probes.size() < hitProbeCount * 2; i 
+= stride) {
+    probes.push_back(rows[i]);
+    probes.push_back(rows.back() + 4096 + probes.size());
+  }
+  return probes;
+}
+
+void setCounters(
+    benchmark::State& state,
+    size_t rowCount,
+    size_t payloadBytes,
+    RowIndexSummary summary,
+    size_t partialCount = 0) {
+  state.counters["rows"] = benchmark::Counter(rowCount);
+  state.counters["payload_bytes"] = benchmark::Counter(payloadBytes);
+  state.counters["payload_bytes_per_row"] = 
benchmark::Counter(static_cast<double>(payloadBytes) / rowCount);
+  state.counters["row_span"] = benchmark::Counter(summary.rowSpan);
+  state.counters["bucket_count"] = benchmark::Counter(summary.bucketCount);
+  state.counters["density_pct"] = benchmark::Counter(summary.densityPercent);
+  if (partialCount > 0) {
+    state.counters["partials"] = benchmark::Counter(partialCount);
+  }
+}
+
+void BM_BuildAndSerialize(benchmark::State& state, RowIndexPattern pattern) {
+  const auto rows = makeRowIndexes(state.range(0), pattern);
+  const auto summary = summarizeRowIndexes(rows);
+  size_t payloadBytes = 0;
+  uint64_t cardinality = 0;
+
+  for (auto _ : state) {
+    RoaringBitmapArray bitmap;
+    for (const auto row : rows) {
+      bitmap.addSafe(row);
+    }
+    const auto payload = bitmap.serializeToString(true);
+    payloadBytes = payload.size();
+    cardinality = bitmap.cardinality();
+    VELOX_CHECK_EQ(cardinality, rows.size());
+    benchmark::DoNotOptimize(payload);
+  }
+
+  state.SetItemsProcessed(state.iterations() * rows.size());
+  state.SetBytesProcessed(state.iterations() * rows.size() * sizeof(uint64_t));
+  setCounters(state, rows.size(), payloadBytes, summary);
+  state.counters["cardinality"] = benchmark::Counter(cardinality);
+}
+
+void BM_DeserializeAndProbe(benchmark::State& state, RowIndexPattern pattern) {
+  const auto rows = makeRowIndexes(state.range(0), pattern);
+  const auto summary = summarizeRowIndexes(rows);
+  const auto payload = buildPayload(rows, true);
+  const auto probes = makeProbeRows(rows);
+  uint64_t hits = 0;
+
+  for (auto _ : state) {
+    RoaringBitmapArray bitmap;
+    bitmap.deserialize(payload.data(), payload.size());
+    VELOX_CHECK_EQ(bitmap.cardinality(), rows.size());
+    uint64_t localHits = 0;
+    for (const auto probe : probes) {
+      localHits += bitmap.containsSafe(probe) ? 1 : 0;
+    }
+    hits = localHits;
+    benchmark::DoNotOptimize(hits);
+  }
+
+  state.SetItemsProcessed(state.iterations() * probes.size());
+  state.SetBytesProcessed(state.iterations() * payload.size());
+  setCounters(state, rows.size(), payload.size(), summary);
+  state.counters["probes"] = benchmark::Counter(probes.size());
+  state.counters["hits"] = benchmark::Counter(hits);
+}
+
+void BM_MergePartials(benchmark::State& state, RowIndexPattern pattern, 
PartialDistribution distribution) {
+  const auto rows = makeRowIndexes(state.range(0), pattern);
+  const auto summary = summarizeRowIndexes(rows);
+  const auto partialCount = static_cast<size_t>(state.range(1));
+  const auto payloads = buildPartialPayloads(rows, partialCount, false, 
distribution);
+  size_t mergedPayloadBytes = 0;
+  uint64_t cardinality = 0;
+
+  for (auto _ : state) {
+    RoaringBitmapArray merged;
+    for (const auto& payload : payloads) {
+      RoaringBitmapArray partial;
+      partial.deserialize(payload.data(), payload.size());
+      merged.merge(partial);
+    }
+    const auto mergedPayload = merged.serializeToString(true);
+    mergedPayloadBytes = mergedPayload.size();
+    cardinality = merged.cardinality();
+    VELOX_CHECK_EQ(cardinality, rows.size());
+    benchmark::DoNotOptimize(mergedPayload);
+  }
+
+  state.SetItemsProcessed(state.iterations() * rows.size());
+  setCounters(state, rows.size(), mergedPayloadBytes, summary, partialCount);
+  state.counters["cardinality"] = benchmark::Counter(cardinality);
+}
+
+} // namespace
+
+BENCHMARK_CAPTURE(BM_BuildAndSerialize, Contiguous_1M, 
RowIndexPattern::kContiguous)
+    ->Arg(1 << 20)
+    ->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(BM_BuildAndSerialize, Sparse_1M, RowIndexPattern::kSparse)
+    ->Arg(1 << 20)
+    ->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(BM_BuildAndSerialize, Clustered_1M, 
RowIndexPattern::kClustered)
+    ->Arg(1 << 20)
+    ->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(BM_BuildAndSerialize, MultiBucket_256K, 
RowIndexPattern::kMultiBucket)
+    ->Arg(1 << 18)
+    ->Unit(benchmark::kMillisecond);
+
+BENCHMARK_CAPTURE(BM_DeserializeAndProbe, Contiguous_1M, 
RowIndexPattern::kContiguous)
+    ->Arg(1 << 20)
+    ->Unit(benchmark::kMicrosecond);
+BENCHMARK_CAPTURE(BM_DeserializeAndProbe, Sparse_1M, RowIndexPattern::kSparse)
+    ->Arg(1 << 20)
+    ->Unit(benchmark::kMicrosecond);
+BENCHMARK_CAPTURE(BM_DeserializeAndProbe, MultiBucket_256K, 
RowIndexPattern::kMultiBucket)
+    ->Arg(1 << 18)
+    ->Unit(benchmark::kMicrosecond);
+
+BENCHMARK_CAPTURE(
+    BM_MergePartials,
+    Contiguous_1M_64Partials,
+    RowIndexPattern::kContiguous,
+    PartialDistribution::kContiguous)
+    ->Args({1 << 20, 64})
+    ->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(
+    BM_MergePartials,
+    Contiguous_1M_64RoundRobinPartials,
+    RowIndexPattern::kContiguous,
+    PartialDistribution::kRoundRobin)
+    ->Args({1 << 20, 64})
+    ->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(BM_MergePartials, Sparse_1M_64Partials, 
RowIndexPattern::kSparse, PartialDistribution::kContiguous)
+    ->Args({1 << 20, 64})
+    ->Unit(benchmark::kMillisecond);
+BENCHMARK_CAPTURE(
+    BM_MergePartials,
+    MultiBucket_256K_64Partials,
+    RowIndexPattern::kMultiBucket,
+    PartialDistribution::kContiguous)
+    ->Args({1 << 18, 64})
+    ->Unit(benchmark::kMillisecond);
+
+BENCHMARK_MAIN();
diff --git a/cpp/velox/compute/delta/RoaringBitmapArray.cpp 
b/cpp/velox/compute/delta/RoaringBitmapArray.cpp
index 7da3364d0b..696a93f5e0 100644
--- a/cpp/velox/compute/delta/RoaringBitmapArray.cpp
+++ b/cpp/velox/compute/delta/RoaringBitmapArray.cpp
@@ -46,6 +46,14 @@ uint32_t readUint32LittleEndian(const char* data) {
       (static_cast<uint32_t>(bytes[2]) << 16) | 
(static_cast<uint32_t>(bytes[3]) << 24);
 }
 
+uint64_t readUint64LittleEndian(const char* data) {
+  const auto* bytes = reinterpret_cast<const uint8_t*>(data);
+  return static_cast<uint64_t>(bytes[0]) | (static_cast<uint64_t>(bytes[1]) << 
8) |
+      (static_cast<uint64_t>(bytes[2]) << 16) | 
(static_cast<uint64_t>(bytes[3]) << 24) |
+      (static_cast<uint64_t>(bytes[4]) << 32) | 
(static_cast<uint64_t>(bytes[5]) << 40) |
+      (static_cast<uint64_t>(bytes[6]) << 48) | 
(static_cast<uint64_t>(bytes[7]) << 56);
+}
+
 void writeUint32LittleEndian(char* data, uint32_t value) {
   auto* bytes = reinterpret_cast<uint8_t*>(data);
   bytes[0] = static_cast<uint8_t>(value & 0xFF);
@@ -54,33 +62,181 @@ void writeUint32LittleEndian(char* data, uint32_t value) {
   bytes[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
 }
 
+void writeUint64LittleEndian(char* data, uint64_t value) {
+  auto* bytes = reinterpret_cast<uint8_t*>(data);
+  bytes[0] = static_cast<uint8_t>(value & 0xFF);
+  bytes[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
+  bytes[2] = static_cast<uint8_t>((value >> 16) & 0xFF);
+  bytes[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
+  bytes[4] = static_cast<uint8_t>((value >> 32) & 0xFF);
+  bytes[5] = static_cast<uint8_t>((value >> 40) & 0xFF);
+  bytes[6] = static_cast<uint8_t>((value >> 48) & 0xFF);
+  bytes[7] = static_cast<uint8_t>((value >> 56) & 0xFF);
+}
+
+uint32_t highBytes(uint64_t value) {
+  return static_cast<uint32_t>(value >> 32);
+}
+
+uint32_t lowBytes(uint64_t value) {
+  return static_cast<uint32_t>(value);
+}
+
+uint64_t composeFromHighLowBytes(uint32_t high, uint32_t low) {
+  return (static_cast<uint64_t>(high) << 32) | static_cast<uint64_t>(low);
+}
+
 } // namespace
 
 void RoaringBitmapArray::addSafe(uint64_t value) {
-  bitmap_.add(value);
+  VELOX_CHECK_LE(
+      value,
+      kMaxRepresentableValue,
+      "Delta RoaringBitmapArray row index {} exceeds max representable value 
{}",
+      value,
+      kMaxRepresentableValue);
+  bitmaps_[highBytes(value)].add(lowBytes(value));
 }
 
 bool RoaringBitmapArray::containsSafe(uint64_t value) const {
-  return bitmap_.contains(value);
+  VELOX_CHECK_LE(
+      value,
+      kMaxRepresentableValue,
+      "Delta RoaringBitmapArray row index {} exceeds max representable value 
{}",
+      value,
+      kMaxRepresentableValue);
+  auto it = bitmaps_.find(highBytes(value));
+  if (it == bitmaps_.end()) {
+    return false;
+  }
+  return it->second.contains(lowBytes(value));
 }
 
-void RoaringBitmapArray::serialize(char* buffer) const {
+void RoaringBitmapArray::merge(const RoaringBitmapArray& other) {
+  for (const auto& [key, bitmap] : other.bitmaps_) {
+    bitmaps_[key] |= bitmap;
+  }
+}
+
+uint64_t RoaringBitmapArray::cardinality() const {
+  uint64_t cardinality = 0;
+  for (const auto& [_, bitmap] : bitmaps_) {
+    cardinality += bitmap.cardinality();
+  }
+  return cardinality;
+}
+
+std::optional<uint64_t> RoaringBitmapArray::last() const {
+  for (auto it = bitmaps_.rbegin(); it != bitmaps_.rend(); ++it) {
+    if (!it->second.isEmpty()) {
+      return composeFromHighLowBytes(it->first, it->second.maximum());
+    }
+  }
+  return std::nullopt;
+}
+
+void RoaringBitmapArray::serializeBitmap(const BitmapMap& bitmaps, char* 
buffer) {
   VELOX_CHECK_NOT_NULL(buffer, "RoaringBitmapArray serialization buffer is 
null");
+  uint64_t nonEmptyBitmapCount = 0;
+  for (const auto& [_, bitmap] : bitmaps) {
+    if (!bitmap.isEmpty()) {
+      ++nonEmptyBitmapCount;
+    }
+  }
+
   writeUint32LittleEndian(buffer, kPortableSerializationFormatMagicNumber);
-  bitmap_.write(buffer + sizeof(uint32_t), true);
+  buffer += sizeof(uint32_t);
+  writeUint64LittleEndian(buffer, nonEmptyBitmapCount);
+  buffer += sizeof(uint64_t);
+
+  for (const auto& [key, bitmap] : bitmaps) {
+    if (bitmap.isEmpty()) {
+      continue;
+    }
+    writeUint32LittleEndian(buffer, key);
+    buffer += sizeof(uint32_t);
+    buffer += bitmap.write(buffer, true);
+  }
+}
+
+void RoaringBitmapArray::serialize(char* buffer) const {
+  serializeBitmap(bitmaps_, buffer);
+}
+
+std::string RoaringBitmapArray::serializeToString(bool optimize) const {
+  if (!optimize) {
+    std::string out(serializedSizeInBytes(bitmaps_), '\0');
+    serializeBitmap(bitmaps_, out.data());
+    return out;
+  }
+
+  auto bitmaps = bitmaps_;
+  for (auto& [_, bitmap] : bitmaps) {
+    bitmap.runOptimize();
+  }
+
+  std::string out(serializedSizeInBytes(bitmaps), '\0');
+  serializeBitmap(bitmaps, out.data());
+  return out;
 }
 
 void RoaringBitmapArray::deserialize(const char* buffer, size_t size) {
   VELOX_CHECK_NOT_NULL(buffer, "RoaringBitmapArray input buffer is null");
-  VELOX_CHECK_GE(size, sizeof(uint32_t), "RoaringBitmapArray payload is too 
small: {}", size);
+  VELOX_CHECK_GE(size, sizeof(uint32_t) + sizeof(uint64_t), 
"RoaringBitmapArray payload is too small: {}", size);
   const auto magic = readUint32LittleEndian(buffer);
   VELOX_CHECK_EQ(
       magic, kPortableSerializationFormatMagicNumber, "Unexpected 
RoaringBitmapArray magic number {}", magic);
-  bitmap_ = roaring::Roaring64Map::readSafe(buffer + sizeof(uint32_t), size - 
sizeof(uint32_t));
+
+  bitmaps_.clear();
+  const char* cursor = buffer + sizeof(uint32_t);
+  size_t remaining = size - sizeof(uint32_t);
+  const auto bitmapCount = readUint64LittleEndian(cursor);
+  cursor += sizeof(uint64_t);
+  remaining -= sizeof(uint64_t);
+
+  uint32_t previousKey = 0;
+  bool hasPreviousKey = false;
+  for (uint64_t i = 0; i < bitmapCount; ++i) {
+    VELOX_CHECK_GE(remaining, sizeof(uint32_t), "RoaringBitmapArray payload 
ended before bitmap key");
+    const auto key = readUint32LittleEndian(cursor);
+    VELOX_CHECK_LE(key, kMaxHighKey, "RoaringBitmapArray bitmap key {} exceeds 
Delta's max high key", key);
+    cursor += sizeof(uint32_t);
+    remaining -= sizeof(uint32_t);
+
+    if (hasPreviousKey) {
+      VELOX_CHECK_GT(key, previousKey, "RoaringBitmapArray bitmap keys are not 
strictly ascending");
+    }
+    hasPreviousKey = true;
+    previousKey = key;
+
+    const auto bitmapSize = 
roaring::api::roaring_bitmap_portable_deserialize_size(cursor, remaining);
+    VELOX_CHECK_GT(bitmapSize, 0, "Invalid serialized roaring bitmap in 
RoaringBitmapArray");
+    VELOX_CHECK_LE(bitmapSize, remaining, "Serialized roaring bitmap exceeds 
remaining payload size");
+    auto bitmap = roaring::Roaring::readSafe(cursor, remaining);
+    VELOX_CHECK(
+        key != kMaxHighKey || bitmap.isEmpty() || bitmap.maximum() <= 
kMaxLowKeyForMaxHighKey,
+        "RoaringBitmapArray bitmap for max high key exceeds Delta's max 
representable value");
+    bitmaps_.emplace(key, std::move(bitmap));
+    cursor += bitmapSize;
+    remaining -= bitmapSize;
+  }
+
+  VELOX_CHECK_EQ(remaining, 0, "RoaringBitmapArray payload has {} trailing 
bytes", remaining);
 }
 
 size_t RoaringBitmapArray::serializedSizeInBytes() const {
-  return sizeof(uint32_t) + bitmap_.getSizeInBytes(true);
+  return serializedSizeInBytes(bitmaps_);
+}
+
+size_t RoaringBitmapArray::serializedSizeInBytes(const BitmapMap& bitmaps) {
+  size_t size = sizeof(uint32_t) + sizeof(uint64_t);
+  for (const auto& [_, bitmap] : bitmaps) {
+    if (bitmap.isEmpty()) {
+      continue;
+    }
+    size += sizeof(uint32_t) + bitmap.getSizeInBytes(true);
+  }
+  return size;
 }
 
 } // namespace gluten::delta
diff --git a/cpp/velox/compute/delta/RoaringBitmapArray.h 
b/cpp/velox/compute/delta/RoaringBitmapArray.h
index 68eb2c9db3..f456145520 100644
--- a/cpp/velox/compute/delta/RoaringBitmapArray.h
+++ b/cpp/velox/compute/delta/RoaringBitmapArray.h
@@ -34,27 +34,46 @@
 
 #include <cstddef>
 #include <cstdint>
-#include <roaring/roaring64map.hh>
+#include <map>
+#include <optional>
+#include <roaring/roaring.hh>
+#include <string>
 
 namespace gluten::delta {
 
 /// Minimal 64-bit roaring bitmap wrapper for Delta deletion-vector payloads.
-/// This intentionally models the payload format consumed by 
DeltaDeletionVectorReader:
-/// a 4-byte magic number followed by CRoaring's portable Roaring64Map 
serialization.
+/// Delta's JVM implementation stores row indexes as an array of 32-bit 
roaring bitmaps keyed by
+/// the high 32 bits. Keeping the same shape avoids Roaring64Map overhead for 
the common case where
+/// per-file row indexes fit in a single 32-bit bitmap.
 class RoaringBitmapArray {
  public:
   static constexpr uint32_t kPortableSerializationFormatMagicNumber = 
1681511377;
+  // Matches Delta JVM RoaringBitmapArray.MAX_REPRESENTABLE_VALUE.
+  static constexpr uint32_t kMaxHighKey = 0x7ffffffe;
+  static constexpr uint32_t kMaxLowKeyForMaxHighKey = 0x80000000;
+  static constexpr uint64_t kMaxRepresentableValue =
+      (static_cast<uint64_t>(kMaxHighKey) << 32) | kMaxLowKeyForMaxHighKey;
 
   void addSafe(uint64_t value);
   bool containsSafe(uint64_t value) const;
+  void merge(const RoaringBitmapArray& other);
+
+  uint64_t cardinality() const;
+  std::optional<uint64_t> last() const;
 
   void serialize(char* buffer) const;
+  std::string serializeToString(bool optimize = false) const;
   void deserialize(const char* buffer, size_t size);
 
   size_t serializedSizeInBytes() const;
 
  private:
-  roaring::Roaring64Map bitmap_;
+  using BitmapMap = std::map<uint32_t, roaring::Roaring>;
+
+  static void serializeBitmap(const BitmapMap& bitmaps, char* buffer);
+  static size_t serializedSizeInBytes(const BitmapMap& bitmaps);
+
+  BitmapMap bitmaps_;
 };
 
 } // namespace gluten::delta
diff --git a/cpp/velox/compute/delta/tests/CMakeLists.txt 
b/cpp/velox/compute/delta/tests/CMakeLists.txt
index eabe94b722..3c65d8443f 100644
--- a/cpp/velox/compute/delta/tests/CMakeLists.txt
+++ b/cpp/velox/compute/delta/tests/CMakeLists.txt
@@ -25,7 +25,7 @@ add_test(
   WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
 
 add_executable(
-  velox_delta_read_test DeltaConnectorTest.cpp
+  velox_delta_read_test DeltaConnectorTest.cpp DeltaBitmapAggregatorTest.cpp
                         DeltaDeletionVectorReaderTest.cpp DeltaSplitTest.cpp)
 
 target_link_libraries(velox_delta_read_test velox roaring
diff --git a/cpp/velox/compute/delta/tests/DeltaBitmapAggregatorTest.cpp 
b/cpp/velox/compute/delta/tests/DeltaBitmapAggregatorTest.cpp
new file mode 100644
index 0000000000..392dac73f1
--- /dev/null
+++ b/cpp/velox/compute/delta/tests/DeltaBitmapAggregatorTest.cpp
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+
+#include <optional>
+
+#include <gtest/gtest.h>
+
+#include "compute/delta/RoaringBitmapArray.h"
+#include "operators/functions/delta/DeltaBitmapAggregator.h"
+#include "velox/common/base/tests/GTestUtils.h"
+#include "velox/exec/tests/utils/AssertQueryBuilder.h"
+#include "velox/exec/tests/utils/HiveConnectorTestBase.h"
+#include "velox/exec/tests/utils/PlanBuilder.h"
+
+namespace gluten::delta {
+namespace {
+
+using facebook::velox::StringView;
+using facebook::velox::exec::test::AssertQueryBuilder;
+using facebook::velox::exec::test::HiveConnectorTestBase;
+using facebook::velox::exec::test::PlanBuilder;
+
+class DeltaBitmapAggregatorTest : public HiveConnectorTestBase {
+ protected:
+  void SetUp() override {
+    HiveConnectorTestBase::SetUp();
+    gluten::registerDeltaBitmapAggregator();
+  }
+
+  RoaringBitmapArray extractBitmap(
+      const facebook::velox::RowVectorPtr& results,
+      facebook::velox::vector_size_t row,
+      facebook::velox::column_index_t column,
+      int64_t expectedCardinality,
+      std::optional<int64_t> expectedLast) {
+    const auto bitmapResult = 
results->childAt(column)->as<facebook::velox::RowVector>();
+    EXPECT_EQ(bitmapResult->childAt(0)->asFlatVector<int64_t>()->valueAt(row), 
expectedCardinality);
+    if (expectedLast.has_value()) {
+      EXPECT_FALSE(bitmapResult->childAt(1)->isNullAt(row));
+      
EXPECT_EQ(bitmapResult->childAt(1)->asFlatVector<int64_t>()->valueAt(row), 
expectedLast.value());
+    } else {
+      EXPECT_TRUE(bitmapResult->childAt(1)->isNullAt(row));
+    }
+
+    const auto payload = 
bitmapResult->childAt(2)->asFlatVector<StringView>()->valueAt(row);
+    RoaringBitmapArray bitmap;
+    bitmap.deserialize(payload.data(), payload.size());
+    return bitmap;
+  }
+};
+
+TEST_F(DeltaBitmapAggregatorTest, SingleAggregationIgnoresNullsAndDuplicates) {
+  const auto input = makeRowVector(
+      {"row_index"}, {makeNullableFlatVector<int64_t>({1, 7, 7, std::nullopt, 
static_cast<int64_t>(1ULL << 33)})});
+
+  const auto plan =
+      PlanBuilder(pool()).values({input}).singleAggregation({}, 
{"bitmapaggregator(row_index) AS dv"}).planNode();
+  const auto results = AssertQueryBuilder(plan).copyResults(pool());
+  ASSERT_EQ(results->size(), 1);
+
+  const auto bitmap = extractBitmap(results, 0, 0, 3, 
static_cast<int64_t>(1ULL << 33));
+  EXPECT_TRUE(bitmap.containsSafe(1));
+  EXPECT_TRUE(bitmap.containsSafe(7));
+  EXPECT_TRUE(bitmap.containsSafe(1ULL << 33));
+  EXPECT_FALSE(bitmap.containsSafe(2));
+}
+
+TEST_F(DeltaBitmapAggregatorTest, 
PartialFinalAggregationMergesPayloadsByGroup) {
+  const auto input = makeRowVector(
+      {"file_id", "row_index"},
+      {makeFlatVector<int64_t>({10, 10, 20, 10, 20, 20}),
+       makeFlatVector<int64_t>({1, 1, 3, static_cast<int64_t>(1ULL << 32), 5, 
5})});
+
+  const auto plan = PlanBuilder(pool())
+                        .values({input})
+                        .partialAggregation({"file_id"}, 
{"bitmapaggregator(row_index) AS dv"})
+                        .finalAggregation()
+                        .planNode();
+  const auto results = AssertQueryBuilder(plan).copyResults(pool());
+  ASSERT_EQ(results->size(), 2);
+
+  for (facebook::velox::vector_size_t row = 0; row < results->size(); ++row) {
+    const auto fileId = 
results->childAt(0)->asFlatVector<int64_t>()->valueAt(row);
+    if (fileId == 10) {
+      const auto bitmap = extractBitmap(results, row, 1, 2, 
static_cast<int64_t>(1ULL << 32));
+      EXPECT_TRUE(bitmap.containsSafe(1));
+      EXPECT_TRUE(bitmap.containsSafe(1ULL << 32));
+    } else {
+      ASSERT_EQ(fileId, 20);
+      const auto bitmap = extractBitmap(results, row, 1, 2, 5);
+      EXPECT_TRUE(bitmap.containsSafe(3));
+      EXPECT_TRUE(bitmap.containsSafe(5));
+    }
+  }
+}
+
+TEST_F(DeltaBitmapAggregatorTest, AllNullInputProducesEmptyBitmap) {
+  const auto input = makeRowVector({"row_index"}, 
{makeNullableFlatVector<int64_t>({std::nullopt})});
+  const auto plan =
+      PlanBuilder(pool()).values({input}).singleAggregation({}, 
{"bitmapaggregator(row_index) AS dv"}).planNode();
+  const auto results = AssertQueryBuilder(plan).copyResults(pool());
+  ASSERT_EQ(results->size(), 1);
+
+  const auto bitmap = extractBitmap(results, 0, 0, 0, std::nullopt);
+  EXPECT_EQ(bitmap.cardinality(), 0);
+}
+
+TEST_F(DeltaBitmapAggregatorTest, RejectsNegativeRowIndexes) {
+  const auto input = makeRowVector({"row_index"}, 
{makeFlatVector<int64_t>({-1})});
+  const auto plan =
+      PlanBuilder(pool()).values({input}).singleAggregation({}, 
{"bitmapaggregator(row_index) AS dv"}).planNode();
+
+  VELOX_ASSERT_THROW(AssertQueryBuilder(plan).copyResults(pool()), "Delta 
bitmap row index cannot be negative");
+}
+
+TEST_F(DeltaBitmapAggregatorTest, RejectsRowIndexesAboveDeltaMax) {
+  const auto tooLarge = 
static_cast<int64_t>(RoaringBitmapArray::kMaxRepresentableValue + 1);
+  const auto input = makeRowVector({"row_index"}, 
{makeFlatVector<int64_t>({tooLarge})});
+  const auto plan =
+      PlanBuilder(pool()).values({input}).singleAggregation({}, 
{"bitmapaggregator(row_index) AS dv"}).planNode();
+
+  VELOX_ASSERT_THROW(AssertQueryBuilder(plan).copyResults(pool()), "exceeds 
max representable value");
+}
+
+} // namespace
+} // namespace gluten::delta
diff --git a/cpp/velox/compute/delta/tests/RoaringBitmapArrayTest.cpp 
b/cpp/velox/compute/delta/tests/RoaringBitmapArrayTest.cpp
index 0eca672608..7d248ec00f 100644
--- a/cpp/velox/compute/delta/tests/RoaringBitmapArrayTest.cpp
+++ b/cpp/velox/compute/delta/tests/RoaringBitmapArrayTest.cpp
@@ -16,23 +16,48 @@
  */
 
 #include "compute/delta/RoaringBitmapArray.h"
+#include "velox/common/base/Exceptions.h"
+#include "velox/common/base/tests/GTestUtils.h"
 
 #include <memory>
 #include <vector>
 
 #include <gtest/gtest.h>
+#include <roaring/roaring.hh>
 
 namespace gluten::delta {
 namespace {
 
+void writeUint32LittleEndian(char* data, uint32_t value) {
+  auto* bytes = reinterpret_cast<uint8_t*>(data);
+  bytes[0] = static_cast<uint8_t>(value & 0xFF);
+  bytes[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
+  bytes[2] = static_cast<uint8_t>((value >> 16) & 0xFF);
+  bytes[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
+}
+
+void writeUint64LittleEndian(char* data, uint64_t value) {
+  auto* bytes = reinterpret_cast<uint8_t*>(data);
+  bytes[0] = static_cast<uint8_t>(value & 0xFF);
+  bytes[1] = static_cast<uint8_t>((value >> 8) & 0xFF);
+  bytes[2] = static_cast<uint8_t>((value >> 16) & 0xFF);
+  bytes[3] = static_cast<uint8_t>((value >> 24) & 0xFF);
+  bytes[4] = static_cast<uint8_t>((value >> 32) & 0xFF);
+  bytes[5] = static_cast<uint8_t>((value >> 40) & 0xFF);
+  bytes[6] = static_cast<uint8_t>((value >> 48) & 0xFF);
+  bytes[7] = static_cast<uint8_t>((value >> 56) & 0xFF);
+}
+
 TEST(RoaringBitmapArrayTest, SerializeRoundTrip) {
   RoaringBitmapArray bitmap;
   bitmap.addSafe(1);
   bitmap.addSafe(7);
+  bitmap.addSafe(7);
   bitmap.addSafe(1ULL << 33);
 
   std::vector<char> serialized(bitmap.serializedSizeInBytes());
   bitmap.serialize(serialized.data());
+  EXPECT_EQ(serialized.size(), bitmap.serializeToString().size());
 
   RoaringBitmapArray restored;
   restored.deserialize(serialized.data(), serialized.size());
@@ -41,12 +66,132 @@ TEST(RoaringBitmapArrayTest, SerializeRoundTrip) {
   EXPECT_TRUE(restored.containsSafe(7));
   EXPECT_TRUE(restored.containsSafe(1ULL << 33));
   EXPECT_FALSE(restored.containsSafe(2));
+  EXPECT_EQ(restored.cardinality(), 3);
+}
+
+TEST(RoaringBitmapArrayTest, EmptyPortableSerializationMatchesDeltaJvmHeader) {
+  RoaringBitmapArray bitmap;
+  const auto serialized = bitmap.serializeToString();
+
+  const std::vector<uint8_t> expectedBytes{0xd1, 0xd3, 0x39, 0x64, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+  ASSERT_EQ(serialized.size(), expectedBytes.size());
+  for (size_t i = 0; i < expectedBytes.size(); ++i) {
+    EXPECT_EQ(static_cast<uint8_t>(serialized[i]), expectedBytes[i]);
+  }
+
+  RoaringBitmapArray restored;
+  restored.deserialize(serialized.data(), serialized.size());
+  EXPECT_EQ(restored.cardinality(), 0);
+  EXPECT_FALSE(restored.last().has_value());
+}
+
+TEST(RoaringBitmapArrayTest, DeserializesDeltaJvmPortablePayloadWithSparseGap) 
{
+  // Delta 3.3.2 portable serialization for values 1, 7 and 1 << 33.
+  // Delta JVM includes the empty intermediate high-key bucket in this sparse
+  // case; native accepts it but writes the compact portable equivalent.
+  const std::vector<uint8_t> deltaJvmBytes{
+      0xd1, 0xd3, 0x39, 0x64, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x3a, 0x30,
+      0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 
0x00, 0x00, 0x01, 0x00, 0x07, 0x00,
+      0x01, 0x00, 0x00, 0x00, 0x3a, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
0x02, 0x00, 0x00, 0x00, 0x3a, 0x30,
+      0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 
0x00, 0x00, 0x00, 0x00};
+
+  RoaringBitmapArray bitmap;
+  bitmap.deserialize(reinterpret_cast<const char*>(deltaJvmBytes.data()), 
deltaJvmBytes.size());
+
+  EXPECT_EQ(bitmap.cardinality(), 3);
+  EXPECT_TRUE(bitmap.containsSafe(1));
+  EXPECT_TRUE(bitmap.containsSafe(7));
+  EXPECT_TRUE(bitmap.containsSafe(1ULL << 33));
+  ASSERT_TRUE(bitmap.last().has_value());
+  EXPECT_EQ(bitmap.last().value(), 1ULL << 33);
+}
+
+TEST(RoaringBitmapArrayTest, SerializesSparseGapAsCompactPortablePayload) {
+  RoaringBitmapArray bitmap;
+  bitmap.addSafe(1);
+  bitmap.addSafe(7);
+  bitmap.addSafe(1ULL << 33);
+
+  const auto serialized = bitmap.serializeToString();
+
+  const std::vector<uint8_t> expectedBytes{0xd1, 0xd3, 0x39, 0x64, 0x02, 0x00, 
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                           0x00, 0x00, 0x00, 0x00, 0x3a, 0x30, 
0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+                                           0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 
0x00, 0x00, 0x01, 0x00, 0x07, 0x00,
+                                           0x02, 0x00, 0x00, 0x00, 0x3a, 0x30, 
0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+                                           0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 
0x00, 0x00, 0x00, 0x00};
+
+  ASSERT_EQ(serialized.size(), expectedBytes.size());
+  for (size_t i = 0; i < expectedBytes.size(); ++i) {
+    EXPECT_EQ(static_cast<uint8_t>(serialized[i]), expectedBytes[i]);
+  }
+}
+
+TEST(RoaringBitmapArrayTest, MergeCardinalityAndLast) {
+  RoaringBitmapArray left;
+  left.addSafe(1);
+  left.addSafe((1ULL << 32) + 5);
+
+  RoaringBitmapArray right;
+  right.addSafe(1);
+  right.addSafe((2ULL << 32) + 3);
+
+  left.merge(right);
+
+  EXPECT_EQ(left.cardinality(), 3);
+  ASSERT_TRUE(left.last().has_value());
+  EXPECT_EQ(left.last().value(), (2ULL << 32) + 3);
+  EXPECT_TRUE(left.containsSafe(1));
+  EXPECT_TRUE(left.containsSafe((1ULL << 32) + 5));
+  EXPECT_TRUE(left.containsSafe((2ULL << 32) + 3));
+}
+
+TEST(RoaringBitmapArrayTest, EnforcesDeltaRowIndexBounds) {
+  EXPECT_EQ(RoaringBitmapArray::kMaxRepresentableValue, 
9223372030412324864ULL);
+
+  RoaringBitmapArray bitmap;
+  bitmap.addSafe(RoaringBitmapArray::kMaxRepresentableValue);
+  EXPECT_TRUE(bitmap.containsSafe(RoaringBitmapArray::kMaxRepresentableValue));
+
+  VELOX_ASSERT_THROW(bitmap.addSafe(RoaringBitmapArray::kMaxRepresentableValue 
+ 1), "exceeds max representable value");
+  VELOX_ASSERT_THROW(
+      bitmap.containsSafe(RoaringBitmapArray::kMaxRepresentableValue + 1), 
"exceeds max representable value");
+}
+
+TEST(RoaringBitmapArrayTest, RejectsPayloadAboveDeltaMaxRowIndex) {
+  roaring::Roaring highBitmap;
+  highBitmap.add(RoaringBitmapArray::kMaxLowKeyForMaxHighKey + 1);
+
+  std::vector<char> payload(sizeof(uint32_t) + sizeof(uint64_t) + 
sizeof(uint32_t) + highBitmap.getSizeInBytes(true));
+  auto* cursor = payload.data();
+  writeUint32LittleEndian(cursor, 
RoaringBitmapArray::kPortableSerializationFormatMagicNumber);
+  cursor += sizeof(uint32_t);
+  writeUint64LittleEndian(cursor, 1);
+  cursor += sizeof(uint64_t);
+  writeUint32LittleEndian(cursor, RoaringBitmapArray::kMaxHighKey);
+  cursor += sizeof(uint32_t);
+  cursor += highBitmap.write(cursor, true);
+  ASSERT_EQ(cursor, payload.data() + payload.size());
+
+  RoaringBitmapArray bitmap;
+  VELOX_ASSERT_THROW(
+      bitmap.deserialize(payload.data(), payload.size()),
+      "bitmap for max high key exceeds Delta's max representable value");
 }
 
 TEST(RoaringBitmapArrayTest, RejectsBadMagic) {
   RoaringBitmapArray bitmap;
-  std::vector<char> invalid(sizeof(uint32_t), '\0');
-  EXPECT_ANY_THROW(bitmap.deserialize(invalid.data(), invalid.size()));
+  std::vector<char> invalid(sizeof(uint32_t) + sizeof(uint64_t), '\0');
+  VELOX_ASSERT_THROW(bitmap.deserialize(invalid.data(), invalid.size()), 
"Unexpected RoaringBitmapArray magic number");
+}
+
+TEST(RoaringBitmapArrayTest, RejectsTrailingBytes) {
+  RoaringBitmapArray empty;
+  auto serialized = empty.serializeToString();
+  serialized.push_back('\0');
+
+  RoaringBitmapArray bitmap;
+  VELOX_ASSERT_THROW(
+      bitmap.deserialize(serialized.data(), serialized.size()), 
"RoaringBitmapArray payload has 1 trailing bytes");
 }
 
 } // namespace
diff --git a/cpp/velox/operators/functions/RegistrationAllFunctions.cc 
b/cpp/velox/operators/functions/RegistrationAllFunctions.cc
index dd1be7805c..752953390d 100644
--- a/cpp/velox/operators/functions/RegistrationAllFunctions.cc
+++ b/cpp/velox/operators/functions/RegistrationAllFunctions.cc
@@ -19,6 +19,7 @@
 #include "operators/functions/Arithmetic.h"
 #include "operators/functions/RowConstructorWithNull.h"
 #include "operators/functions/RowFunctionWithNull.h"
+#include "operators/functions/delta/DeltaBitmapAggregator.h"
 #include "velox/expression/SpecialFormRegistry.h"
 #include "velox/expression/VectorFunction.h"
 #include "velox/functions/iceberg/Register.h"
@@ -94,6 +95,7 @@ void registerAllFunctions() {
   registerFunctionOverwrite();
 
   velox::functions::iceberg::registerFunctions();
+  registerDeltaBitmapAggregator();
 }
 
 } // namespace gluten
diff --git a/cpp/velox/operators/functions/delta/DeltaBitmapAggregator.cc 
b/cpp/velox/operators/functions/delta/DeltaBitmapAggregator.cc
new file mode 100644
index 0000000000..a1008635ae
--- /dev/null
+++ b/cpp/velox/operators/functions/delta/DeltaBitmapAggregator.cc
@@ -0,0 +1,128 @@
+/*
+ * 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.
+ */
+
+#include "operators/functions/delta/DeltaBitmapAggregator.h"
+
+#include <memory>
+#include <optional>
+#include <tuple>
+#include <vector>
+
+#include "compute/delta/RoaringBitmapArray.h"
+#include "velox/exec/SimpleAggregateAdapter.h"
+#include "velox/expression/FunctionSignature.h"
+#include "velox/type/SimpleFunctionApi.h"
+
+using namespace facebook::velox;
+
+namespace gluten {
+namespace {
+
+class DeltaBitmapAggregatorFunction {
+ public:
+  using InputType = Row<int64_t>;
+  using IntermediateType = Varbinary;
+  using OutputType = Row<int64_t, int64_t, Varbinary>;
+
+  static constexpr bool default_null_behavior_ = false;
+
+  static void addRowIndex(delta::RoaringBitmapArray& bitmap, int64_t value) {
+    VELOX_CHECK_GE(value, 0, "Delta bitmap row index cannot be negative: {}", 
value);
+    bitmap.addSafe(static_cast<uint64_t>(value));
+  }
+
+  static bool toIntermediate(exec::out_type<IntermediateType>& out, 
exec::optional_arg_type<int64_t> value) {
+    delta::RoaringBitmapArray bitmap;
+    if (value.has_value()) {
+      addRowIndex(bitmap, value.value());
+    }
+    const auto serialized = bitmap.serializeToString();
+    out.copy_from(StringView(serialized.data(), serialized.size()));
+    return true;
+  }
+
+  struct AccumulatorType {
+    static constexpr bool use_external_memory_ = true;
+
+    explicit AccumulatorType(HashStringAllocator* /* allocator */, 
DeltaBitmapAggregatorFunction* /* fn */) {}
+
+    bool addInput(HashStringAllocator* /* allocator */, 
exec::optional_arg_type<int64_t> value) {
+      if (!value.has_value()) {
+        return false;
+      }
+      addRowIndex(bitmap, value.value());
+      return true;
+    }
+
+    bool combine(HashStringAllocator* /* allocator */, 
exec::optional_arg_type<IntermediateType> other) {
+      if (!other.has_value()) {
+        return false;
+      }
+      const auto serialized = other.value();
+      delta::RoaringBitmapArray otherBitmap;
+      otherBitmap.deserialize(serialized.data(), serialized.size());
+      bitmap.merge(otherBitmap);
+      return true;
+    }
+
+    bool writeIntermediateResult(bool /* nonNullGroup */, 
exec::out_type<IntermediateType>& out) {
+      const auto serialized = bitmap.serializeToString();
+      out.copy_from(StringView(serialized.data(), serialized.size()));
+      return true;
+    }
+
+    bool writeFinalResult(bool /* nonNullGroup */, exec::out_type<OutputType>& 
out) {
+      const auto serialized = bitmap.serializeToString(true);
+      const auto last = bitmap.last();
+      out.copy_from(std::make_tuple(
+          static_cast<int64_t>(bitmap.cardinality()),
+          last.has_value() ? 
std::optional<int64_t>(static_cast<int64_t>(*last)) : std::nullopt,
+          StringView(serialized.data(), serialized.size())));
+      return true;
+    }
+
+    delta::RoaringBitmapArray bitmap;
+  };
+};
+
+} // namespace
+
+void registerDeltaBitmapAggregator(const std::string& prefix, bool 
withCompanionFunctions, bool overwrite) {
+  std::vector<std::shared_ptr<exec::AggregateFunctionSignature>> signatures{
+      exec::AggregateFunctionSignatureBuilder()
+          .argumentType("bigint")
+          .intermediateType("varbinary")
+          .returnType("row(bigint,bigint,varbinary)")
+          .build()};
+
+  exec::registerAggregateFunction(
+      prefix + "bitmapaggregator",
+      std::move(signatures),
+      [](core::AggregationNode::Step step,
+         const std::vector<TypePtr>& argTypes,
+         const TypePtr& resultType,
+         const core::QueryConfig& /* config */) -> 
std::unique_ptr<exec::Aggregate> {
+        VELOX_CHECK_EQ(argTypes.size(), 1, "bitmapaggregator takes one 
argument");
+        VELOX_CHECK_EQ(argTypes[0]->kind(), exec::isRawInput(step) ? 
TypeKind::BIGINT : TypeKind::VARBINARY);
+        return 
std::make_unique<exec::SimpleAggregateAdapter<DeltaBitmapAggregatorFunction>>(
+            step, argTypes, resultType);
+      },
+      withCompanionFunctions,
+      overwrite);
+}
+
+} // namespace gluten
diff --git a/cpp/velox/operators/functions/delta/DeltaBitmapAggregator.h 
b/cpp/velox/operators/functions/delta/DeltaBitmapAggregator.h
new file mode 100644
index 0000000000..a3dba24809
--- /dev/null
+++ b/cpp/velox/operators/functions/delta/DeltaBitmapAggregator.h
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace gluten {
+
+void registerDeltaBitmapAggregator(
+    const std::string& prefix = "",
+    bool withCompanionFunctions = true,
+    bool overwrite = true);
+
+} // namespace gluten
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc 
b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
index 1b1b2cbe85..9c87b7230c 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
@@ -1324,7 +1324,8 @@ bool SubstraitToVeloxPlanValidator::validate(const 
::substrait::AggregateRel& ag
       "regr_intercept",
       "regr_sxy",
       "regr_replacement",
-      "bitmap_construct_agg"};
+      "bitmap_construct_agg",
+      "bitmapaggregator"};
 
   auto udafFuncs = UdfLoader::getInstance()->getRegisteredUdafNames();
 
diff --git 
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/AggregateFunctionsBuilder.scala
 
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/AggregateFunctionsBuilder.scala
index a567903eda..cdf4ec6642 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/AggregateFunctionsBuilder.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/AggregateFunctionsBuilder.scala
@@ -50,6 +50,8 @@ object AggregateFunctionsBuilder {
         ExpressionNames.FIRST_IGNORE_NULL
       case Last(_, ignoreNulls) if ignoreNulls =>
         ExpressionNames.LAST_IGNORE_NULL
+      case _ if isDeltaBitmapAggregator(aggregateFunc) =>
+        ExpressionNames.BITMAP_AGGREGATOR
       case _ =>
         val nameOpt = 
ExpressionMappings.expressionsMap.get(aggregateFunc.getClass)
         if (nameOpt.isEmpty) {
@@ -62,4 +64,18 @@ object AggregateFunctionsBuilder {
         }
     }
   }
+
+  /**
+   * Matches Delta's `BitmapAggregator` -- the aggregate that builds a 
deletion-vector roaring
+   * bitmap for DELETE/MoR. Matched by `prettyName` so this common module 
(`gluten-substrait`) stays
+   * free of a `delta-spark` dependency.
+   *
+   * No serialization-format check is needed. Delta's only construction site
+   * (`DMLWithDeletionVectorsHelper`) always uses the `Portable` format, which 
is exactly the layout
+   * the native aggregate reads; and a non-Portable payload could not slip 
through silently anyway,
+   * since the native `RoaringBitmapArray::deserialize` rejects any buffer 
whose magic number is not
+   * the Portable one.
+   */
+  private def isDeltaBitmapAggregator(aggregateFunc: AggregateFunction): 
Boolean =
+    aggregateFunc.prettyName == ExpressionNames.BITMAP_AGGREGATOR
 }
diff --git 
a/shims/common/src/main/scala/org/apache/gluten/expression/ExpressionNames.scala
 
b/shims/common/src/main/scala/org/apache/gluten/expression/ExpressionNames.scala
index edd285b0f8..ca05f0ade1 100644
--- 
a/shims/common/src/main/scala/org/apache/gluten/expression/ExpressionNames.scala
+++ 
b/shims/common/src/main/scala/org/apache/gluten/expression/ExpressionNames.scala
@@ -33,6 +33,7 @@ object ExpressionNames {
   final val COLLECT_SET = "collect_set"
   final val BLOOM_FILTER_AGG = "bloom_filter_agg"
   final val BITMAP_CONSTRUCT_AGG = "bitmap_construct_agg"
+  final val BITMAP_AGGREGATOR = "bitmapaggregator"
   final val VAR_SAMP = "var_samp"
   final val VAR_POP = "var_pop"
   final val BIT_AND_AGG = "bit_and"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to