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

HappenLee 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 ace58e00a85 [fix](be) Fix nullable handling in group array set 
operations (#66284)
ace58e00a85 is described below

commit ace58e00a8516ee22bd9c3f8184b423e6dc787ae
Author: Jerry Hu <[email protected]>
AuthorDate: Mon Aug 3 11:43:04 2026 +0800

    [fix](be) Fix nullable handling in group array set operations (#66284)
    
    Problem Summary: `group_array_intersect` and `group_array_union`
    discarded the outer nullable type before aggregate function creation. As
    a result, the nullable aggregate wrapper was not installed and masked
    payloads beneath NULL array rows could participate in set operations,
    making semantically equivalent `CASE` and `IF` expressions return
    different results.
    
    The raw aggregate also accepted outer `ColumnNullable` input even though
    row-level NULL filtering belongs to the nullable wrapper. In addition,
    the string intersection path could read the masked nested payload of a
    NULL array element and incorrectly match it as a real string, for
    example preserving an empty string for `[""]` intersect `[NULL]`.
    
    This change preserves the original argument types for aggregate wrapper
    creation and unwraps nullable types only for element type dispatch. The
    raw aggregate now accepts only `ColumnArray`, outer NULL array rows are
    skipped by the nullable wrapper, and nullable array elements remain the
    responsibility of the numeric/string data implementations. The string
    intersection path handles NULL elements without reading their masked
    nested payload.
    
    ### Compatibility
    
    The serialized state layout is unchanged. States produced before this
    fix remain readable, but historical states that already captured the old
    outer-NULL payload semantics cannot be distinguished from valid states
    and require refresh or rebuild to correct historical results.
    
    ### Release note
    
    Fix `group_array_intersect` and `group_array_union` to ignore outer NULL
    array rows consistently, and prevent NULL string elements in
    `group_array_intersect` from matching masked nested values.
    
    ### Check List (For Author)
    
    - Test:
        - Build: `./build.sh --be --fe` (ASAN)
    - Unit Test: `./run-be-ut.sh --run
    --filter='AggGroupArrayIntersectTest.*'` (7 tests passed)
    - Regression test: `query_p0/aggregate/group_array_intersect` (generated
    output and rerun passed)
    - Regression test:
    `query_p0/sql_functions/aggregate_functions/test_aggregate_window_functions`
    (generated output and rerun passed)
    - Regression test: `nereids_syntax_p0/mv/load,agg_sync_mv` (generated
    output and rerun passed)
    - Regression test: the affected P1 MV assertions completed and generated
    the expected results; the suite later hit the unrelated existing
    `Duplicate column name '__stddev_1'` failure
        - Format: `./build-support/check-format.sh`
    - Static analysis: changed-line clang-tidy completed with no diagnostics
    after local-only workarounds for the toolchain resource directory and an
    unrelated baseline `NOLINTEND`
    - Behavior changed: Yes (outer NULL array rows are wrapper-owned and
    ignored, and NULL string elements no longer match masked nested values)
    - Does this need documentation: No
---
 .../aggregate_function_group_array_intersect.cpp   |   2 +-
 .../aggregate_function_group_array_set_op.h        |  25 ++---
 .../aggregate_function_group_array_set_op_impl.h   |   4 +-
 .../aggregate_function_group_array_union.cpp       |   2 +-
 .../aggregate/agg_group_array_intersect_test.cpp   | 117 +++++++++++++++++++++
 .../nereids_syntax_p0/mv/aggregate/agg_sync_mv.out |   4 +-
 .../nereids_syntax_p1/mv/aggregate/agg_sync_mv.out |   4 +-
 .../query_p0/aggregate/group_array_intersect.out   |  14 ++-
 .../test_aggregate_window_functions.out            |   8 +-
 .../aggregate/group_array_intersect.groovy         |  32 ++++++
 10 files changed, 179 insertions(+), 33 deletions(-)

diff --git 
a/be/src/exprs/aggregate/aggregate_function_group_array_intersect.cpp 
b/be/src/exprs/aggregate/aggregate_function_group_array_intersect.cpp
index 05a9fb53c2c..ca5211f8d85 100644
--- a/be/src/exprs/aggregate/aggregate_function_group_array_intersect.cpp
+++ b/be/src/exprs/aggregate/aggregate_function_group_array_intersect.cpp
@@ -37,7 +37,7 @@ AggregateFunctionPtr 
create_aggregate_function_group_array_intersect(
     }
     return 
create_aggregate_function_group_array_impl<GroupArrayNumericIntersectData,
                                                       
GroupArrayStringIntersectData>(
-            {argument_type}, result_is_nullable, attr);
+            argument_types, result_is_nullable, attr);
 }
 
 } // namespace doris
diff --git a/be/src/exprs/aggregate/aggregate_function_group_array_set_op.h 
b/be/src/exprs/aggregate/aggregate_function_group_array_set_op.h
index 385503c514e..380cb4f6d2e 100644
--- a/be/src/exprs/aggregate/aggregate_function_group_array_set_op.h
+++ b/be/src/exprs/aggregate/aggregate_function_group_array_set_op.h
@@ -334,8 +334,10 @@ struct GroupArrayStringIntersectData : public 
GroupArraySetOpStringBaseData {
         } else if (!this->set->empty()) {
             Set new_set = std::make_unique<NullableStringSet>();
             for (size_t i = 0; i < arr_size; ++i) {
-                if (col_null->is_null_at(offset + i) && 
this->set->contain_null()) {
-                    new_set->insert(nullptr);
+                if (col_null->is_null_at(offset + i)) {
+                    if (this->set->contain_null()) {
+                        new_set->insert(nullptr);
+                    }
                 } else {
                     auto src = nested_column_data.get_data_at(offset + i);
                     if (this->set->find((void*)src.data, src.size)) {
@@ -443,16 +445,11 @@ public:
     void reset(AggregateDataPtr __restrict place) const override { 
this->data(place).reset(); }
 
     void check_input_columns_type(const IColumn** columns) const override {
-        const IColumn* column = columns[0];
-        if (const auto* nullable_column = 
check_and_get_column<ColumnNullable>(*column)) {
-            column = &nullable_column->get_nested_column();
-        }
-
-        const auto* array_column = check_and_get_column<ColumnArray>(*column);
+        const auto* array_column = 
check_and_get_column<ColumnArray>(*columns[0]);
         if (UNLIKELY(array_column == nullptr)) {
             throw doris::Exception(Status::InternalError(
                     "Aggregate function {} argument 0 type check failed: 
Column type {} ({}) is "
-                    "not ColumnArray or Nullable(ColumnArray)",
+                    "not ColumnArray",
                     get_name(), columns[0]->get_name(), 
typeid(*columns[0]).name()));
         }
 
@@ -507,14 +504,8 @@ public:
 
     void add(AggregateDataPtr __restrict place, const IColumn** columns, 
ssize_t row_num,
              Arena& arena) const override {
-        const bool col_is_nullable = is_column_nullable(*columns[0]);
-        const ColumnArray& column =
-                col_is_nullable
-                        ? assert_cast<const ColumnArray&, 
TypeCheckOnRelease::DISABLE>(
-                                  assert_cast<const ColumnNullable&, 
TypeCheckOnRelease::DISABLE>(
-                                          *columns[0])
-                                          .get_nested_column())
-                        : assert_cast<const ColumnArray&, 
TypeCheckOnRelease::DISABLE>(*columns[0]);
+        const auto& column =
+                assert_cast<const ColumnArray&, 
TypeCheckOnRelease::DISABLE>(*columns[0]);
 
         const auto& offsets = column.get_offsets();
         const auto offset = offsets[row_num - 1];
diff --git 
a/be/src/exprs/aggregate/aggregate_function_group_array_set_op_impl.h 
b/be/src/exprs/aggregate/aggregate_function_group_array_set_op_impl.h
index b9b9ad8220c..167a29fca2d 100644
--- a/be/src/exprs/aggregate/aggregate_function_group_array_set_op_impl.h
+++ b/be/src/exprs/aggregate/aggregate_function_group_array_set_op_impl.h
@@ -33,8 +33,8 @@ template <template <PrimitiveType> class ImplNumericData, 
typename ImplStringDat
 inline AggregateFunctionPtr create_aggregate_function_group_array_impl(
         const DataTypes& argument_types, const bool result_is_nullable,
         const AggregateFunctionAttr& attr) {
-    const auto& nested_type = remove_nullable(
-            assert_cast<const 
DataTypeArray&>(*(argument_types[0])).get_nested_type());
+    const auto& array_type = assert_cast<const 
DataTypeArray&>(*remove_nullable(argument_types[0]));
+    const auto& nested_type = remove_nullable(array_type.get_nested_type());
     auto pt = nested_type->get_primitive_type();
 
     AggregateFunctionPtr result;
diff --git a/be/src/exprs/aggregate/aggregate_function_group_array_union.cpp 
b/be/src/exprs/aggregate/aggregate_function_group_array_union.cpp
index 2f0303ca199..48861ae6552 100644
--- a/be/src/exprs/aggregate/aggregate_function_group_array_union.cpp
+++ b/be/src/exprs/aggregate/aggregate_function_group_array_union.cpp
@@ -36,7 +36,7 @@ AggregateFunctionPtr 
create_aggregate_function_group_array_union(
     }
     return 
create_aggregate_function_group_array_impl<GroupArrayNumericUnionData,
                                                       
GroupArrayStringUnionData>(
-            {argument_type}, result_is_nullable, attr);
+            argument_types, result_is_nullable, attr);
 }
 
 } // namespace doris
diff --git a/be/test/exprs/aggregate/agg_group_array_intersect_test.cpp 
b/be/test/exprs/aggregate/agg_group_array_intersect_test.cpp
index 0be84b45890..00bcd45cc0a 100644
--- a/be/test/exprs/aggregate/agg_group_array_intersect_test.cpp
+++ b/be/test/exprs/aggregate/agg_group_array_intersect_test.cpp
@@ -19,13 +19,16 @@
 #include <memory>
 #include <string>
 
+#include "common/exception.h"
 #include "core/column/column.h"
 #include "core/column/column_array.h"
+#include "core/column/column_nullable.h"
 #include "core/column/column_string.h"
 #include "core/data_type/data_type_array.h"
 #include "core/data_type/data_type_date.h"
 #include "core/data_type/data_type_date_or_datetime_v2.h"
 #include "core/data_type/data_type_date_time.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/field.h"
@@ -526,4 +529,118 @@ TEST(AggGroupArrayIntersectTest, string_nullable_test) {
     agg_function->destroy(place);
 }
 
+TEST(AggGroupArrayIntersectTest, 
string_null_element_does_not_match_empty_string) {
+    DataTypePtr array_type = 
std::make_shared<DataTypeArray>(std::make_shared<DataTypeString>());
+    DataTypes data_types = {array_type};
+    auto test_column = array_type->create_column();
+    test_column->insert(
+            
Field::create_field<TYPE_ARRAY>({Field::create_field<TYPE_STRING>(std::string())}));
+    test_column->insert(Field::create_field<TYPE_ARRAY>({Field()}));
+
+    AggregateFunctionSimpleFactory factory;
+    register_aggregate_function_group_array_set_op(factory);
+    auto agg_function = factory.get("group_array_intersect", data_types, 
nullptr, false, -1);
+    ASSERT_NE(agg_function, nullptr);
+    std::unique_ptr<char[]> memory(new char[agg_function->size_of_data()]);
+    AggregateDataPtr place = memory.get();
+    agg_function->create(place);
+
+    Arena arena;
+    ColumnRawPtrs columns(data_types.size(), test_column.get());
+    agg_function->check_input_columns_type(columns.data());
+    agg_function->add_batch_single_place(test_column->size(), place, 
columns.data(), arena);
+
+    auto result_column = array_type->create_column();
+    agg_function->insert_result_into(place, *result_column);
+    Field actual_field;
+    result_column->get(0, actual_field);
+    EXPECT_TRUE(actual_field.get<TYPE_ARRAY>().empty());
+
+    agg_function->destroy(place);
+}
+
+TEST(AggGroupArrayIntersectTest, raw_aggregate_rejects_outer_nullable_column) {
+    DataTypePtr array_type = 
std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>());
+    DataTypes data_types = {array_type};
+
+    AggregateFunctionSimpleFactory factory;
+    register_aggregate_function_group_array_set_op(factory);
+    auto agg_function = factory.get("group_array_intersect", data_types, 
nullptr, false, -1);
+    ASSERT_NE(agg_function, nullptr);
+
+    auto outer_nullable_column = 
std::make_shared<DataTypeNullable>(array_type)->create_column();
+    ColumnRawPtrs columns(data_types.size(), outer_nullable_column.get());
+    EXPECT_THROW(agg_function->check_input_columns_type(columns.data()), 
Exception);
+}
+
+void validate_outer_nullable_array(const std::string& function_name, const 
DataTypes& data_types,
+                                   const DataTypePtr& result_type, const 
IColumn& input_column,
+                                   const Array& expected_result) {
+    for (bool enable_null_v2 : {false, true}) {
+        SCOPED_TRACE("function=" + function_name +
+                     ", enable_aggregate_function_null_v2=" + 
std::to_string(enable_null_v2));
+        AggregateFunctionSimpleFactory factory;
+        register_aggregate_function_group_array_set_op(factory);
+        AggregateFunctionAttr attr;
+        attr.enable_aggregate_function_null_v2 = enable_null_v2;
+        auto agg_function =
+                factory.get(function_name, data_types, nullptr, false, -1, 
std::move(attr));
+        ASSERT_NE(agg_function, nullptr);
+        std::unique_ptr<char[]> memory(new char[agg_function->size_of_data()]);
+        AggregateDataPtr place = memory.get();
+        agg_function->create(place);
+
+        Arena arena;
+        ColumnRawPtrs columns(data_types.size(), &input_column);
+        agg_function->check_input_columns_type(columns.data());
+        agg_function->add_batch_single_place(input_column.size(), place, 
columns.data(), arena);
+
+        auto result_column = result_type->create_column();
+        agg_function->insert_result_into(place, *result_column);
+        Field actual_field;
+        result_column->get(0, actual_field);
+        auto actual_result = actual_field.get<TYPE_ARRAY>();
+        auto sorted_expected_result = expected_result;
+        sort_numeric_array<TYPE_INT>(actual_result);
+        sort_numeric_array<TYPE_INT>(sorted_expected_result);
+        EXPECT_EQ(actual_result, sorted_expected_result);
+
+        agg_function->destroy(place);
+    }
+}
+
+TEST(AggGroupArrayIntersectTest, outer_nullable_array_test) {
+    auto nested_type = 
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeInt32>());
+    DataTypePtr array_type = std::make_shared<DataTypeArray>(nested_type);
+    DataTypes data_types = {std::make_shared<DataTypeNullable>(array_type)};
+    auto test_column = data_types[0]->create_column();
+    test_column->insert(Field::create_field<TYPE_ARRAY>(
+            {Field::create_field<TYPE_INT>(1), 
Field::create_field<TYPE_INT>(2)}));
+    test_column->insert(Field());
+    test_column->insert(Field::create_field<TYPE_ARRAY>(
+            {Field::create_field<TYPE_INT>(2), 
Field::create_field<TYPE_INT>(3)}));
+
+    validate_outer_nullable_array("group_array_intersect", data_types, 
array_type, *test_column,
+                                  {Field::create_field<TYPE_INT>(2)});
+}
+
+TEST(AggGroupArrayIntersectTest, group_array_union_skips_outer_null_payload) {
+    auto nested_type = 
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeInt32>());
+    DataTypePtr array_type = std::make_shared<DataTypeArray>(nested_type);
+    DataTypes data_types = {std::make_shared<DataTypeNullable>(array_type)};
+    auto test_column = data_types[0]->create_column();
+    test_column->insert(Field::create_field<TYPE_ARRAY>(
+            {Field::create_field<TYPE_INT>(1), 
Field::create_field<TYPE_INT>(2)}));
+    
test_column->insert(Field::create_field<TYPE_ARRAY>({Field::create_field<TYPE_INT>(99)}));
+    test_column->insert(Field::create_field<TYPE_ARRAY>(
+            {Field::create_field<TYPE_INT>(2), 
Field::create_field<TYPE_INT>(3)}));
+    auto& nullable_column = static_cast<ColumnNullable&>(*test_column);
+    nullable_column.get_null_map_data()[1] = 1;
+
+    validate_outer_nullable_array(
+            "group_array_union", data_types, array_type, *test_column,
+            {Field::create_field<TYPE_INT>(1), 
Field::create_field<TYPE_INT>(2),
+             Field::create_field<TYPE_INT>(3)});
+}
+
 } // namespace doris
diff --git 
a/regression-test/data/nereids_syntax_p0/mv/aggregate/agg_sync_mv.out 
b/regression-test/data/nereids_syntax_p0/mv/aggregate/agg_sync_mv.out
index 18af62dbfa7..f8243b031b1 100644
--- a/regression-test/data/nereids_syntax_p0/mv/aggregate/agg_sync_mv.out
+++ b/regression-test/data/nereids_syntax_p0/mv/aggregate/agg_sync_mv.out
@@ -420,7 +420,7 @@
 11     1
 
 -- !select_group_array_intersect --
-\N     []
+\N     [1]
 0      [1]
 1      [2]
 2      [3]
@@ -435,7 +435,7 @@
 11     [12]
 
 -- !select_group_array_intersect_mv --
-\N     []
+\N     [1]
 0      [1]
 1      [2]
 2      [3]
diff --git 
a/regression-test/data/nereids_syntax_p1/mv/aggregate/agg_sync_mv.out 
b/regression-test/data/nereids_syntax_p1/mv/aggregate/agg_sync_mv.out
index 848d637c916..33058f7d084 100644
--- a/regression-test/data/nereids_syntax_p1/mv/aggregate/agg_sync_mv.out
+++ b/regression-test/data/nereids_syntax_p1/mv/aggregate/agg_sync_mv.out
@@ -420,7 +420,7 @@
 11     1
 
 -- !select_group_array_intersect --
-\N     []
+\N     [1]
 0      [1]
 1      [2]
 2      [3]
@@ -435,7 +435,7 @@
 11     [12]
 
 -- !select_group_array_intersect_mv --
-\N     []
+\N     [1]
 0      [1]
 1      [2]
 2      [3]
diff --git a/regression-test/data/query_p0/aggregate/group_array_intersect.out 
b/regression-test/data/query_p0/aggregate/group_array_intersect.out
index 07b64d8de3e..e4d691e58f0 100644
--- a/regression-test/data/query_p0/aggregate/group_array_intersect.out
+++ b/regression-test/data/query_p0/aggregate/group_array_intersect.out
@@ -1,4 +1,10 @@
 -- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !outer_nullable_case_if --
+[2]    [2]
+
+-- !string_null_element_does_not_match_empty_string --
+[]
+
 -- !int_1 --
 [null]
 
@@ -21,10 +27,10 @@
 [null, 12, 13]
 
 -- !int_8 --
-[]
+[null]
 
 -- !int_9 --
-[]
+[null, 12, 13]
 
 -- !float_1 --
 [6.3, 7.3]
@@ -36,7 +42,7 @@
 [7.3]
 
 -- !datetimev2_1 --
-[]
+["2024-03-24 00:00:00.000"]
 
 -- !datetimev2_2 --
 ["2024-03-24 00:00:00.000"]
@@ -48,7 +54,7 @@
 ["2024-05-23"]
 
 -- !string_1 --
-[]
+[null, "a"]
 
 -- !string_2 --
 ["a"]
diff --git 
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_window_functions.out
 
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_window_functions.out
index 3c3f674ce5f..251162cd3ff 100644
--- 
a/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_window_functions.out
+++ 
b/regression-test/data/query_p0/sql_functions/aggregate_functions/test_aggregate_window_functions.out
@@ -641,10 +641,10 @@ sichuan   [{"cbe":{},"notnull":0,"null":1,"all":1}]
 3      25
 
 -- !agg_window_group_array_intersect --
-1      []
-1      []
-1      []
-1      []
+1      ["1"]
+1      ["1"]
+1      ["1"]
+1      ["1"]
 2      ["2"]
 2      ["2"]
 2      ["2"]
diff --git 
a/regression-test/suites/query_p0/aggregate/group_array_intersect.groovy 
b/regression-test/suites/query_p0/aggregate/group_array_intersect.groovy
index 7492e533455..317cb13cbc7 100644
--- a/regression-test/suites/query_p0/aggregate/group_array_intersect.groovy
+++ b/regression-test/suites/query_p0/aggregate/group_array_intersect.groovy
@@ -48,6 +48,38 @@ suite("group_array_intersect") {
     sql """INSERT INTO `group_array_intersect_test`(id, c_array_bigint) VALUES 
(23, [1234567890123456]), (24, [1234567890123456, 2333333333333333]);"""
     sql """INSERT INTO `group_array_intersect_test`(id, c_array_decimal) 
VALUES (25, [1.34,2.00188888888888888]), (26, [1.34,2.00123344444455555]);"""
 
+    qt_outer_nullable_case_if """
+        SELECT
+            array_sort(group_array_intersect(
+                CASE id
+                    WHEN 0 THEN [1, 2]
+                    WHEN 1 THEN CAST(NULL AS ARRAY<INT>)
+                    ELSE [2, 3]
+                END
+            )),
+            array_sort(group_array_intersect(
+                IF(id = 1, CAST(NULL AS ARRAY<INT>),
+                   IF(id = 0, [1, 2], [2, 3]))
+            ))
+        FROM group_array_intersect_test
+        WHERE id BETWEEN 0 AND 2
+    """
+
+    qt_string_null_element_does_not_match_empty_string """
+        SELECT result
+        FROM (
+            SELECT id, array_sort(group_array_intersect(c_array_string) OVER (
+                ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+            )) AS result
+            FROM (
+                SELECT 1 AS id, CAST([''] AS ARRAY<STRING>) AS c_array_string
+                UNION ALL
+                SELECT 2 AS id, CAST([NULL] AS ARRAY<STRING>) AS c_array_string
+            ) t
+        ) w
+        WHERE id = 2
+    """
+
     qt_int_1 """select array_sort(group_array_intersect(c_array_int)) from 
group_array_intersect_test where id in (6, 12);"""
     qt_int_2 """select array_sort(group_array_intersect(c_array_int)) from 
group_array_intersect_test where id in (14, 12);"""
     qt_int_3 """select array_sort(group_array_intersect(c_array_int)) from 
group_array_intersect_test where id in (0, 6);"""


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

Reply via email to