This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 1654ee418a6 [branch-4-2](function) Backport nested MAP cast
deduplication to branch-4-2 (#69110)
1654ee418a6 is described below
commit 1654ee418a627360ae04a8d47c8c2451d226fca3
Author: Chenyang Sun <[email protected]>
AuthorDate: Thu Oct 15 23:33:34 2026 +0800
[branch-4-2](function) Backport nested MAP cast deduplication to branch-4-2
(#69110)
## What problem does this PR solve?
Backport the nested MAP duplicate-key handling from Apache Doris PR
#68639 to branch-4.2.
String casts to MAP, ARRAY, and STRUCT now remove duplicate keys from
MAPs nested in arrays, structs, and MAP values, retaining the last
value. The backport removes the old top-level-only MAP wrapper and
normalizes the shared string-to-complex cast path.
## Scope
- Backport the three functional commits from #68639.
- Resolve the branch-4.2 conflicts while preserving branch-specific cast
code.
- Add nested MAP regression and unit coverage for strict and non-strict
modes.
## Validation
- clang-format 16 check passed.
- git diff --check passed.
- Full BE build and unit test were not run in this worktree because no
branch-4.2 build artifact was available.
Related upstream PR: #68639
---------
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 | 49 ++-------------
be/test/exprs/function/cast/cast_to_map_test.cpp | 50 ++++++++++++++++
.../test_cast_string_to_nested_map_dedup.out | 43 +++++++++++++
.../test_cast_string_to_nested_map_dedup.groovy | 70 ++++++++++++++++++++++
5 files changed, 196 insertions(+), 44 deletions(-)
diff --git a/be/src/exprs/function/cast/cast_base.cpp
b/be/src/exprs/function/cast/cast_base.cpp
index 04ea2cab562..e5a548502a8 100644
--- a/be/src/exprs/function/cast/cast_base.cpp
+++ b/be/src/exprs/function/cast/cast_base.cpp
@@ -19,9 +19,35 @@
#include <cstdint>
+#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 {
+// 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) {
@@ -201,6 +227,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();
}
@@ -235,6 +262,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 ca6b8e5e90b..c47a8ecf710 100644
--- a/be/src/exprs/function/cast/cast_to_map.h
+++ b/be/src/exprs/function/cast/cast_to_map.h
@@ -21,54 +21,16 @@
#include "exprs/function/cast/cast_base.h"
namespace doris::CastWrapper {
-#include "common/compile_check_begin.h"
-
-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) {
@@ -120,5 +82,4 @@ WrapperType create_map_wrapper(FunctionContext* context,
const DataTypePtr& from
return Status::OK();
};
}
-#include "common/compile_check_end.h"
} // namespace doris::CastWrapper
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 f3020d94f69..f6c882c8570 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"
@@ -179,4 +181,52 @@ TEST_F(FunctionCastTest,
test_from_string_to_map_string_string) {
builder.build(), false);
}
+// 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..6beac6d6c48
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/cast_function/test_cast_string_to_nested_map_dedup.out
@@ -0,0 +1,43 @@
+-- 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]