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

mrhhsg 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 078e667a7ba [fix](column) Make struct hashing respect the outer 
nullable mask (#67962)
078e667a7ba is described below

commit 078e667a7ba3112ffb50b9e80109e031ded3d47c
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Oct 15 15:54:27 2026 +0800

    [fix](column) Make struct hashing respect the outer nullable mask (#67962)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Problem Summary:
    
    `ColumnStruct::update_crc32c_batch` / `update_crc32c_single` (and the
    xxHash / zlib-crc range variants) ignored the `null_map` passed in by an
    outer `ColumnNullable` and kept hashing the nested field columns. A NULL
    struct row can still carry an arbitrary hidden payload in its fields,
    for example `IF(cond, NULL, named_struct(...))` keeps the else-branch
    value under the NULL flag. Two logically equal NULL structs therefore
    got
    different hashes and the local hash shuffle sent them to different
    aggregation instances, so `GROUP BY` on a nullable struct key silently
    produced one extra group per distinct hidden payload.
    
    `ColumnArray` and `ColumnMap` already skip the NULL rows of the outer
    mask; this change applies the same pattern to `ColumnStruct` for every
    hash entry point that receives a null map. Rows without a null map keep
    the existing column-wise batch path, so hashes of non-NULL rows are
    unchanged.
    
    ### Release note
    
    Hashing of a NULL `STRUCT` value no longer depends on the hidden field
    payload stored under the NULL flag, so `GROUP BY` / `DISTINCT` on
    nullable `STRUCT` keys (including `STRUCT` nested in `ARRAY` / `MAP`)
    stop splitting equal NULL keys. Consequences of the changed hash value:
    
    - During a rolling upgrade, old and new BEs may route the same NULL
    `STRUCT` shuffle key to different receivers when the hidden payloads are
    identical (for example stored NULLs); the divergence ends with the
    upgrade. `SET GLOBAL enable_new_shuffle_hash_method = false` keeps
    old/new BEs consistent only for top-level nullable `STRUCT` keys, not
    for `STRUCT` nested in `ARRAY` / `MAP` / another `STRUCT`.
    - An `agg_state<multi_distinct_count(ARRAY<...STRUCT...>)>` column
    written by an old BE stores xxHash identifiers of its `ARRAY` values;
    after the upgrade, an `ARRAY` containing a NULL `STRUCT` element gets a
    new identifier, so such a persisted state can count that `ARRAY` value
    once more than before when merged with new data.
    
    ### Check List (For Author)
    
    - Test:
        - Unit Test: `ColumnStructTest.*RespectsOuterNullMap` cover crc32c
          batch/single, xxHash batch/range and zlib-crc range hashing of a
          nullable struct whose NULL rows carry different payloads.
        - Regression test:
    `query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle`
    groups `IF(cond, NULL, named_struct(...))` keys through a local hash
          shuffle and checks the result matches the non-local-shuffle plan.
    - Behavior changed: Yes. NULL `STRUCT` rows are excluded from the hash
    input, so hash values of nullable `STRUCT` keys change (see release
    note).
    - Does this need documentation: No
    
    https://claude.ai/code/session_01WTHEM5JUWYV8RqrDr9B62p
    
    
    ### Follow-up review fix
    
    All-NULL outer STRUCT masks now return before allocating hash scratch or
    hashing hidden ARRAY/MAP fields. The batch xxHash and CRC32C tests use a
    variable-length ARRAY child with a traversal counter to verify zero
    child work for 33 all-NULL rows and continued dispatch for a mixed mask.
    
    Validation at `1ed9b8212bc9933ce85f7aced0fd4302abb33a03`:
    - `./run-be-ut.sh --run --filter='ColumnStructTest.*' -j2`: 10/10
    passed.
    - `./build.sh --be -j4` (ASAN): passed.
    - clang-format, check-format, build hygiene, and `git diff --check`:
    passed.
    - clang-tidy attempted; it remains blocked by pre-existing `stddef.h`,
    `NOLINTEND`, and nested-conditional diagnostics outside the new lines.
---
 be/src/core/column/column_struct.cpp               | 110 ++++++++-
 be/test/core/column/column_struct_test.cpp         | 270 +++++++++++++++++++++
 .../column_array_update_crc32c_batch_42.out        |   4 +-
 ...n_array_update_crc32c_batch_42.out_with_nullmap |   4 +-
 .../column_array_update_crc32c_single_42.out       |   2 +-
 ..._array_update_crc32c_single_42.out_with_nullmap |   2 +-
 ...egate_groupby_nullable_struct_local_shuffle.out |  22 ++
 ...te_groupby_nullable_struct_local_shuffle.groovy |  72 ++++++
 8 files changed, 470 insertions(+), 16 deletions(-)

diff --git a/be/src/core/column/column_struct.cpp 
b/be/src/core/column/column_struct.cpp
index fb15785df8b..edcf8a6a4b3 100644
--- a/be/src/core/column/column_struct.cpp
+++ b/be/src/core/column/column_struct.cpp
@@ -21,12 +21,15 @@
 #include "core/column/column_struct.h"
 
 #include <functional>
+#include <vector>
 
 #include "core/assert_cast.h"
+#include "core/custom_allocator.h"
 #include "core/data_type/primitive_type.h"
 #include "core/typeid_cast.h"
 #include "exec/sort/sort_block.h"
 #include "pdqsort.h"
+#include "util/simd/bits.h"
 
 class SipHash;
 namespace doris {
@@ -218,24 +221,96 @@ void ColumnStruct::update_hash_with_value(size_t n, 
SipHash& hash) const {
     }
 }
 
+namespace {
+
+// Invoke `func(run_start, run_end)` for every maximal run of rows in [start, 
end) that are not
+// NULL according to `null_data`. An all-zero mask yields the single run 
[start, end), so the
+// masked and unmasked range paths produce identical hashes and the same 
number of calls.
+template <typename Func>
+void for_each_non_null_run(const uint8_t* __restrict null_data, size_t start, 
size_t end,
+                           Func&& func) {
+    size_t i = start;
+    while (i < end) {
+        while (i < end && null_data[i] != 0) {
+            ++i;
+        }
+        size_t run_start = i;
+        while (i < end && null_data[i] == 0) {
+            ++i;
+        }
+        if (run_start < i) {
+            func(run_start, i);
+        }
+    }
+}
+
+// Batch-hash every row through the field columns, then restore the rows that 
are NULL at the
+// outer level. Surviving rows keep each field's batch hash contract (which 
may differ from the
+// single/range contract, e.g. the width of a NULL default), and the field 
columns are still
+// called once per column instead of once per row.
+template <typename HashType, typename Func>
+void batch_hash_skipping_null_rows(HashType* __restrict hashes, size_t rows,
+                                   const uint8_t* __restrict null_data, Func&& 
hash_all_rows) {
+    // No field hash can survive when every row is NULL; retain the incoming 
seeds.
+    if (!simd::contain_zero(null_data, rows)) {
+        return;
+    }
+    DorisVector<HashType> saved(hashes, hashes + rows);
+    hash_all_rows();
+    for (size_t i = 0; i < rows; ++i) {
+        if (null_data[i] != 0) {
+            hashes[i] = saved[i];
+        }
+    }
+}
+
+} // namespace
+
+// `null_data` is the null map of an outer Nullable wrapping this struct. A 
NULL struct row
+// may still carry arbitrary payload in the field columns (e.g. produced by 
IF/CASE), so those
+// rows must be skipped here rather than hashed by their hidden payload. The 
field columns are
+// hashed with a null `null_data` because their own NULL semantics are handled 
by themselves.
 void ColumnStruct::update_xxHash_with_value(size_t start, size_t end, 
uint64_t& hash,
                                             const uint8_t* __restrict 
null_data) const {
-    for (const auto& column : columns) {
-        column->update_xxHash_with_value(start, end, hash, nullptr);
+    if (null_data) {
+        for (const auto& column : columns) {
+            for_each_non_null_run(null_data, start, end, [&](size_t run_start, 
size_t run_end) {
+                column->update_xxHash_with_value(run_start, run_end, hash, 
nullptr);
+            });
+        }
+    } else {
+        for (const auto& column : columns) {
+            column->update_xxHash_with_value(start, end, hash, nullptr);
+        }
     }
 }
 
 void ColumnStruct::update_crc_with_value(size_t start, size_t end, uint32_t& 
hash,
                                          const uint8_t* __restrict null_data) 
const {
-    for (const auto& column : columns) {
-        column->update_crc_with_value(start, end, hash, nullptr);
+    if (null_data) {
+        for (const auto& column : columns) {
+            for_each_non_null_run(null_data, start, end, [&](size_t run_start, 
size_t run_end) {
+                column->update_crc_with_value(run_start, run_end, hash, 
nullptr);
+            });
+        }
+    } else {
+        for (const auto& column : columns) {
+            column->update_crc_with_value(start, end, hash, nullptr);
+        }
     }
 }
 
 void ColumnStruct::update_hashes_with_value(uint64_t* __restrict hashes,
                                             const uint8_t* __restrict 
null_data) const {
-    for (const auto& column : columns) {
-        column->update_hashes_with_value(hashes, null_data);
+    auto hash_all_rows = [&]() {
+        for (const auto& column : columns) {
+            column->update_hashes_with_value(hashes, nullptr);
+        }
+    };
+    if (null_data) {
+        batch_hash_skipping_null_rows(hashes, size(), null_data, 
hash_all_rows);
+    } else {
+        hash_all_rows();
     }
 }
 
@@ -259,15 +334,30 @@ void ColumnStruct::update_crcs_with_value(uint32_t* 
__restrict hash, PrimitiveTy
 
 void ColumnStruct::update_crc32c_batch(uint32_t* __restrict hashes,
                                        const uint8_t* __restrict null_map) 
const {
-    for (const auto& column : columns) {
-        column->update_crc32c_batch(hashes, nullptr);
+    auto hash_all_rows = [&]() {
+        for (const auto& column : columns) {
+            column->update_crc32c_batch(hashes, nullptr);
+        }
+    };
+    if (null_map) {
+        batch_hash_skipping_null_rows(hashes, size(), null_map, hash_all_rows);
+    } else {
+        hash_all_rows();
     }
 }
 
 void ColumnStruct::update_crc32c_single(size_t start, size_t end, uint32_t& 
hash,
                                         const uint8_t* __restrict null_map) 
const {
-    for (const auto& column : columns) {
-        column->update_crc32c_single(start, end, hash, nullptr);
+    if (null_map) {
+        for (const auto& column : columns) {
+            for_each_non_null_run(null_map, start, end, [&](size_t run_start, 
size_t run_end) {
+                column->update_crc32c_single(run_start, run_end, hash, 
nullptr);
+            });
+        }
+    } else {
+        for (const auto& column : columns) {
+            column->update_crc32c_single(start, end, hash, nullptr);
+        }
     }
 }
 
diff --git a/be/test/core/column/column_struct_test.cpp 
b/be/test/core/column/column_struct_test.cpp
index 2597f3f1b30..49994ef7cc6 100644
--- a/be/test/core/column/column_struct_test.cpp
+++ b/be/test/core/column/column_struct_test.cpp
@@ -21,14 +21,23 @@
 #include <gtest/gtest-test-part.h>
 #include <gtest/gtest.h>
 
+#include <cstddef>
 #include <cstdint>
 #include <iostream>
+#include <limits>
+#include <string>
+#include <typeinfo>
+#include <utility>
+#include <vector>
 
 #include "core/block/block.h"
 #include "core/column/column_array.h"
 #include "core/column/column_const.h"
+#include "core/column/column_dummy.h"
 #include "core/column/column_map.h"
 #include "core/column/column_nullable.h"
+#include "core/column/column_vector.h"
+#include "core/cow.h"
 #include "core/data_type/data_type.h"
 #include "core/data_type/data_type_array.h"
 #include "core/data_type/data_type_date.h"
@@ -39,6 +48,7 @@
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_string.h"
 #include "core/data_type/data_type_struct.h"
+#include "core/field.h"
 #include "core/types.h"
 
 namespace doris {
@@ -50,6 +60,266 @@ protected:
     void TearDown() override {}
 };
 
+namespace {
+
+// An ARRAY element column that records how many hidden elements the ARRAY 
hash visits.
+class HashCountingColumn final : public COWHelper<IColumnDummy, 
HashCountingColumn> {
+private:
+    friend class COWHelper<IColumnDummy, HashCountingColumn>;
+
+    explicit HashCountingColumn(size_t rows) { s = rows; }
+
+public:
+    std::string get_name() const override { return "HashCounting"; }
+    MutableColumnPtr clone_dummy(size_t rows) const override {
+        return HashCountingColumn::create(rows);
+    }
+    bool structure_equals(const IColumn& rhs) const override {
+        return typeid(rhs) == typeid(HashCountingColumn);
+    }
+
+    void update_xxHash_with_value(size_t start, size_t end, uint64_t&,
+                                  const uint8_t*) const override {
+        xx_hashed_elements += end - start;
+    }
+    void update_crc32c_single(size_t start, size_t end, uint32_t&, const 
uint8_t*) const override {
+        crc_hashed_elements += end - start;
+    }
+
+    mutable size_t xx_hashed_elements = 0;
+    mutable size_t crc_hashed_elements = 0;
+};
+
+struct AllNullArrayFixture {
+    static constexpr size_t rows = 33; // Cover both the SIMD chunk and scalar 
tail.
+    static constexpr size_t elements_per_row = 3;
+
+    HashCountingColumn* counter;
+    ColumnPtr plain_struct;
+    ColumnPtr nullable;
+    std::vector<uint8_t> null_map = std::vector<uint8_t>(rows, 1);
+
+    AllNullArrayFixture() {
+        auto elements = HashCountingColumn::create(rows * elements_per_row);
+        counter = elements.get();
+        auto offsets = ColumnArray::ColumnOffsets::create();
+        auto nullable_null_map = ColumnUInt8::create();
+        for (size_t i = 1; i <= rows; ++i) {
+            offsets->insert_value(i * elements_per_row);
+            nullable_null_map->insert_value(1);
+        }
+        Columns fields;
+        fields.push_back(ColumnArray::create(std::move(elements), 
std::move(offsets)));
+        plain_struct = ColumnStruct::create(std::move(fields));
+        nullable = ColumnNullable::create(plain_struct, 
std::move(nullable_null_map));
+    }
+};
+
+// Nullable(Struct<x: Nullable(Int32)>) with 4 rows. Rows 1 and 3 are NULL at 
the outer level
+// but keep different hidden payloads in the field column, which is what 
IF/CASE produce when
+// only one branch is a NULL literal. Rows 0 and 3 share the same payload but 
differ in nullness;
+// NULL is hashed as the field's default value, so only the NULL/NULL equality 
is asserted.
+struct NullableStructFixture {
+    ColumnPtr nullable;
+    const ColumnStruct* plain_struct = nullptr;
+    std::vector<uint8_t> null_map = {0, 1, 0, 1};
+
+    NullableStructFixture() {
+        auto field = ColumnNullable::create(ColumnInt32::create(), 
ColumnUInt8::create());
+        for (int32_t v : {0, std::numeric_limits<int32_t>::min(), 1, 0}) {
+            field->insert(Field::create_field<TYPE_INT>(v));
+        }
+        Columns fields;
+        fields.push_back(std::move(field));
+        ColumnPtr struct_column = ColumnStruct::create(std::move(fields));
+        plain_struct = assert_cast<const ColumnStruct*>(struct_column.get());
+
+        auto null_map_column = ColumnUInt8::create();
+        for (auto v : null_map) {
+            null_map_column->insert_value(v);
+        }
+        nullable = ColumnNullable::create(struct_column, 
std::move(null_map_column));
+    }
+};
+
+} // namespace
+
+TEST_F(ColumnStructTest, Crc32cBatchRespectsOuterNullMap) {
+    NullableStructFixture fixture;
+    std::vector<uint32_t> hashes(4, 0);
+    fixture.nullable->update_crc32c_batch(hashes.data(), nullptr);
+    std::vector<uint32_t> plain_hashes(4, 0);
+    fixture.plain_struct->update_crc32c_batch(plain_hashes.data(), nullptr);
+
+    // Two logical NULLs must hash identically regardless of hidden payload.
+    EXPECT_EQ(hashes[1], hashes[3]);
+    // Non-NULL rows hash exactly like the plain struct column.
+    EXPECT_EQ(hashes[0], plain_hashes[0]);
+    EXPECT_EQ(hashes[2], plain_hashes[2]);
+
+    // Calling the struct directly with a null map skips the NULL rows.
+    std::vector<uint32_t> masked_hashes(4, 0);
+    fixture.plain_struct->update_crc32c_batch(masked_hashes.data(), 
fixture.null_map.data());
+    EXPECT_EQ(masked_hashes[0], plain_hashes[0]);
+    EXPECT_EQ(masked_hashes[1], 0);
+    EXPECT_EQ(masked_hashes[2], plain_hashes[2]);
+    EXPECT_EQ(masked_hashes[3], 0);
+}
+
+TEST_F(ColumnStructTest, Crc32cSingleRespectsOuterNullMap) {
+    NullableStructFixture fixture;
+    auto hash_row = [&](const IColumn& column, size_t row) {
+        uint32_t hash = 0;
+        column.update_crc32c_single(row, row + 1, hash, nullptr);
+        return hash;
+    };
+    EXPECT_EQ(hash_row(*fixture.nullable, 1), hash_row(*fixture.nullable, 3));
+    EXPECT_EQ(hash_row(*fixture.nullable, 0), hash_row(*fixture.plain_struct, 
0));
+    EXPECT_EQ(hash_row(*fixture.nullable, 2), hash_row(*fixture.plain_struct, 
2));
+
+    // A multi-row range with a null map only hashes the non-NULL rows.
+    uint32_t masked = 0;
+    fixture.plain_struct->update_crc32c_single(0, 4, masked, 
fixture.null_map.data());
+    uint32_t expected = 0;
+    fixture.plain_struct->update_crc32c_single(0, 1, expected, nullptr);
+    fixture.plain_struct->update_crc32c_single(2, 3, expected, nullptr);
+    EXPECT_EQ(masked, expected);
+}
+
+TEST_F(ColumnStructTest, XxHashRespectsOuterNullMap) {
+    NullableStructFixture fixture;
+    std::vector<uint64_t> hashes(4, 0);
+    fixture.nullable->update_hashes_with_value(hashes.data(), nullptr);
+    std::vector<uint64_t> plain_hashes(4, 0);
+    fixture.plain_struct->update_hashes_with_value(plain_hashes.data(), 
nullptr);
+    EXPECT_EQ(hashes[1], hashes[3]);
+    EXPECT_EQ(hashes[0], plain_hashes[0]);
+    EXPECT_EQ(hashes[2], plain_hashes[2]);
+
+    auto hash_row = [&](const IColumn& column, size_t row) {
+        uint64_t hash = 0;
+        column.update_xxHash_with_value(row, row + 1, hash, nullptr);
+        return hash;
+    };
+    EXPECT_EQ(hash_row(*fixture.nullable, 1), hash_row(*fixture.nullable, 3));
+    EXPECT_EQ(hash_row(*fixture.nullable, 0), hash_row(*fixture.plain_struct, 
0));
+    EXPECT_EQ(hash_row(*fixture.nullable, 2), hash_row(*fixture.plain_struct, 
2));
+}
+
+TEST_F(ColumnStructTest, CrcRangeRespectsOuterNullMap) {
+    NullableStructFixture fixture;
+    auto hash_row = [&](const IColumn& column, size_t row) {
+        uint32_t hash = 0;
+        column.update_crc_with_value(row, row + 1, hash, nullptr);
+        return hash;
+    };
+    EXPECT_EQ(hash_row(*fixture.nullable, 1), hash_row(*fixture.nullable, 3));
+    EXPECT_EQ(hash_row(*fixture.nullable, 0), hash_row(*fixture.plain_struct, 
0));
+    EXPECT_EQ(hash_row(*fixture.nullable, 2), hash_row(*fixture.plain_struct, 
2));
+}
+
+// Struct<x: Nullable(BIGINT)> whose row 0 has a NULL field and row 1 a 
non-NULL field, wrapped
+// once with an outer NULL on row 1 and once without. Row 0 is the same 
logical value in both
+// blocks and must hash identically: the batch path keeps the field's batch 
NULL encoding even
+// when an unrelated outer NULL forces the masked path.
+TEST_F(ColumnStructTest, 
BatchHashKeepsChildBatchContractWithUnrelatedOuterNull) {
+    auto make_block = [](const std::vector<uint8_t>& outer_null_map) {
+        auto field = ColumnNullable::create(ColumnInt64::create(), 
ColumnUInt8::create());
+        field->insert_default();
+        field->insert(Field::create_field<TYPE_BIGINT>(int64_t(5)));
+        Columns fields;
+        fields.push_back(std::move(field));
+        ColumnPtr struct_column = ColumnStruct::create(std::move(fields));
+        auto null_map_column = ColumnUInt8::create();
+        for (auto v : outer_null_map) {
+            null_map_column->insert_value(v);
+        }
+        return ColumnNullable::create(struct_column, 
std::move(null_map_column));
+    };
+    ColumnPtr with_outer_null = make_block({0, 1});
+    ColumnPtr without_outer_null = make_block({0, 0});
+
+    constexpr uint32_t seed32 = 0x12345678;
+    std::vector<uint32_t> crc_a(2, seed32);
+    std::vector<uint32_t> crc_b(2, seed32);
+    with_outer_null->update_crc32c_batch(crc_a.data(), nullptr);
+    without_outer_null->update_crc32c_batch(crc_b.data(), nullptr);
+    EXPECT_EQ(crc_a[0], crc_b[0]);
+
+    constexpr uint64_t seed64 = 0x123456789abcdefULL;
+    std::vector<uint64_t> xx_a(2, seed64);
+    std::vector<uint64_t> xx_b(2, seed64);
+    with_outer_null->update_hashes_with_value(xx_a.data(), nullptr);
+    without_outer_null->update_hashes_with_value(xx_b.data(), nullptr);
+    EXPECT_EQ(xx_a[0], xx_b[0]);
+}
+
+TEST_F(ColumnStructTest, AllNullBatchSkipsVariableLengthChildXxHash) {
+    AllNullArrayFixture fixture;
+    std::vector<uint64_t> hashes(fixture.rows, 11);
+    fixture.plain_struct->update_hashes_with_value(hashes.data(), 
fixture.null_map.data());
+    EXPECT_EQ(fixture.counter->xx_hashed_elements, 0);
+    for (auto hash : hashes) {
+        EXPECT_EQ(hash, 11);
+    }
+
+    fixture.nullable->update_hashes_with_value(hashes.data(), nullptr);
+    EXPECT_EQ(fixture.counter->xx_hashed_elements, 0);
+    for (auto hash : hashes) {
+        EXPECT_EQ(hash, hashes[0]);
+    }
+
+    fixture.null_map.back() = 0;
+    fixture.plain_struct->update_hashes_with_value(hashes.data(), 
fixture.null_map.data());
+    EXPECT_GT(fixture.counter->xx_hashed_elements, 0);
+}
+
+TEST_F(ColumnStructTest, AllNullBatchSkipsVariableLengthChildCrc32c) {
+    AllNullArrayFixture fixture;
+    std::vector<uint32_t> hashes(fixture.rows, 13);
+    fixture.plain_struct->update_crc32c_batch(hashes.data(), 
fixture.null_map.data());
+    EXPECT_EQ(fixture.counter->crc_hashed_elements, 0);
+    for (auto hash : hashes) {
+        EXPECT_EQ(hash, 13);
+    }
+
+    fixture.nullable->update_crc32c_batch(hashes.data(), nullptr);
+    EXPECT_EQ(fixture.counter->crc_hashed_elements, 0);
+    for (auto hash : hashes) {
+        EXPECT_EQ(hash, hashes[0]);
+    }
+
+    fixture.null_map.back() = 0;
+    fixture.plain_struct->update_crc32c_batch(hashes.data(), 
fixture.null_map.data());
+    EXPECT_GT(fixture.counter->crc_hashed_elements, 0);
+}
+
+TEST_F(ColumnStructTest, RangeHashWithAllZeroMaskMatchesUnmasked) {
+    NullableStructFixture fixture;
+    std::vector<uint8_t> zeros(4, 0);
+    {
+        uint32_t masked = 7;
+        uint32_t plain = 7;
+        fixture.plain_struct->update_crc32c_single(0, 4, masked, zeros.data());
+        fixture.plain_struct->update_crc32c_single(0, 4, plain, nullptr);
+        EXPECT_EQ(masked, plain);
+    }
+    {
+        uint64_t masked = 7;
+        uint64_t plain = 7;
+        fixture.plain_struct->update_xxHash_with_value(0, 4, masked, 
zeros.data());
+        fixture.plain_struct->update_xxHash_with_value(0, 4, plain, nullptr);
+        EXPECT_EQ(masked, plain);
+    }
+    {
+        uint32_t masked = 7;
+        uint32_t plain = 7;
+        fixture.plain_struct->update_crc_with_value(0, 4, masked, 
zeros.data());
+        fixture.plain_struct->update_crc_with_value(0, 4, plain, nullptr);
+        EXPECT_EQ(masked, plain);
+    }
+}
+
 TEST_F(ColumnStructTest, StructTypeTesterase) {
     DataTypePtr key_type = (std::make_shared<DataTypeString>());
     DataTypePtr value_type = (std::make_shared<DataTypeInt32>());
diff --git 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out
index 33e8d33173a..4ba6d12d5fd 100644
--- 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out
+++ 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out
@@ -6,11 +6,11 @@
 2912776853
 3311270576
 213263290
-3622140960
+213263290
 973448334
 4140789047
 422470270
 1499855502
 1781817829
 3138811262
-1757854213
\ No newline at end of file
+243623783
\ No newline at end of file
diff --git 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out_with_nullmap
 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out_with_nullmap
index 33e8d33173a..4ba6d12d5fd 100644
--- 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out_with_nullmap
+++ 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_batch_42.out_with_nullmap
@@ -6,11 +6,11 @@
 2912776853
 3311270576
 213263290
-3622140960
+213263290
 973448334
 4140789047
 422470270
 1499855502
 1781817829
 3138811262
-1757854213
\ No newline at end of file
+243623783
\ No newline at end of file
diff --git 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out
index be0367bd02e..15e46575237 100644
--- 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out
+++ 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out
@@ -1 +1 @@
-2261631211;834435238;2794456307
\ No newline at end of file
+2583553548;3415904288;2794456307
\ No newline at end of file
diff --git 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out_with_nullmap
 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out_with_nullmap
index be0367bd02e..15e46575237 100644
--- 
a/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out_with_nullmap
+++ 
b/be/test/expected_result/vec/columns/column_array_update_crc32c_single_42.out_with_nullmap
@@ -1 +1 @@
-2261631211;834435238;2794456307
\ No newline at end of file
+2583553548;3415904288;2794456307
\ No newline at end of file
diff --git 
a/regression-test/data/query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle.out
 
b/regression-test/data/query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle.out
new file mode 100644
index 00000000000..f94a7ac40b0
--- /dev/null
+++ 
b/regression-test/data/query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle.out
@@ -0,0 +1,22 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !local_shuffle_crc32c_groups --
+\N     2
+{"x":1}        1
+
+-- !local_shuffle_crc32c_summary --
+2      3       1       2
+
+-- !local_shuffle_legacy_hash_groups --
+\N     2
+{"x":1}        1
+
+-- !local_shuffle_legacy_hash_summary --
+2      3       1       2
+
+-- !no_local_shuffle_groups --
+\N     2
+{"x":1}        1
+
+-- !no_local_shuffle_summary --
+2      3       1       2
+
diff --git 
a/regression-test/suites/query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle.groovy
 
b/regression-test/suites/query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle.groovy
new file mode 100644
index 00000000000..0e672743469
--- /dev/null
+++ 
b/regression-test/suites/query_p0/aggregate/aggregate_groupby_nullable_struct_local_shuffle.groovy
@@ -0,0 +1,72 @@
+// 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("aggregate_groupby_nullable_struct_local_shuffle") {
+    // IF(cond, NULL, named_struct(...)) yields logical NULL structs whose 
hidden field payloads
+    // differ per row. GROUP BY must treat them as one group no matter how the 
rows are hashed
+    // by the local shuffle before aggregation.
+    sql """ DROP TABLE IF EXISTS agg_groupby_nullable_struct_ls """
+    sql """
+        CREATE TABLE agg_groupby_nullable_struct_ls (
+            pk INT NOT NULL
+        )
+        DUPLICATE KEY(pk)
+        DISTRIBUTED BY HASH(pk) BUCKETS 1
+        PROPERTIES (
+          "replication_num" = "1"
+        )
+    """
+    sql """ INSERT INTO agg_groupby_nullable_struct_ls VALUES (2), (7), (11) 
"""
+
+    def groupQuery = """
+        SELECT k, COUNT(*) AS c
+        FROM (
+            SELECT IF(pk IN (2, 7), NULL,
+                      named_struct('x', CASE WHEN pk = 2 THEN 0
+                                             WHEN pk = 7 THEN -2147483648
+                                             ELSE 1 END)) AS k
+            FROM agg_groupby_nullable_struct_ls
+        ) z
+        GROUP BY k
+    """
+    def summaryQuery = """
+        SELECT COUNT(*) AS groups_n, SUM(c) AS rows_n, MIN(c) AS min_c, MAX(c) 
AS max_c
+        FROM (${groupQuery}) q
+    """
+
+    // Force a local hash shuffle on the struct key before the single-phase 
aggregation.
+    sql """ SET enable_local_shuffle_planner = true """
+    sql """ SET enable_query_cache = false """
+    sql """ SET agg_phase = 1 """
+    sql """ SET enable_bucketed_hash_agg = false """
+    sql """ SET experimental_use_serial_exchange = true """
+    sql """ SET ignore_storage_data_distribution = true """
+    sql """ SET parallel_pipeline_task_num = 3 """
+
+    sql """ SET enable_local_shuffle = true """
+    sql """ SET enable_new_shuffle_hash_method = true """
+    order_qt_local_shuffle_crc32c_groups "${groupQuery}"
+    order_qt_local_shuffle_crc32c_summary "${summaryQuery}"
+
+    sql """ SET enable_new_shuffle_hash_method = false """
+    order_qt_local_shuffle_legacy_hash_groups "${groupQuery}"
+    order_qt_local_shuffle_legacy_hash_summary "${summaryQuery}"
+
+    sql """ SET enable_local_shuffle = false """
+    order_qt_no_local_shuffle_groups "${groupQuery}"
+    order_qt_no_local_shuffle_summary "${summaryQuery}"
+}


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

Reply via email to