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

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


The following commit(s) were added to refs/heads/master by this push:
     new 771ae4251be [feat](fe-be) Add DataSketches HLL downsample function 
(#69257)
771ae4251be is described below

commit 771ae4251bed00e04e1c06111b8717ee58be35a0
Author: nooneuse <[email protected]>
AuthorDate: Wed Oct 28 18:36:59 2026 +0800

    [feat](fe-be) Add DataSketches HLL downsample function (#69257)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    
    Apache DataSketches HLL sketches may occupy significant storage space
    when a large `lgK` is used. For historical or cold partitions, retaining
    the original precision may no longer be necessary, but Doris previously
    had no scalar function that could read a serialized DataSketches HLL
    sketch and output another serialized sketch with a lower `lgK`.
    
    This PR introduces the following function:
    
    ```sql
    datasketches_hll_downsample(sketch, target_lg_k [, target_type])
    ```
    
    The function accepts a serialized Apache DataSketches HLL sketch and
    produces another serialized sketch whose `lgK` is:
    
    ```text
    min(source_lg_k, target_lg_k)
    ```
    
    It can be used with `INSERT OVERWRITE` to rewrite sketch columns in
    historical partitions, reducing their serialized size without rebuilding
    the sketches from raw upstream data.
    
    #### Arguments
    
    - `sketch`
      - A serialized Apache DataSketches HLL sketch.
      - Supported Doris types: `STRING`, `VARCHAR`, and `VARBINARY`.
      - The returned value uses the same type family as the input.
    - `target_lg_k`
      - A constant integer between `4` and `21`.
      - The function never increases the source sketch's `lgK`.
    - `target_type`
      - An optional constant string.
      - Supported values: `HLL_4`, `HLL_6`, and `HLL_8`.
      - The default value is `HLL_4`.
    
    When the source sketch already has an `lgK` less than or equal to
    `target_lg_k` and already uses the requested target type, the function
    returns the original serialized bytes unchanged.
    
    If the source `lgK` is already lower than the requested value but the
    target type is different, only the HLL representation type is converted;
    the source `lgK` is preserved.
    
    `NULL` input produces `NULL`. Invalid parameters or malformed serialized
    sketches return an explicit error.
    
    > Lowering `lgK` is irreversible and reduces the accuracy available to
    subsequent HLL unions and estimates. The target value should therefore
    be selected according to the acceptable accuracy for each data retention
    period. Dense sketches generally receive the most significant storage
    reduction, while sparse LIST or SET mode sketches may see little or no
    reduction.
    
    #### Usage examples
    
    Downsample a sketch to at most `lgK = 10` using the default `HLL_4`
    representation:
    
    ```sql
    SELECT datasketches_hll_downsample(sketch_column, 10)
    FROM user_sketches;
    ```
    
    Use `HLL_6` as the target representation:
    
    ```sql
    SELECT datasketches_hll_downsample(sketch_column, 10, 'HLL_6')
    FROM user_sketches;
    ```
    
    Rewrite the sketch column of a historical partition while preserving the
    other columns:
    
    ```sql
    INSERT OVERWRITE TABLE user_sketches PARTITION(p202401)
    SELECT
        event_date,
        dimension_id,
        datasketches_hll_downsample(sketch_column, 10, 'HLL_4'),
        other_column
    FROM user_sketches PARTITION(p202401);
    ```
    
    Check whether a partition contains at least one sketch that still
    requires conversion:
    
    ```sql
    SELECT 1
    FROM user_sketches PARTITION(p202401)
    WHERE datasketches_hll_downsample(
              sketch_column,
              10,
              'HLL_4'
          ) <> sketch_column
    LIMIT 1;
    ```
    
    An empty result means that every non-NULL sketch in the partition
    already satisfies the requested `lgK` and target type.
    
    The converted sketches remain compatible with the existing DataSketches
    HLL aggregate function:
    
    ```sql
    SELECT datasketches_hll_union_agg(
               datasketches_hll_downsample(sketch_column, 10)
           )
    FROM user_sketches;
    ```
    
    ### Release note
    
    Add the `DATASKETCHES_HLL_DOWNSAMPLE` scalar function for converting
    serialized Apache DataSketches HLL sketches to a configurable lower
    `lgK` and optional `HLL_4`, `HLL_6`, or `HLL_8` representation.
    
    The function supports `STRING`, `VARCHAR`, and `VARBINARY` inputs,
    propagates `NULL`, never increases the source `lgK`, and returns the
    original bytes when no conversion is required.
    
    Lowering `lgK` reduces serialized storage for dense sketches at the cost
    of lower estimation accuracy. The operation is irreversible. All BE
    nodes should be upgraded before the function is used.
    
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [x] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [x] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 be/src/exprs/CMakeLists.txt                        |   3 +
 ...aggregate_function_datasketches_hll_union_agg.h |  64 +-
 be/src/exprs/datasketches_hll_util.cpp             | 417 +++++++++
 be/src/exprs/datasketches_hll_util.h               | 138 +++
 .../exprs/function/function_datasketches_hll.cpp   | 287 ++++++
 be/src/exprs/function/simple_function_factory.h    |   2 +
 .../function/function_datasketches_hll_test.cpp    | 958 +++++++++++++++++++++
 .../doris/catalog/BuiltinScalarFunctions.java      |   2 +
 .../expression/rules/FoldConstantRuleOnBE.java     |   4 +-
 .../scalar/DataSketchesHllDownsample.java          | 185 ++++
 .../expressions/visitor/ScalarFunctionVisitor.java |   5 +
 .../scalar/DataSketchesHllDownsampleTest.java      | 107 +++
 .../test_datasketches_hll_downsample.out           | 147 ++++
 .../test_datasketches_hll_downsample_auxiliary.out |  62 ++
 .../test_datasketches_hll_downsample.groovy        | 169 ++++
 ...st_datasketches_hll_downsample_auxiliary.groovy |  94 ++
 16 files changed, 2596 insertions(+), 48 deletions(-)

diff --git a/be/src/exprs/CMakeLists.txt b/be/src/exprs/CMakeLists.txt
index 3965155e4a6..8d13a698d41 100644
--- a/be/src/exprs/CMakeLists.txt
+++ b/be/src/exprs/CMakeLists.txt
@@ -56,6 +56,9 @@ pch_reuse(Exprs)
 #    object must shadow a never-pulled archive member, but a unity batch is
 #    pulled in for its siblings and the linker sees a duplicate definition.
 set(EXPRS_UNITY_SKIP
+    # Keep the DataSketches template-heavy implementation out of the 
registration batches.
+    ${CMAKE_CURRENT_SOURCE_DIR}/datasketches_hll_util.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/function/function_datasketches_hll.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/aggregate/aggregate_function_array_agg.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/aggregate/aggregate_function_avg.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/aggregate/aggregate_function_bit.cpp
diff --git 
a/be/src/exprs/aggregate/aggregate_function_datasketches_hll_union_agg.h 
b/be/src/exprs/aggregate/aggregate_function_datasketches_hll_union_agg.h
index c0575dadd4c..dc13e6d8d38 100644
--- a/be/src/exprs/aggregate/aggregate_function_datasketches_hll_union_agg.h
+++ b/be/src/exprs/aggregate/aggregate_function_datasketches_hll_union_agg.h
@@ -41,6 +41,7 @@
 #include "exec/common/hash_table/hash.h"
 #include "exec/common/hash_table/phmap_fwd_decl.h"
 #include "exprs/aggregate/aggregate_function.h"
+#include "exprs/datasketches_hll_util.h"
 #include "util/var_int.h"
 template <typename T>
 struct HashCRC32;
@@ -56,9 +57,9 @@ struct AggregateFunctionHllSketchData {
     static constexpr uint8_t EMPTY_STATE_LOG_K = 12;
     static constexpr uint8_t MIN_UNION_LOG_K = 7;
     static constexpr uint8_t DEFAULT_UNION_LOG_K = 
datasketches::hll_constants::MAX_LOG_K;
-    using Alloc = CustomStdAllocator<uint8_t>;
-    using Sketch = datasketches::hll_sketch_alloc<Alloc>;
-    using Union = datasketches::hll_union_alloc<Alloc>;
+    using Alloc = datasketches_hll::Alloc;
+    using Sketch = datasketches_hll::Sketch;
+    using Union = datasketches_hll::Union;
 
     std::optional<Union> hll_union_data;
 
@@ -68,18 +69,23 @@ struct AggregateFunctionHllSketchData {
         if (sketch_data.is_empty()) {
             return;
         }
-        if (!hll_union_data.has_value()) {
-            hll_union_data.emplace(lg_max_k, Alloc());
-        }
         try {
+            if (!hll_union_data.has_value()) {
+                hll_union_data.emplace(lg_max_k, Alloc());
+            }
             hll_union_data->update(sketch_data);
         } catch (const doris::Exception& e) {
+            // A failed library update cannot be reused; the wrapper reclaims 
its allocations
+            // without dereferencing the possibly dangling library gadget.
+            hll_union_data.reset();
             throw Exception(e.code(), "Internal error happened when update HLL 
sketch: {}",
                             e.to_string());
         } catch (const std::exception& e) {
+            hll_union_data.reset();
             throw Exception(ErrorCode::INTERNAL_ERROR,
                             "Internal error happened when update HLL sketch: 
{}", e.what());
         } catch (...) {
+            hll_union_data.reset();
             throw Exception(ErrorCode::INTERNAL_ERROR,
                             "Internal error happened when update HLL sketch: 
unknown exception.");
         }
@@ -96,12 +102,15 @@ struct AggregateFunctionHllSketchData {
                 hll_union_data.emplace(lg_max_k, Alloc());
                 hll_union_data->update(current);
             } catch (const doris::Exception& e) {
+                hll_union_data.reset();
                 throw Exception(e.code(), "Internal error happened when update 
HLL sketch: {}",
                                 e.to_string());
             } catch (const std::exception& e) {
+                hll_union_data.reset();
                 throw Exception(ErrorCode::INTERNAL_ERROR,
                                 "Internal error happened when update HLL 
sketch: {}", e.what());
             } catch (...) {
+                hll_union_data.reset();
                 throw Exception(
                         ErrorCode::INTERNAL_ERROR,
                         "Internal error happened when update HLL sketch: 
unknown exception.");
@@ -140,23 +149,7 @@ struct AggregateFunctionHllSketchData {
     void read(BufferReadable& buf) {
         StringRef d;
         buf.read_binary(d);
-
-        auto cache = [&]() -> Sketch {
-            try {
-                return Sketch::deserialize(d.data, d.size, Alloc());
-            } catch (const doris::Exception& e) {
-                throw Exception(e.code(), "Failed to deserialize HLL sketch 
when read: {}",
-                                e.to_string());
-            } catch (const std::exception& e) {
-                throw Exception(ErrorCode::CORRUPTION, "HLL sketch data 
corrupted when read: {}",
-                                e.what());
-            } catch (...) {
-                throw Exception(ErrorCode::CORRUPTION,
-                                "HLL sketch data corrupted when read: unknown 
exception.");
-            }
-        }();
-
-        merge(cache);
+        merge(datasketches_hll::deserialize(d, "read"));
     }
     double get_result() const {
         if (hll_union_data.has_value()) {
@@ -249,30 +242,7 @@ private:
             const auto& src_column = assert_cast<const typename 
PrimitiveTypeTraits<T>::ColumnType&,
                                                  
TypeCheckOnRelease::DISABLE>(column);
             StringRef value = 
src_column.get_data_at(static_cast<size_t>(row_num));
-            if (value.empty()) {
-                throw Exception(ErrorCode::CORRUPTION,
-                                "HLL sketch data corrupted when add: empty 
input.");
-            }
-
-            using Sketch = typename Data::Sketch;
-            using Alloc = typename Data::Alloc;
-
-            auto sketch_data = [&]() -> Sketch {
-                try {
-                    return Sketch::deserialize(value.begin(), value.size, 
Alloc());
-                } catch (const doris::Exception& e) {
-                    throw Exception(e.code(), "Failed to deserialize HLL 
sketch when add: {}",
-                                    e.to_string());
-                } catch (const std::exception& e) {
-                    throw Exception(ErrorCode::CORRUPTION, "HLL sketch data 
corrupted when add: {}",
-                                    e.what());
-                } catch (...) {
-                    throw Exception(ErrorCode::CORRUPTION,
-                                    "HLL sketch data corrupted when add: 
unknown exception.");
-                }
-            }();
-
-            data.merge(sketch_data, lg_max_k);
+            data.merge(datasketches_hll::deserialize(value, "add"), lg_max_k);
         }
     }
 };
diff --git a/be/src/exprs/datasketches_hll_util.cpp 
b/be/src/exprs/datasketches_hll_util.cpp
new file mode 100644
index 00000000000..c443b6cdaba
--- /dev/null
+++ b/be/src/exprs/datasketches_hll_util.cpp
@@ -0,0 +1,417 @@
+// 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 "exprs/datasketches_hll_util.h"
+
+#include <bthread/bthread.h>
+
+#include <cstring>
+#include <exception>
+#include <mutex>
+#include <new>
+#include <stdexcept>
+
+#include "common/check.h"
+#include "common/exception.h"
+#include "common/status.h"
+#include "util/debug_points.h"
+
+namespace doris::datasketches_hll {
+
+struct alignas(std::max_align_t) AllocationHeader {
+    AllocationHeader* previous;
+    AllocationHeader* next;
+    detail::AllocationOwner* owner;
+    size_t count;
+};
+
+namespace {
+
+using namespace datasketches::hll_constants;
+
+thread_local detail::AllocationOwner* pthread_scope = nullptr;
+
+bthread_key_t scope_key() {
+    static bthread_key_t key;
+    static std::once_flag once;
+    std::call_once(once, [] { DORIS_CHECK(bthread_key_create(&key, nullptr) == 
0); });
+    return key;
+}
+
+detail::AllocationOwner* current_scope() {
+    return bthread_self() == 0
+                   ? pthread_scope
+                   : 
static_cast<detail::AllocationOwner*>(bthread_getspecific(scope_key()));
+}
+
+void set_scope(detail::AllocationOwner* scope) {
+    if (bthread_self() == 0) {
+        pthread_scope = scope;
+    } else {
+        DORIS_CHECK(bthread_setspecific(scope_key(), scope) == 0);
+    }
+}
+
+class ActiveAllocationOwner {
+public:
+    explicit ActiveAllocationOwner(detail::AllocationOwner* owner) : 
_previous(current_scope()) {
+        set_scope(owner);
+    }
+    ~ActiveAllocationOwner() { set_scope(_previous); }
+
+private:
+    detail::AllocationOwner* _previous;
+};
+
+// Reuse the library's protected static header readers without constructing a 
sketch or
+// duplicating the mode/type bit decoding. This abstract adapter is never 
instantiated.
+struct HeaderReader : datasketches::HllSketchImpl<Alloc> {
+    using datasketches::HllSketchImpl<Alloc>::extractCurMode;
+    using datasketches::HllSketchImpl<Alloc>::extractTgtHllType;
+};
+
+uint32_t read_uint32(const uint8_t* bytes) {
+    uint32_t value;
+    std::memcpy(&value, bytes, sizeof(value));
+    return value;
+}
+
+void check_set_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool 
compact) {
+    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START, size);
+    const auto count = read_uint32(bytes + HASH_SET_COUNT_INT);
+    // SET promotes to HLL above 3/4 of its largest table (K/8 entries). Bound 
the count before
+    // computeLgArrInts(), whose power-of-two and load-factor arithmetic uses 
uint32_t.
+    if (lg_k <= 7 || count > (RESIZE_NUMER * (uint32_t {1} << (lg_k - 3))) / 
RESIZE_DENOM) {
+        throw std::invalid_argument("Invalid HLL SET coupon count or lgK");
+    }
+    size_t items = count;
+    if (!compact) {
+        // The library evaluates 1 << lgArr before checking the payload. For 
example, lgArr=32
+        // is an undefined shift. Preserve its support for older images with 
an unset lgArr.
+        const uint8_t lg_arr = bytes[LG_ARR_BYTE] < LG_INIT_SET_SIZE
+                                       ? 
datasketches::HllUtil<Alloc>::computeLgArrInts(
+                                                 datasketches::SET, count, 
lg_k)
+                                       : bytes[LG_ARR_BYTE];
+        if (lg_arr > lg_k - 3) {
+            throw std::invalid_argument("Invalid HLL SET array exponent");
+        }
+        items = size_t {1} << lg_arr;
+    }
+    datasketches::HllUtil<Alloc>::checkMemSize(HASH_SET_INT_ARR_START + items 
* sizeof(uint32_t),
+                                               size);
+    if (!compact) {
+        // newSet() copies the whole table but trusts the declared count. With 
ten occupied
+        // slots and count=1, a later compact serializer allocates 16 bytes 
and writes 52.
+        uint32_t occupied = 0;
+        for (size_t i = 0; i < items; ++i) {
+            occupied += read_uint32(bytes + HASH_SET_INT_ARR_START + i * 
sizeof(uint32_t)) != EMPTY;
+        }
+        if (occupied != count) {
+            throw std::invalid_argument("HLL SET coupon count does not match 
occupied slots");
+        }
+    }
+}
+
+void check_aux_map(const uint8_t* bytes, size_t size, size_t offset, uint8_t 
lg_k, uint32_t count,
+                   bool compact) {
+    const uint32_t registers = uint32_t {1} << lg_k;
+    // There can be at most one auxiliary entry per register. This also bounds 
the uint32_t
+    // capacity arithmetic used by the library to rebuild a compact map.
+    if (count > registers) {
+        throw std::invalid_argument("HLL_4 auxiliary count exceeds register 
count");
+    }
+    size_t items = count;
+    if (!compact) {
+        const auto lg_arr = bytes[LG_ARR_BYTE];
+        // An updatable map starts at LG_AUX_ARR_INTS[lgK] and grows at 3/4 
load, so at most
+        // K entries need 2*K slots. Reject bad exponents before the library's 
1 << lgArr.
+        if (lg_arr < LG_AUX_ARR_INTS[lg_k] || lg_arr > lg_k + 1) {
+            throw std::invalid_argument("Invalid HLL_4 auxiliary array 
exponent");
+        }
+        items = size_t {1} << lg_arr;
+    }
+    datasketches::HllUtil<Alloc>::checkMemSize(offset + items * 
sizeof(uint32_t), size);
+
+    // AuxHashMap::deserialize owns a raw map until it returns. mustAdd() 
throws for duplicate
+    // slots before that ownership transfers, leaking the map. Check the same 
masked slot keys
+    // before allocating it. This bitmap is needed only for sketches with an 
auxiliary map.
+    DorisVector<uint8_t> seen(registers / 8, 0);
+    for (size_t i = 0; i < items; ++i) {
+        const auto pair = read_uint32(bytes + offset + i * sizeof(uint32_t));
+        if (pair == EMPTY) {
+            // Compact maps contain only real entries. Treating EMPTY as slot 
0 would let
+            // an AUX_TOKEN at register 0 pass this check, but 
mustFindValueFor(0) still fails.
+            if (compact) {
+                throw std::invalid_argument("HLL_4 compact auxiliary map 
contains an EMPTY entry");
+            }
+            continue;
+        }
+        const auto slot = datasketches::HllUtil<Alloc>::getLow26(pair) & 
(registers - 1);
+        const auto bit = static_cast<uint8_t>(1U << (slot % 8));
+        if (seen[slot / 8] & bit) {
+            throw std::invalid_argument("HLL_4 auxiliary map contains 
duplicate slots");
+        }
+        seen[slot / 8] |= bit;
+    }
+    // A nonempty map is insufficient: every AUX_TOKEN must refer to a slot in 
that map.
+    // For example, token at register 0 with only slot 3 in the map makes 
mergeHll() throw
+    // after allocating its target array. Reject that image even on the 
unchanged-byte path.
+    for (uint32_t slot = 0; slot < registers; ++slot) {
+        const auto byte = bytes[HLL_BYTE_ARR_START + slot / 2];
+        const auto value = static_cast<uint8_t>((byte >> ((slot % 2) * 4)) & 
loNibbleMask);
+        if (value == AUX_TOKEN && !(seen[slot / 8] & (1U << (slot % 8)))) {
+            throw std::invalid_argument("HLL_4 AUX_TOKEN has no matching 
auxiliary slot");
+        }
+    }
+}
+
+bool check_hll_size(const uint8_t* bytes, size_t size, uint8_t lg_k, bool 
compact) {
+    datasketches::HllUtil<Alloc>::checkMemSize(HLL_BYTE_ARR_START, size);
+    const auto type = HeaderReader::extractTgtHllType(bytes[MODE_BYTE]);
+    const size_t offset =
+            HLL_BYTE_ARR_START + 
datasketches::HllArray<Alloc>::hllArrBytes(type, lg_k);
+    datasketches::HllUtil<Alloc>::checkMemSize(offset, size);
+    const auto aux_count = read_uint32(bytes + AUX_COUNT_INT);
+    if (type != datasketches::HLL_4) {
+        // newHll() installs any nonempty auxiliary map through an unchecked 
Hll4Array cast.
+        // HLL_6/HLL_8 have no map member, so accepting such an image writes 
outside the object.
+        if (aux_count != 0) {
+            throw std::invalid_argument("HLL_6/HLL_8 cannot contain an 
auxiliary map");
+        }
+        return false;
+    }
+    if (aux_count != 0) {
+        check_aux_map(bytes, size, offset, lg_k, aux_count, compact);
+        return true;
+    }
+    // HLL_4 uses nibble 0xf to reference its auxiliary map. The library 
accepts auxCount=0
+    // with such a nibble, then dereferences a null map during 
union/conversion, even on a no-op.
+    for (size_t i = HLL_BYTE_ARR_START; i < offset; ++i) {
+        if ((bytes[i] & loNibbleMask) == AUX_TOKEN ||
+            ((bytes[i] >> 4) & loNibbleMask) == AUX_TOKEN) {
+            throw std::invalid_argument("HLL_4 contains AUX_TOKEN without an 
auxiliary map");
+        }
+    }
+    return false;
+}
+
+} // namespace
+
+AllocationScope::AllocationScope()
+        : _previous(current_scope()), _exceptions(std::uncaught_exceptions()) {
+    set_scope(&_owner);
+}
+
+AllocationScope::~AllocationScope() {
+    set_scope(_previous);
+    if (std::uncaught_exceptions() == _exceptions) {
+        _owner.release();
+    }
+}
+
+detail::AllocationOwner::~AllocationOwner() {
+    clear();
+}
+
+void detail::AllocationOwner::clear() noexcept {
+    while (_head != nullptr) {
+        auto* header = _head;
+        _head = header->next;
+        CustomStdAllocator<AllocationHeader>().deallocate(header, 
header->count);
+    }
+}
+
+void detail::AllocationOwner::release() noexcept {
+    while (_head != nullptr) {
+        auto* header = _head;
+        _head = header->next;
+        // Returned sketches keep normal library ownership after a successful 
call.
+        header->owner = nullptr;
+    }
+}
+
+void* allocate(size_t bytes) {
+    DBUG_EXECUTE_IF("datasketches_hll.before_allocate", { DBUG_RUN_CALLBACK(); 
});
+    auto* scope = current_scope();
+    const size_t count =
+            1 + bytes / sizeof(AllocationHeader) + (bytes % 
sizeof(AllocationHeader) != 0);
+    auto* header = CustomStdAllocator<AllocationHeader>().allocate(count);
+    *header = {.previous = nullptr,
+               .next = scope == nullptr ? nullptr : scope->_head,
+               .owner = scope,
+               .count = count};
+    if (scope != nullptr) {
+        if (header->next != nullptr) {
+            header->next->previous = header;
+        }
+        scope->_head = header;
+    }
+    return header + 1;
+}
+
+void deallocate(void* ptr) noexcept {
+    auto* header = static_cast<AllocationHeader*>(ptr) - 1;
+    if (header->owner != nullptr) {
+        if (header->previous != nullptr) {
+            header->previous->next = header->next;
+        } else {
+            header->owner->_head = header->next;
+        }
+        if (header->next != nullptr) {
+            header->next->previous = header->previous;
+        }
+    }
+    CustomStdAllocator<AllocationHeader>().deallocate(header, header->count);
+}
+
+struct Union::Impl {
+    using LibraryUnion = datasketches::hll_union_alloc<Alloc>;
+
+    detail::AllocationOwner allocations;
+    alignas(LibraryUnion) std::byte storage[sizeof(LibraryUnion)];
+    bool valid = false;
+
+    LibraryUnion* get() { return 
std::launder(reinterpret_cast<LibraryUnion*>(storage)); }
+    const LibraryUnion* get() const {
+        return std::launder(reinterpret_cast<const LibraryUnion*>(storage));
+    }
+
+    ~Impl() {
+        if (valid) {
+            get()->~LibraryUnion();
+        }
+        // Failed library updates can leave a dangling gadget pointer. 
AllocationOwner frees
+        // the remaining blocks directly, instead of dereferencing that 
invalid gadget.
+    }
+};
+
+void Union::Deleter::operator()(Impl* ptr) const noexcept {
+    ptr->~Impl();
+    datasketches_hll::deallocate(ptr);
+}
+
+Union::Union(uint8_t lg_k, const Alloc& allocator)
+        : _impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
+    ActiveAllocationOwner scope(&_impl->allocations);
+    new (_impl->storage) Impl::LibraryUnion(lg_k, allocator);
+    _impl->valid = true;
+}
+
+Union::~Union() = default;
+Union::Union(Union&&) noexcept = default;
+Union& Union::operator=(Union&&) noexcept = default;
+
+Union::Union(const Union& other) : 
_impl(new(datasketches_hll::allocate(sizeof(Impl))) Impl()) {
+    DORIS_CHECK(other._impl->valid);
+    ActiveAllocationOwner scope(&_impl->allocations);
+    new (_impl->storage) Impl::LibraryUnion(*other._impl->get());
+    _impl->valid = true;
+}
+
+Union& Union::operator=(const Union& other) {
+    Union copy(other);
+    *this = std::move(copy);
+    return *this;
+}
+
+void Union::update(const Sketch& sketch) {
+    DORIS_CHECK(_impl->valid);
+    ActiveAllocationOwner scope(&_impl->allocations);
+    try {
+        _impl->get()->update(sketch);
+    } catch (...) {
+        // The library can free its gadget during promotion and then throw 
before replacing
+        // the pointer. This state must be discarded; its allocation owner 
remains intact.
+        _impl->valid = false;
+        throw;
+    }
+}
+
+Sketch Union::get_result(datasketches::target_hll_type type) const {
+    DORIS_CHECK(_impl->valid);
+    AllocationScope scope;
+    return _impl->get()->get_result(type);
+}
+
+double Union::get_estimate() const {
+    DORIS_CHECK(_impl->valid);
+    return _impl->get()->get_estimate();
+}
+
+double Union::get_lower_bound(uint8_t std_devs) const {
+    DORIS_CHECK(_impl->valid);
+    return _impl->get()->get_lower_bound(std_devs);
+}
+
+double Union::get_upper_bound(uint8_t std_devs) const {
+    DORIS_CHECK(_impl->valid);
+    return _impl->get()->get_upper_bound(std_devs);
+}
+
+uint8_t Union::get_lg_config_k() const {
+    DORIS_CHECK(_impl->valid);
+    return _impl->get()->get_lg_config_k();
+}
+
+double Union::get_rel_err(bool upper, bool unioned, uint8_t lg_k, uint8_t 
std_devs) {
+    return Impl::LibraryUnion::get_rel_err(upper, unioned, lg_k, std_devs);
+}
+
+Sketch deserialize(const StringRef& value, std::string_view action) {
+    AllocationScope allocations;
+    // The library reads the preamble byte before it checks the length.
+    if (value.size == 0) {
+        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when 
{}: empty input.",
+                        action);
+    }
+    try {
+        // All modes have an eight-byte preamble. An invalid lgK would 
otherwise reach shifts used
+        // to size the register/coupon arrays before the library validates it.
+        datasketches::HllUtil<Alloc>::checkMemSize(EMPTY_SKETCH_SIZE_BYTES, 
value.size);
+        const auto* bytes = reinterpret_cast<const uint8_t*>(value.data);
+        const auto lg_k = 
datasketches::HllUtil<Alloc>::checkLgK(bytes[LG_K_BYTE]);
+        const auto mode = HeaderReader::extractCurMode(bytes[MODE_BYTE]);
+        const bool compact = (bytes[FLAGS_BYTE] & COMPACT_FLAG_MASK) != 0;
+        bool has_aux_map = false;
+        if (mode == datasketches::SET) {
+            check_set_size(bytes, value.size, lg_k, compact);
+        } else if (mode == datasketches::HLL) {
+            has_aux_map = check_hll_size(bytes, value.size, lg_k, compact);
+        }
+        // AuxHashMap::deserialize reads uint32_t directly. STRING rows have 
no alignment
+        // guarantee: a 233-byte HLL_6 before this HLL_4 leaves its auxiliary 
entries unaligned.
+        // Only this library reader needs an aligned copy; the other payloads 
use memcpy.
+        if (has_aux_map && reinterpret_cast<uintptr_t>(value.data) % 
alignof(uint32_t) != 0) {
+            DorisVector<uint32_t> aligned(value.size / sizeof(uint32_t) +
+                                          (value.size % sizeof(uint32_t) != 
0));
+            std::memcpy(aligned.data(), value.data, value.size);
+            return Sketch::deserialize(aligned.data(), value.size, Alloc());
+        }
+        return Sketch::deserialize(value.data, value.size, Alloc());
+    } catch (const doris::Exception& e) {
+        throw Exception(e.code(), "Failed to deserialize HLL sketch when {}: 
{}", action,
+                        e.to_string());
+    } catch (const std::exception& e) {
+        throw Exception(ErrorCode::CORRUPTION, "HLL sketch data corrupted when 
{}: {}", action,
+                        e.what());
+    } catch (...) {
+        throw Exception(ErrorCode::CORRUPTION,
+                        "HLL sketch data corrupted when {}: unknown 
exception.", action);
+    }
+}
+
+} // namespace doris::datasketches_hll
diff --git a/be/src/exprs/datasketches_hll_util.h 
b/be/src/exprs/datasketches_hll_util.h
new file mode 100644
index 00000000000..f683e31d1a0
--- /dev/null
+++ b/be/src/exprs/datasketches_hll_util.h
@@ -0,0 +1,138 @@
+// 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 <cstddef>
+#include <cstdint>
+#include <hll.hpp>
+#include <memory>
+#include <string_view>
+
+#include "core/custom_allocator.h"
+#include "core/string_ref.h"
+
+namespace doris::datasketches_hll {
+
+struct AllocationHeader;
+void* allocate(size_t bytes);
+void deallocate(void* ptr) noexcept;
+
+namespace detail {
+class AllocationOwner {
+public:
+    AllocationOwner() = default;
+    ~AllocationOwner();
+    AllocationOwner(const AllocationOwner&) = delete;
+    AllocationOwner& operator=(const AllocationOwner&) = delete;
+    void clear() noexcept;
+    void release() noexcept;
+
+private:
+    friend void* datasketches_hll::allocate(size_t bytes);
+    friend void datasketches_hll::deallocate(void* ptr) noexcept;
+    AllocationHeader* _head = nullptr;
+};
+} // namespace detail
+
+// DataSketches uses allocator.allocate() + placement new in its factories. 
When a constructor
+// or a later merge throws, some allocations never reach an owning sketch. 
Declare this scope
+// before local sketches, and destroy/reset any existing union before an 
exception leaves it.
+// Normal destructors run first; the scope then frees only the allocations 
they did not release.
+class AllocationScope {
+public:
+    AllocationScope();
+    ~AllocationScope();
+
+    AllocationScope(const AllocationScope&) = delete;
+    AllocationScope& operator=(const AllocationScope&) = delete;
+
+private:
+    detail::AllocationOwner _owner;
+    detail::AllocationOwner* _previous;
+    int _exceptions;
+};
+
+// Stateless across copies and rebinds. The current call's scope records the 
allocation, so a
+// union can copy registers from a sketch created in an earlier scope or on 
another thread.
+template <typename T>
+class SketchAllocator {
+public:
+    using value_type = T;
+    SketchAllocator() = default;
+    template <typename U>
+    SketchAllocator(const SketchAllocator<U>&) noexcept {}
+
+    T* allocate(size_t count) {
+        static_assert(alignof(T) <= alignof(std::max_align_t));
+        return static_cast<T*>(datasketches_hll::allocate(count * sizeof(T)));
+    }
+    void deallocate(T* ptr, size_t) noexcept { 
datasketches_hll::deallocate(ptr); }
+
+    template <typename U>
+    bool operator==(const SketchAllocator<U>&) const noexcept {
+        return true;
+    }
+    template <typename U>
+    bool operator!=(const SketchAllocator<U>&) const noexcept {
+        return false;
+    }
+};
+
+// Shared by the DataSketches HLL aggregate and scalar functions so that both 
track memory through
+// Doris allocators and report broken input the same way.
+using Alloc = SketchAllocator<uint8_t>;
+using Sketch = datasketches::hll_sketch_alloc<Alloc>;
+
+// The library can free its old gadget during LIST/SET promotion before 
installing the new
+// pointer. If a subsequent allocation fails, that union is not safe to 
destroy. Keep its
+// allocations under Doris ownership and discard the failed state without 
invoking that
+// invalid library destructor. The public sketch format and estimates remain 
unchanged.
+class Union {
+public:
+    explicit Union(uint8_t lg_k, const Alloc& allocator = Alloc());
+    ~Union();
+    Union(Union&&) noexcept;
+    Union& operator=(Union&&) noexcept;
+    Union(const Union&);
+    Union& operator=(const Union&);
+
+    void update(const Sketch& sketch);
+    Sketch get_result(datasketches::target_hll_type type = 
datasketches::HLL_4) const;
+    double get_estimate() const;
+    double get_lower_bound(uint8_t std_devs) const;
+    double get_upper_bound(uint8_t std_devs) const;
+    uint8_t get_lg_config_k() const;
+    static double get_rel_err(bool upper, bool unioned, uint8_t lg_k, uint8_t 
std_devs);
+
+private:
+    struct Impl;
+    struct Deleter {
+        void operator()(Impl* ptr) const noexcept;
+    };
+    std::unique_ptr<Impl, Deleter> _impl;
+};
+
+// Deserializes a serialized DataSketches HLL sketch. `action` names the 
caller in the error
+// message, e.g. "add" or "read".
+//
+// Empty bytes are rejected before the library reads them. A doris::Exception 
keeps its code, so
+// MEM_ALLOC_FAILED raised by the allocator is not reported as corrupted data; 
any other failure of
+// the library means the bytes are not a valid sketch and becomes CORRUPTION.
+Sketch deserialize(const StringRef& value, std::string_view action);
+
+} // namespace doris::datasketches_hll
diff --git a/be/src/exprs/function/function_datasketches_hll.cpp 
b/be/src/exprs/function/function_datasketches_hll.cpp
new file mode 100644
index 00000000000..f44dedd5e5d
--- /dev/null
+++ b/be/src/exprs/function/function_datasketches_hll.cpp
@@ -0,0 +1,287 @@
+// 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 <exception>
+#include <hll.hpp>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "common/check.h"
+#include "common/exception.h"
+#include "common/status.h"
+#include "core/assert_cast.h"
+#include "core/block/block.h"
+#include "core/block/column_numbers.h"
+#include "core/block/column_with_type_and_name.h"
+#include "core/column/column.h"
+#include "core/column/column_const.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_varbinary.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_varbinary.h"
+#include "core/string_ref.h"
+#include "exprs/datasketches_hll_util.h"
+#include "exprs/function/function.h"
+#include "exprs/function/simple_function_factory.h"
+#include "exprs/function_context.h"
+#include "util/string_util.h"
+
+namespace doris {
+
+// datasketches_hll_downsample(sketch, lg_k [, tgt_type]) rewrites a 
serialized DataSketches HLL
+// sketch so that its lgK is min(lg_k, source lgK) and its HLL type is 
tgt_type (HLL_4 by default).
+//
+// A sketch that already has lgK <= lg_k and type tgt_type is returned byte 
for byte after it is
+// fully deserialized. Callers rely on this: `datasketches_hll_downsample(sk, 
k, t) <> sk` is true
+// exactly when sk's lgK is above k or its type differs from t, which tells 
whether a row still
+// needs to be rewritten. For example, with lg_k = 12 and tgt_type = HLL_4:
+//   lgK 14 HLL_8 -> lgK 12 HLL_4 (new bytes)
+//   lgK 10 HLL_8 -> lgK 10 HLL_4 (new bytes, only the type changes)
+//   lgK 10 HLL_4 -> the input bytes
+class FunctionDataSketchesHllDownsample : public IFunction {
+public:
+    static constexpr auto name = "datasketches_hll_downsample";
+
+    static FunctionPtr create() { return 
std::make_shared<FunctionDataSketchesHllDownsample>(); }
+
+    String get_name() const override { return name; }
+
+    size_t get_number_of_arguments() const override { return 0; }
+
+    bool is_variadic() const override { return true; }
+
+    DataTypePtr get_return_type_impl(const DataTypes& arguments) const 
override {
+        // use_default_implementation_for_nulls() is disabled below, so, 
unlike the usual
+        // get_return_type_impl contract, arguments here keep their original 
nullability and the
+        // wrapping is not automatic: replicate it by hand from every 
argument, as FE does for a
+        // PropagateNullable function.
+        const bool nullable = std::ranges::any_of(
+                arguments, [](const DataTypePtr& type) { return 
type->is_nullable(); });
+        DataTypePtr base = remove_nullable(arguments[0]);
+        return nullable ? make_nullable(base) : base;
+    }
+
+    // lg_k and tgt_type are validated before the NULL check of the sketch, so 
the generic
+    // nullable-argument shortcut must not return NULL for, e.g., 
downsample(NULL, 30) first.
+    bool use_default_implementation_for_nulls() const override { return false; 
}
+
+    Status open(FunctionContext* context, FunctionContext::FunctionStateScope 
scope) override {
+        if (scope == FunctionContext::FunctionStateScope::THREAD_LOCAL) {
+            return Status::OK();
+        }
+        auto state = std::make_shared<DownsampleState>();
+        // FE folds the arguments to literals when it can. A constant FE 
cannot fold, e.g.
+        // `10 + crc32('') % 2`, may not be evaluated in open either, then it 
is read from the
+        // first row in execute.
+        if (context->is_col_constant(1)) {
+            
RETURN_IF_ERROR(parse_lg_k(*context->get_constant_col(1)->column_ptr, 
state->lg_k));
+        }
+        if (context->get_num_args() == 2) {
+            state->tgt_type = datasketches::HLL_4;
+        } else if (context->is_col_constant(2)) {
+            RETURN_IF_ERROR(
+                    parse_tgt_type(*context->get_constant_col(2)->column_ptr, 
state->tgt_type));
+        }
+        context->set_function_state(scope, state);
+        return Status::OK();
+    }
+
+    Status execute_impl(FunctionContext* context, Block& block, const 
ColumnNumbers& arguments,
+                        uint32_t result, size_t input_rows_count) const 
override {
+        const auto* state = reinterpret_cast<const DownsampleState*>(
+                
context->get_function_state(FunctionContext::FunctionStateScope::FRAGMENT_LOCAL));
+        DORIS_CHECK(state != nullptr);
+        uint8_t lg_k = 0;
+        if (state->lg_k.has_value()) {
+            lg_k = *state->lg_k;
+        } else {
+            
RETURN_IF_ERROR(read_lg_k(*block.get_by_position(arguments[1]).column, lg_k));
+        }
+        datasketches::target_hll_type tgt_type = datasketches::HLL_4;
+        if (state->tgt_type.has_value()) {
+            tgt_type = *state->tgt_type;
+        } else {
+            
RETURN_IF_ERROR(read_tgt_type(*block.get_by_position(arguments[2]).column, 
tgt_type));
+        }
+
+        const ColumnWithTypeAndName& sketch_arg = 
block.get_by_position(arguments[0]);
+        if (sketch_arg.type->is_nullable() && 
sketch_arg.get_nullable_column_info().only_null) {
+            block.get_by_position(result).column =
+                    
block.get_by_position(result).type->create_column_const(input_rows_count,
+                                                                            
Field());
+            return Status::OK();
+        }
+        const auto [sketch_column, sketch_const] = 
unpack_if_const(sketch_arg.column);
+        const NullMap* null_map = nullptr;
+        ColumnPtr sketch_values = sketch_column;
+        if (const auto* nullable = 
check_and_get_column<ColumnNullable>(sketch_column.get())) {
+            null_map = &nullable->get_null_map_data();
+            sketch_values = nullable->get_nested_column_ptr();
+        }
+
+        MutableColumnPtr res;
+        if (const auto* strings = 
check_and_get_column<ColumnString>(sketch_values.get())) {
+            res = execute_column(*strings, null_map, sketch_const, 
input_rows_count, lg_k,
+                                 tgt_type);
+        } else if (const auto* binaries =
+                           
check_and_get_column<ColumnVarbinary>(sketch_values.get())) {
+            res = execute_column(*binaries, null_map, sketch_const, 
input_rows_count, lg_k,
+                                 tgt_type);
+        } else {
+            return Status::RuntimeError("Illegal column {} of argument of 
function {}",
+                                        sketch_arg.column->get_name(), 
get_name());
+        }
+
+        if (!block.get_by_position(result).type->is_nullable()) {
+            block.replace_by_position(result, std::move(res));
+            return Status::OK();
+        }
+        if (sketch_const) {
+            // wrap_in_nullable skips constant arguments; a constant sketch 
here is not NULL, since
+            // an all-NULL sketch returned above.
+            block.replace_by_position(
+                    result, ColumnNullable::create(std::move(res),
+                                                   
ColumnUInt8::create(input_rows_count, 0)));
+            return Status::OK();
+        }
+        block.replace_by_position(result, 
wrap_in_nullable(ColumnPtr(std::move(res)), block,
+                                                           arguments, 
input_rows_count));
+        return Status::OK();
+    }
+
+private:
+    // Same bounds as the DataSketches library accepts for a sketch.
+    static constexpr int64_t MIN_LG_K = datasketches::hll_constants::MIN_LOG_K;
+    static constexpr int64_t MAX_LG_K = datasketches::hll_constants::MAX_LOG_K;
+
+    struct DownsampleState {
+        // Empty when the argument is not evaluated in open; it is then read 
in execute.
+        std::optional<uint8_t> lg_k;
+        std::optional<datasketches::target_hll_type> tgt_type;
+    };
+
+    static Status parse_lg_k(const IColumn& column, std::optional<uint8_t>& 
lg_k) {
+        uint8_t value = 0;
+        RETURN_IF_ERROR(read_lg_k(column, value));
+        lg_k = value;
+        return Status::OK();
+    }
+
+    static Status parse_tgt_type(const IColumn& column,
+                                 std::optional<datasketches::target_hll_type>& 
tgt_type) {
+        datasketches::target_hll_type value = datasketches::HLL_4;
+        RETURN_IF_ERROR(read_tgt_type(column, value));
+        tgt_type = value;
+        return Status::OK();
+    }
+
+    // Reads lg_k from the first row. FE rejects a literal outside the range 
or NULL, so these
+    // errors are only reachable through a constant evaluated by BE.
+    static Status read_lg_k(const IColumn& column, uint8_t& lg_k) {
+        if (column.is_null_at(0)) {
+            return Status::InvalidArgument("{}'s lg_k must be between {} and 
{}, but was NULL",
+                                           name, MIN_LG_K, MAX_LG_K);
+        }
+        const int64_t value = remove_nullable(column.get_ptr())->get_int(0);
+        if (value < MIN_LG_K || value > MAX_LG_K) {
+            return Status::InvalidArgument("{}'s lg_k must be between {} and 
{}, but was {}", name,
+                                           MIN_LG_K, MAX_LG_K, value);
+        }
+        lg_k = static_cast<uint8_t>(value);
+        return Status::OK();
+    }
+
+    static Status read_tgt_type(const IColumn& column, 
datasketches::target_hll_type& tgt_type) {
+        if (column.is_null_at(0)) {
+            return Status::InvalidArgument(
+                    "{}'s tgt_type must be one of HLL_4, HLL_6, HLL_8, but was 
NULL", name);
+        }
+        const std::string value = 
remove_nullable(column.get_ptr())->get_data_at(0).to_string();
+        const std::string lower = to_lower(value);
+        if (lower == "hll_4") {
+            tgt_type = datasketches::HLL_4;
+        } else if (lower == "hll_6") {
+            tgt_type = datasketches::HLL_6;
+        } else if (lower == "hll_8") {
+            tgt_type = datasketches::HLL_8;
+        } else {
+            return Status::InvalidArgument(
+                    "{}'s tgt_type must be one of HLL_4, HLL_6, HLL_8, but was 
'{}'", name, value);
+        }
+        return Status::OK();
+    }
+
+    template <typename ColumnType>
+    static MutableColumnPtr execute_column(const ColumnType& sketches, const 
NullMap* null_map,
+                                           bool sketch_const, size_t 
input_rows_count, uint8_t lg_k,
+                                           datasketches::target_hll_type 
tgt_type) {
+        auto res = ColumnType::create();
+        for (size_t i = 0; i < input_rows_count; ++i) {
+            const size_t row = index_check_const(i, sketch_const);
+            if (null_map != nullptr && (*null_map)[row]) {
+                res->insert_default();
+                continue;
+            }
+            downsample(sketches.get_data_at(row), lg_k, tgt_type, *res);
+        }
+        return res;
+    }
+
+    template <typename ColumnType>
+    static void downsample(const StringRef& value, uint8_t lg_k,
+                           datasketches::target_hll_type tgt_type, ColumnType& 
res) {
+        const datasketches_hll::Sketch sketch = 
datasketches_hll::deserialize(value, "downsample");
+        if (sketch.get_lg_config_k() <= lg_k && sketch.get_target_type() == 
tgt_type) {
+            res.insert_data(value.data, value.size);
+            return;
+        }
+        try {
+            // A union capped at min(lg_k, source lgK) folds the registers of 
a dense sketch and
+            // replays the coupons of a LIST/SET sketch, without ever raising 
the source lgK.
+            datasketches_hll::Union hll_union(std::min(lg_k, 
sketch.get_lg_config_k()),
+                                              datasketches_hll::Alloc());
+            hll_union.update(sketch);
+            const auto bytes = 
hll_union.get_result(tgt_type).serialize_compact();
+            res.insert_data(reinterpret_cast<const char*>(bytes.data()), 
bytes.size());
+        } catch (const doris::Exception& e) {
+            throw Exception(e.code(), "Internal error happened when downsample 
HLL sketch: {}",
+                            e.to_string());
+        } catch (const std::exception& e) {
+            throw Exception(ErrorCode::INTERNAL_ERROR,
+                            "Internal error happened when downsample HLL 
sketch: {}", e.what());
+        } catch (...) {
+            throw Exception(
+                    ErrorCode::INTERNAL_ERROR,
+                    "Internal error happened when downsample HLL sketch: 
unknown exception.");
+        }
+    }
+};
+
+void register_function_datasketches_hll(SimpleFunctionFactory& factory) {
+    factory.register_function<FunctionDataSketchesHllDownsample>();
+}
+
+} // namespace doris
diff --git a/be/src/exprs/function/simple_function_factory.h 
b/be/src/exprs/function/simple_function_factory.h
index 60d5bb14827..075135eccc9 100644
--- a/be/src/exprs/function/simple_function_factory.h
+++ b/be/src/exprs/function/simple_function_factory.h
@@ -35,6 +35,7 @@ void register_function_size(SimpleFunctionFactory& factory);
 void register_function_comparison(SimpleFunctionFactory& factory);
 void register_function_comparison_eq_for_null(SimpleFunctionFactory& factory);
 void register_function_hll(SimpleFunctionFactory& factory);
+void register_function_datasketches_hll(SimpleFunctionFactory& factory);
 void register_function_logical(SimpleFunctionFactory& factory);
 void register_function_cast(SimpleFunctionFactory& factory);
 void register_function_encode_varchar(SimpleFunctionFactory& factory);
@@ -284,6 +285,7 @@ public:
             register_function_quantile_state(instance);
             register_function_bitmap_variadic(instance);
             register_function_hll(instance);
+            register_function_datasketches_hll(instance);
             register_function_comparison(instance);
             register_function_encode_varchar(instance);
             register_function_decode_as_varchar(instance);
diff --git a/be/test/exprs/function/function_datasketches_hll_test.cpp 
b/be/test/exprs/function/function_datasketches_hll_test.cpp
new file mode 100644
index 00000000000..9e29a423ad5
--- /dev/null
+++ b/be/test/exprs/function/function_datasketches_hll_test.cpp
@@ -0,0 +1,958 @@
+// 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 <bthread/bthread.h>
+#include <gtest/gtest.h>
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <cstring>
+#include <functional>
+#include <hll.hpp>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "common/config.h"
+#include "common/exception.h"
+#include "common/status.h"
+#include "core/block/block.h"
+#include "core/column/column_const.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_varbinary.h"
+#include "core/column/column_vector.h"
+#include "core/data_type/data_type_nullable.h"
+#include "core/data_type/data_type_number.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_varbinary.h"
+#include "core/string_buffer.hpp"
+#include "exprs/aggregate/aggregate_function_datasketches_hll_union_agg.h"
+#include "exprs/datasketches_hll_util.h"
+#include "exprs/function/function.h"
+#include "exprs/function/simple_function_factory.h"
+#include "exprs/function_context.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
+#include "testutil/function_utils.h"
+#include "util/debug_points.h"
+
+namespace doris {
+
+namespace {
+
+class ScopedSketchAllocationFailure {
+public:
+    explicit ScopedSketchAllocationFailure(size_t fail_at)
+            : _old_enabled(config::enable_debug_points) {
+        config::enable_debug_points = true;
+        DebugPoints::instance()->add_with_callback(
+                "datasketches_hll.before_allocate", 
std::function<void()>([this, fail_at] {
+                    ++calls;
+                    if (calls == fail_at) {
+                        throw Exception(ErrorCode::MEM_ALLOC_FAILED,
+                                        "Injected HLL allocation failure");
+                    }
+                }));
+    }
+
+    ~ScopedSketchAllocationFailure() {
+        DebugPoints::instance()->remove("datasketches_hll.before_allocate");
+        config::enable_debug_points = _old_enabled;
+    }
+
+    size_t calls = 0;
+
+private:
+    bool _old_enabled;
+};
+
+} // namespace
+
+class FunctionDataSketchesHllDownsampleTest : public testing::Test {
+protected:
+    static void put_uint32(std::string& bytes, size_t offset, uint32_t value) {
+        std::memcpy(bytes.data() + offset, &value, sizeof(value));
+    }
+
+    static std::string aux_sketch_bytes(int lg_k, bool compact) {
+        // Force genuine HLL_4 exceptions without searching for extremely rare 
hash values.
+        datasketches::Hll4Array<std::allocator<uint8_t>> sketch(lg_k, true, 
{});
+        sketch.couponUpdate(datasketches::HllUtil<>::pair(1, 20));
+        sketch.couponUpdate(datasketches::HllUtil<>::pair(3, 21));
+        auto bytes = sketch.serialize(compact, 0);
+        return {reinterpret_cast<const char*>(bytes.data()), bytes.size()};
+    }
+
+    static std::string missing_aux_slot(bool compact) {
+        using namespace datasketches::hll_constants;
+        auto bytes = aux_sketch_bytes(8, compact);
+        const size_t offset = HLL_BYTE_ARR_START + (1 << 7);
+        if (compact) {
+            bytes.resize(offset + sizeof(uint32_t));
+        }
+        std::fill(bytes.begin() + offset, bytes.end(), '\0');
+        put_uint32(bytes, AUX_COUNT_INT, 1);
+        // Registers 1 and 3 are AUX_TOKEN, but only register 3 has an 
auxiliary record.
+        put_uint32(bytes, offset, datasketches::HllUtil<>::pair(3, 21));
+        return bytes;
+    }
+
+    static std::string empty_compact_aux_entry() {
+        using namespace datasketches::hll_constants;
+        datasketches::Hll4Array<std::allocator<uint8_t>> sketch(8, true, {});
+        sketch.couponUpdate(datasketches::HllUtil<>::pair(0, 20));
+        auto serialized = sketch.serialize(true, 0);
+        std::string bytes(reinterpret_cast<const char*>(serialized.data()), 
serialized.size());
+        put_uint32(bytes, HLL_BYTE_ARR_START + (1 << 7), EMPTY);
+        return bytes;
+    }
+
+    static void verify_unaligned_conversion(const std::string& prefix, const 
std::string& bytes,
+                                            PrimitiveType primitive, int 
target,
+                                            datasketches::target_hll_type type,
+                                            const std::string& name) {
+        auto block = input({prefix, bytes}, target, name, primitive);
+        const auto raw = block.get_by_position(0).column->get_data_at(1);
+        ASSERT_NE(reinterpret_cast<uintptr_t>(raw.data) % alignof(uint32_t), 
0);
+        ASSERT_TRUE(execute(block).ok());
+        if (target >= 8 && type == datasketches::HLL_4) {
+            EXPECT_EQ(result(block, 1), bytes);
+            return;
+        }
+        const auto reference = 
datasketches::hll_sketch::deserialize(bytes.data(), bytes.size());
+        datasketches::hll_union expected_union(std::min(target, 8));
+        expected_union.update(reference);
+        const auto expected = 
expected_union.get_result(type).serialize_compact();
+        EXPECT_EQ(result(block, 1),
+                  std::string(reinterpret_cast<const char*>(expected.data()), 
expected.size()));
+    }
+
+    static void verify_set_count_corruption(const std::string& bytes) {
+        for (auto primitive : {TYPE_STRING, TYPE_VARCHAR, TYPE_VARBINARY}) {
+            for (int target : {4, 8, 21}) {
+                for (const auto& type : {"HLL_4", "HLL_6", "HLL_8"}) {
+                    auto block = input({bytes}, target, type, primitive);
+                    const auto status = execute(block);
+                    EXPECT_EQ(status.code(), ErrorCode::CORRUPTION);
+                    EXPECT_NE(status.to_string().find("coupon count does not 
match occupied slots"),
+                              std::string::npos);
+                }
+            }
+        }
+    }
+
+    static void verify_conversion_allocation_failure(const std::string& bytes,
+                                                     const std::string& type, 
size_t fail_at) {
+        SCOPED_TRACE(testing::Message() << type << '/' << fail_at);
+        ScopedSketchAllocationFailure failure(fail_at);
+        auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                        
"HllConversionAllocationFailure");
+        auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const auto baseline = tracker->consumption();
+        {
+            auto block = input({bytes}, 8, type);
+            EXPECT_EQ(execute(block).code(), ErrorCode::MEM_ALLOC_FAILED);
+        }
+        EXPECT_EQ(failure.calls, fail_at);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_EQ(tracker->consumption(), baseline);
+    }
+
+    static void expect_corruption(const std::string& bytes, const std::string& 
message,
+                                  const std::string& type = "HLL_4") {
+        // The matching type at lgK=21 also covers the unchanged-byte path.
+        for (int target : {4, 21}) {
+            for (auto primitive : {TYPE_STRING, TYPE_VARBINARY}) {
+                auto block = input({bytes}, target, type, primitive);
+                auto status = execute(block);
+                EXPECT_EQ(status.code(), ErrorCode::CORRUPTION);
+                EXPECT_NE(status.to_string().find(message), std::string::npos);
+            }
+        }
+    }
+
+    static std::string sketch_bytes(int lg_k, datasketches::target_hll_type 
type, uint64_t n,
+                                    bool compact = true, bool full = false) {
+        datasketches::hll_sketch sketch(lg_k, type, full);
+        for (uint64_t i = 0; i < n; ++i) {
+            sketch.update(i);
+        }
+        auto bytes = compact ? sketch.serialize_compact() : 
sketch.serialize_updatable();
+        return {reinterpret_cast<const char*>(bytes.data()), bytes.size()};
+    }
+
+    static ColumnPtr integer(int value, size_t rows = 1) {
+        auto column = ColumnInt32::create();
+        column->insert_value(value);
+        return ColumnConst::create(std::move(column), rows);
+    }
+
+    static ColumnPtr text(const std::string& value, size_t rows = 1) {
+        auto column = ColumnString::create();
+        column->insert_data(value.data(), value.size());
+        return ColumnConst::create(std::move(column), rows);
+    }
+
+    static Status execute(Block& block, bool constants_in_open = true) {
+        const auto args = block.get_columns_with_type_and_name();
+        DataTypes types;
+        ColumnNumbers indexes;
+        std::vector<std::shared_ptr<ColumnPtrWrapper>> constants;
+        bool nullable = false;
+        for (size_t i = 0; i < args.size(); ++i) {
+            types.push_back(args[i].type);
+            indexes.push_back(i);
+            nullable |= args[i].type->is_nullable();
+            constants.push_back(constants_in_open && 
is_column_const(*args[i].column)
+                                        ? 
std::make_shared<ColumnPtrWrapper>(args[i].column)
+                                        : nullptr);
+        }
+        auto result_type = nullable ? make_nullable(remove_nullable(types[0])) 
: types[0];
+        auto function = SimpleFunctionFactory::instance().get_function(
+                "datasketches_hll_downsample", args, result_type);
+        EXPECT_NE(function, nullptr);
+        FunctionUtils utils(result_type, types, false);
+        auto* context = utils.get_fn_ctx();
+        context->set_constant_cols(constants);
+        RETURN_IF_ERROR(
+                function->open(context, 
FunctionContext::FunctionStateScope::FRAGMENT_LOCAL));
+        RETURN_IF_ERROR(function->open(context, 
FunctionContext::FunctionStateScope::THREAD_LOCAL));
+        auto rows = block.rows();
+        block.insert({nullptr, result_type, "result"});
+        auto status = function->execute(context, block, indexes, args.size(), 
rows);
+        EXPECT_TRUE(
+                function->close(context, 
FunctionContext::FunctionStateScope::THREAD_LOCAL).ok());
+        EXPECT_TRUE(
+                function->close(context, 
FunctionContext::FunctionStateScope::FRAGMENT_LOCAL).ok());
+        return status;
+    }
+
+    static Block input(const std::vector<std::optional<std::string>>& values, 
int lg_k,
+                       const std::optional<std::string>& type = std::nullopt,
+                       PrimitiveType primitive = TYPE_STRING, bool constant = 
false,
+                       bool nullable = false) {
+        DataTypePtr data_type = primitive == TYPE_VARBINARY
+                                        ? 
DataTypePtr(std::make_shared<DataTypeVarbinary>())
+                                        : std::make_shared<DataTypeString>(-1, 
primitive);
+        auto column = data_type->create_column();
+        auto null_map = ColumnUInt8::create();
+        for (const auto& value : values) {
+            if (value.has_value()) {
+                column->insert_data(value->data(), value->size());
+            } else {
+                column->insert_default();
+            }
+            null_map->insert_value(!value.has_value());
+        }
+        ColumnPtr data = std::move(column);
+        if (nullable) {
+            data = ColumnNullable::create(data, std::move(null_map));
+            data_type = make_nullable(data_type);
+        }
+        if (constant) {
+            data = ColumnConst::create(data, values.size());
+        }
+        Block block;
+        block.insert({data, data_type, "sk"});
+        block.insert({integer(lg_k, values.size()), 
std::make_shared<DataTypeInt32>(), "lg_k"});
+        if (type.has_value()) {
+            block.insert({text(*type, values.size()), 
std::make_shared<DataTypeString>(), "type"});
+        }
+        return block;
+    }
+
+    static std::string result(const Block& block, size_t row = 0) {
+        return block.get_by_position(block.columns() - 
1).column->get_data_at(row).to_string();
+    }
+
+    static void verify_conversion(const std::string& src, int src_k,
+                                  datasketches::target_hll_type src_type, 
uint64_t n, int target,
+                                  datasketches::target_hll_type target_type,
+                                  const std::string& name) {
+        SCOPED_TRACE(testing::Message()
+                     << src_k << '/' << target << '/' << n << '/' << src_type 
<< '/' << name);
+        Block block = input({src}, target, name);
+        ASSERT_TRUE(execute(block).ok());
+        auto out = result(block);
+        auto sketch = datasketches::hll_sketch::deserialize(out.data(), 
out.size());
+        EXPECT_EQ(sketch.get_lg_config_k(), std::min(src_k, target));
+        EXPECT_EQ(sketch.get_target_type(), target_type);
+        EXPECT_EQ(sketch.is_empty(), n == 0);
+        EXPECT_EQ(out == src, src_k <= target && src_type == target_type);
+        auto again = input({out}, target, name);
+        ASSERT_TRUE(execute(again).ok());
+        EXPECT_EQ(result(again), out);
+    }
+
+    static void verify_nullable(PrimitiveType type, bool constant, const 
std::string& src) {
+        auto block = input({src}, 10, "HLL_4", type, constant, true);
+        ASSERT_TRUE(execute(block).ok());
+        auto output = result(block);
+        auto mixed = input({std::nullopt, src, std::nullopt, src}, 10, 
"HLL_4", type, false, true);
+        ASSERT_TRUE(execute(mixed).ok());
+        const auto& col = mixed.get_by_position(3).column;
+        EXPECT_TRUE(col->is_null_at(0));
+        EXPECT_TRUE(col->is_null_at(2));
+        EXPECT_EQ(result(mixed, 1), output);
+        EXPECT_EQ(result(mixed, 3), output);
+        auto all_null = input({std::nullopt}, 10, "HLL_4", type, constant, 
true);
+        ASSERT_TRUE(execute(all_null).ok());
+        EXPECT_TRUE(all_null.get_by_position(3).column->is_null_at(0));
+    }
+};
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
ModesTypesPrecisionAndIdempotence) {
+    const std::vector<datasketches::target_hll_type> types = {
+            datasketches::HLL_4, datasketches::HLL_6, datasketches::HLL_8};
+    const std::vector<std::string> names = {"HLL_4", "hll_6", "HlL_8"};
+    for (auto src_type : types) {
+        for (int src_k : {4, 7, 10, 15, 21}) {
+            for (uint64_t n : {0, 5, 20, 4000}) {
+                for (bool compact : {false, true}) {
+                    auto src = sketch_bytes(src_k, src_type, n, compact);
+                    for (int target : {4, 10, 15, 21}) {
+                        for (size_t t = 0; t < types.size(); ++t) {
+                            verify_conversion(src, src_k, src_type, n, target, 
types[t], names[t]);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
DenseHighPrecisionAndDefaultType) {
+    for (const auto& pair : {std::pair {21, 15}, std::pair {15, 10}}) {
+        auto src = sketch_bytes(pair.first, datasketches::HLL_8, 100000, true, 
true);
+        auto block = input({src}, pair.second);
+        ASSERT_TRUE(execute(block).ok());
+        auto out = result(block);
+        auto low = datasketches::hll_sketch::deserialize(out.data(), 
out.size());
+        EXPECT_EQ(low.get_lg_config_k(), pair.second);
+        EXPECT_EQ(low.get_target_type(), datasketches::HLL_4);
+        EXPECT_LT(out.size(), src.size() / 10);
+        auto direct_bytes = sketch_bytes(pair.second, datasketches::HLL_4, 
100000);
+        auto direct =
+                datasketches::hll_sketch::deserialize(direct_bytes.data(), 
direct_bytes.size());
+        EXPECT_NEAR(low.get_composite_estimate(), 
direct.get_composite_estimate(), 1e-8);
+        auto empty = input({sketch_bytes(21, datasketches::HLL_8, 0, true, 
true)}, pair.second);
+        ASSERT_TRUE(execute(empty).ok());
+        EXPECT_EQ(result(empty).size(), 8);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, NullableConstAndBinaryColumns) {
+    for (auto type : {TYPE_STRING, TYPE_VARCHAR, TYPE_VARBINARY}) {
+        auto src = sketch_bytes(12, datasketches::HLL_8, 1000);
+        for (bool constant : {false, true}) {
+            verify_nullable(type, constant, src);
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
BeEvaluatedConstantsAndInvalidParameters) {
+    auto src = sketch_bytes(15, datasketches::HLL_8, 4000);
+    auto block = input({src, src}, 10, "HLL_6");
+    // Constant expressions can arrive as full columns and may be unavailable 
in open().
+    block.get_by_position(1).column = integer(10, 
2)->convert_to_full_column_if_const();
+    block.get_by_position(2).column = text("HLL_6", 
2)->convert_to_full_column_if_const();
+    ASSERT_TRUE(execute(block, false).ok());
+    auto out = result(block);
+    EXPECT_EQ(result(block, 1), out);
+    EXPECT_EQ(datasketches::hll_sketch::deserialize(out.data(), 
out.size()).get_lg_config_k(), 10);
+    for (bool in_open : {false, true}) {
+        for (int k : {3, 22, 268}) {
+            auto invalid = input({std::nullopt}, k, "HLL_4", TYPE_STRING, 
true, true);
+            EXPECT_EQ(execute(invalid, in_open).code(), 
ErrorCode::INVALID_ARGUMENT);
+        }
+        auto invalid = input({src}, 10, "bad");
+        EXPECT_EQ(execute(invalid, in_open).code(), 
ErrorCode::INVALID_ARGUMENT);
+        for (size_t arg : {1, 2}) {
+            auto null_arg = input({src}, 10, "HLL_4");
+            auto& c = null_arg.get_by_position(arg);
+            auto data = remove_nullable(c.type)->create_column();
+            data->insert_default();
+            c.column = ColumnConst::create(
+                    ColumnNullable::create(std::move(data), 
ColumnUInt8::create(1, 1)), 1);
+            c.type = make_nullable(c.type);
+            EXPECT_EQ(execute(null_arg, in_open).code(), 
ErrorCode::INVALID_ARGUMENT);
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, InvalidSketchIncludingFastPath) {
+    auto dense = sketch_bytes(12, datasketches::HLL_4, 1000);
+    std::vector<std::string> invalid {"", "x", dense.substr(0, dense.size() / 
2)};
+    for (size_t pos : {1, 2, 3}) {
+        auto bad = dense;
+        bad[pos] = static_cast<char>(255);
+        invalid.push_back(bad);
+    }
+    // Missing auxiliary map with an AUX_TOKEN is accepted by upstream 
deserialize, but would
+    // dereference a null map in union/conversion. Reject it even on the 
unchanged-byte path.
+    auto missing_aux = dense;
+    std::fill_n(missing_aux.begin() + 
datasketches::hll_constants::AUX_COUNT_INT, 4, '\0');
+    missing_aux[datasketches::hll_constants::HLL_BYTE_ARR_START] |= 0xf;
+    invalid.push_back(missing_aux);
+    for (const auto& bytes : invalid) {
+        auto block = input({bytes}, 21, "HLL_4");
+        EXPECT_EQ(execute(block).code(), ErrorCode::CORRUPTION);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, AllocationFailureKeepsErrorCode) 
{
+    auto bytes = sketch_bytes(12, datasketches::HLL_8, 1000);
+    const auto old = config::mem_alloc_fault_probability;
+    config::mem_alloc_fault_probability = 1;
+    ++enable_thread_catch_bad_alloc;
+    try {
+        static_cast<void>(datasketches_hll::deserialize(StringRef(bytes), 
"downsample"));
+        ADD_FAILURE() << "Expected allocation failure";
+    } catch (const Exception& e) {
+        EXPECT_EQ(e.code(), ErrorCode::MEM_ALLOC_FAILED);
+    }
+    --enable_thread_catch_bad_alloc;
+    config::mem_alloc_fault_probability = old;
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, RejectAuxMapsOnHll6AndHll8) {
+    using namespace datasketches::hll_constants;
+    for (auto type : {datasketches::HLL_6, datasketches::HLL_8}) {
+        for (bool compact : {false, true}) {
+            auto bytes = sketch_bytes(8, type, 1, compact, true);
+            const auto offset = bytes.size();
+            // Supply the full auxiliary payload: a short image would throw 
before the unsafe
+            // Hll4Array cast, hiding the regression this test is meant to 
exercise.
+            bytes.resize(offset + sizeof(uint32_t) * (compact ? 1 : 4), '\0');
+            put_uint32(bytes, AUX_COUNT_INT, 1);
+            bytes[LG_ARR_BYTE] = 2;
+            put_uint32(bytes, offset, datasketches::HllUtil<>::pair(3, 20));
+            expect_corruption(bytes, "cannot contain an auxiliary map",
+                              type == datasketches::HLL_6 ? "HLL_6" : "HLL_8");
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
ArrayExponentsAndTruncatedPayloads) {
+    using namespace datasketches::hll_constants;
+    const auto set = sketch_bytes(8, datasketches::HLL_4, 10, false);
+    ASSERT_EQ(static_cast<uint8_t>(set[PREAMBLE_INTS_BYTE]), HASH_SET_PREINTS);
+    const auto aux = aux_sketch_bytes(8, false);
+    for (uint8_t exponent : {31, 32, 63, 64, 255}) {
+        SCOPED_TRACE(static_cast<int>(exponent));
+        auto bad_set = set;
+        bad_set[LG_ARR_BYTE] = static_cast<char>(exponent);
+        bad_set.resize(HASH_SET_INT_ARR_START);
+        expect_corruption(bad_set, "SET array exponent");
+        auto bad_aux = aux;
+        bad_aux[LG_ARR_BYTE] = static_cast<char>(exponent);
+        expect_corruption(bad_aux, "auxiliary array exponent");
+    }
+    auto small_aux = aux;
+    small_aux[LG_ARR_BYTE] = 0;
+    expect_corruption(small_aux, "auxiliary array exponent");
+    // Valid exponents still require a complete array before the library 
allocates anything.
+    expect_corruption(set.substr(0, set.size() - 1), "corrupted");
+    expect_corruption(aux.substr(0, aux.size() - 1), "corrupted");
+    auto compact_aux = aux_sketch_bytes(8, true);
+    expect_corruption(compact_aux.substr(0, compact_aux.size() - 1), 
"corrupted");
+    auto huge_count = set;
+    huge_count[LG_ARR_BYTE] = 0;
+    put_uint32(huge_count, HASH_SET_COUNT_INT, UINT32_MAX);
+    expect_corruption(huge_count, "SET coupon count");
+    put_uint32(compact_aux, AUX_COUNT_INT, UINT32_MAX);
+    expect_corruption(compact_aux, "auxiliary count exceeds register count");
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
AuxiliarySlotsAreUniqueAndDoNotLeak) {
+    using namespace datasketches::hll_constants;
+    for (bool compact : {false, true}) {
+        auto bytes = aux_sketch_bytes(8, compact);
+        const size_t offset = HLL_BYTE_ARR_START + (1 << 7);
+        std::fill(bytes.begin() + offset, bytes.end(), '\0');
+        put_uint32(bytes, offset, datasketches::HllUtil<>::pair(3, 20));
+        // Raw keys differ but deserialize masks both down to register 3.
+        put_uint32(bytes, offset + 4, datasketches::HllUtil<>::pair(3 + (1 << 
8), 21));
+        expect_corruption(bytes, "duplicate slots");
+
+        auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                        
"HllAuxiliaryMapError");
+        auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const auto baseline = tracker->consumption();
+        for (int i = 0; i < 32; ++i) {
+            try {
+                
static_cast<void>(datasketches_hll::deserialize(StringRef(bytes), "test"));
+                ADD_FAILURE() << "Expected duplicate auxiliary slots to fail";
+            } catch (const Exception& e) {
+                EXPECT_EQ(e.code(), ErrorCode::CORRUPTION);
+            }
+        }
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_EQ(tracker->consumption(), baseline);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
ValidAuxiliaryMapsAndLegacyArrayExponents) {
+    using namespace datasketches::hll_constants;
+    for (int lg_k : {4, 8, 12}) {
+        for (bool compact : {false, true}) {
+            auto bytes = aux_sketch_bytes(lg_k, compact);
+            if (compact) {
+                // Compact auxiliary images historically omit lgArr; the 
library ignores it.
+                bytes[LG_ARR_BYTE] = static_cast<char>(255);
+            }
+            for (const auto& [type, name] : {std::pair {datasketches::HLL_4, 
"HLL_4"},
+                                             std::pair {datasketches::HLL_6, 
"HLL_6"},
+                                             std::pair {datasketches::HLL_8, 
"HLL_8"}}) {
+                verify_conversion(bytes, lg_k, datasketches::HLL_4, 2, 4, 
type, name);
+                verify_conversion(bytes, lg_k, datasketches::HLL_4, 2, 21, 
type, name);
+            }
+        }
+    }
+    for (bool compact : {false, true}) {
+        auto bytes = sketch_bytes(8, datasketches::HLL_4, 10, compact);
+        bytes[LG_ARR_BYTE] = compact ? static_cast<char>(255) : 0;
+        verify_conversion(bytes, 8, datasketches::HLL_4, 10, 4, 
datasketches::HLL_4, "HLL_4");
+        verify_conversion(bytes, 8, datasketches::HLL_4, 10, 21, 
datasketches::HLL_4, "HLL_4");
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
RejectMissingAuxiliarySlotsWithoutLeaks) {
+    for (bool compact : {false, true}) {
+        const auto bytes = missing_aux_slot(compact);
+        expect_corruption(bytes, "no matching auxiliary slot");
+        for (const auto& type : {"HLL_6", "HLL_8"}) {
+            expect_corruption(bytes, "no matching auxiliary slot", type);
+        }
+        auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                        
"HllMissingAuxiliarySlot");
+        auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const auto baseline = tracker->consumption();
+        for (int i = 0; i < 32; ++i) {
+            try {
+                
static_cast<void>(datasketches_hll::deserialize(StringRef(bytes), "test"));
+                ADD_FAILURE() << "Expected missing auxiliary slot to fail";
+            } catch (const Exception& e) {
+                EXPECT_EQ(e.code(), ErrorCode::CORRUPTION);
+            }
+        }
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_EQ(tracker->consumption(), baseline);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
UnalignedAuxiliaryMapsKeepSketchContents) {
+    for (int lg_k : {4, 8, 12}) {
+        for (bool compact : {false, true}) {
+            const auto bytes = aux_sketch_bytes(lg_k, compact);
+            const auto reference =
+                    datasketches::hll_sketch::deserialize(bytes.data(), 
bytes.size());
+            const auto expected = reference.serialize_updatable();
+            for (size_t offset = 0; offset < alignof(uint32_t); ++offset) {
+                SCOPED_TRACE(testing::Message() << lg_k << '/' << compact << 
'/' << offset);
+                DorisVector<uint32_t> storage((bytes.size() + offset + 3) / 
sizeof(uint32_t));
+                auto* start = reinterpret_cast<char*>(storage.data()) + offset;
+                std::memcpy(start, bytes.data(), bytes.size());
+                const auto sketch =
+                        datasketches_hll::deserialize(StringRef(start, 
bytes.size()), "test");
+                const auto actual = sketch.serialize_updatable();
+                EXPECT_EQ(std::string(reinterpret_cast<const 
char*>(actual.data()), actual.size()),
+                          std::string(reinterpret_cast<const 
char*>(expected.data()),
+                                      expected.size()));
+                EXPECT_DOUBLE_EQ(sketch.get_composite_estimate(),
+                                 reference.get_composite_estimate());
+            }
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
MixedRowsKeepUnalignedConversionResults) {
+    const auto prefix = sketch_bytes(8, datasketches::HLL_6, 1000, true, true);
+    ASSERT_EQ(prefix.size(), 233);
+    for (bool compact : {false, true}) {
+        const auto bytes = aux_sketch_bytes(8, compact);
+        for (auto primitive : {TYPE_STRING, TYPE_VARCHAR, TYPE_VARBINARY}) {
+            for (int target : {4, 8, 21}) {
+                for (const auto& [type, name] : {std::pair 
{datasketches::HLL_4, "HLL_4"},
+                                                 std::pair 
{datasketches::HLL_6, "HLL_6"},
+                                                 std::pair 
{datasketches::HLL_8, "HLL_8"}}) {
+                    verify_unaligned_conversion(prefix, bytes, primitive, 
target, type, name);
+                }
+            }
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
UnalignedAggregateStateKeepsEstimate) {
+    using Data = AggregateFunctionHllSketchData<TYPE_STRING>;
+    for (bool compact : {false, true}) {
+        const auto bytes = aux_sketch_bytes(8, compact);
+        const auto source = datasketches_hll::deserialize(StringRef(bytes), 
"test");
+        Data expected;
+        expected.merge(source);
+        auto buffer = ColumnString::create();
+        BufferWritable writer(*buffer);
+        writer.write_binary(StringRef(bytes));
+        writer.commit();
+        BufferReadable inspect(buffer->get_data_at(0));
+        StringRef payload;
+        inspect.read_binary(payload);
+        ASSERT_NE(reinterpret_cast<uintptr_t>(payload.data) % 
alignof(uint32_t), 0);
+        BufferReadable reader(buffer->get_data_at(0));
+        Data actual;
+        actual.read(reader);
+        EXPECT_DOUBLE_EQ(actual.get_result(), expected.get_result());
+        const auto actual_bytes = 
actual.hll_union_data->get_result().serialize_compact();
+        const auto expected_bytes = 
expected.hll_union_data->get_result().serialize_compact();
+        EXPECT_EQ(std::string(reinterpret_cast<const 
char*>(actual_bytes.data()),
+                              actual_bytes.size()),
+                  std::string(reinterpret_cast<const 
char*>(expected_bytes.data()),
+                              expected_bytes.size()));
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
UnalignedDeserializationFailuresAreBalanced) {
+    const auto bytes = aux_sketch_bytes(8, true);
+    DorisVector<uint32_t> storage((bytes.size() + 4) / sizeof(uint32_t));
+    auto* start = reinterpret_cast<char*>(storage.data()) + 1;
+    std::memcpy(start, bytes.data(), bytes.size());
+    const StringRef input(start, bytes.size());
+    size_t allocations = 0;
+    {
+        ScopedSketchAllocationFailure count_only(0);
+        const auto sketch = datasketches_hll::deserialize(input, "test");
+        allocations = count_only.calls;
+    }
+    ASSERT_GT(allocations, 1);
+    for (size_t fail_at = 1; fail_at <= allocations; ++fail_at) {
+        auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                        
"HllUnalignedAllocationFailure");
+        auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const auto baseline = tracker->consumption();
+        ScopedSketchAllocationFailure failure(fail_at);
+        try {
+            static_cast<void>(datasketches_hll::deserialize(input, "test"));
+            ADD_FAILURE() << "Expected allocation failure";
+        } catch (const Exception& e) {
+            EXPECT_EQ(e.code(), ErrorCode::MEM_ALLOC_FAILED);
+        }
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_EQ(tracker->consumption(), baseline);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
RejectCompactEmptyAuxiliaryEntry) {
+    const auto bytes = empty_compact_aux_entry();
+    for (const auto& type : {"HLL_4", "HLL_6", "HLL_8"}) {
+        expect_corruption(bytes, "compact auxiliary map contains an EMPTY 
entry", type);
+    }
+    using Data = AggregateFunctionHllSketchData<TYPE_STRING>;
+    auto buffer = ColumnString::create();
+    BufferWritable writer(*buffer);
+    writer.write_binary(StringRef(bytes));
+    writer.commit();
+    BufferReadable reader(buffer->get_data_at(0));
+    Data data;
+    try {
+        data.read(reader);
+        ADD_FAILURE() << "Expected compact EMPTY auxiliary entry to fail";
+    } catch (const Exception& e) {
+        EXPECT_EQ(e.code(), ErrorCode::CORRUPTION);
+        EXPECT_NE(e.to_string().find("compact auxiliary map contains an EMPTY 
entry"),
+                  std::string::npos);
+    }
+    EXPECT_FALSE(data.hll_union_data.has_value());
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, RejectUpdatableSetCountMismatch) 
{
+    using namespace datasketches::hll_constants;
+    for (auto type : {datasketches::HLL_4, datasketches::HLL_6, 
datasketches::HLL_8}) {
+        const auto valid = sketch_bytes(8, type, 10, false);
+        ASSERT_EQ(static_cast<uint8_t>(valid[PREAMBLE_INTS_BYTE]), 
HASH_SET_PREINTS);
+        ASSERT_EQ(valid.size(), HASH_SET_INT_ARR_START + 32 * 
sizeof(uint32_t));
+        for (uint32_t count : {0, 1, 9, 11, 24}) {
+            auto bytes = valid;
+            put_uint32(bytes, HASH_SET_COUNT_INT, count);
+            verify_set_count_corruption(bytes);
+        }
+        // The legacy unset lgArr is reconstructed by the library from count=1 
as 32 slots;
+        // the payload is still complete, so the occupied-slot check must 
reject it too.
+        auto legacy = valid;
+        legacy[LG_ARR_BYTE] = 0;
+        put_uint32(legacy, HASH_SET_COUNT_INT, 1);
+        verify_set_count_corruption(legacy);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest,
+       SetCountMismatchFailsBeforeAllocationAndAggregateRead) {
+    using namespace datasketches::hll_constants;
+    auto bytes = sketch_bytes(8, datasketches::HLL_4, 10, false);
+    put_uint32(bytes, HASH_SET_COUNT_INT, 1);
+    auto tracker =
+            MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER, 
"HllSetCountMismatch");
+    auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    const auto baseline = tracker->consumption();
+    {
+        ScopedSketchAllocationFailure count_only(0);
+        for (int i = 0; i < 32; ++i) {
+            try {
+                
static_cast<void>(datasketches_hll::deserialize(StringRef(bytes), "test"));
+                ADD_FAILURE() << "Expected SET count mismatch to fail";
+            } catch (const Exception& e) {
+                EXPECT_EQ(e.code(), ErrorCode::CORRUPTION);
+            }
+        }
+        EXPECT_EQ(count_only.calls, 0);
+        auto buffer = ColumnString::create();
+        BufferWritable writer(*buffer);
+        writer.write_binary(StringRef(bytes));
+        writer.commit();
+        BufferReadable reader(buffer->get_data_at(0));
+        AggregateFunctionHllSketchData<TYPE_STRING> data;
+        try {
+            data.read(reader);
+            ADD_FAILURE() << "Expected aggregate SET count mismatch to fail";
+        } catch (const Exception& e) {
+            EXPECT_EQ(e.code(), ErrorCode::CORRUPTION);
+            EXPECT_NE(e.to_string().find("coupon count does not match occupied 
slots"),
+                      std::string::npos);
+        }
+        EXPECT_FALSE(data.hll_union_data.has_value());
+        EXPECT_EQ(count_only.calls, 0);
+    }
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    EXPECT_EQ(tracker->consumption(), baseline);
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
ValidSetCountsAndLegacyExponentsKeepResults) {
+    using namespace datasketches::hll_constants;
+    for (auto type : {datasketches::HLL_4, datasketches::HLL_6, 
datasketches::HLL_8}) {
+        for (uint64_t count : {10, 80, 300}) {
+            for (bool compact : {false, true}) {
+                auto bytes = sketch_bytes(12, type, count, compact);
+                ASSERT_EQ(static_cast<uint8_t>(bytes[PREAMBLE_INTS_BYTE]), 
HASH_SET_PREINTS);
+                const auto reference =
+                        datasketches::hll_sketch::deserialize(bytes.data(), 
bytes.size());
+                // Preserve the library's reconstruction of an unset 
serialized array exponent.
+                bytes[LG_ARR_BYTE] = 0;
+                const auto parsed = 
datasketches_hll::deserialize(StringRef(bytes), "test");
+                const auto expected = reference.serialize_compact();
+                const auto actual = parsed.serialize_compact();
+                EXPECT_EQ(std::string(reinterpret_cast<const 
char*>(actual.data()), actual.size()),
+                          std::string(reinterpret_cast<const 
char*>(expected.data()),
+                                      expected.size()));
+                EXPECT_DOUBLE_EQ(parsed.get_estimate(), 
reference.get_estimate());
+            }
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
EveryDeserializationAllocationFailureIsBalanced) {
+    for (const auto& bytes : {sketch_bytes(8, datasketches::HLL_8, 1, true, 
true),
+                              sketch_bytes(8, datasketches::HLL_4, 10, false),
+                              aux_sketch_bytes(8, true), aux_sketch_bytes(8, 
false)}) {
+        size_t allocations = 0;
+        {
+            ScopedSketchAllocationFailure count_only(0);
+            auto sketch = datasketches_hll::deserialize(StringRef(bytes), 
"test");
+            EXPECT_FALSE(sketch.is_empty());
+            allocations = count_only.calls;
+        }
+        ASSERT_GT(allocations, 1);
+        for (size_t fail_at = 1; fail_at <= allocations; ++fail_at) {
+            SCOPED_TRACE(fail_at);
+            ScopedSketchAllocationFailure failure(fail_at);
+            auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                            
"HllDeserializeAllocationFailure");
+            auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+            thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+            const auto baseline = tracker->consumption();
+            try {
+                
static_cast<void>(datasketches_hll::deserialize(StringRef(bytes), "test"));
+                ADD_FAILURE() << "Expected allocation failure";
+            } catch (const Exception& e) {
+                EXPECT_EQ(e.code(), ErrorCode::MEM_ALLOC_FAILED);
+            }
+            EXPECT_EQ(failure.calls, fail_at);
+            thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+            EXPECT_EQ(tracker->consumption(), baseline);
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
EveryConversionAllocationFailureIsBalanced) {
+    for (const auto& bytes :
+         {aux_sketch_bytes(12, true), sketch_bytes(12, datasketches::HLL_4, 
200, true)}) {
+        for (const auto& type : {"HLL_4", "HLL_6", "HLL_8"}) {
+            size_t allocations = 0;
+            {
+                auto block = input({bytes}, 8, type);
+                ScopedSketchAllocationFailure count_only(0);
+                ASSERT_TRUE(execute(block).ok());
+                allocations = count_only.calls;
+            }
+            ASSERT_GT(allocations, 1);
+            for (size_t fail_at = 1; fail_at <= allocations; ++fail_at) {
+                verify_conversion_allocation_failure(bytes, type, fail_at);
+            }
+        }
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
AggregateUpdateFailureResetsStateAndMemory) {
+    using Data = AggregateFunctionHllSketchData<TYPE_STRING>;
+    const auto large_bytes = sketch_bytes(12, datasketches::HLL_8, 1000, true, 
true);
+    const auto small_bytes = sketch_bytes(8, datasketches::HLL_8, 1000, true, 
true);
+    auto large = datasketches_hll::deserialize(StringRef(large_bytes), "test");
+    auto small = datasketches_hll::deserialize(StringRef(small_bytes), "test");
+    size_t allocations = 0;
+    {
+        Data data;
+        data.merge(large);
+        ScopedSketchAllocationFailure count_only(0);
+        data.merge(small);
+        allocations = count_only.calls;
+    }
+    ASSERT_GT(allocations, 1);
+    for (size_t fail_at = 1; fail_at <= allocations; ++fail_at) {
+        SCOPED_TRACE(fail_at);
+        auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                        
"HllAggregateAllocationFailure");
+        auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const auto baseline = tracker->consumption();
+        {
+            Data data;
+            data.merge(large);
+            ScopedSketchAllocationFailure failure(fail_at);
+            try {
+                data.merge(small);
+                ADD_FAILURE() << "Expected aggregate allocation failure";
+            } catch (const Exception& e) {
+                EXPECT_EQ(e.code(), ErrorCode::MEM_ALLOC_FAILED);
+            }
+            EXPECT_FALSE(data.hll_union_data.has_value());
+        }
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_EQ(tracker->consumption(), baseline);
+    }
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
SuccessfulAllocationScopeTransfersOwnership) {
+    const auto bytes = aux_sketch_bytes(8, true);
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                    
"HllAllocationScopeOwnership");
+    auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    const auto baseline = tracker->consumption();
+    {
+        // A successful deserialize returns a sketch whose allocations outlive 
its scope.
+        auto sketch = datasketches_hll::deserialize(StringRef(bytes), "test");
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_GT(tracker->consumption(), baseline);
+        EXPECT_EQ(sketch.get_target_type(), datasketches::HLL_4);
+    }
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    EXPECT_EQ(tracker->consumption(), baseline);
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
UnionCopyAndMoveKeepIndependentOwnership) {
+    const auto bytes = aux_sketch_bytes(8, true);
+    auto sketch = datasketches_hll::deserialize(StringRef(bytes), "test");
+    auto tracker =
+            MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER, 
"HllUnionCopyMove");
+    auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    const auto baseline = tracker->consumption();
+    {
+        datasketches_hll::Union source(8);
+        source.update(sketch);
+        const auto estimate = source.get_estimate();
+        datasketches_hll::Union copy(source);
+        datasketches_hll::Union moved(std::move(copy));
+        EXPECT_DOUBLE_EQ(moved.get_estimate(), estimate);
+        moved = source;
+        EXPECT_DOUBLE_EQ(moved.get_estimate(), estimate);
+    }
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    EXPECT_EQ(tracker->consumption(), baseline);
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
NestedAllocationScopesReleaseOnlyTheirOwnMemory) {
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                    
"HllNestedAllocationScope");
+    auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    const auto baseline = tracker->consumption();
+    {
+        datasketches_hll::AllocationScope outer;
+        auto* parent = datasketches_hll::Alloc().allocate(32);
+        try {
+            datasketches_hll::AllocationScope inner;
+            static_cast<void>(datasketches_hll::Alloc().allocate(64));
+            throw Exception(ErrorCode::MEM_ALLOC_FAILED, "Simulated orphan in 
nested call");
+        } catch (const Exception&) {
+        }
+        parent[0] = 42;
+        EXPECT_EQ(parent[0], 42);
+        datasketches_hll::Alloc().deallocate(parent, 32);
+    }
+    thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+    EXPECT_EQ(tracker->consumption(), baseline);
+}
+
+TEST_F(FunctionDataSketchesHllDownsampleTest, 
AllocationScopesAreIsolatedAcrossBthreads) {
+    const auto work = +[](void*) -> void* {
+        ScopedInitThreadContext thread;
+        auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                        
"HllBthreadAllocationScope");
+        auto switch_tracker = SwitchThreadMemTrackerLimiter(tracker);
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        const auto baseline = tracker->consumption();
+        for (int i = 0; i < 8; ++i) {
+            try {
+                datasketches_hll::AllocationScope scope;
+                static_cast<void>(datasketches_hll::Alloc().allocate(64));
+                // Yield while the call owns allocations, so another bthread 
can enter its
+                // own scope on the same worker. A pthread-local scope alone 
is insufficient.
+                bthread_usleep(1000);
+                throw Exception(ErrorCode::MEM_ALLOC_FAILED, "Simulated 
bthread orphan");
+            } catch (const Exception&) {
+            }
+        }
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        EXPECT_EQ(tracker->consumption(), baseline);
+        return nullptr;
+    };
+    std::array<bthread_t, 2> threads;
+    for (auto& thread : threads) {
+        ASSERT_EQ(bthread_start_background(&thread, nullptr, work, nullptr), 
0);
+    }
+    for (auto thread : threads) {
+        EXPECT_EQ(bthread_join(thread, nullptr), 0);
+    }
+}
+
+} // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
index 7f0140e8303..2d7aaecd38d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
@@ -163,6 +163,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CurrentUser;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.CutIpv6;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CutToFirstSignificantSubdomain;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.DamerauLevenshteinDistance;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.DataSketchesHllDownsample;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Database;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Date;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.DateDiff;
@@ -773,6 +774,7 @@ public class BuiltinScalarFunctions implements 
FunctionHelper {
             scalar(CutIpv6.class, "cut_ipv6"),
             scalar(CutToFirstSignificantSubdomain.class, 
"cut_to_first_significant_subdomain"),
             scalar(DamerauLevenshteinDistance.class, 
"damerau_levenshtein_distance"),
+            scalar(DataSketchesHllDownsample.class, 
"datasketches_hll_downsample"),
             scalar(Database.class, "database", "schema", "current_database"),
             scalar(Date.class, "date"),
             scalar(DateDiff.class, "datediff"),
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
index 57a5a5fe1d9..1e885d9a2c8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/FoldConstantRuleOnBE.java
@@ -42,6 +42,7 @@ import org.apache.doris.nereids.trees.expressions.Match;
 import org.apache.doris.nereids.trees.expressions.functions.BoundFunction;
 import org.apache.doris.nereids.trees.expressions.functions.ai.AIFunction;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.TableGeneratingFunction;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.DataSketchesHllDownsample;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.FromBase64;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.Ipv6StringToNumOrDefault;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.Ipv6StringToNumOrNull;
@@ -254,7 +255,8 @@ public class FoldConstantRuleOnBE implements 
ExpressionPatternRuleFactory {
 
         // Skip those function to avoid incorrect binary data processing 
during constant folding
         if (expr instanceof FromBase64 || expr instanceof Ipv6StringToNumOrNull
-                || expr instanceof Ipv6StringToNumOrDefault || expr instanceof 
AIFunction) {
+                || expr instanceof Ipv6StringToNumOrDefault || expr instanceof 
AIFunction
+                || expr instanceof DataSketchesHllDownsample) {
             return true;
         }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DataSketchesHllDownsample.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DataSketchesHllDownsample.java
new file mode 100644
index 00000000000..c2a63c3056f
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DataSketchesHllDownsample.java
@@ -0,0 +1,185 @@
+// 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.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLikeLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.VarBinaryType;
+import org.apache.doris.nereids.types.VarcharType;
+import org.apache.doris.nereids.util.ExpressionUtils;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * ScalarFunction 'datasketches_hll_downsample'.
+ *
+ * <p>datasketches_hll_downsample(sketch, lg_k [, tgt_type]) rewrites a 
serialized Apache DataSketches HLL
+ * sketch so that its lgK is min(lg_k, source lgK) and its HLL type is 
tgt_type (HLL_4 by default). A sketch
+ * that already satisfies both is returned byte for byte.
+ */
+public class DataSketchesHllDownsample extends ScalarFunction
+        implements ExplicitlyCastableSignature, PropagateNullable {
+
+    public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+            
FunctionSignature.ret(StringType.INSTANCE).args(StringType.INSTANCE, 
IntegerType.INSTANCE),
+            FunctionSignature.ret(VarcharType.SYSTEM_DEFAULT)
+                    .args(VarcharType.SYSTEM_DEFAULT, IntegerType.INSTANCE),
+            
FunctionSignature.ret(VarBinaryType.INSTANCE).args(VarBinaryType.INSTANCE, 
IntegerType.INSTANCE),
+            FunctionSignature.ret(StringType.INSTANCE)
+                    .args(StringType.INSTANCE, IntegerType.INSTANCE, 
VarcharType.SYSTEM_DEFAULT),
+            FunctionSignature.ret(VarcharType.SYSTEM_DEFAULT)
+                    .args(VarcharType.SYSTEM_DEFAULT, IntegerType.INSTANCE, 
VarcharType.SYSTEM_DEFAULT),
+            FunctionSignature.ret(VarBinaryType.INSTANCE)
+                    .args(VarBinaryType.INSTANCE, IntegerType.INSTANCE, 
VarcharType.SYSTEM_DEFAULT)
+    );
+
+    // the bounds the DataSketches library accepts for a sketch's lgK
+    private static final int MIN_LG_K = 4;
+    private static final int MAX_LG_K = 21;
+    private static final Set<String> TGT_TYPES = ImmutableSet.of("HLL_4", 
"HLL_6", "HLL_8");
+
+    /**
+     * constructor with 2 arguments.
+     */
+    public DataSketchesHllDownsample(Expression arg0, Expression arg1) {
+        super("datasketches_hll_downsample", arg0, arg1);
+    }
+
+    /**
+     * constructor with 3 arguments.
+     */
+    public DataSketchesHllDownsample(Expression arg0, Expression arg1, 
Expression arg2) {
+        super("datasketches_hll_downsample", arg0, arg1, arg2);
+    }
+
+    /** constructor for withChildren and reuse signature */
+    private DataSketchesHllDownsample(ScalarFunctionParams functionParams) {
+        super(functionParams);
+    }
+
+    @Override
+    public void checkLegalityBeforeTypeCoercion() {
+        DataType sketchType = getArgumentType(0);
+        if (!(sketchType.isStringLikeType() || sketchType.isVarBinaryType() || 
sketchType.isNullType())) {
+            throw new AnalysisException(getName()
+                    + " function's first argument should be of 
STRING/VARCHAR/VARBINARY type, but was "
+                    + sketchType);
+        }
+        // validate the value FE can evaluate here, because constant folding, 
e.g. of
+        // datasketches_hll_downsample(null, 30), may remove this function 
before checkLegalityAfterRewrite;
+        // the types are checked before the type coercion casts the arguments 
to the signature
+        checkLgK(ExpressionUtils.foldConstantArgument(getArgument(1)));
+        if (arity() == 3) {
+            checkTgtType(ExpressionUtils.foldConstantArgument(getArgument(2)));
+        }
+    }
+
+    @Override
+    public void checkLegalityAfterRewrite() {
+        checkLgK(getArgument(1));
+        if (arity() == 3) {
+            checkTgtType(getArgument(2));
+        }
+    }
+
+    private void checkLgK(Expression lgK) {
+        if (!lgK.isConstant() || !lgK.getDataType().isIntegralType()) {
+            throw new AnalysisException(getName() + " requires lg_k to be a 
constant integer: " + toSql());
+        }
+        // the value of a constant FE cannot fold is validated by BE when it 
is evaluated
+        if (!(lgK instanceof Literal)) {
+            return;
+        }
+        if (!(lgK instanceof IntegerLikeLiteral)) {
+            throw new AnalysisException(getName() + " requires lg_k to be a 
constant integer: " + toSql());
+        }
+        // compare the full value: narrowing a wide literal to int would wrap 
it, e.g. 4294967308 into 12
+        BigDecimal value = ((IntegerLikeLiteral) lgK).getBigDecimalValue();
+        if (value.compareTo(BigDecimal.valueOf(MIN_LG_K)) < 0 || 
value.compareTo(BigDecimal.valueOf(MAX_LG_K)) > 0) {
+            throw new AnalysisException(getName() + " requires lg_k to be 
between " + MIN_LG_K + " and "
+                    + MAX_LG_K + ", but was " + value);
+        }
+    }
+
+    private void checkTgtType(Expression tgtType) {
+        if (!tgtType.isConstant() || 
!tgtType.getDataType().isStringLikeType()) {
+            throw new AnalysisException(getName() + " requires tgt_type to be 
a constant string: " + toSql());
+        }
+        if (!(tgtType instanceof Literal)) {
+            return;
+        }
+        if (!(tgtType instanceof StringLikeLiteral)) {
+            throw new AnalysisException(getName() + " requires tgt_type to be 
a constant string: " + toSql());
+        }
+        String value = ((StringLikeLiteral) tgtType).getStringValue();
+        if (!TGT_TYPES.contains(value.toUpperCase(Locale.ROOT))) {
+            throw new AnalysisException(getName() + " requires tgt_type to be 
one of HLL_4, HLL_6, HLL_8, but was '"
+                    + value + "'");
+        }
+    }
+
+    /**
+     * withChildren.
+     */
+    @Override
+    public DataSketchesHllDownsample withChildren(List<Expression> children) {
+        Preconditions.checkArgument(children.size() == 2 || children.size() == 
3);
+        return new DataSketchesHllDownsample(getFunctionParams(children));
+    }
+
+    @Override
+    public List<FunctionSignature> getSignatures() {
+        // Catalog matching treats VARCHAR and STRING as interchangeable. 
Select the input family
+        // first so the earlier STRING overload cannot change a VARCHAR result 
into STRING.
+        DataType input = getArgumentType(0);
+        DataType output = input.isVarBinaryType() ? VarBinaryType.INSTANCE
+                : input.isVarcharType() ? VarcharType.SYSTEM_DEFAULT : 
StringType.INSTANCE;
+        return SIGNATURES.stream().filter(signature -> 
signature.returnType.equals(output))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean foldable() {
+        // Keep parameter validation on BE even for a NULL sketch when, for 
example, lg_k contains
+        // crc32() and cannot be evaluated on FE. Binary results also cannot 
become FE strings.
+        return false;
+    }
+
+    @Override
+    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+        return visitor.visitDataSketchesHllDownsample(this, context);
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
index 865669a0632..b5120939365 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
@@ -173,6 +173,7 @@ import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CurrentUser;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.CutIpv6;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.CutToFirstSignificantSubdomain;
 import 
org.apache.doris.nereids.trees.expressions.functions.scalar.DamerauLevenshteinDistance;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.DataSketchesHllDownsample;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Database;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.Date;
 import org.apache.doris.nereids.trees.expressions.functions.scalar.DateDiff;
@@ -1185,6 +1186,10 @@ public interface ScalarFunctionVisitor<R, C> {
         return visitScalarFunction(cutIpv6, context);
     }
 
+    default R visitDataSketchesHllDownsample(DataSketchesHllDownsample 
function, C context) {
+        return visitScalarFunction(function, context);
+    }
+
     default R visitDatabase(Database database, C context) {
         return visitScalarFunction(database, context);
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DataSketchesHllDownsampleTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DataSketchesHllDownsampleTest.java
new file mode 100644
index 00000000000..988696464f3
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/DataSketchesHllDownsampleTest.java
@@ -0,0 +1,107 @@
+// 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.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.rules.analysis.ExpressionAnalyzer;
+import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE;
+import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.LargeIntLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.VarBinaryType;
+import org.apache.doris.nereids.types.VarcharType;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+
+class DataSketchesHllDownsampleTest {
+    private static final Expression SKETCH = SlotReference.of("sk", 
StringType.INSTANCE);
+
+    private DataSketchesHllDownsample analyze(Expression sketch, Expression k, 
Expression type) {
+        return (DataSketchesHllDownsample) 
ExpressionAnalyzer.analyzeFunction(null, null,
+                type == null ? new DataSketchesHllDownsample(sketch, k)
+                        : new DataSketchesHllDownsample(sketch, k, type));
+    }
+
+    @Test
+    void testInputTypesAndArity() {
+        for (DataType type : ImmutableList.of(StringType.INSTANCE, 
VarcharType.SYSTEM_DEFAULT,
+                VarBinaryType.INSTANCE)) {
+            Expression input = SlotReference.of("sk", type);
+            Assertions.assertEquals(type, analyze(input, new 
IntegerLiteral(12), null).getDataType());
+            DataSketchesHllDownsample fn = analyze(input, new 
IntegerLiteral(12), new StringLiteral("HLL_4"));
+            Assertions.assertEquals(type, fn.getDataType());
+            Assertions.assertEquals(fn.getArguments(), 
fn.withChildren(fn.getArguments()).getArguments());
+        }
+        Assertions.assertThrows(AnalysisException.class,
+                () -> analyze(new IntegerLiteral(1), new IntegerLiteral(12), 
null));
+    }
+
+    @Test
+    void testBoundsAndTypes() {
+        for (int k : new int[] {4, 7, 10, 15, 21}) {
+            for (String type : ImmutableList.of("HLL_4", "hll_6", "HlL_8")) {
+                Assertions.assertDoesNotThrow(() -> analyze(SKETCH, new 
IntegerLiteral(k), new StringLiteral(type)));
+            }
+        }
+        for (Expression invalid : ImmutableList.of(new IntegerLiteral(3), new 
IntegerLiteral(22),
+                new LargeIntLiteral(new BigInteger("18446744073709551628")),
+                new StringLiteral("12"), new NullLiteral(IntegerType.INSTANCE),
+                SlotReference.of("k", IntegerType.INSTANCE))) {
+            Assertions.assertThrows(AnalysisException.class, () -> 
analyze(SKETCH, invalid, null));
+            // Invalid parameters must be rejected before NULL propagation can 
remove the call.
+            Assertions.assertThrows(AnalysisException.class, () -> analyze(new 
NullLiteral(), invalid, null));
+        }
+        for (Expression invalid : ImmutableList.of(new StringLiteral("HLL_5"), 
new IntegerLiteral(4),
+                new NullLiteral(StringType.INSTANCE), SlotReference.of("type", 
StringType.INSTANCE))) {
+            Assertions.assertThrows(AnalysisException.class,
+                    () -> analyze(SKETCH, new IntegerLiteral(12), invalid));
+        }
+    }
+
+    @Test
+    void testConstantExpressionsAndRewriteValidation() {
+        DataSketchesHllDownsample fn = analyze(SKETCH,
+                new Add(new IntegerLiteral(10), new IntegerLiteral(2)),
+                new Concat(new StringLiteral("HLL_"), new StringLiteral("4")));
+        Expression folded = FoldConstantRuleOnFE.evaluateWithoutContext(fn);
+        Assertions.assertDoesNotThrow(folded::checkLegalityAfterRewrite);
+        Assertions.assertEquals(new IntegerLiteral(12), folded.child(1));
+        Expression onlyBe = new Add(new IntegerLiteral(10), new Crc32(new 
StringLiteral("")));
+        Assertions.assertDoesNotThrow(() -> analyze(SKETCH, onlyBe, 
null).checkLegalityAfterRewrite());
+        Expression nullCall = analyze(new NullLiteral(), onlyBe, null);
+        Assertions.assertInstanceOf(DataSketchesHllDownsample.class,
+                FoldConstantRuleOnFE.evaluateWithoutContext(nullCall));
+        DataSketchesHllDownsample invalid = fn.withChildren(
+                ImmutableList.of(SKETCH, new IntegerLiteral(30), new 
StringLiteral("HLL_4")));
+        Assertions.assertThrows(AnalysisException.class, 
invalid::checkLegalityAfterRewrite);
+        DataSketchesHllDownsample invalidType = fn.withChildren(
+                ImmutableList.of(SKETCH, new IntegerLiteral(12), new 
StringLiteral("bad")));
+        Assertions.assertThrows(AnalysisException.class, 
invalidType::checkLegalityAfterRewrite);
+    }
+}
diff --git 
a/regression-test/data/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample.out
 
b/regression-test/data/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample.out
new file mode 100644
index 00000000000..8924538f0a9
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample.out
@@ -0,0 +1,147 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !matrix --
+1      48      04      02      true
+2      28      04      00      true
+3      8       04      00      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      53      04      06      true
+2      28      04      04      true
+3      8       04      04      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      56      04      0A      true
+2      28      04      08      true
+3      8       04      08      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      104     07      02      true
+2      28      07      00      true
+3      8       07      00      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      137     07      06      true
+2      28      07      04      true
+3      8       07      04      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      168     07      0A      true
+2      28      07      08      true
+3      8       07      08      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      552     0A      02      true
+2      28      0A      00      true
+3      8       0A      00      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      809     0A      06      true
+2      28      0A      04      true
+3      8       0A      04      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      1064    0A      0A      true
+2      28      0A      08      true
+3      8       0A      08      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      2088    0C      02      true
+2      28      0F      00      true
+3      8       0C      00      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      3113    0C      06      true
+2      28      0F      04      true
+3      8       0C      04      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      4136    0C      0A      true
+2      28      0F      08      true
+3      8       0C      08      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      2088    0C      02      true
+2      28      15      00      true
+3      8       0C      00      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      3113    0C      06      true
+2      28      15      04      true
+3      8       0C      04      true
+4      \N      \N      \N      \N
+
+-- !matrix --
+1      4136    0C      0A      true
+2      28      15      08      true
+3      8       0C      08      true
+4      \N      \N      \N      \N
+
+-- !default --
+1      true    true
+2      true    true
+3      true    true
+4      \N      \N
+5      true    true
+
+-- !varchar --
+1      true
+2      true
+3      true
+4      \N
+5      true
+
+-- !binary --
+1      true
+2      true
+3      true
+4      \N
+5      true
+
+-- !union --
+4944   4944
+
+-- !constant --
+552    552     true
+
+-- !constant_expr --
+1      true    true
+2      true    true
+3      true    true
+4      \N      \N
+5      true    true
+
+-- !unfolded --
+1      true
+2      true
+3      true
+4      \N
+5      true
+
+-- !probe --
+1
+
+-- !overwrite --
+2024-01-01     1       11      552     0A      02
+2024-01-01     2       22      28      0A      00
+2024-01-01     3       33      8       0A      00
+2024-01-01     4       44      \N      \N      \N
+2024-02-01     5       55      4136    0C      0A
+
+-- !no_match --
+
+-- !no_pending --
+4      3       0
+
diff --git 
a/regression-test/data/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample_auxiliary.out
 
b/regression-test/data/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample_auxiliary.out
new file mode 100644
index 00000000000..19dd5bc09f4
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample_auxiliary.out
@@ -0,0 +1,62 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !input --
+1      233
+2      176
+3      200
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !results --
+1      true    true    true
+2      true    true    true
+3      true    true    true
+
+-- !noop --
+2      true
+3      true
+
+-- !serialization --
+true
+
+-- !aggregate --
+2      true
+3      true
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample.groovy
 
b/regression-test/suites/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample.groovy
new file mode 100644
index 00000000000..48b1c9575b9
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample.groovy
@@ -0,0 +1,169 @@
+// 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.
+
+suite("test_datasketches_hll_downsample") {
+    // Fixtures produced by datasketches-cpp 46025e9: update(int32_t i), i in 
[0,n), HLL_8.
+    // lgK=21/n=5 stays LIST; lgK=12/n=5000 is dense; lgK=12/n=0 is empty.
+    def sparse = 'AgEHFQMIBQjL18IEK/L7BoYv+Q11gWYHgbxdBg=='
+    def dense = 
'CgEHDAAIAAp0NJgpTVCzQAAAAFDYUJ9AAAAAAAAAAADFBAAAAAAAAAQEAwEAAQABAAIDBQABAgADAAMDAAICAQAAAQMBAwMDAQIAAAABAgIAAgQCAgMFAAQAAgEDAAEAAAUAAgAEAwICAgYDCAMEAQIBAAACAggAAwMAAgIAAQIDAAIAAgEABAEBAwIAAwEABgQDAQICAAEBBQEDAQEDAgIABAADAAACBAIDAAMEAgADAgMAAwUAAQEEAwABAQIBAQAHAQECAAADAQECAwIDAgMBAAECAQAAAQYFAAEDBgIBAQABAQMBAQIAAwEBAwECAQEBBgAABwEAAAICAAMDBgACAgAABAMBAAACAAEDAAIDAQAAAAIBAQYCAgQAAwEFBAMCAQMBAQABBQMBAgACAwMDAgMCAAECAAECAQEBAgICAgACBAEBAQIBAwIBAAQAAQEAAgABAgIACQIDBAMGBgAABgE
 [...]
+    def empty = 'AgEHDAMMAAg='
+    sql "DROP TABLE IF EXISTS test_ds_hll_downsample"
+    sql """CREATE TABLE test_ds_hll_downsample (
+          dt DATE, id INT, sk STRING, payload INT)
+        DUPLICATE KEY(dt, id)
+        PARTITION BY RANGE(dt) (
+          PARTITION p_old VALUES [('2024-01-01'), ('2024-02-01')),
+          PARTITION p_new VALUES [('2024-02-01'), ('2024-03-01')))
+        DISTRIBUTED BY HASH(id) BUCKETS 1 PROPERTIES ('replication_num'='1')"""
+    sql """INSERT INTO test_ds_hll_downsample VALUES
+        ('2024-01-01',1,from_base64('${dense}'),11),
+        ('2024-01-01',2,from_base64('${sparse}'),22),
+        ('2024-01-01',3,from_base64('${empty}'),33),
+        ('2024-01-01',4,NULL,44),
+        ('2024-02-01',5,from_base64('${dense}'),55)"""
+
+    for (int k : [4, 7, 10, 15, 21]) {
+        for (String t : ['HLL_4', 'hll_6', 'HlL_8']) {
+            // Header lgK and type bits are inspected through HEX, not through 
UTF-8 SUBSTR(sk).
+            order_qt_matrix """SELECT id, length(out), substr(hex(out),7,2), 
substr(hex(out),15,2),
+                datasketches_hll_downsample(out,${k},'${t}') = out AS 
idempotent
+                FROM (SELECT id,datasketches_hll_downsample(sk,${k},'${t}') AS 
out
+                      FROM test_ds_hll_downsample PARTITION(p_old)) d ORDER BY 
id"""
+        }
+    }
+    order_qt_default """SELECT id,
+        datasketches_hll_downsample(sk,10) = 
datasketches_hll_downsample(sk,10,'HLL_4'),
+        datasketches_hll_downsample(sk,21,'HLL_8') = sk
+        FROM test_ds_hll_downsample ORDER BY id"""
+    order_qt_varchar """SELECT id,
+        datasketches_hll_downsample(CAST(sk AS VARCHAR(65533)),10) = 
datasketches_hll_downsample(sk,10)
+        FROM test_ds_hll_downsample ORDER BY id"""
+    order_qt_binary """SELECT id,
+        hex(datasketches_hll_downsample(CAST(sk AS VARBINARY),10)) = 
hex(datasketches_hll_downsample(sk,10))
+        FROM test_ds_hll_downsample ORDER BY id"""
+    order_qt_union """SELECT 
round(datasketches_hll_union_agg(datasketches_hll_downsample(sk,10))),
+        round(datasketches_hll_union_agg(sk,10))
+        FROM test_ds_hll_downsample WHERE id=1"""
+    order_qt_constant """SELECT 
length(datasketches_hll_downsample(from_base64('${dense}'),10)),
+        length(datasketches_hll_downsample(from_base64_binary('${dense}'),10)),
+        datasketches_hll_downsample(NULL,10) IS NULL"""
+    order_qt_constant_expr """SELECT id,
+        datasketches_hll_downsample(sk,8+2,concat('HLL_','4')) = 
datasketches_hll_downsample(sk,10),
+        datasketches_hll_downsample(sk,10 + CAST(crc32('') AS INT),
+            IF(crc32('')=0,'HLL_4','HLL_6')) = 
datasketches_hll_downsample(sk,10)
+        FROM test_ds_hll_downsample ORDER BY id"""
+    sql 'SET debug_skip_fold_constant=true'
+    order_qt_unfolded """SELECT id,
+        datasketches_hll_downsample(sk,8+2,concat('HLL_','4')) = 
datasketches_hll_downsample(sk,10)
+        FROM test_ds_hll_downsample ORDER BY id"""
+    sql 'SET debug_skip_fold_constant=false'
+    order_qt_probe """SELECT id FROM test_ds_hll_downsample PARTITION(p_old)
+        WHERE datasketches_hll_downsample(sk,10) <> sk ORDER BY id LIMIT 1"""
+    sql """INSERT OVERWRITE TABLE test_ds_hll_downsample PARTITION(p_old)
+        SELECT dt,id,datasketches_hll_downsample(sk,10),payload
+        FROM test_ds_hll_downsample PARTITION(p_old)"""
+    order_qt_overwrite """SELECT 
dt,id,payload,length(sk),substr(hex(sk),7,2),substr(hex(sk),15,2)
+        FROM test_ds_hll_downsample ORDER BY dt,id"""
+    order_qt_no_match """SELECT 1 FROM test_ds_hll_downsample PARTITION(p_old)
+        WHERE datasketches_hll_downsample(sk,10) <> sk LIMIT 1"""
+    order_qt_no_pending """SELECT count(*),count(sk),
+        coalesce(sum(datasketches_hll_downsample(sk,10) <> sk),0)
+        FROM test_ds_hll_downsample PARTITION(p_old)"""
+    for (String arg : ['3','22','4294967308','18446744073709551628']) {
+        test {
+            sql "SELECT datasketches_hll_downsample(sk,${arg}) FROM 
test_ds_hll_downsample"
+            exception 'requires lg_k to be between 4 and 21'
+        }
+    }
+    for (String arg : ['NULL','10.5',"'10'",'id']) {
+        test {
+            sql "SELECT datasketches_hll_downsample(sk,${arg}) FROM 
test_ds_hll_downsample"
+            exception 'requires lg_k to be a constant integer'
+        }
+    }
+    for (String arg : ['NULL','4','sk']) {
+        test {
+            sql "SELECT datasketches_hll_downsample(sk,10,${arg}) FROM 
test_ds_hll_downsample"
+            exception 'requires tgt_type to be a constant string'
+        }
+    }
+    test {
+        sql "SELECT datasketches_hll_downsample(NULL,30)"
+        exception 'requires lg_k to be between 4 and 21'
+    }
+    test {
+        sql "SELECT datasketches_hll_downsample(sk,10,'HLL_5') FROM 
test_ds_hll_downsample"
+        exception 'requires tgt_type to be one of HLL_4, HLL_6, HLL_8'
+    }
+    test {
+        sql "SELECT datasketches_hll_downsample(id,10) FROM 
test_ds_hll_downsample"
+        exception 'first argument should be of STRING/VARCHAR/VARBINARY type'
+    }
+    test {
+        sql "SELECT datasketches_hll_downsample(NULL,22 + CAST(crc32('') AS 
INT))"
+        exception "between 4 and 21"
+    }
+    test {
+        // The compact HLL_4 below has auxCount=0. Replace the first register 
byte by AUX_TOKEN.
+        sql """SELECT datasketches_hll_downsample(
+            unhex(concat(substr(h,1,80),'0F',substr(h,83))),21)
+            FROM (SELECT 
hex(datasketches_hll_downsample(from_base64('${dense}'),12)) AS h) t"""
+        exception "AUX_TOKEN without an auxiliary map"
+    }
+    // datasketches-cpp 46025e9 fixtures with complete malformed auxiliary 
maps, or lgArr=32.
+    // Cover the shared deserialization entry through both the scalar and the 
existing aggregate.
+    def malformedAux = [
+        
['0a01070402280006000000000000f03f0000000000002f4000000000000000000f000000010000000000000000000000040000000003000050',
+         'cannot contain an auxiliary map'],
+        
['0a0107040228000a000000000000f03f0000000000002f4000000000000000000f000000010000000000000000000000000000010000000003000050',
+         'cannot contain an auxiliary map'],
+        ['03010708200000010a000000', 'SET array exponent'],
+        
['0a01070420200002565d6e7f888800400000003000002c4000000000000000000e00000002000000f0f000000000000000000000010000500000000003000054',
+         'auxiliary array exponent'],
+        
['0a01070402200002565d6e7f888800400000003000002c4000000000000000000e00000002000000f0f000000000000003000050130000540000000000000000',
+         'duplicate slots'],
+        
['0a01070402280002565d6e7f888800400000003000002c4000000000000000000e00000002000000f0f00000000000000300005013000054',
+         'duplicate slots']
+    ]
+    for (def bad : malformedAux) {
+        test {
+            sql "SELECT datasketches_hll_downsample(unhex('${bad[0]}'),21)"
+            exception bad[1]
+        }
+        test {
+            sql "SELECT datasketches_hll_union_agg(unhex('${bad[0]}'))"
+            exception bad[1]
+        }
+    }
+    test {
+        // Keep only auxiliary slot 3, but leave the AUX_TOKEN at register 1 
unchanged.
+        sql """SELECT datasketches_hll_downsample(unhex(
+            
'0a01070402280002565d6e7f888800400000003000002c4000000000000000000e00000001000000f0f000000000000003000054'),21)"""
+        exception 'no matching auxiliary slot'
+    }
+    test {
+        sql """SELECT datasketches_hll_union_agg(unhex(
+            
'0a01070402280002565d6e7f888800400000003000002c4000000000000000000e00000001000000f0f000000000000003000054'))"""
+        exception 'no matching auxiliary slot'
+    }
+    for (String bad : ["''", "from_base64('AA==')", "'not a sketch'"]) {
+        test {
+            sql "SELECT datasketches_hll_downsample(${bad},21)"
+            exception 'CORRUPTION'
+        }
+    }
+}
diff --git 
a/regression-test/suites/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample_auxiliary.groovy
 
b/regression-test/suites/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample_auxiliary.groovy
new file mode 100644
index 00000000000..ae403798760
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/hll_functions/test_datasketches_hll_downsample_auxiliary.groovy
@@ -0,0 +1,94 @@
+// 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.
+
+suite("test_datasketches_hll_downsample_auxiliary") {
+    // datasketches-cpp 46025e9 Hll4Array(lgK=8), coupons pair(1,20), 
pair(3,21).
+    // The complete register array is f0,f0 followed by 126 zero bytes.
+    def compact = 
"concat('0a0107080328000286068707080800400000000300c06f400000000000000000fe00000002000000',
 'f0f0', repeat('00',126), '0100005003000054')"
+    def updatable = 
"concat('0a0107080320000286068707080800400000000300c06f400000000000000000fe00000002000000',
 'f0f0', repeat('00',126), '00000000010000500000000003000054', repeat('00',16))"
+    sql 'DROP TABLE IF EXISTS test_ds_hll_downsample_auxiliary'
+    sql """CREATE TABLE test_ds_hll_downsample_auxiliary (id INT, sk STRING)
+        DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ('replication_num'='1')"""
+    sql """INSERT INTO test_ds_hll_downsample_auxiliary VALUES
+        (1,datasketches_hll_downsample(unhex(${compact}),8,'HLL_6')),
+        (2,unhex(${compact})), (3,unhex(${updatable}))"""
+    order_qt_input """SELECT id,length(sk) FROM 
test_ds_hll_downsample_auxiliary ORDER BY id"""
+    for (int k : [4,8,21]) {
+        for (String type : ['HLL_4','HLL_6','HLL_8']) {
+            order_qt_results """SELECT id,
+                hex(datasketches_hll_downsample(sk,${k},'${type}')) =
+                    hex(IF(id=3 AND ${k}>=8 AND '${type}'='HLL_4', sk,
+                        
datasketches_hll_downsample(unhex(${compact}),${k},'${type}'))),
+                hex(datasketches_hll_downsample(CAST(sk AS 
VARBINARY),${k},'${type}')) =
+                    hex(datasketches_hll_downsample(sk,${k},'${type}')),
+                hex(datasketches_hll_downsample(CAST(sk AS 
VARCHAR(65533)),${k},'${type}')) =
+                    hex(datasketches_hll_downsample(sk,${k},'${type}'))
+                FROM test_ds_hll_downsample_auxiliary ORDER BY id"""
+        }
+    }
+    order_qt_noop """SELECT id,datasketches_hll_downsample(sk,21,'HLL_4')=sk
+        FROM test_ds_hll_downsample_auxiliary WHERE id IN (2,3) ORDER BY id"""
+    order_qt_serialization """SELECT
+        hex(datasketches_hll_downsample(sk,4))=
+            hex(datasketches_hll_downsample(unhex(${compact}),4))
+        FROM test_ds_hll_downsample_auxiliary WHERE id=3 ORDER BY id"""
+    order_qt_aggregate """SELECT id,
+        datasketches_hll_union_agg(sk)=
+            datasketches_hll_union_agg(unhex(${compact}))
+        FROM test_ds_hll_downsample_auxiliary WHERE id IN (2,3) GROUP BY id 
ORDER BY id"""
+
+    // Only register 0 is AUX_TOKEN; auxCount=1, but its compact entry was 
replaced by EMPTY.
+    def bad = 
"concat('0a01070803280002000000000000f03f0000000200e06f400000000000000000ff00000001000000',
 '0f', repeat('00',131))"
+    for (int k : [4,21]) {
+        for (String type : ['HLL_4','HLL_6','HLL_8']) {
+            test {
+                sql "SELECT 
datasketches_hll_downsample(unhex(${bad}),${k},'${type}')"
+                exception 'compact auxiliary map contains an EMPTY entry'
+            }
+            test {
+                sql "SELECT datasketches_hll_downsample(CAST(unhex(${bad}) AS 
VARBINARY),${k},'${type}')"
+                exception 'compact auxiliary map contains an EMPTY entry'
+            }
+        }
+    }
+    test {
+        sql "SELECT datasketches_hll_union_agg(unhex(${bad}))"
+        exception 'compact auxiliary map contains an EMPTY entry'
+    }
+
+    // Complete updatable lgK=8 HLL_4 SET from update(int32_t i), i in [0,10).
+    // Only change the count; all 32 table slots remain present and ten are 
occupied.
+    def setImage = 
'03010708050000010a0000000000000081bc5d0600000000000000000000000000000000862ff90d00000000000000000000000000000000cbd7c20400000000000000000000000000000000c1e9170500000000d21673070000000034a2610e7581660700000000000000000000000000000000fc2d420a7b65e6082bf2fb06000000000000000000000000'
+    for (String count : ['01000000','0b000000']) {
+        def malformed = 
"unhex(concat(substr('${setImage}',1,16),'${count}',substr('${setImage}',25)))"
+        for (int k : [4,8,21]) {
+            test {
+                sql "SELECT 
datasketches_hll_downsample(${malformed},${k},'HLL_6')"
+                exception 'coupon count does not match occupied slots'
+            }
+            test {
+                sql "SELECT datasketches_hll_downsample(CAST(${malformed} AS 
VARBINARY),${k},'HLL_4')"
+                exception 'coupon count does not match occupied slots'
+            }
+        }
+        test {
+            sql "SELECT datasketches_hll_union_agg(${malformed})"
+            exception 'coupon count does not match occupied slots'
+        }
+    }
+}


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

Reply via email to