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 82d44b4cc14 [fix](function) Remove duplicated keys of nested maps in 
string casts (#68639)
82d44b4cc14 is described below

commit 82d44b4cc14575125714c21587452169ada1e145
Author: Chenyang Sun <[email protected]>
AuthorDate: Thu Oct 15 10:02:13 2026 +0800

    [fix](function) Remove duplicated keys of nested maps in string casts 
(#68639)
    
    ### What problem does this PR solve?
    
    A string cast to MAP keeps the last value of a duplicated key, but the
    maps inside an ARRAY, a STRUCT, or an ARRAY or STRUCT map value keep
    both entries, so size() counts both and element_at() returns the first
    value:
    
        SELECT CAST('[{"a":1,"a":2}]' AS ARRAY<MAP<STRING, INT>>);
        -- before: [{"a":1, "a":2}]
        -- after:  [{"a":2}]
    
    
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] 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:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] 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 -->
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 be/src/exprs/function/cast/cast_base.cpp           | 28 +++++++++
 be/src/exprs/function/cast/cast_to_map.h           | 48 ++-------------
 be/test/exprs/function/cast/cast_to_map_test.cpp   | 50 ++++++++++++++++
 .../test_cast_string_to_nested_map_dedup.out       | 44 ++++++++++++++
 .../test_cast_string_to_nested_map_dedup.groovy    | 70 ++++++++++++++++++++++
 5 files changed, 197 insertions(+), 43 deletions(-)

diff --git a/be/src/exprs/function/cast/cast_base.cpp 
b/be/src/exprs/function/cast/cast_base.cpp
index 00f29e7a9b2..c62403baa4a 100644
--- a/be/src/exprs/function/cast/cast_base.cpp
+++ b/be/src/exprs/function/cast/cast_base.cpp
@@ -21,6 +21,9 @@
 #include <cstdint>
 #include <utility>
 
+#include "core/column/column_array.h"
+#include "core/column/column_map.h"
+#include "core/column/column_struct.h"
 #include "util/jsonb_writer.h"
 namespace doris::CastWrapper {
 
@@ -73,6 +76,29 @@ ChildNullMask build_child_null_mask(const 
NullMap::value_type* parent_null_map,
             .mask_holder = nullptr};
 }
 
+// Remove the duplicated keys of every map in the column, also the maps inside 
arrays, structs
+// and map values. The last value of a key wins, as in map().
+static Status deduplicate_map_keys(IColumn& column) {
+    if (auto* nullable = check_and_get_column<ColumnNullable>(column)) {
+        return deduplicate_map_keys(nullable->get_nested_column());
+    }
+    if (auto* array = check_and_get_column<ColumnArray>(column)) {
+        return deduplicate_map_keys(array->get_data());
+    }
+    if (auto* st = check_and_get_column<ColumnStruct>(column)) {
+        for (size_t i = 0; i < st->tuple_size(); ++i) {
+            RETURN_IF_ERROR(deduplicate_map_keys(st->get_column(i)));
+        }
+        return Status::OK();
+    }
+    if (auto* map = check_and_get_column<ColumnMap>(column)) {
+        // Values last: the ones dropped with a duplicated key are not visited.
+        RETURN_IF_ERROR(map->deduplicate_keys());
+        return deduplicate_map_keys(map->get_values());
+    }
+    return Status::OK();
+}
+
 Status cast_from_generic_to_jsonb(FunctionContext* context, Block& block,
                                   const ColumnNumbers& arguments, uint32_t 
result,
                                   size_t input_rows_count, const 
NullMap::value_type* null_map) {
@@ -252,6 +278,7 @@ Status cast_from_string_to_complex_type(FunctionContext* 
context, Block& block,
         }
     }
 
+    RETURN_IF_ERROR(deduplicate_map_keys(nested_column));
     block.get_by_position(result).column = std::move(to_column);
     return Status::OK();
 }
@@ -286,6 +313,7 @@ Status 
cast_from_string_to_complex_type_strict_mode(FunctionContext* context, Bl
             nullable_col_to.get_null_map_data().push_back(0);
         }
     }
+    RETURN_IF_ERROR(deduplicate_map_keys(nested_column));
     block.get_by_position(result).column = std::move(to_column);
     return Status::OK();
 }
diff --git a/be/src/exprs/function/cast/cast_to_map.h 
b/be/src/exprs/function/cast/cast_to_map.h
index 4714d22805d..b811e4a21ce 100644
--- a/be/src/exprs/function/cast/cast_to_map.h
+++ b/be/src/exprs/function/cast/cast_to_map.h
@@ -23,53 +23,15 @@
 #include "exprs/function/cast/cast_base.h"
 
 namespace doris::CastWrapper {
-
-inline Status deduplicate_map_keys_in_result(Block& block, uint32_t result) {
-    auto result_column_name = block.get_by_position(result).column->get_name();
-    auto mutable_result_column = 
IColumn::mutate(std::move(block.get_by_position(result).column));
-
-    if (auto* nullable_column = 
check_and_get_column<ColumnNullable>(*mutable_result_column)) {
-        auto nested_column = 
IColumn::mutate(nullable_column->get_nested_column_ptr());
-        auto* map_column = check_and_get_column<ColumnMap>(*nested_column);
-        if (!map_column) {
-            return Status::RuntimeError("Illegal column {} for function CAST 
AS MAP",
-                                        result_column_name);
-        }
-
-        RETURN_IF_ERROR(map_column->deduplicate_keys(true));
-        ColumnPtr nested_column_ptr = std::move(nested_column);
-        nullable_column->change_nested_column(nested_column_ptr);
-    } else {
-        auto* map_column = 
check_and_get_column<ColumnMap>(*mutable_result_column);
-        if (!map_column) {
-            return Status::RuntimeError("Illegal column {} for function CAST 
AS MAP",
-                                        result_column_name);
-        }
-
-        RETURN_IF_ERROR(map_column->deduplicate_keys(true));
-    }
-
-    block.get_by_position(result).column = std::move(mutable_result_column);
-    return Status::OK();
-}
-
-inline WrapperType wrap_string_to_map_wrapper(WrapperType wrapper) {
-    return [wrapper = std::move(wrapper)](FunctionContext* context, Block& 
block,
-                                          const ColumnNumbers& arguments, 
uint32_t result,
-                                          size_t input_rows_count,
-                                          const NullMap::value_type* null_map 
= nullptr) {
-        RETURN_IF_ERROR(wrapper(context, block, arguments, result, 
input_rows_count, null_map));
-        return deduplicate_map_keys_in_result(block, result);
-    };
-}
-
 //TODO(Amory) . Need support more cast for key , value for map
 WrapperType create_map_wrapper(FunctionContext* context, const DataTypePtr& 
from_type,
                                const DataTypeMap& to_type) {
     if (is_string_type(from_type->get_primitive_type())) {
-        auto wrapper = context->enable_strict_mode() ? 
cast_from_string_to_complex_type_strict_mode
-                                                     : 
cast_from_string_to_complex_type;
-        return wrap_string_to_map_wrapper(wrapper);
+        if (context->enable_strict_mode()) {
+            return cast_from_string_to_complex_type_strict_mode;
+        } else {
+            return cast_from_string_to_complex_type;
+        }
     }
     const auto* from = check_and_get_data_type<DataTypeMap>(from_type.get());
     if (!from) {
diff --git a/be/test/exprs/function/cast/cast_to_map_test.cpp 
b/be/test/exprs/function/cast/cast_to_map_test.cpp
index 77d0a9f15f0..7753dc55bf3 100644
--- a/be/test/exprs/function/cast/cast_to_map_test.cpp
+++ b/be/test/exprs/function/cast/cast_to_map_test.cpp
@@ -25,10 +25,12 @@
 
 #include "core/column/column.h"
 #include "core/column/column_map.h"
+#include "core/data_type/data_type_array.h"
 #include "core/data_type/data_type_map.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_struct.h"
 #include "core/data_type/primitive_type.h"
 #include "core/field.h"
 #include "exprs/function/cast/cast_base.h"
@@ -276,4 +278,52 @@ TEST_F(FunctionCastTest, 
test_cast_map_unchanged_key_with_null_row) {
     EXPECT_EQ(to_type->to_string(*block.get_by_position(1).column, 1), 
"{1:2}");
 }
 
+// A duplicated key keeps its last value in every map of the result, also in 
the maps inside
+// arrays, structs and map values, as it does in the top level map.
+TEST_F(FunctionCastTest, test_from_string_deduplicates_nested_map_keys) {
+    auto map_type = 
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeMap>(
+            
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>()),
+            
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeInt32>())));
+    auto string_type = 
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeString>());
+    auto array_of_map_type =
+            
std::make_shared<DataTypeNullable>(std::make_shared<DataTypeArray>(map_type));
+
+    struct Case {
+        DataTypePtr to_type;
+        std::string from;
+        std::string expected;
+    };
+    std::vector<Case> cases = {
+            {map_type, R"({"a":1,"a":2})", R"({"a":2})"},
+            {array_of_map_type, R"([{"a":1,"a":2}])", R"([{"a":2}])"},
+            {std::make_shared<DataTypeNullable>(
+                     std::make_shared<DataTypeStruct>(DataTypes {map_type}, 
Strings {"m"})),
+             R"({"m":{"a":1,"a":2}})", R"({"m":{"a":2}})"},
+            {std::make_shared<DataTypeNullable>(
+                     std::make_shared<DataTypeMap>(string_type, 
array_of_map_type)),
+             R"({"outer":[{"a":1,"a":2}]})", R"({"outer":[{"a":2}]})"},
+            {std::make_shared<DataTypeNullable>(
+                     std::make_shared<DataTypeMap>(string_type, 
array_of_map_type)),
+             R"({"outer":[{"a":1,"a":2}],"outer":[{"b":1,"b":2}]})", 
R"({"outer":[{"b":2}]})"},
+    };
+
+    for (bool is_strict_mode : {false, true}) {
+        for (const auto& c : cases) {
+            auto ctx = create_context(is_strict_mode);
+            DataTypePtr from_type = std::make_shared<DataTypeString>();
+            auto fn = get_cast_wrapper(ctx.get(), from_type, c.to_type);
+            ASSERT_TRUE(fn != nullptr);
+
+            Block block = {
+                    {ColumnHelper::create_column<DataTypeString>({c.from}), 
from_type, "from"},
+                    {nullptr, c.to_type, "to"},
+            };
+            ASSERT_TRUE(fn(ctx.get(), block, {0}, 1, block.rows(), nullptr));
+            EXPECT_EQ(c.to_type->to_string(*block.get_by_position(1).column, 
0), c.expected)
+                    << "from: " << c.from << ", to: " << c.to_type->get_name()
+                    << ", strict mode: " << is_strict_mode;
+        }
+    }
+}
+
 } // namespace doris
diff --git 
a/regression-test/data/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.out
 
b/regression-test/data/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.out
new file mode 100644
index 00000000000..682df23194a
--- /dev/null
+++ 
b/regression-test/data/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.out
@@ -0,0 +1,44 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !top --
+{"a":2}
+
+-- !map_in_map --
+{"outer":{"a":2}}
+
+-- !array --
+[{"a":2}]      1       2
+
+-- !struct --
+{"m":{"a":2}}  1
+
+-- !array_in_map --
+{"outer":[{"a":2}]}    2
+
+-- !struct_in_map --
+{"outer":{"m":{"a":2}}}
+
+-- !top --
+{"a":2}
+
+-- !map_in_map --
+{"outer":{"a":2}}
+
+-- !array --
+[{"a":2}]      1       2
+
+-- !struct --
+{"m":{"a":2}}  1
+
+-- !array_in_map --
+{"outer":[{"a":2}]}    2
+
+-- !struct_in_map --
+{"outer":{"m":{"a":2}}}
+
+-- !column --
+1      [{"a":2}]
+2      [{"a":1}, {"b":3}]
+3      [{"a":1, "b":2}]
+4      []
+5      \N
+
diff --git 
a/regression-test/suites/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.groovy
 
b/regression-test/suites/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.groovy
new file mode 100644
index 00000000000..cfc791e2a09
--- /dev/null
+++ 
b/regression-test/suites/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.groovy
@@ -0,0 +1,70 @@
+// 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_cast_string_to_nested_map_dedup") {
+    // A duplicated key keeps its last value in every map of a string cast, 
also in the maps
+    // inside arrays, structs and map values, as it does in the top level map.
+    def check = {
+        qt_top """ SELECT CAST('{"a":1,"a":2}' AS MAP<STRING, INT>) """
+        qt_map_in_map """ SELECT CAST('{"outer":{"a":1,"a":2}}' AS MAP<STRING, 
MAP<STRING, INT>>) """
+        qt_array """
+            SELECT CAST('[{"a":1,"a":2}]' AS ARRAY<MAP<STRING, INT>>) AS c,
+                   size(element_at(CAST('[{"a":1,"a":2}]' AS ARRAY<MAP<STRING, 
INT>>), 1)),
+                   element_at(element_at(CAST('[{"a":1,"a":2}]' AS 
ARRAY<MAP<STRING, INT>>), 1), 'a')
+        """
+        qt_struct """
+            SELECT CAST('{"m":{"a":1,"a":2}}' AS STRUCT<m:MAP<STRING, INT>>) 
AS c,
+                   size(struct_element(CAST('{"m":{"a":1,"a":2}}' AS 
STRUCT<m:MAP<STRING, INT>>), 'm'))
+        """
+        qt_array_in_map """
+            SELECT CAST('{"outer":[{"a":1,"a":2}]}' AS MAP<STRING, 
ARRAY<MAP<STRING, INT>>>) AS c,
+                   element_at(element_at(element_at(
+                       CAST('{"outer":[{"a":1,"a":2}]}' AS MAP<STRING, 
ARRAY<MAP<STRING, INT>>>),
+                       'outer'), 1), 'a')
+        """
+        qt_struct_in_map """
+            SELECT CAST('{"outer":{"m":{"a":1,"a":2}}}' AS MAP<STRING, 
STRUCT<m:MAP<STRING, INT>>>)
+        """
+    }
+
+    sql """ set enable_strict_cast = false """
+    check()
+    sql """ set enable_strict_cast = true """
+    check()
+    sql """ set enable_strict_cast = false """
+
+    sql """ DROP TABLE IF EXISTS test_cast_string_to_nested_map_dedup """
+    sql """
+        CREATE TABLE test_cast_string_to_nested_map_dedup (
+            id INT,
+            s STRING
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        INSERT INTO test_cast_string_to_nested_map_dedup VALUES
+            (1, '[{"a":1,"a":2}]'),
+            (2, '[{"a":1}, {"b":1,"b":2,"b":3}]'),
+            (3, '[{"a":1,"b":2}]'),
+            (4, '[]'),
+            (5, NULL)
+    """
+    order_qt_column """
+        SELECT id, CAST(s AS ARRAY<MAP<STRING, INT>>) FROM 
test_cast_string_to_nested_map_dedup
+    """
+}


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

Reply via email to