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 ecf7732adbd [fix](function) Support cross-dimensional casts for null
arrays (#65456)
ecf7732adbd is described below
commit ecf7732adbdb2cdbfb926ab2ba6bc83e86c9f80e
Author: Mryange <[email protected]>
AuthorDate: Fri Jul 17 08:49:26 2026 +0800
[fix](function) Support cross-dimensional casts for null arrays (#65456)
### What problem does this PR solve?
When constant folding was disabled, BE rejected casts from null-only
arrays to arrays with a different number of dimensions, although FE
constant folding already supported them. Root cause: the BE array cast
checked dimensionality before recognizing that the source element type
represented a NULL literal.
This change allows cross-dimensional array casts when the source nested
type is a NULL literal, while ordinary arrays still require matching
dimensions. For example, `CAST([NULL, NULL] AS ARRAY<ARRAY<INT>>)` now
returns `[NULL, NULL]`: BE casts the two nested NULL values to
`Nullable<Array<Int>>` and rebuilds the outer array with its original
offsets.
### Release note
Support casting null-only arrays to array types with different
dimensions in BE.
### 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 -->
---
be/src/exprs/function/cast/cast_to_array.h | 17 +--
be/test/exprs/function/cast/cast_to_array_test.cpp | 91 ++++++++++++++++
.../cast/test_cast_null_array_to_nested_array.out | 37 +++++++
.../test_cast_null_array_to_nested_array.groovy | 120 +++++++++++++++++++++
4 files changed, 259 insertions(+), 6 deletions(-)
diff --git a/be/src/exprs/function/cast/cast_to_array.h
b/be/src/exprs/function/cast/cast_to_array.h
index c1d96c25f35..d4eb6886530 100644
--- a/be/src/exprs/function/cast/cast_to_array.h
+++ b/be/src/exprs/function/cast/cast_to_array.h
@@ -16,11 +16,19 @@
// under the License.
#include "core/column/column_array.h"
-#include "core/column/column_nullable.h"
#include "core/data_type/data_type_array.h"
#include "exprs/function/cast/cast_base.h"
namespace doris::CastWrapper {
+inline bool has_null_literal_leaf(const DataTypePtr& type) {
+ const auto type_without_nullable = remove_nullable(type);
+ if (const auto* array_type =
+
check_and_get_data_type<DataTypeArray>(type_without_nullable.get())) {
+ return has_null_literal_leaf(array_type->get_nested_type());
+ }
+ return type_without_nullable->is_null_literal();
+}
+
WrapperType create_array_wrapper(FunctionContext* context, const DataTypePtr&
from_type_untyped,
const DataTypeArray& to_type) {
/// Conversion from String through parsing.
@@ -42,11 +50,8 @@ WrapperType create_array_wrapper(FunctionContext* context,
const DataTypePtr& fr
DataTypePtr from_nested_type = from_type->get_nested_type();
- /// In query SELECT CAST([] AS Array(Array(String))) from type is
Array(Nothing)
- bool from_empty_array = from_nested_type->get_primitive_type() ==
INVALID_TYPE;
-
if (from_type->get_number_of_dimensions() !=
to_type.get_number_of_dimensions() &&
- !from_empty_array) {
+ !has_null_literal_leaf(from_nested_type)) {
return CastWrapper::create_unsupport_wrapper(
"CAST AS Array can only be performed between same-dimensional
array types");
}
@@ -90,4 +95,4 @@ WrapperType create_array_wrapper(FunctionContext* context,
const DataTypePtr& fr
return Status::OK();
};
}
-} // namespace doris::CastWrapper
\ No newline at end of file
+} // namespace doris::CastWrapper
diff --git a/be/test/exprs/function/cast/cast_to_array_test.cpp
b/be/test/exprs/function/cast/cast_to_array_test.cpp
index 3ac5ac4476d..bf3b8a95b16 100644
--- a/be/test/exprs/function/cast/cast_to_array_test.cpp
+++ b/be/test/exprs/function/cast/cast_to_array_test.cpp
@@ -21,6 +21,7 @@
#include <vector>
#include "core/column/column_array.h"
+#include "core/data_type/data_type_array.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"
@@ -196,4 +197,94 @@ TEST_F(FunctionCastTest, test_from_string_to_array_bool) {
builder.build(), false);
}
+TEST_F(FunctionCastTest, test_from_null_literal_array_to_nested_array) {
+ auto null_literal_type = std::make_shared<DataTypeBool>();
+ null_literal_type->set_null_literal(true);
+ auto from_nested_type =
std::make_shared<DataTypeNullable>(null_literal_type);
+ auto from_type = std::make_shared<DataTypeArray>(from_nested_type);
+
+ auto from_nested_column =
+ ColumnHelper::create_nullable_column<DataTypeBool>({0, 0, 0},
{true, true, true});
+ auto from_offsets = ColumnHelper::create_column_offsets<TYPE_UINT64>({0,
1, 3});
+ auto from_column = ColumnArray::create(std::move(from_nested_column),
std::move(from_offsets));
+
+ auto to_type = std::make_shared<DataTypeArray>(
+
std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>()));
+
+ auto ctx = create_context(false);
+ auto fn = get_cast_wrapper(ctx.get(), from_type, to_type);
+ ASSERT_TRUE(fn != nullptr);
+
+ Block block = {
+ {std::move(from_column), from_type, "from"},
+ {nullptr, to_type, "to"},
+ };
+ ASSERT_TRUE(fn(ctx.get(), block, {0}, 1, block.rows(), nullptr));
+
+ const auto* result_array =
+
check_and_get_column<ColumnArray>(block.get_by_position(1).column.get());
+ ASSERT_NE(result_array, nullptr);
+ EXPECT_EQ(result_array->get_offsets(), ColumnArray::Offsets64({0, 1, 3}));
+
+ const auto* result_nested_nullable =
+ check_and_get_column<ColumnNullable>(&result_array->get_data());
+ ASSERT_NE(result_nested_nullable, nullptr);
+ EXPECT_EQ(result_nested_nullable->get_null_map_data(), NullMap({1, 1, 1}));
+
+ const auto* result_nested_array =
+
check_and_get_column<ColumnArray>(&result_nested_nullable->get_nested_column());
+ ASSERT_NE(result_nested_array, nullptr);
+ EXPECT_EQ(result_nested_array->size(), 3);
+ EXPECT_EQ(result_nested_array->get_offsets(), ColumnArray::Offsets64({0,
0, 0}));
+}
+
+TEST_F(FunctionCastTest, test_from_nested_null_literal_array_to_deeper_array) {
+ auto null_literal_type = std::make_shared<DataTypeBool>();
+ null_literal_type->set_null_literal(true);
+ auto from_inner_type = std::make_shared<DataTypeArray>(null_literal_type);
+ auto from_type = std::make_shared<DataTypeArray>(from_inner_type);
+
+ auto leaf_column = ColumnHelper::create_nullable_column<DataTypeBool>({0},
{true});
+ auto inner_offsets = ColumnHelper::create_column_offsets<TYPE_UINT64>({1});
+ auto inner_column = ColumnArray::create(std::move(leaf_column),
std::move(inner_offsets));
+ auto inner_null_map = ColumnHelper::create_column<DataTypeUInt8>({false});
+ auto nullable_inner_column =
+ ColumnNullable::create(std::move(inner_column),
std::move(inner_null_map));
+ auto outer_offsets = ColumnHelper::create_column_offsets<TYPE_UINT64>({1});
+ auto from_column =
+ ColumnArray::create(std::move(nullable_inner_column),
std::move(outer_offsets));
+
+ auto to_type =
std::make_shared<DataTypeArray>(std::make_shared<DataTypeArray>(
+
std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>())));
+
+ auto ctx = create_context(false);
+ auto fn = get_cast_wrapper(ctx.get(), from_type, to_type);
+ ASSERT_TRUE(fn != nullptr);
+
+ Block block = {
+ {std::move(from_column), from_type, "from"},
+ {nullptr, to_type, "to"},
+ };
+ ASSERT_TRUE(fn(ctx.get(), block, {0}, 1, block.rows(), nullptr));
+ EXPECT_EQ(to_type->to_string(*block.get_by_position(1).column, 0),
"[[null]]");
+}
+
+TEST_F(FunctionCastTest, test_from_non_null_array_to_nested_array_is_rejected)
{
+ ColumnArrayBuilder<DataTypeInt32> builder;
+ builder.add({1});
+ auto from = builder.build();
+ auto to_type = std::make_shared<DataTypeArray>(
+
std::make_shared<DataTypeArray>(std::make_shared<DataTypeInt32>()));
+
+ auto ctx = create_context(false);
+ auto fn = get_cast_wrapper(ctx.get(), from.type, to_type);
+ ASSERT_TRUE(fn != nullptr);
+
+ Block block = {
+ from,
+ {nullptr, to_type, "to"},
+ };
+ EXPECT_FALSE(fn(ctx.get(), block, {0}, 1, block.rows(), nullptr));
+}
+
} // namespace doris
diff --git
a/regression-test/data/function_p0/cast/test_cast_null_array_to_nested_array.out
b/regression-test/data/function_p0/cast/test_cast_null_array_to_nested_array.out
new file mode 100644
index 00000000000..4a8dd0c8b72
--- /dev/null
+++
b/regression-test/data/function_p0/cast/test_cast_null_array_to_nested_array.out
@@ -0,0 +1,37 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !cast_empty_array_to_nested_array_fold --
+[]
+
+-- !cast_null_array_to_nested_array_fold --
+[null]
+
+-- !cast_nulls_array_to_nested_array_fold --
+[null, null]
+
+-- !cast_nested_null_array_to_deeper_array_fold --
+[[null]]
+
+-- !cast_deeply_nested_null_array_to_deeper_array_fold --
+[[[null]]]
+
+-- !cast_nested_null_array_to_same_dimension_fold --
+[[null]]
+
+-- !cast_empty_array_to_nested_array_be --
+[]
+
+-- !cast_null_array_to_nested_array_be --
+[null]
+
+-- !cast_nulls_array_to_nested_array_be --
+[null, null]
+
+-- !cast_nested_null_array_to_deeper_array_be --
+[[null]]
+
+-- !cast_deeply_nested_null_array_to_deeper_array_be --
+[[[null]]]
+
+-- !cast_nested_null_array_to_same_dimension_be --
+[[null]]
+
diff --git
a/regression-test/suites/function_p0/cast/test_cast_null_array_to_nested_array.groovy
b/regression-test/suites/function_p0/cast/test_cast_null_array_to_nested_array.groovy
new file mode 100644
index 00000000000..a777a90eff7
--- /dev/null
+++
b/regression-test/suites/function_p0/cast/test_cast_null_array_to_nested_array.groovy
@@ -0,0 +1,120 @@
+// 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_null_array_to_nested_array") {
+ sql "set debug_skip_fold_constant = false;"
+
+ qt_cast_empty_array_to_nested_array_fold """
+ select cast([] as array<array<int>>);
+ """
+
+ qt_cast_null_array_to_nested_array_fold """
+ select cast([null] as array<array<int>>);
+ """
+
+ qt_cast_nulls_array_to_nested_array_fold """
+ select cast([null, null] as array<array<int>>);
+ """
+
+ qt_cast_nested_null_array_to_deeper_array_fold """
+ select cast([[null]] as array<array<array<int>>>);
+ """
+
+ qt_cast_deeply_nested_null_array_to_deeper_array_fold """
+ select cast([[[null]]] as array<array<array<array<int>>>>);
+ """
+
+ qt_cast_nested_null_array_to_same_dimension_fold """
+ select cast([[null]] as array<array<int>>);
+ """
+
+ test {
+ sql "select cast([[null]] as array<int>);"
+ exception "can not cast from origin type"
+ }
+
+ test {
+ sql "select cast([[[null]]] as array<array<int>>);"
+ exception "can not cast from origin type"
+ }
+
+ test {
+ sql "select cast([[1]] as array<array<array<int>>>);"
+ exception "can not cast from origin type"
+ }
+
+ test {
+ sql "select cast([1] as array<array<int>>);"
+ exception "can not cast from origin type ARRAY<TINYINT> to target
type=ARRAY<ARRAY<INT>>"
+ }
+
+ test {
+ sql "select cast([1, null] as array<array<int>>);"
+ exception "can not cast from origin type ARRAY<TINYINT> to target
type=ARRAY<ARRAY<INT>>"
+ }
+
+ sql "set debug_skip_fold_constant = true;"
+
+ qt_cast_empty_array_to_nested_array_be """
+ select cast([] as array<array<int>>);
+ """
+
+ qt_cast_null_array_to_nested_array_be """
+ select cast([null] as array<array<int>>);
+ """
+
+ qt_cast_nulls_array_to_nested_array_be """
+ select cast([null, null] as array<array<int>>);
+ """
+
+ qt_cast_nested_null_array_to_deeper_array_be """
+ select cast([[null]] as array<array<array<int>>>);
+ """
+
+ qt_cast_deeply_nested_null_array_to_deeper_array_be """
+ select cast([[[null]]] as array<array<array<array<int>>>>);
+ """
+
+ qt_cast_nested_null_array_to_same_dimension_be """
+ select cast([[null]] as array<array<int>>);
+ """
+
+ test {
+ sql "select cast([[null]] as array<int>);"
+ exception "can not cast from origin type"
+ }
+
+ test {
+ sql "select cast([[[null]]] as array<array<int>>);"
+ exception "can not cast from origin type"
+ }
+
+ test {
+ sql "select cast([[1]] as array<array<array<int>>>);"
+ exception "can not cast from origin type"
+ }
+
+ test {
+ sql "select cast([1] as array<array<int>>);"
+ exception "can not cast from origin type ARRAY<TINYINT> to target
type=ARRAY<ARRAY<INT>>"
+ }
+
+ test {
+ sql "select cast([1, null] as array<array<int>>);"
+ exception "can not cast from origin type ARRAY<TINYINT> to target
type=ARRAY<ARRAY<INT>>"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]