This is an automated email from the ASF dual-hosted git repository.
Mryange 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 04b57232a48 [Feat](map) Support function map_from_array/entries
(#67045)
04b57232a48 is described below
commit 04b57232a48f9587857deaac89c0d6e7baf363ca
Author: linrrarity <[email protected]>
AuthorDate: Mon Aug 24 16:57:18 2026 +0800
[Feat](map) Support function map_from_array/entries (#67045)
## Release note
Support function `map_from_array` and `map_from_entries'
doc: https://github.com/apache/doris-website/pull/4083
```sql
SELECT map_from_arrays([1, 2], [10, 20]) AS result;
+--------------+
| result |
+--------------+
| {1:10, 2:20} |
+--------------+
SELECT map_from_entries(array(struct(1, 10), struct(2, 20))) AS result;
+--------------+
| result |
+--------------+
| {1:10, 2:20} |
+--------------+
```
---
be/src/exprs/function/function_map.cpp | 196 +++++++++++++
be/test/exprs/function/function_map_test.cpp | 313 ++++++++++++++++++++-
.../doris/catalog/BuiltinScalarFunctions.java | 4 +
.../expressions/functions/scalar/MapEntries.java | 10 +-
.../functions/scalar/MapFromArrays.java | 97 +++++++
.../functions/scalar/MapFromEntries.java | 108 +++++++
.../expressions/visitor/ScalarFunctionVisitor.java | 10 +
.../scalar/MapConstructionFunctionsTest.java | 214 ++++++++++++++
.../map_functions/test_map_from_arrays_entries.out | 41 +++
.../test_map_from_arrays_entries.groovy | 213 ++++++++++++++
10 files changed, 1204 insertions(+), 2 deletions(-)
diff --git a/be/src/exprs/function/function_map.cpp
b/be/src/exprs/function/function_map.cpp
index e0b06e7450d..bd1b8281904 100644
--- a/be/src/exprs/function/function_map.cpp
+++ b/be/src/exprs/function/function_map.cpp
@@ -38,6 +38,7 @@
#include "core/column/column_const.h"
#include "core/column/column_map.h"
#include "core/column/column_nullable.h"
+#include "core/column/column_struct.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type.h"
#include "core/data_type/data_type_array.h"
@@ -49,6 +50,7 @@
#include "core/data_type/primitive_type.h"
#include "core/typeid_cast.h"
#include "core/types.h"
+#include "exec/common/util.hpp"
#include "exprs/aggregate/aggregate_function.h"
#include "exprs/function/array/function_array_index.h"
#include "exprs/function/function.h"
@@ -61,6 +63,124 @@ class FunctionContext;
namespace doris {
+class FunctionMapFromArrays : public IFunction {
+public:
+ static constexpr auto name = "map_from_arrays";
+ static FunctionPtr create() { return
std::make_shared<FunctionMapFromArrays>(); }
+
+ String get_name() const override { return name; }
+ size_t get_number_of_arguments() const override { return 2; }
+ bool use_default_implementation_for_nulls() const override { return false;
}
+
+ DataTypePtr get_return_type_impl(const DataTypes& arguments) const
override {
+ const auto& key_array_type =
+ assert_cast<const
DataTypeArray&>(*remove_nullable(arguments[0]));
+ const auto& value_array_type =
+ assert_cast<const
DataTypeArray&>(*remove_nullable(arguments[1]));
+ auto map_type =
+
std::make_shared<DataTypeMap>(make_nullable(key_array_type.get_nested_type()),
+
make_nullable(value_array_type.get_nested_type()));
+ return have_nullable(arguments) ? make_nullable(std::move(map_type)) :
map_type;
+ }
+
+ Status execute_impl(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
+ uint32_t result, size_t input_rows_count) const
override {
+ const auto& [key_column, key_is_const] =
+ unpack_if_const(block.get_by_position(arguments[0]).column);
+ const auto& [value_column, value_is_const] =
+ unpack_if_const(block.get_by_position(arguments[1]).column);
+
+ auto result_null_map = ColumnUInt8::create(input_rows_count, 0);
+ auto& result_null_map_data = result_null_map->get_data();
+ auto merge_null_map = [&](const ColumnPtr& column, bool is_const) ->
const IColumn& {
+ if (const auto* null_col =
check_and_get_column<ColumnNullable>(column.get())) {
+ VectorizedUtils::update_null_map(result_null_map_data,
+
null_col->get_null_map_data(), is_const);
+ return null_col->get_nested_column();
+ }
+ return *column;
+ };
+ const auto& keys =
+ assert_cast<const ColumnArray&>(merge_null_map(key_column,
key_is_const));
+ const auto& values =
+ assert_cast<const ColumnArray&>(merge_null_map(value_column,
value_is_const));
+ bool has_mismatched_null_row = false;
+ RETURN_IF_ERROR(check_arguments(keys, key_is_const, values,
value_is_const,
+ result_null_map_data, input_rows_count,
+ has_mismatched_null_row));
+
+ ColumnPtr result_keys = keys.get_data_ptr();
+ ColumnPtr result_values = values.get_data_ptr();
+ ColumnPtr result_offsets = keys.get_offsets_ptr();
+ if (has_mismatched_null_row || key_is_const || value_is_const) {
+ auto filtered_keys = keys.get_data().clone_empty();
+ auto filtered_values = values.get_data().clone_empty();
+ auto filtered_offsets = ColumnArray::ColumnOffsets::create();
+ filtered_offsets->reserve(input_rows_count);
+
+ size_t output_offset = 0;
+ for (size_t row = 0; row < input_rows_count; ++row) {
+ if (!result_null_map_data[row]) {
+ const size_t key_row = index_check_const(row,
key_is_const);
+ const size_t value_row = index_check_const(row,
value_is_const);
+ const size_t key_begin = key_row == 0 ? 0 :
keys.get_offsets()[key_row - 1];
+ const size_t value_begin =
+ value_row == 0 ? 0 :
values.get_offsets()[value_row - 1];
+ const size_t entry_count = keys.size_at(key_row);
+ filtered_keys->insert_range_from(keys.get_data(),
key_begin, entry_count);
+ filtered_values->insert_range_from(values.get_data(),
value_begin, entry_count);
+ output_offset += entry_count;
+ }
+ filtered_offsets->insert_value(output_offset);
+ }
+ result_keys = std::move(filtered_keys);
+ result_values = std::move(filtered_values);
+ result_offsets = std::move(filtered_offsets);
+ }
+
+ auto result_map =
+ ColumnMap::create(make_nullable(result_keys),
make_nullable(result_values),
+ std::move(result_offsets));
+ RETURN_IF_ERROR(result_map->deduplicate_keys());
+ if (block.get_by_position(result).type->is_nullable()) {
+ block.replace_by_position(result,
ColumnNullable::create(std::move(result_map),
+
std::move(result_null_map)));
+ } else {
+ block.replace_by_position(result, std::move(result_map));
+ }
+ return Status::OK();
+ }
+
+private:
+ /// Non-const key/value columns must contain input_rows_count rows. A
const array
+ /// is broadcast from its single nested row. Every non-null result row
must have
+ /// equally sized key/value arrays; a mismatched NULL row is marked for
rebuilding
+ /// because its nested payload cannot be shared by the result map.
+ static Status check_arguments(const ColumnArray& keys, bool key_is_const,
+ const ColumnArray& values, bool
value_is_const,
+ const NullMap& result_null_map, size_t
input_rows_count,
+ bool& has_mismatched_null_row) {
+ if ((!key_is_const && keys.size() != input_rows_count) ||
+ (!value_is_const && values.size() != input_rows_count)) {
+ return Status::InvalidArgument(
+ "Key and value arrays of function {} must have the same
length", name);
+ }
+ for (size_t row = 0; row < input_rows_count; ++row) {
+ const size_t key_size = keys.size_at(index_check_const(row,
key_is_const));
+ const size_t value_size = values.size_at(index_check_const(row,
value_is_const));
+ if (key_size == value_size) {
+ continue;
+ }
+ if (!result_null_map[row]) {
+ return Status::InvalidArgument(
+ "Key and value arrays of function {} must have the
same length", name);
+ }
+ has_mismatched_null_row = true;
+ }
+ return Status::OK();
+ }
+};
+
// construct a map
// map(key1, value2, key2, value2) -> {key1: value2, key2: value2}
class FunctionMap : public IFunction {
@@ -337,6 +457,80 @@ public:
}
};
+class FunctionMapFromEntries : public IFunction {
+public:
+ static constexpr auto name = "map_from_entries";
+ static FunctionPtr create() { return
std::make_shared<FunctionMapFromEntries>(); }
+
+ String get_name() const override { return name; }
+ size_t get_number_of_arguments() const override { return 1; }
+ bool use_default_implementation_for_nulls() const override { return false;
}
+
+ DataTypePtr get_return_type_impl(const DataTypes& arguments) const
override {
+ if (arguments[0]->is_null_literal()) {
+ return make_nullable(std::make_shared<DataTypeMap>(arguments[0],
arguments[0]));
+ }
+ const auto& array_type = assert_cast<const
DataTypeArray&>(*remove_nullable(arguments[0]));
+ const auto& struct_type =
+ assert_cast<const
DataTypeStruct&>(*remove_nullable(array_type.get_nested_type()));
+ DCHECK_EQ(struct_type.get_elements().size(), 2);
+ auto map_type =
std::make_shared<DataTypeMap>(make_nullable(struct_type.get_element(0)),
+
make_nullable(struct_type.get_element(1)));
+ return arguments[0]->is_nullable() ?
make_nullable(std::move(map_type)) : map_type;
+ }
+
+ Status execute_impl(FunctionContext* context, Block& block, const
ColumnNumbers& arguments,
+ uint32_t result, size_t input_rows_count) const
override {
+ ColumnPtr entries_column = block.get_by_position(arguments[0]).column;
+ const auto* nullable_array =
check_and_get_column<ColumnNullable>(entries_column.get());
+ if (nullable_array != nullptr) {
+ entries_column = nullable_array->get_nested_column_ptr();
+ }
+
+ const auto& entries = assert_cast<const ColumnArray&>(*entries_column);
+ const auto& nullable_entries = assert_cast<const
ColumnNullable&>(entries.get_data());
+ RETURN_IF_ERROR(check_arguments(entries, nullable_entries,
nullable_array));
+
+ const auto& entry_struct =
+ assert_cast<const
ColumnStruct&>(nullable_entries.get_nested_column());
+ auto result_map =
ColumnMap::create(make_nullable(entry_struct.get_column_ptr(0)),
+
make_nullable(entry_struct.get_column_ptr(1)),
+ entries.get_offsets_ptr());
+ RETURN_IF_ERROR(result_map->deduplicate_keys());
+ if (nullable_array != nullptr) {
+ block.replace_by_position(
+ result, ColumnNullable::create(std::move(result_map),
+
nullable_array->get_null_map_column_ptr()));
+ } else {
+ block.replace_by_position(result, std::move(result_map));
+ }
+ return Status::OK();
+ }
+
+private:
+ /// Every element of a non-null outer array must be a non-null struct
entry.
+ /// Null outer arrays are skipped, while null key or value fields inside a
+ /// non-null struct are allowed.
+ static Status check_arguments(const ColumnArray& entries,
+ const ColumnNullable& nullable_entries,
+ const ColumnNullable* nullable_array) {
+ if (!nullable_entries.has_null()) {
+ return Status::OK();
+ }
+ for (size_t row = 0; row < entries.size(); ++row) {
+ if (nullable_array != nullptr && nullable_array->is_null_at(row)) {
+ continue;
+ }
+ const size_t begin = row == 0 ? 0 : entries.get_offsets()[row - 1];
+ const size_t end = entries.get_offsets()[row];
+ if (nullable_entries.has_null(begin, end)) {
+ return Status::InvalidArgument("Map entry of function {}
cannot be null", name);
+ }
+ }
+ return Status::OK();
+ }
+};
+
class FunctionStrToMap : public IFunction {
public:
static constexpr auto name = "str_to_map";
@@ -790,12 +984,14 @@ private:
};
void register_function_map(SimpleFunctionFactory& factory) {
+ factory.register_function<FunctionMapFromArrays>();
factory.register_function<FunctionMap>();
factory.register_function<FunctionMapContains<true>>();
factory.register_function<FunctionMapContains<false>>();
factory.register_function<FunctionMapKeysOrValues<true>>();
factory.register_function<FunctionMapKeysOrValues<false>>();
factory.register_function<FunctionMapEntries>();
+ factory.register_function<FunctionMapFromEntries>();
factory.register_function<FunctionStrToMap>();
factory.register_function<FunctionMapContainsEntry>();
factory.register_function<FunctionDeduplicateMap>();
diff --git a/be/test/exprs/function/function_map_test.cpp
b/be/test/exprs/function/function_map_test.cpp
index c791b2b25cd..22cdd72256f 100644
--- a/be/test/exprs/function/function_map_test.cpp
+++ b/be/test/exprs/function/function_map_test.cpp
@@ -18,16 +18,87 @@
#include <fmt/core.h>
#include <gtest/gtest.h>
+#include <optional>
#include <string>
+#include <vector>
+#include "core/assert_cast.h"
#include "core/column/column_array.h"
+#include "core/column/column_const.h"
#include "core/column/column_map.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_struct.h"
+#include "core/column/column_vector.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_struct.h"
#include "core/types.h"
#include "exprs/function/function_test_util.h"
namespace doris {
+namespace {
+
+MutableColumnPtr make_nullable_int_column(const
std::vector<std::optional<int32_t>>& values) {
+ auto nested = ColumnInt32::create();
+ auto null_map = ColumnUInt8::create();
+ for (const auto& value : values) {
+ nested->insert_value(value.value_or(0));
+ null_map->insert_value(value.has_value() ? 0 : 1);
+ }
+ return ColumnNullable::create(std::move(nested), std::move(null_map));
+}
+
+MutableColumnPtr make_offsets(const std::vector<size_t>& offsets) {
+ auto result = ColumnArray::ColumnOffsets::create();
+ for (size_t offset : offsets) {
+ result->insert_value(offset);
+ }
+ return result;
+}
+
+ColumnPtr make_int_array(const std::vector<std::optional<int32_t>>& values,
+ const std::vector<size_t>& offsets) {
+ return ColumnArray::create(make_nullable_int_column(values),
make_offsets(offsets));
+}
+
+ColumnPtr make_int_entry_array(const std::vector<std::optional<int32_t>>& keys,
+ const std::vector<std::optional<int32_t>>&
values,
+ const std::vector<size_t>& offsets,
+ const std::vector<bool>& null_entries = {}) {
+ auto entries = ColumnStruct::create(
+ Columns {make_nullable_int_column(keys),
make_nullable_int_column(values)});
+ auto null_map = ColumnUInt8::create(entries->size(), 0);
+ for (size_t i = 0; i < null_entries.size(); ++i) {
+ null_map->get_data()[i] = null_entries[i];
+ }
+ return ColumnArray::create(ColumnNullable::create(std::move(entries),
std::move(null_map)),
+ make_offsets(offsets));
+}
+
+int32_t get_nullable_int(const IColumn& column, size_t row) {
+ const auto& nullable = assert_cast<const ColumnNullable&>(column);
+ return assert_cast<const
ColumnInt32&>(nullable.get_nested_column()).get_element(row);
+}
+
+Status execute_map_function(const std::string& name, Block& block, const
ColumnNumbers& arguments,
+ uint32_t result, const DataTypePtr& return_type) {
+ ColumnsWithTypeAndName argument_template;
+ for (uint32_t argument : arguments) {
+ argument_template.push_back(block.get_by_position(argument));
+ }
+ auto function =
+ SimpleFunctionFactory::instance().get_function(name,
argument_template, return_type);
+ if (function == nullptr) {
+ return Status::InternalError("function {} is not registered", name);
+ }
+ return function->execute(nullptr, block, arguments, result, block.rows());
+}
+
+} // namespace
+
TEST(FunctionMapTest, deduplicate_map) {
const std::string func_name = "deduplicate_map";
@@ -81,4 +152,244 @@ TEST(FunctionMapTest, deduplicate_map) {
ASSERT_EQ(map_size, 8) << "deduplicate map failed at row " << i;
}
}
-} // namespace doris
\ No newline at end of file
+
+TEST(FunctionMapTest, map_from_arrays) {
+ auto nullable_int = make_nullable(std::make_shared<DataTypeInt32>());
+ auto array_type = std::make_shared<DataTypeArray>(nullable_int);
+ auto map_type = std::make_shared<DataTypeMap>(nullable_int, nullable_int);
+
+ {
+ Block block;
+ block.insert({make_int_array({1, 1, 2}, {2, 3}), array_type, "keys"});
+ block.insert({make_int_array({10, 20, 30}, {2, 3}), array_type,
"values"});
+ block.insert({nullptr, map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_arrays", block, {0, 1}, 2,
map_type).ok());
+ const auto& result = assert_cast<const
ColumnMap&>(*block.get_by_position(2).column);
+ ASSERT_EQ(result.get_offsets()[0], 1);
+ ASSERT_EQ(result.get_offsets()[1], 2);
+ EXPECT_EQ(get_nullable_int(result.get_keys(), 0), 1);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 0), 20);
+ EXPECT_EQ(get_nullable_int(result.get_keys(), 1), 2);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 1), 30);
+ }
+
+ {
+ Block block;
+ block.insert({make_int_array({1, 2, 3}, {2, 3}), array_type, "keys"});
+ block.insert({make_int_array({10, 20, 30}, {1, 3}), array_type,
"values"});
+ block.insert({nullptr, map_type, "result"});
+
+ auto status = execute_map_function("map_from_arrays", block, {0, 1},
2, map_type);
+ ASSERT_TRUE(status.is<ErrorCode::INVALID_ARGUMENT>()) <<
status.to_string();
+ EXPECT_NE(status.to_string().find("Key and value arrays of function
map_from_arrays must "
+ "have the same length"),
+ std::string::npos);
+ }
+
+ {
+ Block block;
+ block.insert({ColumnConst::create(make_int_array({1}, {1}), 2),
array_type, "keys"});
+ block.insert({make_int_array({10, 20}, {1, 2}), array_type, "values"});
+ block.insert({nullptr, map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_arrays", block, {0, 1}, 2,
map_type).ok());
+ const auto& result = assert_cast<const
ColumnMap&>(*block.get_by_position(2).column);
+ EXPECT_EQ(result.get_offsets()[0], 1);
+ EXPECT_EQ(result.get_offsets()[1], 2);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 0), 10);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 1), 20);
+ }
+
+ {
+ Block block;
+ block.insert({make_int_array({1, 2}, {1, 2}), array_type, "keys"});
+ block.insert({ColumnConst::create(make_int_array({10}, {1}), 2),
array_type, "values"});
+ block.insert({nullptr, map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_arrays", block, {0, 1}, 2,
map_type).ok());
+ const auto& result = assert_cast<const
ColumnMap&>(*block.get_by_position(2).column);
+ EXPECT_EQ(result.get_offsets()[0], 1);
+ EXPECT_EQ(result.get_offsets()[1], 2);
+ EXPECT_EQ(get_nullable_int(result.get_keys(), 0), 1);
+ EXPECT_EQ(get_nullable_int(result.get_keys(), 1), 2);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 0), 10);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 1), 10);
+ }
+}
+
+TEST(FunctionMapTest, map_from_arrays_nullable) {
+ auto nullable_int = make_nullable(std::make_shared<DataTypeInt32>());
+ auto array_type = std::make_shared<DataTypeArray>(nullable_int);
+ auto nullable_array_type = make_nullable(array_type);
+ auto map_type = std::make_shared<DataTypeMap>(nullable_int, nullable_int);
+ auto nullable_map_type = make_nullable(map_type);
+ auto key_null_map = ColumnUInt8::create();
+ key_null_map->insert_value(0);
+ key_null_map->insert_value(0);
+ key_null_map->insert_value(1);
+ key_null_map->insert_value(0);
+ key_null_map->insert_value(0);
+ auto value_null_map = ColumnUInt8::create();
+ value_null_map->insert_value(0);
+ value_null_map->insert_value(0);
+ value_null_map->insert_value(0);
+ value_null_map->insert_value(0);
+ value_null_map->insert_value(0);
+
+ auto keys = make_int_array({1, 2, 99, 3, 4, 5}, {1, 2, 3, 5, 6});
+ auto values = make_int_array({10, 20, 990, 30, 40, 50}, {1, 2, 3, 5, 6});
+ const auto& key_array = assert_cast<const ColumnArray&>(*keys);
+ const auto& value_array = assert_cast<const ColumnArray&>(*values);
+ const auto* key_data = key_array.get_data_ptr().get();
+ const auto* value_data = value_array.get_data_ptr().get();
+ const auto* key_offsets = key_array.get_offsets_ptr().get();
+
+ Block block;
+ block.insert({ColumnNullable::create(std::move(keys),
std::move(key_null_map)),
+ nullable_array_type, "keys"});
+ block.insert({ColumnNullable::create(std::move(values),
std::move(value_null_map)),
+ nullable_array_type, "values"});
+ block.insert({nullptr, nullable_map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_arrays", block, {0, 1}, 2,
nullable_map_type).ok());
+ const auto& result = assert_cast<const
ColumnNullable&>(*block.get_by_position(2).column);
+ EXPECT_FALSE(result.is_null_at(0));
+ EXPECT_FALSE(result.is_null_at(1));
+ EXPECT_TRUE(result.is_null_at(2));
+ EXPECT_FALSE(result.is_null_at(3));
+ EXPECT_FALSE(result.is_null_at(4));
+ const auto& nested = assert_cast<const
ColumnMap&>(result.get_nested_column());
+ EXPECT_EQ(nested.get_keys_ptr().get(), key_data);
+ EXPECT_EQ(nested.get_values_ptr().get(), value_data);
+ EXPECT_EQ(nested.get_offsets_ptr().get(), key_offsets);
+ EXPECT_EQ(nested.get_offsets()[0], 1);
+ EXPECT_EQ(nested.get_offsets()[1], 2);
+ EXPECT_EQ(nested.get_offsets()[2], 3);
+ EXPECT_EQ(nested.get_offsets()[3], 5);
+ EXPECT_EQ(nested.get_offsets()[4], 6);
+ EXPECT_EQ(get_nullable_int(nested.get_keys(), 3), 3);
+ EXPECT_EQ(get_nullable_int(nested.get_values(), 3), 30);
+}
+
+TEST(FunctionMapTest, map_from_arrays_mismatched_nullable_payload) {
+ auto nullable_int = make_nullable(std::make_shared<DataTypeInt32>());
+ auto array_type = std::make_shared<DataTypeArray>(nullable_int);
+ auto nullable_array_type = make_nullable(array_type);
+ auto nullable_map_type =
+ make_nullable(std::make_shared<DataTypeMap>(nullable_int,
nullable_int));
+ auto key_null_map = ColumnUInt8::create();
+ key_null_map->insert_value(0);
+ key_null_map->insert_value(1);
+ key_null_map->insert_value(0);
+ auto value_null_map = ColumnUInt8::create(3, 0);
+
+ Block block;
+ block.insert({ColumnNullable::create(make_int_array({1, 98, 99, 3}, {1, 3,
4}),
+ std::move(key_null_map)),
+ nullable_array_type, "keys"});
+ block.insert({ColumnNullable::create(make_int_array({10, 990, 30}, {1, 2,
3}),
+ std::move(value_null_map)),
+ nullable_array_type, "values"});
+ block.insert({nullptr, nullable_map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_arrays", block, {0, 1}, 2,
nullable_map_type).ok());
+ const auto& result = assert_cast<const
ColumnNullable&>(*block.get_by_position(2).column);
+ EXPECT_FALSE(result.is_null_at(0));
+ EXPECT_TRUE(result.is_null_at(1));
+ EXPECT_FALSE(result.is_null_at(2));
+ const auto& nested = assert_cast<const
ColumnMap&>(result.get_nested_column());
+ EXPECT_EQ(nested.get_offsets()[0], 1);
+ EXPECT_EQ(nested.get_offsets()[1], 1);
+ EXPECT_EQ(nested.get_offsets()[2], 2);
+ EXPECT_EQ(get_nullable_int(nested.get_keys(), 1), 3);
+ EXPECT_EQ(get_nullable_int(nested.get_values(), 1), 30);
+}
+
+TEST(FunctionMapTest, map_from_entries) {
+ auto nullable_int = make_nullable(std::make_shared<DataTypeInt32>());
+ auto struct_type = std::make_shared<DataTypeStruct>(DataTypes
{nullable_int, nullable_int},
+ Strings {"key",
"value"});
+ auto array_type =
std::make_shared<DataTypeArray>(make_nullable(struct_type));
+ auto map_type = std::make_shared<DataTypeMap>(nullable_int, nullable_int);
+
+ {
+ Block block;
+ block.insert(
+ {make_int_entry_array({1, 1, 2}, {10, 20, 30}, {2, 3}),
array_type, "entries"});
+ block.insert({nullptr, map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_entries", block, {0}, 1,
map_type).ok());
+ const auto& result = assert_cast<const
ColumnMap&>(*block.get_by_position(1).column);
+ ASSERT_EQ(result.get_offsets()[0], 1);
+ ASSERT_EQ(result.get_offsets()[1], 2);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 0), 20);
+ EXPECT_EQ(get_nullable_int(result.get_values(), 1), 30);
+ }
+
+ {
+ Block block;
+ block.insert({ColumnConst::create(make_int_entry_array({1, 2}, {10,
20}, {2}), 3),
+ array_type, "entries"});
+ block.insert({nullptr, map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_entries", block, {0}, 1,
map_type).ok());
+ const auto& result = assert_cast<const
ColumnConst&>(*block.get_by_position(1).column);
+ EXPECT_EQ(result.size(), 3);
+ }
+
+ {
+ auto nullable_array_type = make_nullable(array_type);
+ auto nullable_map_type = make_nullable(map_type);
+ auto null_map = ColumnUInt8::create(1, 0);
+
+ Block block;
+ block.insert({ColumnConst::create(
+ ColumnNullable::create(
+ make_int_entry_array({std::nullopt},
{std::nullopt}, {1}),
+ std::move(null_map)),
+ 3),
+ nullable_array_type, "entries"});
+ block.insert({nullptr, nullable_map_type, "result"});
+
+ ASSERT_TRUE(
+ execute_map_function("map_from_entries", block, {0}, 1,
nullable_map_type).ok());
+ const auto& result = assert_cast<const
ColumnConst&>(*block.get_by_position(1).column);
+ EXPECT_EQ(result.size(), 3);
+ }
+
+ {
+ Block block;
+ block.insert({make_int_entry_array({1}, {10}, {1}, {true}),
array_type, "entries"});
+ block.insert({nullptr, map_type, "result"});
+
+ auto status = execute_map_function("map_from_entries", block, {0}, 1,
map_type);
+ ASSERT_TRUE(status.is<ErrorCode::INVALID_ARGUMENT>()) <<
status.to_string();
+ }
+}
+
+TEST(FunctionMapTest, map_from_entries_nullable) {
+ auto nullable_int = make_nullable(std::make_shared<DataTypeInt32>());
+ auto struct_type = std::make_shared<DataTypeStruct>(DataTypes
{nullable_int, nullable_int},
+ Strings {"key",
"value"});
+ auto array_type =
std::make_shared<DataTypeArray>(make_nullable(struct_type));
+ auto nullable_array_type = make_nullable(array_type);
+ auto map_type = std::make_shared<DataTypeMap>(nullable_int, nullable_int);
+ auto nullable_map_type = make_nullable(map_type);
+ auto null_map = ColumnUInt8::create();
+ null_map->insert_value(0);
+ null_map->insert_value(1);
+
+ Block block;
+ block.insert(
+ {ColumnNullable::create(make_int_entry_array({1, 2}, {10, 20}, {1,
2}, {false, true}),
+ std::move(null_map)),
+ nullable_array_type, "entries"});
+ block.insert({nullptr, nullable_map_type, "result"});
+
+ ASSERT_TRUE(execute_map_function("map_from_entries", block, {0}, 1,
nullable_map_type).ok());
+ const auto& result = assert_cast<const
ColumnNullable&>(*block.get_by_position(1).column);
+ EXPECT_FALSE(result.is_null_at(0));
+ EXPECT_TRUE(result.is_null_at(1));
+}
+} // namespace doris
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
index 762f3afc0f6..62369888db9 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
@@ -335,6 +335,8 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapContainsEn
import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapContainsKey;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapContainsValue;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapEntries;
+import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapFromArrays;
+import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapFromEntries;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapKeys;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapSize;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapValues;
@@ -917,6 +919,8 @@ public class BuiltinScalarFunctions implements
FunctionHelper {
scalar(MapContainsKey.class, "map_contains_key"),
scalar(MapContainsValue.class, "map_contains_value"),
scalar(MapEntries.class, "map_entries"),
+ scalar(MapFromArrays.class, "map_from_arrays"),
+ scalar(MapFromEntries.class, "map_from_entries"),
scalar(MapKeys.class, "map_keys"),
scalar(MapSize.class, "map_size"),
scalar(MapValues.class, "map_values"),
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapEntries.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapEntries.java
index 22a14c31df3..8d40f8fbb30 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapEntries.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapEntries.java
@@ -20,6 +20,7 @@ package
org.apache.doris.nereids.trees.expressions.functions.scalar;
import org.apache.doris.catalog.FunctionSignature;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.PreferPushDownProject;
+import org.apache.doris.nereids.trees.expressions.functions.ComputePrecision;
import org.apache.doris.nereids.trees.expressions.functions.CustomSignature;
import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
import org.apache.doris.nereids.trees.expressions.functions.SearchSignature;
@@ -42,7 +43,7 @@ import java.util.List;
* fields 'key' and 'value'.
*/
public class MapEntries extends ScalarFunction
- implements UnaryExpression, CustomSignature, PropagateNullable,
PreferPushDownProject {
+ implements UnaryExpression, ComputePrecision, CustomSignature,
PropagateNullable, PreferPushDownProject {
/**
* constructor with 1 argument.
@@ -94,4 +95,11 @@ public class MapEntries extends ScalarFunction
return null; // unreachable
}
}
+
+ // Prevent MAP<DECIMAL(38,0), DECIMAL(38,38)> from being resolved as
+ // MAP<DECIMAL(38,6), DECIMAL(38,6)>, and nested DATETIMEV2(6) as
DATETIMEV2(0).
+ @Override
+ public FunctionSignature computePrecision(FunctionSignature signature) {
+ return signature;
+ }
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapFromArrays.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapFromArrays.java
new file mode 100644
index 00000000000..8626296cd20
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapFromArrays.java
@@ -0,0 +1,97 @@
+// 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.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.PreferPushDownProject;
+import org.apache.doris.nereids.trees.expressions.functions.ComputePrecision;
+import
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.MapType;
+import org.apache.doris.nereids.types.NullType;
+import org.apache.doris.nereids.types.TinyIntType;
+import org.apache.doris.nereids.types.coercion.AnyDataType;
+import org.apache.doris.nereids.types.coercion.FollowToAnyDataType;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/** Construct a Map from key and value arrays with identical per-row offsets.
*/
+public class MapFromArrays extends ScalarFunction
+ implements BinaryExpression, ComputePrecision,
ExplicitlyCastableSignature, PropagateNullable,
+ PreferPushDownProject {
+
+ public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+ FunctionSignature.ret(MapType.of(new FollowToAnyDataType(0), new
FollowToAnyDataType(1)))
+ .args(ArrayType.of(new AnyDataType(0)), ArrayType.of(new
AnyDataType(1))));
+
+ public MapFromArrays(Expression keys, Expression values) {
+ super("map_from_arrays", keys, values);
+ }
+
+ private MapFromArrays(ScalarFunctionParams functionParams) {
+ super(functionParams);
+ }
+
+ @Override
+ public MapFromArrays withChildren(List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 2);
+ return new MapFromArrays(getFunctionParams(children));
+ }
+
+ @Override
+ public List<FunctionSignature> getSignatures() {
+ return SIGNATURES;
+ }
+
+ /**
+ * Keep the resolved key and value types independent. The default
precision promotion merges
+ * decimal and time types from all arguments, which can lose precision
across the two map sides.
+ */
+ @Override
+ public FunctionSignature computePrecision(FunctionSignature signature) {
+ return signature;
+ }
+
+ @Override
+ public FunctionSignature computeSignature(FunctionSignature signature) {
+ FunctionSignature resolvedSignature =
super.computeSignature(signature);
+ DataType returnType = TypeCoercionUtils.replaceSpecifiedType(
+ resolvedSignature.returnType, NullType.class,
TinyIntType.INSTANCE);
+ FunctionSignature normalizedSignature = resolvedSignature
+ .withArgumentTypes(getArguments(), (index, argumentType,
argument) ->
+ TypeCoercionUtils.replaceSpecifiedType(
+ argumentType, NullType.class,
TinyIntType.INSTANCE))
+ .withReturnType(returnType);
+ normalizedSignature.returnType.validateDataType();
+ return normalizedSignature;
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitMapFromArrays(this, context);
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapFromEntries.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapFromEntries.java
new file mode 100644
index 00000000000..e2f6d1ec01c
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapFromEntries.java
@@ -0,0 +1,108 @@
+// 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.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.PreferPushDownProject;
+import org.apache.doris.nereids.trees.expressions.functions.ComputePrecision;
+import org.apache.doris.nereids.trees.expressions.functions.CustomSignature;
+import org.apache.doris.nereids.trees.expressions.functions.PropagateNullable;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.MapType;
+import org.apache.doris.nereids.types.NullType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
+import org.apache.doris.nereids.types.TinyIntType;
+import org.apache.doris.nereids.util.TypeCoercionUtils;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/** Construct a Map from an Array of two-field Struct entries. */
+public class MapFromEntries extends ScalarFunction
+ implements UnaryExpression, ComputePrecision, CustomSignature,
PropagateNullable, PreferPushDownProject {
+
+ public MapFromEntries(Expression entries) {
+ super("map_from_entries", entries);
+ }
+
+ private MapFromEntries(ScalarFunctionParams functionParams) {
+ super(functionParams);
+ }
+
+ @Override
+ public MapFromEntries withChildren(List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new MapFromEntries(getFunctionParams(children));
+ }
+
+ @Override
+ public FunctionSignature customSignature() {
+ DataType inputType = getArgumentType(0);
+ if (inputType.isNullType()) {
+ inputType = ArrayType.of(defaultStructType());
+ }
+ if (!(inputType instanceof ArrayType)) {
+ throw new AnalysisException(
+ "map_from_entries requires an array of structs with
exactly two fields");
+ }
+ DataType itemType = ((ArrayType) inputType).getItemType();
+ if (itemType.isNullType()) {
+ inputType = ArrayType.of(defaultStructType());
+ } else if (!(itemType instanceof StructType)) {
+ throw new AnalysisException(
+ "map_from_entries requires an array of structs with
exactly two fields");
+ } else {
+ inputType = TypeCoercionUtils.replaceSpecifiedType(
+ inputType, NullType.class, TinyIntType.INSTANCE);
+ }
+ List<StructField> fields = ((StructType) ((ArrayType)
inputType).getItemType()).getFields();
+ if (fields.size() != 2) {
+ throw new AnalysisException(
+ "map_from_entries requires an array of structs with
exactly two fields");
+ }
+ MapType resultType = MapType.of(fields.get(0).getDataType(),
fields.get(1).getDataType());
+ resultType.validateDataType();
+ return FunctionSignature.ret(resultType).args(inputType);
+ }
+
+ private static StructType defaultStructType() {
+ return new StructType(ImmutableList.of(
+ new StructField("key", TinyIntType.INSTANCE, true, ""),
+ new StructField("value", TinyIntType.INSTANCE, true, "")));
+ }
+
+ // Prevent STRUCT<DECIMAL(38,0), DECIMAL(38,38)> from being resolved as
+ // STRUCT<DECIMAL(38,6), DECIMAL(38,6)>, and nested DATETIMEV2(6) as
DATETIMEV2(0).
+ @Override
+ public FunctionSignature computePrecision(FunctionSignature signature) {
+ return signature;
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitMapFromEntries(this, context);
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
index 6b73a00b854..c0f03da1789 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
@@ -354,6 +354,8 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapContainsEn
import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapContainsKey;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapContainsValue;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapEntries;
+import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapFromArrays;
+import
org.apache.doris.nereids.trees.expressions.functions.scalar.MapFromEntries;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapKeys;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapSize;
import org.apache.doris.nereids.trees.expressions.functions.scalar.MapValues;
@@ -2842,6 +2844,14 @@ public interface ScalarFunctionVisitor<R, C> {
return visitScalarFunction(mapEntries, context);
}
+ default R visitMapFromArrays(MapFromArrays mapFromArrays, C context) {
+ return visitScalarFunction(mapFromArrays, context);
+ }
+
+ default R visitMapFromEntries(MapFromEntries mapFromEntries, C context) {
+ return visitScalarFunction(mapFromEntries, context);
+ }
+
default R visitMapKeys(MapKeys mapKeys, C context) {
return visitScalarFunction(mapKeys, context);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapConstructionFunctionsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapConstructionFunctionsTest.java
new file mode 100644
index 00000000000..89219419db1
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/MapConstructionFunctionsTest.java
@@ -0,0 +1,214 @@
+// 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.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteTestHelper;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DateTimeV2Type;
+import org.apache.doris.nereids.types.DecimalV3Type;
+import org.apache.doris.nereids.types.MapType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
+import org.apache.doris.nereids.types.TinyIntType;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class MapConstructionFunctionsTest {
+
+ private static final NereidsParser PARSER = new NereidsParser();
+
+ @Test
+ public void testMapFromArraysCanBeAnalyzed() {
+ Expression map = analyze("map_from_arrays([1, 2], [10, 20])");
+ Assertions.assertTrue(map instanceof MapFromArrays);
+ Assertions.assertEquals(
+ MapType.of(TinyIntType.INSTANCE, TinyIntType.INSTANCE),
map.getDataType());
+
+ Assertions.assertThrows(RuntimeException.class,
+ () -> analyze("map_from_arrays([1, 2], 10)"));
+
+ AnalysisException complexKeyException =
Assertions.assertThrows(AnalysisException.class,
+ () -> analyze("map_from_arrays([[1]], [10])"));
+ Assertions.assertTrue(complexKeyException.getMessage().contains(
+ "MAP key type must be a primitive type"),
complexKeyException::getMessage);
+
+ assertMapFromArraysSignature("map_from_arrays([null], [null])",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromArraysSignature("map_from_arrays(null, null)",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromArraysSignature("map_from_arrays([], [])",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromArraysSignature(
+ "map_from_arrays(map_keys(map(null, null)),
map_values(map(null, null)))",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromArraysSignature("map_from_arrays([1], [[null]])",
+ TinyIntType.INSTANCE, ArrayType.of(TinyIntType.INSTANCE));
+ }
+
+ @Test
+ public void testMapFromArraysPreservesIndependentPrecision() {
+ DataType decimalKeyType = DecimalV3Type.createDecimalV3Type(38, 0);
+ DataType decimalValueType = DecimalV3Type.createDecimalV3Type(38, 38);
+ assertMapFromArraysSignature(
+ "map_from_arrays(cast([1] as array<decimalv3(38, 0)>),"
+ + " cast([0.12345678901234567890123456789012345678]"
+ + " as array<decimalv3(38, 38)>))",
+ decimalKeyType, decimalValueType);
+
+ Expression map = analyze(
+ "map_from_arrays(cast(['2026-01-01 00:00:00'] as
array<datetimev2(0)>),"
+ + " array(struct(cast('2026-01-01 00:00:00.123456' as
datetimev2(6)))))");
+ FunctionSignature signature = ((MapFromArrays) map).getSignature();
+ DataType keyType = ((ArrayType) signature.getArgType(0)).getItemType();
+ DataType valueType = ((ArrayType)
signature.getArgType(1)).getItemType();
+ Assertions.assertEquals(DateTimeV2Type.of(0), keyType);
+ Assertions.assertTrue(valueType instanceof StructType);
+ Assertions.assertEquals(DateTimeV2Type.of(6),
+ ((StructType) valueType).getFields().get(0).getDataType());
+ Assertions.assertEquals(MapType.of(keyType, valueType),
signature.returnType);
+ }
+
+ @Test
+ public void testMapFromEntriesCanBeAnalyzed() {
+ Expression map = analyze("map_from_entries(array(struct(1, 10),
struct(2, 20)))");
+ Assertions.assertTrue(map instanceof MapFromEntries);
+ Assertions.assertEquals(
+ MapType.of(TinyIntType.INSTANCE, TinyIntType.INSTANCE),
map.getDataType());
+
+ Expression nullMap = analyze("map_from_entries(NULL)");
+ Assertions.assertTrue(nullMap instanceof MapFromEntries);
+ Assertions.assertEquals(
+ MapType.of(TinyIntType.INSTANCE, TinyIntType.INSTANCE),
nullMap.getDataType());
+
+ Assertions.assertThrows(AnalysisException.class,
+ () -> analyze("map_from_entries(1)"));
+ Assertions.assertThrows(AnalysisException.class,
+ () -> analyze("map_from_entries(array(struct(1)))"));
+ Assertions.assertThrows(AnalysisException.class,
+ () -> analyze("map_from_entries(array(struct(1, 2, 3)))"));
+
+ AnalysisException complexKeyException =
Assertions.assertThrows(AnalysisException.class,
+ () -> analyze("map_from_entries(array(struct([1], 10)))"));
+ Assertions.assertTrue(complexKeyException.getMessage().contains(
+ "MAP key type must be a primitive type"),
complexKeyException::getMessage);
+
+ assertMapFromEntriesSignature("map_from_entries([])",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromEntriesSignature("map_from_entries([null])",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromEntriesSignature("map_from_entries(array(struct(1,
null)))",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromEntriesSignature("map_from_entries(map_entries(map(null,
null)))",
+ TinyIntType.INSTANCE, TinyIntType.INSTANCE);
+ assertMapFromEntriesSignature("map_from_entries(array(struct(1,
[null])))",
+ TinyIntType.INSTANCE, ArrayType.of(TinyIntType.INSTANCE));
+ }
+
+ @Test
+ public void testMapFromEntriesPreservesIndependentPrecision() {
+ DataType decimalKeyType = DecimalV3Type.createDecimalV3Type(38, 0);
+ DataType decimalValueType = DecimalV3Type.createDecimalV3Type(38, 38);
+ assertMapFromEntriesSignature(
+ "map_from_entries(map_entries(map(cast(1 as decimalv3(38, 0)),"
+ + " cast(0.12345678901234567890123456789012345678"
+ + " as decimalv3(38, 38)))))",
+ decimalKeyType, decimalValueType);
+
+ DataType timeKeyType = DateTimeV2Type.of(0);
+ DataType timeValueType = new StructType(ImmutableList.of(
+ new StructField("col1", DateTimeV2Type.of(6), true, "")));
+ assertMapFromEntriesSignature(
+ "map_from_entries(map_entries(map("
+ + "cast('2026-01-01 00:00:00' as datetimev2(0)),"
+ + " struct(cast('2026-01-01 00:00:00.123456' as
datetimev2(6))))))",
+ timeKeyType, timeValueType);
+ }
+
+ @Test
+ public void testMapEntriesPreservesIndependentPrecision() {
+ DataType decimalKeyType = DecimalV3Type.createDecimalV3Type(38, 0);
+ DataType decimalValueType = DecimalV3Type.createDecimalV3Type(38, 38);
+ assertMapEntriesSignature(
+ "map_entries(map(cast(1 as decimalv3(38, 0)),"
+ + " cast(0.12345678901234567890123456789012345678"
+ + " as decimalv3(38, 38))))",
+ decimalKeyType, decimalValueType);
+
+ DataType timeKeyType = DateTimeV2Type.of(0);
+ DataType timeValueType = new StructType(ImmutableList.of(
+ new StructField("col1", DateTimeV2Type.of(6), true, "")));
+ assertMapEntriesSignature(
+ "map_entries(map(cast('2026-01-01 00:00:00' as datetimev2(0)),"
+ + " struct(cast('2026-01-01 00:00:00.123456' as
datetimev2(6)))))",
+ timeKeyType, timeValueType);
+ }
+
+ private void assertMapFromArraysSignature(String sql, DataType keyType,
DataType valueType) {
+ Expression map = analyze(sql);
+ Assertions.assertTrue(map instanceof MapFromArrays);
+ FunctionSignature signature = ((MapFromArrays) map).getSignature();
+ Assertions.assertEquals(MapType.of(keyType, valueType),
signature.returnType);
+ Assertions.assertEquals(ArrayType.of(keyType),
signature.getArgType(0));
+ Assertions.assertEquals(ArrayType.of(valueType),
signature.getArgType(1));
+ assertNoNullType(signature);
+ }
+
+ private void assertMapFromEntriesSignature(String sql, DataType keyType,
DataType valueType) {
+ Expression map = analyze(sql);
+ Assertions.assertTrue(map instanceof MapFromEntries);
+ FunctionSignature signature = ((MapFromEntries) map).getSignature();
+ Assertions.assertEquals(MapType.of(keyType, valueType),
signature.returnType);
+ Assertions.assertTrue(signature.getArgType(0) instanceof ArrayType);
+ DataType itemType = ((ArrayType)
signature.getArgType(0)).getItemType();
+ Assertions.assertTrue(itemType instanceof StructType);
+ Assertions.assertEquals(keyType, ((StructType)
itemType).getFields().get(0).getDataType());
+ Assertions.assertEquals(valueType, ((StructType)
itemType).getFields().get(1).getDataType());
+ assertNoNullType(signature);
+ }
+
+ private void assertMapEntriesSignature(String sql, DataType keyType,
DataType valueType) {
+ Expression entries = analyze(sql);
+ Assertions.assertTrue(entries instanceof MapEntries);
+ FunctionSignature signature = ((MapEntries) entries).getSignature();
+ MapType mapType = MapType.of(keyType, valueType);
+ Assertions.assertEquals(mapType, signature.getArgType(0));
+ Assertions.assertEquals(mapType, entries.child(0).getDataType());
+ DataType itemType = ((ArrayType) signature.returnType).getItemType();
+ Assertions.assertTrue(itemType instanceof StructType);
+ Assertions.assertEquals(keyType, ((StructType)
itemType).getFields().get(0).getDataType());
+ Assertions.assertEquals(valueType, ((StructType)
itemType).getFields().get(1).getDataType());
+ }
+
+ private void assertNoNullType(FunctionSignature signature) {
+ signature.returnType.validateDataType();
+ for (DataType argumentType : signature.argumentsTypes) {
+ argumentType.validateDataType();
+ }
+ }
+
+ private Expression analyze(String sql) {
+ return
ExpressionRewriteTestHelper.typeCoercion(PARSER.parseExpression(sql));
+ }
+}
diff --git
a/regression-test/data/query_p0/sql_functions/map_functions/test_map_from_arrays_entries.out
b/regression-test/data/query_p0/sql_functions/map_functions/test_map_from_arrays_entries.out
new file mode 100644
index 00000000000..474716bc040
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/map_functions/test_map_from_arrays_entries.out
@@ -0,0 +1,41 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !map_from_arrays_1 --
+2 10 20
+
+-- !map_from_arrays_2 --
+1 20
+
+-- !map_from_arrays_3 --
+1 2 10 20
+2 0 \N \N
+3 \N \N \N
+
+-- !map_from_arrays_4 --
+\N
+
+-- !map_from_arrays_5 --
+{null:10, 2:null}
+
+-- !map_from_arrays_6 --
+\N
+
+-- !map_from_entries_1 --
+2 10 20
+
+-- !map_from_entries_2 --
+1 20
+
+-- !map_from_entries_3 --
+1 2 10 20
+2 0 \N \N
+3 \N \N \N
+
+-- !map_from_entries_4 --
+\N
+
+-- !map_from_entries_5 --
+\N
+
+-- !map_from_entries_6 --
+{null:10, 2:null}
+
diff --git
a/regression-test/suites/query_p0/sql_functions/map_functions/test_map_from_arrays_entries.groovy
b/regression-test/suites/query_p0/sql_functions/map_functions/test_map_from_arrays_entries.groovy
new file mode 100644
index 00000000000..28800340455
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/map_functions/test_map_from_arrays_entries.groovy
@@ -0,0 +1,213 @@
+// 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_map_from_arrays_entries", "p0") {
+ sql "set enable_nereids_planner = true"
+ sql "set enable_fallback_to_original_planner = false"
+ sql "set enable_decimal256 = false"
+ sql "drop table if exists test_map_from_arrays_entries"
+ sql """
+ create table test_map_from_arrays_entries (
+ id int,
+ m map<int, int>,
+ decimal_m map<decimalv3(38, 0), decimalv3(38, 38)>,
+ time_m map<datetimev2(0), struct<f:datetimev2(6)>>
+ )
+ duplicate key(id)
+ distributed by hash(id) buckets 1
+ properties("replication_num" = "1")
+ """
+ sql """
+ insert into test_map_from_arrays_entries values
+ (1, map(1, 10, 2, 20),
+ map(cast(1 as decimalv3(38, 0)),
+ cast(0.12345678901234567890123456789012345678 as
decimalv3(38, 38))),
+ map(cast('2026-01-01 00:00:00' as datetimev2(0)),
+ named_struct('f',
+ cast('2026-01-01 00:00:00.123456' as datetimev2(6))))),
+ (2, cast(map() as map<int, int>), null, null),
+ (3, null, null, null)
+ """
+
+ qt_map_from_arrays_1 """
+ select map_size(r), r[1], r[2]
+ from (select map_from_arrays([1, 2], [10, 20]) r) t
+ """
+
+ qt_map_from_arrays_2 """
+ select map_size(r), r[1]
+ from (select map_from_arrays([1, 1], [10, 20]) r) t
+ """
+
+ order_qt_map_from_arrays_3 """
+ select id, map_size(r), r[1], r[2]
+ from (
+ select id, map_from_arrays(map_keys(m), map_values(m)) r
+ from test_map_from_arrays_entries
+ ) t
+ order by id
+ """
+
+ qt_map_from_arrays_4 """
+ select map_from_arrays(
+ cast(null as array<int>), cast(null as array<int>))
+ """
+
+ qt_map_from_arrays_5 """
+ select map_from_arrays(
+ array(cast(null as int), 2), array(10, cast(null as int)))
+ """
+
+ qt_map_from_arrays_6 """
+ select map_from_arrays(cast(null as array<int>), [10, 20])
+ """
+
+ qt_map_from_entries_1 """
+ select map_size(r), r[1], r[2]
+ from (
+ select map_from_entries(array(struct(1, 10), struct(2, 20))) r
+ ) t
+ """
+
+ qt_map_from_entries_2 """
+ select map_size(r), r[1]
+ from (
+ select map_from_entries(array(struct(1, 10), struct(1, 20))) r
+ ) t
+ """
+
+ order_qt_map_from_entries_3 """
+ select id, map_size(r), r[1], r[2]
+ from (
+ select id, map_from_entries(map_entries(m)) r
+ from test_map_from_arrays_entries
+ ) t
+ order by id
+ """
+
+ qt_map_from_entries_4 """
+ select map_from_entries(cast(null as array<struct<k:int,v:int>>))
+ """
+
+ qt_map_from_entries_5 """
+ select map_from_entries(null)
+ """
+
+ qt_map_from_entries_6 """
+ select map_from_entries(array(
+ struct(cast(null as int), 10),
+ struct(2, cast(null as int))))
+ """
+
+ test {
+ sql """
+ select if(
+ cast(map_from_entries(map_entries(decimal_m)) as
string)
+ like '%0.12345678901234567890123456789012345678%',
+ 1, 0),
+ if(cast(map_from_entries(map_entries(time_m)) as string)
+ like '%.123456%', 1, 0)
+ from test_map_from_arrays_entries
+ where id = 1
+ """
+ result([[1, 1]])
+ }
+
+ test {
+ sql """
+ select if(cast(map_from_entries(map_entries(map(
+ cast(1 as decimalv3(38, 0)),
+ cast(0.12345678901234567890123456789012345678
+ as decimalv3(38, 38))))) as string)
+ like '%0.12345678901234567890123456789012345678%', 1,
0),
+ if(cast(map_from_entries(map_entries(map(
+ cast('2026-01-01 00:00:00' as datetimev2(0)),
+ struct(cast('2026-01-01 00:00:00.123456'
+ as datetimev2(6)))))) as string)
+ like '%.123456%', 1, 0)
+ """
+ result([[1, 1]])
+ }
+
+ testFoldConst("select map_from_arrays([1, 2], ['a', 'b'])")
+ testFoldConst("select map_from_arrays([1, 1], [10, 20])")
+ testFoldConst("select map_from_arrays(array(cast(null as int), 2),"
+ + " array(10, cast(null as int)))")
+ testFoldConst("select map_from_arrays(cast([] as array<int>), cast([] as
array<int>))")
+ testFoldConst("select map_from_arrays([null], [null])")
+ testFoldConst("select map_from_arrays(null, null)")
+ testFoldConst("select map_from_arrays([], [])")
+ testFoldConst("select map_from_arrays(map_keys(map(null, null)),"
+ + " map_values(map(null, null)))")
+ testFoldConst("select map_from_arrays([1], [[null]])")
+ testFoldConst("select map_values(map_from_arrays("
+ + "cast([1] as array<decimalv3(38, 0)>),"
+ + "cast([0.12345678901234567890123456789012345678]"
+ + " as array<decimalv3(38, 38)>)))[1]")
+ testFoldConst("select
microsecond(struct_element(map_values(map_from_arrays("
+ + "cast(['2026-01-01 00:00:00'] as array<datetimev2(0)>),"
+ + "array(struct(cast('2026-01-01 00:00:00.123456' as
datetimev2(6)))))"
+ + ")[1], 1))")
+ testFoldConst("select map_from_entries(array(struct(1, 'a'), struct(2,
'b')))")
+ testFoldConst("select map_from_entries(array(struct(1, 10), struct(1,
20)))")
+ testFoldConst("select map_from_entries(array(struct(cast(null as int),
10),"
+ + " struct(2, cast(null as int))))")
+ testFoldConst("select map_from_entries(cast([] as
array<struct<k:int,v:int>>))")
+ testFoldConst("select map_from_entries(cast(null as
array<struct<k:int,v:int>>))")
+ testFoldConst("select map_from_entries(null)")
+ testFoldConst("select map_from_entries([])")
+ testFoldConst("select map_from_entries(array(struct(1, null)))")
+ testFoldConst("select map_from_entries(map_entries(map(null, null)))")
+ testFoldConst("select map_from_entries(array(struct(1, [null])))")
+ testFoldConst("select map_from_entries(map_entries(map("
+ + "cast(1 as decimalv3(38, 0)),"
+ + "cast(0.12345678901234567890123456789012345678"
+ + " as decimalv3(38, 38)))))")
+ testFoldConst("select cast(map_from_entries(map_entries(map("
+ + "cast('2026-01-01 00:00:00' as datetimev2(0)),"
+ + "struct(cast('2026-01-01 00:00:00.123456' as datetimev2(6))))"
+ + ")) as string)")
+
+ test {
+ sql "select map_from_arrays([1, 2], [10])"
+ exception "Key and value arrays of function map_from_arrays must have
the same length"
+ }
+ test {
+ sql "select map_from_arrays([[1]], [10])"
+ exception "MAP key type must be a primitive type"
+ }
+ test {
+ sql "select map_from_entries(1)"
+ exception "requires an array of structs with exactly two fields"
+ }
+ test {
+ sql "select map_from_entries(array(struct(1)))"
+ exception "requires an array of structs with exactly two fields"
+ }
+ test {
+ sql "select map_from_entries(array(struct(1, 2, 3)))"
+ exception "requires an array of structs with exactly two fields"
+ }
+ test {
+ sql "select map_from_entries(array(cast(null as struct<k:int,v:int>)))"
+ exception "Map entry of function map_from_entries cannot be null"
+ }
+ test {
+ sql "select map_from_entries([null])"
+ exception "Map entry of function map_from_entries cannot be null"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]