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

morningman 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 339e087b11f [fix](be) Reject incompatible Arrow arrays without 
crashing Stream Load (#69872)
339e087b11f is described below

commit 339e087b11f760e9e45ce33d94862b4b63f1d9ee
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Sat Oct 31 02:14:43 2026 +0000

    [fix](be) Reject incompatible Arrow arrays without crashing Stream Load 
(#69872)
    
    ### What problem does this PR solve?
    
    Issue Number: close #69858
    
    Related PR: None
    
    Problem Summary:
    
    The shared Arrow BOOLEAN and DECIMAL SerDes dereference unchecked
    `dynamic_cast` results. Passing an `Int32Array` to the BOOLEAN reader or
    a `StringArray` to the DECIMAL64 reader can therefore terminate the BE
    instead of returning a load error. MAP and STRUCT have the same
    unchecked input-cast pattern.
    
    Reject incompatible arrays before accessing buffers, decimal metadata,
    or values, independently of `enable_arrow_input_validation`. Reject
    mismatched STRUCT field counts before indexing children. Add the column
    name and source/destination types to V1/V2 Arrow load errors.
    
    The current V2 Stream Load reader already binds supported input types
    with safe Arrow casts. Preserve those conversions, including valid
    integer-to-BOOLEAN and string-to-DECIMAL inputs, and test the shared
    SerDes directly for incompatible arrays.
---
 .../data_type_serde/data_type_decimal_serde.cpp    |  15 +
 .../core/data_type_serde/data_type_map_serde.cpp   |   5 +
 .../data_type_serde/data_type_number_serde.cpp     |   5 +
 .../data_type_serde/data_type_struct_serde.cpp     |  11 +-
 be/src/format/arrow/arrow_stream_reader.cpp        |  13 +-
 be/src/format_v2/arrow/arrow_stream_reader.cpp     |  17 +-
 .../data_type_serde_arrow_validation_test.cpp      | 145 ++++++++
 be/test/format_v2/table/load_reader_test.cpp       |  57 +++
 .../arrow_type_validation/array_good.arrow         | Bin 0 -> 1000 bytes
 .../arrow_type_validation/array_nested_null.arrow  | Bin 0 -> 752 bytes
 .../arrow_type_validation/array_null.arrow         | Bin 0 -> 744 bytes
 .../arrow_type_validation/array_nullable.arrow     | Bin 0 -> 1264 bytes
 .../arrow_type_validation/bad_array.arrow          | Bin 0 -> 800 bytes
 .../arrow_type_validation/bad_bool.arrow           | Bin 0 -> 648 bytes
 .../arrow_type_validation/bad_decimal.arrow        | Bin 0 -> 648 bytes
 .../arrow_type_validation/bad_map.arrow            | Bin 0 -> 1104 bytes
 .../arrow_type_validation/bad_struct.arrow         | Bin 0 -> 752 bytes
 .../arrow_type_validation/bool_good.arrow          | Bin 0 -> 784 bytes
 .../arrow_type_validation/bool_late_failure.arrow  | Bin 0 -> 1224 bytes
 .../arrow_type_validation/bool_null.arrow          | Bin 0 -> 584 bytes
 .../arrow_type_validation/bool_nullable.arrow      | Bin 0 -> 1000 bytes
 .../arrow_type_validation/decimal128_good.arrow    | Bin 0 -> 808 bytes
 .../arrow_type_validation/decimal128_null.arrow    | Bin 0 -> 600 bytes
 .../decimal128_nullable.arrow                      | Bin 0 -> 1032 bytes
 .../arrow_type_validation/decimal256_good.arrow    | Bin 0 -> 848 bytes
 .../arrow_type_validation/decimal256_null.arrow    | Bin 0 -> 624 bytes
 .../decimal256_nullable.arrow                      | Bin 0 -> 1088 bytes
 .../decimal_late_failure.arrow                     | Bin 0 -> 1240 bytes
 .../arrow_type_validation/decimal_string.arrow     | Bin 0 -> 872 bytes
 .../stream_load/arrow_type_validation/generate.py  |  98 ++++++
 .../stream_load/arrow_type_validation/int32.arrow  | Bin 0 -> 784 bytes
 .../arrow_type_validation/int32_null.arrow         | Bin 0 -> 584 bytes
 .../arrow_type_validation/long_struct.arrow        | Bin 0 -> 824 bytes
 .../arrow_type_validation/map_good.arrow           | Bin 0 -> 1408 bytes
 .../arrow_type_validation/map_nested_null.arrow    | Bin 0 -> 1056 bytes
 .../arrow_type_validation/map_null.arrow           | Bin 0 -> 1040 bytes
 .../arrow_type_validation/map_nullable.arrow       | Bin 0 -> 1776 bytes
 .../overflow_decimal128.arrow                      | Bin 0 -> 616 bytes
 .../overflow_decimal256.arrow                      | Bin 0 -> 712 bytes
 .../arrow_type_validation/scale_loss_decimal.arrow | Bin 0 -> 592 bytes
 .../arrow_type_validation/short_struct.arrow       | Bin 0 -> 536 bytes
 .../arrow_type_validation/struct_good.arrow        | Bin 0 -> 920 bytes
 .../arrow_type_validation/struct_nested_null.arrow | Bin 0 -> 688 bytes
 .../arrow_type_validation/struct_null.arrow        | Bin 0 -> 688 bytes
 .../arrow_type_validation/struct_nullable.arrow    | Bin 0 -> 1168 bytes
 .../test_arrow_stream_load_type_validation.out     | 391 +++++++++++++++++++++
 .../test_arrow_stream_load_type_validation.groovy  | 148 ++++++++
 47 files changed, 895 insertions(+), 10 deletions(-)

diff --git a/be/src/core/data_type_serde/data_type_decimal_serde.cpp 
b/be/src/core/data_type_serde/data_type_decimal_serde.cpp
index 7c8e92a5788..c43efb709ec 100644
--- a/be/src/core/data_type_serde/data_type_decimal_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_decimal_serde.cpp
@@ -921,6 +921,11 @@ Status 
DataTypeDecimalSerDe<T>::read_column_from_arrow(IColumn& column,
     // Decimal<Int128I> for deicmalv3
     if constexpr (T == TYPE_DECIMALV2) {
         const auto* concrete_array = dynamic_cast<const 
arrow::DecimalArray*>(arrow_array);
+        // A string or a different decimal width cannot be read as Decimal128, 
including NULLs.
+        if (concrete_array == nullptr) {
+            return Status::InvalidArgument("Expected Arrow Decimal128Array for 
{}, got {}",
+                                           get_name(), 
arrow_array->type()->ToString());
+        }
         const auto* arrow_decimal_type =
                 static_cast<const 
arrow::DecimalType*>(arrow_array->type().get());
         const auto arrow_scale = arrow_decimal_type->scale();
@@ -948,6 +953,11 @@ Status 
DataTypeDecimalSerDe<T>::read_column_from_arrow(IColumn& column,
         }
     } else if constexpr (T == TYPE_DECIMAL32 || T == TYPE_DECIMAL64 || T == 
TYPE_DECIMAL128I) {
         const auto* concrete_array = dynamic_cast<const 
arrow::DecimalArray*>(arrow_array);
+        // A string or a different decimal width cannot be read as Decimal128, 
including NULLs.
+        if (concrete_array == nullptr) {
+            return Status::InvalidArgument("Expected Arrow Decimal128Array for 
{}, got {}",
+                                           get_name(), 
arrow_array->type()->ToString());
+        }
         for (auto value_i = start; value_i < end; ++value_i) {
             const auto* value = concrete_array->Value(value_i);
             auto decimal_value = unaligned_load<FieldType>(value);
@@ -955,6 +965,11 @@ Status 
DataTypeDecimalSerDe<T>::read_column_from_arrow(IColumn& column,
         }
     } else if constexpr (T == TYPE_DECIMAL256) {
         const auto* concrete_array = dynamic_cast<const 
arrow::Decimal256Array*>(arrow_array);
+        // Reject incompatible arrays before Value(), independently of buffer 
validation.
+        if (concrete_array == nullptr) {
+            return Status::InvalidArgument("Expected Arrow Decimal256Array, 
got {}",
+                                           arrow_array->type()->ToString());
+        }
         for (auto value_i = start; value_i < end; ++value_i) {
             auto decimal_value = 
unaligned_load<FieldType>(concrete_array->Value(value_i));
             column_data.emplace_back(decimal_value);
diff --git a/be/src/core/data_type_serde/data_type_map_serde.cpp 
b/be/src/core/data_type_serde/data_type_map_serde.cpp
index eb5e16f0770..b36815f1046 100644
--- a/be/src/core/data_type_serde/data_type_map_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_map_serde.cpp
@@ -549,6 +549,11 @@ Status DataTypeMapSerDe::read_column_from_arrow(IColumn& 
column, const arrow::Ar
     auto& column_map = static_cast<ColumnMap&>(column);
     auto& offsets_data = column_map.get_offsets();
     const auto* concrete_map = dynamic_cast<const 
arrow::MapArray*>(arrow_array);
+    // A valid non-MAP Arrow column has no map offsets; reject it before 
accessing them.
+    if (concrete_map == nullptr) {
+        return Status::InvalidArgument("Expected Arrow MapArray, got {}",
+                                       arrow_array->type()->ToString());
+    }
     auto arrow_offsets_array = concrete_map->offsets();
     auto* arrow_offsets = 
dynamic_cast<arrow::Int32Array*>(arrow_offsets_array.get());
     if (config::enable_arrow_input_validation) {
diff --git a/be/src/core/data_type_serde/data_type_number_serde.cpp 
b/be/src/core/data_type_serde/data_type_number_serde.cpp
index a4b1d2b7cc4..1ce576b2813 100644
--- a/be/src/core/data_type_serde/data_type_number_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_number_serde.cpp
@@ -956,6 +956,11 @@ Status 
DataTypeNumberSerDe<T>::read_column_from_arrow(IColumn& column,
     // now uint8 for bool
     if constexpr (T == TYPE_BOOLEAN) {
         const auto* concrete_array = dynamic_cast<const 
arrow::BooleanArray*>(arrow_array);
+        // An Int32Array sent to a BOOLEAN column must fail even when buffer 
validation is off.
+        if (concrete_array == nullptr) {
+            return Status::InvalidArgument("Expected Arrow BooleanArray, got 
{}",
+                                           arrow_array->type()->ToString());
+        }
         if (config::enable_arrow_input_validation) {
             check_arrow_boolean_buffer(*concrete_array);
         }
diff --git a/be/src/core/data_type_serde/data_type_struct_serde.cpp 
b/be/src/core/data_type_serde/data_type_struct_serde.cpp
index a295dc2799f..bb7e2573146 100644
--- a/be/src/core/data_type_serde/data_type_struct_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_struct_serde.cpp
@@ -551,7 +551,16 @@ Status 
DataTypeStructSerDe::read_column_from_arrow(IColumn& column, const arrow:
     }
     auto& struct_column = static_cast<ColumnStruct&>(column);
     const auto* concrete_struct = dynamic_cast<const 
arrow::StructArray*>(arrow_array);
-    DCHECK_EQ(struct_column.tuple_size(), concrete_struct->num_fields());
+    // Reject non-STRUCT input before reading its fields, even with buffer 
validation disabled.
+    if (concrete_struct == nullptr) {
+        return Status::InvalidArgument("Expected Arrow StructArray, got {}",
+                                       arrow_array->type()->ToString());
+    }
+    // A shorter input struct would otherwise index past its children; extra 
fields lose data.
+    if (struct_column.tuple_size() != concrete_struct->num_fields()) {
+        return Status::InvalidArgument("Expected Arrow StructArray with {} 
fields, got {}",
+                                       struct_column.tuple_size(), 
concrete_struct->num_fields());
+    }
     for (auto i = 0; i < struct_column.tuple_size(); ++i) {
         RETURN_IF_ERROR(elem_serdes_ptrs[i]->read_column_from_arrow(
                 struct_column.get_column(i), concrete_struct->field(i).get(), 
start, end, ctz));
diff --git a/be/src/format/arrow/arrow_stream_reader.cpp 
b/be/src/format/arrow/arrow_stream_reader.cpp
index 7000c55507b..f6ef6157e58 100644
--- a/be/src/format/arrow/arrow_stream_reader.cpp
+++ b/be/src/format/arrow/arrow_stream_reader.cpp
@@ -114,10 +114,15 @@ Status ArrowStreamReader::_do_get_next_block(Block* 
block, size_t* read_rows, bo
                                                  column_name_in_block, 
column_name);
                 }
 
-                RETURN_IF_ERROR(
-                        columns_guard.get_datatype_by_position(c)
-                                ->get_serde()
-                                ->read_column_from_arrow(*columns[c], column, 
0, num_rows, _ctzz));
+                const auto& data_type = 
columns_guard.get_datatype_by_position(c);
+                auto status = 
data_type->get_serde()->read_column_from_arrow(*columns[c], column, 0,
+                                                                             
num_rows, _ctzz);
+                // Type mismatches are load errors; retain their code and 
identify the input column.
+                if (!status.ok()) {
+                    return status.prepend("Arrow column '" + column_name + "' 
(" +
+                                          column->type()->ToString() + " -> " +
+                                          data_type->get_name() + "): ");
+                }
             } catch (Exception& e) {
                 return Status::InternalError("Failed to convert from arrow to 
block: {}", e.what());
             }
diff --git a/be/src/format_v2/arrow/arrow_stream_reader.cpp 
b/be/src/format_v2/arrow/arrow_stream_reader.cpp
index c2a30f35f8a..2576dc5361d 100644
--- a/be/src/format_v2/arrow/arrow_stream_reader.cpp
+++ b/be/src/format_v2/arrow/arrow_stream_reader.cpp
@@ -429,10 +429,16 @@ Status ArrowStreamReader::_prepare_arrays() {
         
RETURN_IF_ERROR(normalize_arrow_array(_batch->column(_arrow_positions[id.value()]),
                                               _arrow_pool.get(), &array));
         arrow::compute::ExecContext context(_arrow_pool.get());
-        std::shared_ptr<arrow::DataType> binding;
-        RETURN_DORIS_STATUS_IF_RESULT_ERROR(
-                binding, input_binding_type(array->type(), 
_arrow_types[id.value()],
-                                            _file_slots[id.value()]->type()));
+        auto binding_result = input_binding_type(array->type(), 
_arrow_types[id.value()],
+                                                 
_file_slots[id.value()]->type());
+        // A STRUCT field-count mismatch needs the same column context as a 
failed value cast.
+        if (!binding_result.ok()) {
+            return Status::InvalidArgument(
+                    "Cannot decode Arrow column '{}' ({} -> {}): {}",
+                    _file_slots[id.value()]->col_name(), 
array->type()->ToString(),
+                    _arrow_types[id.value()]->ToString(), 
binding_result.status().ToString());
+        }
+        auto binding = std::move(binding_result).ValueUnsafe();
         // Bind differing numeric widths before concrete SerDe array casts. 
Timestamp units
         // stay on the wire even inside containers, so Doris retains its own 
rounding semantics.
         if (!array->type()->Equals(binding)) {
@@ -443,8 +449,9 @@ Status ArrowStreamReader::_prepare_arrays() {
             options.allow_invalid_utf8 = true;
             auto converted = arrow::compute::Cast(array, binding, options, 
&context);
             if (!converted.ok()) {
-                return Status::InvalidArgument("Cannot decode Arrow column 
'{}': {}",
+                return Status::InvalidArgument("Cannot decode Arrow column 
'{}' ({} -> {}): {}",
                                                
_file_slots[id.value()]->col_name(),
+                                               array->type()->ToString(), 
binding->ToString(),
                                                converted.status().ToString());
             }
             array = converted->make_array();
diff --git 
a/be/test/core/data_type_serde/data_type_serde_arrow_validation_test.cpp 
b/be/test/core/data_type_serde/data_type_serde_arrow_validation_test.cpp
index d6b2e4f57bd..f4f90d73c31 100644
--- a/be/test/core/data_type_serde/data_type_serde_arrow_validation_test.cpp
+++ b/be/test/core/data_type_serde/data_type_serde_arrow_validation_test.cpp
@@ -35,6 +35,13 @@
 #include "core/column/column_nullable.h"
 #include "core/column/column_string.h"
 #include "core/column/column_varbinary.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_decimal.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/data_type_serde/data_type_array_serde.h"
 #include "core/data_type_serde/data_type_datev2_serde.h"
@@ -514,4 +521,142 @@ TEST(DataTypeSerDeArrowValidationTest, 
RejectsMapOffsetsBeyondKeysLength) {
             "map offsets beyond keys length should be rejected");
 }
 
+// These arrays are well formed: the failure is their pairing with the 
destination type,
+// not a malformed buffer. Empty/all-NULL input must also reject the 
incompatible schema.
+TEST(DataTypeSerDeArrowValidationTest, RejectsIncompatibleArrayTypes) {
+    const DataTypes types = {
+            std::make_shared<DataTypeUInt8>(),
+            std::make_shared<DataTypeDecimalV2>(27, 9),
+            std::make_shared<DataTypeDecimal32>(9, 2),
+            std::make_shared<DataTypeDecimal64>(18, 2),
+            std::make_shared<DataTypeDecimal128>(38, 2),
+            std::make_shared<DataTypeDecimal256>(76, 2),
+            
std::make_shared<DataTypeArray>(make_nullable(std::make_shared<DataTypeUInt8>())),
+            
std::make_shared<DataTypeMap>(make_nullable(std::make_shared<DataTypeString>()),
+                                          
make_nullable(std::make_shared<DataTypeUInt8>())),
+            std::make_shared<DataTypeStruct>(
+                    DataTypes 
{make_nullable(std::make_shared<DataTypeUInt8>())}, Strings {"b"})};
+    arrow::Int32Builder builder;
+    ASSERT_TRUE(builder.Append(1).ok());
+    std::shared_ptr<arrow::Array> non_null;
+    ASSERT_TRUE(builder.Finish(&non_null).ok());
+    for (const bool validation : {false, true}) {
+        ScopedArrowInputValidation scoped_validation(validation);
+        for (const auto& type : types) {
+            for (const bool nullable : {false, true}) {
+                const auto target = nullable ? make_nullable(type) : type;
+                for (const auto length : {0, 1}) {
+                    const auto nulls = arrow::MakeArrayOfNull(arrow::int32(), 
length);
+                    ASSERT_TRUE(nulls.ok());
+                    for (const auto& input : {non_null, *nulls}) {
+                        SCOPED_TRACE(target->get_name() + ": " + 
input->ToString());
+                        auto column = target->create_column();
+                        const auto status = 
target->get_serde()->read_column_from_arrow(
+                                *column, input.get(), 0, input->length(), 
cctz::utc_time_zone());
+                        EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT) 
<< status;
+                    }
+                }
+            }
+        }
+    }
+}
+
+TEST(DataTypeSerDeArrowValidationTest, 
RejectsStringAndWrongWidthDecimalArrays) {
+    const DataTypes types = {
+            std::make_shared<DataTypeDecimalV2>(27, 9), 
std::make_shared<DataTypeDecimal32>(9, 2),
+            std::make_shared<DataTypeDecimal64>(18, 2), 
std::make_shared<DataTypeDecimal128>(38, 2),
+            std::make_shared<DataTypeDecimal256>(76, 2)};
+    arrow::StringBuilder builder;
+    ASSERT_TRUE(builder.Append("1.50").ok());
+    std::shared_ptr<arrow::Array> strings;
+    ASSERT_TRUE(builder.Finish(&strings).ok());
+    for (const bool validation : {false, true}) {
+        ScopedArrowInputValidation scoped_validation(validation);
+        for (const auto& type : types) {
+            const auto wrong_width = type->get_primitive_type() == 
TYPE_DECIMAL256
+                                             ? arrow::decimal128(10, 2)
+                                             : arrow::decimal256(76, 2);
+            auto decimal = arrow::MakeArrayOfNull(wrong_width, 1);
+            ASSERT_TRUE(decimal.ok());
+            for (const bool nullable : {false, true}) {
+                const auto target = nullable ? make_nullable(type) : type;
+                for (const auto& input : {strings, *decimal}) {
+                    auto column = target->create_column();
+                    const auto status = 
target->get_serde()->read_column_from_arrow(
+                            *column, input.get(), 0, input->length(), 
cctz::utc_time_zone());
+                    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT) << 
status;
+                }
+            }
+        }
+    }
+}
+
+TEST(DataTypeSerDeArrowValidationTest, RejectsStructFieldCountMismatch) {
+    const auto boolean = make_nullable(std::make_shared<DataTypeUInt8>());
+    const auto type = std::make_shared<DataTypeStruct>(DataTypes {boolean}, 
Strings {"b"});
+    for (const bool validation : {false, true}) {
+        ScopedArrowInputValidation scoped_validation(validation);
+        for (const auto& arrow_type :
+             {arrow::struct_({}), arrow::struct_({arrow::field("b", 
arrow::boolean()),
+                                                  arrow::field("extra", 
arrow::boolean())})}) {
+            auto input = arrow::MakeArrayOfNull(arrow_type, 1);
+            ASSERT_TRUE(input.ok());
+            auto column = type->create_column();
+            const auto status = 
type->get_serde()->read_column_from_arrow(*column, input->get(), 0,
+                                                                          1, 
cctz::utc_time_zone());
+            EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT) << status;
+        }
+    }
+}
+
+TEST(DataTypeSerDeArrowValidationTest, RejectsIncompatibleNestedArrayTypes) {
+    const auto boolean = make_nullable(std::make_shared<DataTypeUInt8>());
+    const DataTypes targets = {
+            std::make_shared<DataTypeArray>(boolean),
+            
std::make_shared<DataTypeMap>(make_nullable(std::make_shared<DataTypeString>()),
+                                          boolean),
+            std::make_shared<DataTypeStruct>(DataTypes {boolean}, Strings 
{"b"})};
+    const std::vector<std::shared_ptr<arrow::DataType>> inputs = {
+            arrow::list(arrow::int32()), arrow::map(arrow::utf8(), 
arrow::int32()),
+            arrow::struct_({arrow::field("b", arrow::int32())})};
+    for (const bool validation : {false, true}) {
+        ScopedArrowInputValidation scoped_validation(validation);
+        for (size_t i = 0; i < targets.size(); ++i) {
+            auto input = arrow::MakeArrayOfNull(inputs[i], 1);
+            ASSERT_TRUE(input.ok());
+            auto column = targets[i]->create_column();
+            const auto status = 
targets[i]->get_serde()->read_column_from_arrow(
+                    *column, input->get(), 0, 1, cctz::utc_time_zone());
+            EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT) << status;
+        }
+    }
+}
+
+TEST(DataTypeSerDeArrowValidationTest, 
AcceptsMatchingArrayTypesWithValidationOnAndOff) {
+    const DataTypes targets = {std::make_shared<DataTypeUInt8>(),
+                               std::make_shared<DataTypeDecimalV2>(27, 9),
+                               std::make_shared<DataTypeDecimal32>(9, 2),
+                               std::make_shared<DataTypeDecimal64>(18, 2),
+                               std::make_shared<DataTypeDecimal128>(38, 2),
+                               std::make_shared<DataTypeDecimal256>(76, 2)};
+    const std::vector<std::shared_ptr<arrow::DataType>> inputs = {
+            arrow::boolean(),         arrow::decimal128(27, 9), 
arrow::decimal128(9, 2),
+            arrow::decimal128(18, 2), arrow::decimal128(38, 2), 
arrow::decimal256(76, 2)};
+    for (const bool validation : {false, true}) {
+        ScopedArrowInputValidation scoped_validation(validation);
+        for (size_t i = 0; i < targets.size(); ++i) {
+            const auto target = make_nullable(targets[i]);
+            for (const auto length : {0, 2}) {
+                auto input = arrow::MakeArrayOfNull(inputs[i], length);
+                ASSERT_TRUE(input.ok());
+                auto column = target->create_column();
+                const auto status = 
target->get_serde()->read_column_from_arrow(
+                        *column, input->get(), 0, length, 
cctz::utc_time_zone());
+                EXPECT_TRUE(status.ok()) << status;
+                EXPECT_EQ(column->size(), length);
+            }
+        }
+    }
+}
+
 } // namespace doris
diff --git a/be/test/format_v2/table/load_reader_test.cpp 
b/be/test/format_v2/table/load_reader_test.cpp
index 1620afd9c7c..5d78c25c487 100644
--- a/be/test/format_v2/table/load_reader_test.cpp
+++ b/be/test/format_v2/table/load_reader_test.cpp
@@ -1238,6 +1238,63 @@ TEST_F(LoadArrowPipeTest, 
NumericIntStillRejectsAnOverflowingUInt32) {
     ASSERT_TRUE(reader.close().ok());
 }
 
+TEST_F(LoadArrowPipeTest, ConvertsTheReportedInt32BooleanInput) {
+    set_column_type(make_nullable(std::make_shared<DataTypeUInt8>()));
+    arrow::Int32Builder builder;
+    ASSERT_TRUE(builder.AppendValues({1, 0}).ok());
+    auto values = builder.Finish();
+    ASSERT_TRUE(values.ok());
+    append_batch(arrow::RecordBatch::Make(
+            arrow::schema({arrow::field("raw_value", arrow::int32())}), 2, 
{*values}));
+    ASSERT_TRUE(_pipe->finish().ok());
+    LoadTableReader reader(source.get(), destination.get());
+    ASSERT_TRUE(prepare(reader, FileFormat::ARROW).ok());
+    Block output(destination->slots(), 0);
+    bool eos = false;
+    const auto status = reader.get_block(&output, &eos);
+    ASSERT_TRUE(status.ok()) << status;
+    ASSERT_EQ(output.rows(), 2);
+    const auto& column = assert_cast<const 
ColumnNullable&>(*output.get_by_position(0).column);
+    EXPECT_EQ(column.get_nested_column().get_int(0), 1);
+    EXPECT_EQ(column.get_nested_column().get_int(1), 0);
+    ASSERT_TRUE(reader.close().ok());
+}
+
+TEST_F(LoadArrowPipeTest, InvalidBooleanInputIncludesTheColumnAndBothTypes) {
+    set_column_type(make_nullable(std::make_shared<DataTypeUInt8>()));
+    append_stream({"not-a-boolean"}, "raw_value");
+    ASSERT_TRUE(_pipe->finish().ok());
+    LoadTableReader reader(source.get(), destination.get());
+    ASSERT_TRUE(prepare(reader, FileFormat::ARROW).ok());
+    Block output(destination->slots(), 0);
+    bool eos = false;
+    const auto status = reader.get_block(&output, &eos);
+    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT) << status;
+    EXPECT_NE(status.to_string().find("Arrow column 'raw_value' (string -> 
bool)"),
+              std::string::npos)
+            << status;
+    ASSERT_TRUE(reader.close().ok());
+}
+
+TEST_F(LoadArrowPipeTest, StructFieldCountErrorIncludesTheColumnAndBothTypes) {
+    set_column_type(make_nullable(std::make_shared<DataTypeStruct>(
+            DataTypes {make_nullable(std::make_shared<DataTypeUInt8>())}, 
Strings {"b"})));
+    auto input = arrow::MakeArrayOfNull(arrow::struct_({}), 1);
+    ASSERT_TRUE(input.ok());
+    append_batch(arrow::RecordBatch::Make(
+            arrow::schema({arrow::field("raw_value", (*input)->type())}), 1, 
{*input}));
+    ASSERT_TRUE(_pipe->finish().ok());
+    LoadTableReader reader(source.get(), destination.get());
+    ASSERT_TRUE(prepare(reader, FileFormat::ARROW).ok());
+    Block output(destination->slots(), 0);
+    bool eos = false;
+    const auto status = reader.get_block(&output, &eos);
+    EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT) << status;
+    EXPECT_NE(status.to_string().find("Arrow column 'raw_value'"), 
std::string::npos) << status;
+    EXPECT_NE(status.to_string().find("struct<> -> struct<b: bool>"), 
std::string::npos) << status;
+    ASSERT_TRUE(reader.close().ok());
+}
+
 TEST_F(LoadArrowPipeTest, ReadsCompressedIpcBatches) {
     arrow::StringBuilder builder;
     ASSERT_TRUE(builder.Append("compressed").ok());
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/array_good.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_good.arrow
new file mode 100644
index 00000000000..8f9582f75bf
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_good.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/array_nested_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_nested_null.arrow
new file mode 100644
index 00000000000..1a9f1fe782f
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_nested_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/array_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_null.arrow
new file mode 100644
index 00000000000..17417a0d953
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/array_nullable.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_nullable.arrow
new file mode 100644
index 00000000000..5a173171062
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/array_nullable.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_array.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_array.arrow
new file mode 100644
index 00000000000..44b8c9f0209
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_array.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_bool.arrow 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_bool.arrow
new file mode 100644
index 00000000000..51b17e507a8
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_bool.arrow 
differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_decimal.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_decimal.arrow
new file mode 100644
index 00000000000..817d7433186
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_decimal.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_map.arrow 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_map.arrow
new file mode 100644
index 00000000000..da671fe9f95
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_map.arrow 
differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_struct.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_struct.arrow
new file mode 100644
index 00000000000..7410209d5bc
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bad_struct.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_good.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_good.arrow
new file mode 100644
index 00000000000..3ab67a238bf
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_good.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_late_failure.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_late_failure.arrow
new file mode 100644
index 00000000000..9ba33b868a8
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_late_failure.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_null.arrow
new file mode 100644
index 00000000000..17d9c9c0a84
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_nullable.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_nullable.arrow
new file mode 100644
index 00000000000..c9f9c27ed96
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/bool_nullable.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_good.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_good.arrow
new file mode 100644
index 00000000000..a933afb5f54
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_good.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_null.arrow
new file mode 100644
index 00000000000..a002be35b95
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_nullable.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_nullable.arrow
new file mode 100644
index 00000000000..0564c1f7203
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal128_nullable.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_good.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_good.arrow
new file mode 100644
index 00000000000..df0bedb460f
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_good.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_null.arrow
new file mode 100644
index 00000000000..d2e653d5da2
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_nullable.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_nullable.arrow
new file mode 100644
index 00000000000..02ea6825749
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal256_nullable.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal_late_failure.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal_late_failure.arrow
new file mode 100644
index 00000000000..1f45c2e62a0
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal_late_failure.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal_string.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal_string.arrow
new file mode 100644
index 00000000000..8fc9e0c60a3
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/decimal_string.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/generate.py 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/generate.py
new file mode 100644
index 00000000000..dda5d5d8d14
--- /dev/null
+++ b/regression-test/data/load_p0/stream_load/arrow_type_validation/generate.py
@@ -0,0 +1,98 @@
+# 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.
+
+"""Regenerate checked-in IPC fixtures with Python 3.12 and pyarrow==21.0.0.
+
+The regression suite only reads the .arrow files and does not require Python.
+Every stream includes an empty batch, followed by one-row batches, to exercise 
EOF,
+nullable values and batch transitions. Failed loads can follow a complete 
valid stream.
+"""
+
+from decimal import Decimal
+from pathlib import Path
+
+import pyarrow as pa
+import pyarrow.ipc as ipc
+
+ROOT = Path(__file__).resolve().parent
+
+
+def stream(data_type, values):
+    schema = pa.schema([pa.field("k", pa.int32(), False), pa.field("v", 
data_type)])
+    sink = pa.BufferOutputStream()
+    with ipc.new_stream(sink, schema) as writer:
+        writer.write_batch(
+            pa.record_batch(
+                [pa.array([], pa.int32()), pa.array([], data_type)], 
schema=schema
+            )
+        )
+        for key, value in enumerate(values, 1):
+            writer.write_batch(
+                pa.record_batch(
+                    [pa.array([key], pa.int32()), pa.array([value], 
data_type)], schema=schema
+                )
+            )
+    return sink.getvalue().to_pybytes()
+
+
+def write(name, data_type, values):
+    (ROOT / (name + ".arrow")).write_bytes(stream(data_type, values))
+
+
+TYPES = {
+    "bool": (pa.bool_(), [True, False]),
+    "decimal128": (pa.decimal128(38, 2), [Decimal("1.50"), Decimal("-2.25")]),
+    "decimal256": (pa.decimal256(76, 2), [Decimal("1.50"), Decimal("-2.25")]),
+    "array": (pa.list_(pa.bool_()), [[True, False], []]),
+    "map": (pa.map_(pa.string(), pa.bool_()), [[("x", True)], []]),
+    "struct": (pa.struct([("b", pa.bool_())]), [{"b": True}, {"b": False}]),
+}
+for name, (data_type, values) in TYPES.items():
+    write(name + "_good", data_type, values)
+    write(name + "_nullable", data_type, values + [None])
+    write(name + "_null", data_type, [None])
+
+# These are the original issue's wire types; V2 safely casts their valid 
values.
+write("int32", pa.int32(), [1, 0])
+write("int32_null", pa.int32(), [None])
+write("decimal_string", pa.string(), ["1.50", "-2.25"])
+write("bad_bool", pa.string(), ["not-a-boolean"])
+write("bad_decimal", pa.string(), ["not-a-decimal"])
+# Exceed the declared precision without overflowing the source Arrow decimal's 
bit width.
+write("overflow_decimal128", pa.decimal256(76, 2), [Decimal("1" + "0" * 40 + 
".00")])
+write("overflow_decimal256", pa.string(), ["1" + "0" * 74 + ".00"])
+write("scale_loss_decimal", pa.decimal128(10, 3), [Decimal("1.234")])
+write("bad_array", pa.list_(pa.string()), [["not-a-boolean"]])
+write("bad_map", pa.map_(pa.string(), pa.string()), [[("x", "not-a-boolean")]])
+write("bad_struct", pa.struct([("b", pa.string())]), [{"b": "not-a-boolean"}])
+write("short_struct", pa.struct([]), [{}])
+write(
+    "long_struct",
+    pa.struct([("b", pa.bool_()), ("extra", pa.bool_())]),
+    [{"b": True, "extra": False}],
+)
+write("array_nested_null", pa.list_(pa.bool_()), [[True, None]])
+write("map_nested_null", pa.map_(pa.string(), pa.bool_()), [[("x", None)]])
+write("struct_nested_null", pa.struct([("b", pa.bool_())]), [{"b": None}])
+# The first stream produces rows before the second stream fails. Neither may 
commit.
+(ROOT / "bool_late_failure.arrow").write_bytes(
+    stream(pa.bool_(), [True]) + stream(pa.string(), ["not-a-boolean"])
+)
+(ROOT / "decimal_late_failure.arrow").write_bytes(
+    stream(pa.decimal128(38, 2), [Decimal("1.50")])
+    + stream(pa.string(), ["not-a-decimal"])
+)
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/int32.arrow 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/int32.arrow
new file mode 100644
index 00000000000..4df4eb54696
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/int32.arrow 
differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/int32_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/int32_null.arrow
new file mode 100644
index 00000000000..810874e0389
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/int32_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/long_struct.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/long_struct.arrow
new file mode 100644
index 00000000000..4c7b21a45d8
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/long_struct.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/map_good.arrow 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_good.arrow
new file mode 100644
index 00000000000..6205a3bf5c7
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_good.arrow 
differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/map_nested_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_nested_null.arrow
new file mode 100644
index 00000000000..623549acf45
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_nested_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/map_null.arrow 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_null.arrow
new file mode 100644
index 00000000000..09385d58da6
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_null.arrow 
differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/map_nullable.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_nullable.arrow
new file mode 100644
index 00000000000..7d8dfcb7b0e
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/map_nullable.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/overflow_decimal128.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/overflow_decimal128.arrow
new file mode 100644
index 00000000000..facdfe27a6f
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/overflow_decimal128.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/overflow_decimal256.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/overflow_decimal256.arrow
new file mode 100644
index 00000000000..92e1be291b0
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/overflow_decimal256.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/scale_loss_decimal.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/scale_loss_decimal.arrow
new file mode 100644
index 00000000000..05bfcbcc556
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/scale_loss_decimal.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/short_struct.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/short_struct.arrow
new file mode 100644
index 00000000000..1d8a6d535d6
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/short_struct.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_good.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_good.arrow
new file mode 100644
index 00000000000..730e7569246
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_good.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_nested_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_nested_null.arrow
new file mode 100644
index 00000000000..f2c07dae76d
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_nested_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_null.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_null.arrow
new file mode 100644
index 00000000000..a0cbba1e581
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_null.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_nullable.arrow
 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_nullable.arrow
new file mode 100644
index 00000000000..2fde0d12e25
Binary files /dev/null and 
b/regression-test/data/load_p0/stream_load/arrow_type_validation/struct_nullable.arrow
 differ
diff --git 
a/regression-test/data/load_p0/stream_load/test_arrow_stream_load_type_validation.out
 
b/regression-test/data/load_p0/stream_load/test_arrow_stream_load_type_validation.out
new file mode 100644
index 00000000000..4855fc75c63
--- /dev/null
+++ 
b/regression-test/data/load_p0/stream_load/test_arrow_stream_load_type_validation.out
@@ -0,0 +1,391 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !bool_required_bad --
+0
+
+-- !bool_required_rollback --
+0
+
+-- !bool_required_not_null --
+0
+
+-- !bool_required_recovery --
+1      true
+2      false
+
+-- !bool_required_conversion_0 --
+1      true
+2      false
+
+-- !bool_nullable_bad --
+0
+
+-- !bool_nullable_rollback --
+0
+
+-- !bool_nullable_recovery --
+1      true
+2      false
+3      \N
+
+-- !bool_nullable_conversion_0 --
+1      true
+2      false
+
+-- !bool_nullable_typed_null --
+1      \N
+
+-- !decimal32_required_bad --
+0
+
+-- !decimal32_required_overflow --
+0
+
+-- !decimal32_required_scale_loss --
+0
+
+-- !decimal32_required_integer_precision --
+0
+
+-- !decimal32_required_rollback --
+0
+
+-- !decimal32_required_not_null --
+0
+
+-- !decimal32_required_recovery --
+1      1.50
+2      -2.25
+
+-- !decimal32_required_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal32_required_conversion_1 --
+1      1.50
+2      -2.25
+
+-- !decimal32_nullable_bad --
+0
+
+-- !decimal32_nullable_overflow --
+0
+
+-- !decimal32_nullable_scale_loss --
+0
+
+-- !decimal32_nullable_integer_precision --
+0
+
+-- !decimal32_nullable_null_integer_precision --
+0
+
+-- !decimal32_nullable_rollback --
+0
+
+-- !decimal32_nullable_recovery --
+1      1.50
+2      -2.25
+3      \N
+
+-- !decimal32_nullable_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal32_nullable_conversion_1 --
+1      1.50
+2      -2.25
+
+-- !decimal32_nullable_typed_null --
+1      \N
+
+-- !decimal64_required_bad --
+0
+
+-- !decimal64_required_overflow --
+0
+
+-- !decimal64_required_scale_loss --
+0
+
+-- !decimal64_required_rollback --
+0
+
+-- !decimal64_required_not_null --
+0
+
+-- !decimal64_required_recovery --
+1      1.50
+2      -2.25
+
+-- !decimal64_required_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal64_required_conversion_1 --
+1      1.00
+2      0.00
+
+-- !decimal64_required_conversion_2 --
+1      1.50
+2      -2.25
+
+-- !decimal64_nullable_bad --
+0
+
+-- !decimal64_nullable_overflow --
+0
+
+-- !decimal64_nullable_scale_loss --
+0
+
+-- !decimal64_nullable_rollback --
+0
+
+-- !decimal64_nullable_recovery --
+1      1.50
+2      -2.25
+3      \N
+
+-- !decimal64_nullable_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal64_nullable_conversion_1 --
+1      1.00
+2      0.00
+
+-- !decimal64_nullable_conversion_2 --
+1      1.50
+2      -2.25
+
+-- !decimal64_nullable_typed_null --
+1      \N
+
+-- !decimal128_required_bad --
+0
+
+-- !decimal128_required_overflow --
+0
+
+-- !decimal128_required_scale_loss --
+0
+
+-- !decimal128_required_rollback --
+0
+
+-- !decimal128_required_not_null --
+0
+
+-- !decimal128_required_recovery --
+1      1.50
+2      -2.25
+
+-- !decimal128_required_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal128_required_conversion_1 --
+1      1.00
+2      0.00
+
+-- !decimal128_required_conversion_2 --
+1      1.50
+2      -2.25
+
+-- !decimal128_nullable_bad --
+0
+
+-- !decimal128_nullable_overflow --
+0
+
+-- !decimal128_nullable_scale_loss --
+0
+
+-- !decimal128_nullable_rollback --
+0
+
+-- !decimal128_nullable_recovery --
+1      1.50
+2      -2.25
+3      \N
+
+-- !decimal128_nullable_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal128_nullable_conversion_1 --
+1      1.00
+2      0.00
+
+-- !decimal128_nullable_conversion_2 --
+1      1.50
+2      -2.25
+
+-- !decimal128_nullable_typed_null --
+1      \N
+
+-- !decimal256_required_bad --
+0
+
+-- !decimal256_required_overflow --
+0
+
+-- !decimal256_required_scale_loss --
+0
+
+-- !decimal256_required_rollback --
+0
+
+-- !decimal256_required_not_null --
+0
+
+-- !decimal256_required_recovery --
+1      1.50
+2      -2.25
+
+-- !decimal256_required_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal256_required_conversion_1 --
+1      1.00
+2      0.00
+
+-- !decimal256_required_conversion_2 --
+1      1.50
+2      -2.25
+
+-- !decimal256_nullable_bad --
+0
+
+-- !decimal256_nullable_overflow --
+0
+
+-- !decimal256_nullable_scale_loss --
+0
+
+-- !decimal256_nullable_rollback --
+0
+
+-- !decimal256_nullable_recovery --
+1      1.50
+2      -2.25
+3      \N
+
+-- !decimal256_nullable_conversion_0 --
+1      1.50
+2      -2.25
+
+-- !decimal256_nullable_conversion_1 --
+1      1.00
+2      0.00
+
+-- !decimal256_nullable_conversion_2 --
+1      1.50
+2      -2.25
+
+-- !decimal256_nullable_typed_null --
+1      \N
+
+-- !array_required_bad --
+0
+
+-- !array_required_container --
+0
+
+-- !array_required_not_null --
+0
+
+-- !array_required_recovery --
+1      [1, 0]
+2      []
+
+-- !array_required_conversion_0 --
+1      [1, null]
+
+-- !array_nullable_bad --
+0
+
+-- !array_nullable_container --
+0
+
+-- !array_nullable_recovery --
+1      [1, 0]
+2      []
+3      \N
+
+-- !array_nullable_conversion_0 --
+1      [1, null]
+
+-- !map_required_bad --
+0
+
+-- !map_required_container --
+0
+
+-- !map_required_not_null --
+0
+
+-- !map_required_recovery --
+1      {"x":1}
+2      {}
+
+-- !map_required_conversion_0 --
+1      {"x":null}
+
+-- !map_nullable_bad --
+0
+
+-- !map_nullable_container --
+0
+
+-- !map_nullable_recovery --
+1      {"x":1}
+2      {}
+3      \N
+
+-- !map_nullable_conversion_0 --
+1      {"x":null}
+
+-- !struct_required_bad --
+0
+
+-- !struct_required_container --
+0
+
+-- !struct_required_short_struct --
+0
+
+-- !struct_required_long_struct --
+0
+
+-- !struct_required_not_null --
+0
+
+-- !struct_required_recovery --
+1      {"b":1}
+2      {"b":0}
+
+-- !struct_required_conversion_0 --
+1      {"b":null}
+
+-- !struct_nullable_bad --
+0
+
+-- !struct_nullable_container --
+0
+
+-- !struct_nullable_short_struct --
+0
+
+-- !struct_nullable_long_struct --
+0
+
+-- !struct_nullable_recovery --
+1      {"b":1}
+2      {"b":0}
+3      \N
+
+-- !struct_nullable_conversion_0 --
+1      {"b":null}
+
diff --git 
a/regression-test/suites/load_p0/stream_load/test_arrow_stream_load_type_validation.groovy
 
b/regression-test/suites/load_p0/stream_load/test_arrow_stream_load_type_validation.groovy
new file mode 100644
index 00000000000..adc567efceb
--- /dev/null
+++ 
b/regression-test/suites/load_p0/stream_load/test_arrow_stream_load_type_validation.groovy
@@ -0,0 +1,148 @@
+// 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_arrow_stream_load_type_validation", "p0") {
+    sql "set enable_decimal256 = true"
+    def backendIps = [:]
+    def backendPorts = [:]
+    getBackendIpHttpPort(backendIps, backendPorts)
+    // Pin every failed load and its recovery load to the same BE, without 
HTTP retries.
+    def backendId = backendIps.keySet().first()
+    def backendIp = backendIps[backendId]
+    def backendPort = backendPorts[backendId] as int
+
+    def load = { tableName, fixture, expectedError, typeError = true ->
+        streamLoad {
+            table tableName
+            directToBe backendIp, backendPort
+            retryIfHttpError false
+            set 'format', 'arrow'
+            set 'columns', 'k,v'
+            set 'strict_mode', 'true'
+            set 'max_filter_ratio', '0'
+            file "arrow_type_validation/${fixture}.arrow"
+            time 30000
+            check { result, exception, startTime, endTime ->
+                if (exception != null) {
+                    throw exception
+                }
+                def response = parseJson(result)
+                if (expectedError != null) {
+                    assertEquals('Fail', response.Status)
+                    if (typeError) {
+                        assertTrue(response.Message.contains("Arrow column 
'v'"), response.Message)
+                        assertTrue(response.Message.contains('->'), 
response.Message)
+                    }
+                    
assertTrue(response.Message.toLowerCase().contains(expectedError.toLowerCase()),
 response.Message)
+                } else {
+                    assertEquals('Success', response.Status)
+                    assertEquals(0, response.NumberFilteredRows)
+                }
+            }
+        }
+    }
+
+    // Different decimal storage widths share the decimal128 wire 
representation.
+    def cases = [
+        [name: 'bool', type: 'BOOLEAN', good: 'bool', bad: 'bad_bool', reason: 
'boolean',
+            conversions: ['int32'], late: 'bool_late_failure'],
+        [name: 'decimal32', type: 'DECIMAL(9,2)', good: 'decimal128', bad: 
'bad_decimal',
+            reason: 'decimal', conversions: ['decimal_string', 
'decimal256_good'],
+            late: 'decimal_late_failure'],
+        [name: 'decimal64', type: 'DECIMAL(18,2)', good: 'decimal128', bad: 
'bad_decimal',
+            reason: 'decimal', conversions: ['decimal_string', 'int32', 
'decimal256_good'],
+            late: 'decimal_late_failure'],
+        [name: 'decimal128', type: 'DECIMAL(38,2)', good: 'decimal128', bad: 
'bad_decimal',
+            reason: 'decimal', conversions: ['decimal_string', 'int32', 
'decimal256_good'],
+            late: 'decimal_late_failure'],
+        [name: 'decimal256', type: 'DECIMAL(76,2)', good: 'decimal256', bad: 
'bad_decimal',
+            reason: 'decimal', conversions: ['decimal_string', 'int32', 
'decimal128_good'],
+            late: 'decimal_late_failure'],
+        [name: 'array', type: 'ARRAY<BOOLEAN>', good: 'array', bad: 
'bad_array',
+            reason: 'boolean', conversions: ['array_nested_null']],
+        [name: 'map', type: 'MAP<STRING,BOOLEAN>', good: 'map', bad: 'bad_map',
+            reason: 'boolean', conversions: ['map_nested_null']],
+        [name: 'struct', type: 'STRUCT<b:BOOLEAN>', good: 'struct', bad: 
'bad_struct',
+            reason: 'boolean', conversions: ['struct_nested_null']],
+    ]
+    cases.each { c ->
+        [false, true].each { nullable ->
+            // This is a type/nullability matrix, so each case retains a 
separate table for debugging.
+            def suffix = "${c.name}_${nullable ? 'nullable' : 'required'}"
+            def tableName = "test_arrow_type_${suffix}"
+            sql "DROP TABLE IF EXISTS ${tableName}"
+            sql """CREATE TABLE ${tableName} (k INT NOT NULL, v ${c.type} 
${nullable ? 'NULL' : 'NOT NULL'})
+                DUPLICATE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1
+                PROPERTIES ("replication_num" = "1")"""
+
+            load(tableName, c.bad, c.reason)
+            "qt_${suffix}_bad"("SELECT count(*) FROM ${tableName}")
+            if (c.name.startsWith('decimal')) {
+                load(tableName, c.name == 'decimal256' ? 'overflow_decimal256' 
: 'overflow_decimal128', 'precision')
+                "qt_${suffix}_overflow"("SELECT count(*) FROM ${tableName}")
+                load(tableName, 'scale_loss_decimal', 'data loss')
+                "qt_${suffix}_scale_loss"("SELECT count(*) FROM ${tableName}")
+            }
+            if (c.name == 'decimal32') {
+                // Arrow requires precision 12 to cast int32 to a scale-two 
decimal, even for 0/1.
+                load(tableName, 'int32', 'precision')
+                "qt_${suffix}_integer_precision"("SELECT count(*) FROM 
${tableName}")
+                if (nullable) {
+                    load(tableName, 'int32_null', 'precision')
+                    "qt_${suffix}_null_integer_precision"("SELECT count(*) 
FROM ${tableName}")
+                }
+            }
+            if (c.name in ['array', 'map', 'struct']) {
+                load(tableName, 'int32', '')
+                "qt_${suffix}_container"("SELECT count(*) FROM ${tableName}")
+            }
+            if (c.name == 'struct') {
+                ['short_struct', 'long_struct'].each { fixture ->
+                    load(tableName, fixture, 'field count')
+                    "qt_${suffix}_${fixture}"("SELECT count(*) FROM 
${tableName}")
+                }
+            }
+            if (c.late != null) {
+                load(tableName, c.late, c.reason)
+                "qt_${suffix}_rollback"("SELECT count(*) FROM ${tableName}")
+            }
+
+            if (!nullable) {
+                // Correctly typed NULL input still has to obey the 
destination NOT NULL constraint.
+                load(tableName, "${c.good}_null", '', false)
+                "qt_${suffix}_not_null"("SELECT count(*) FROM ${tableName}")
+            }
+            // An empty batch and multiple nonempty batches precede the final 
NULL in nullable controls.
+            load(tableName, "${c.good}_${nullable ? 'nullable' : 'good'}", 
null)
+            sql 'sync'
+            "order_qt_${suffix}_recovery"("SELECT * FROM ${tableName} ORDER BY 
k")
+
+            c.conversions.eachWithIndex { fixture, index ->
+                sql "TRUNCATE TABLE ${tableName}"
+                load(tableName, fixture, null)
+                sql 'sync'
+                "order_qt_${suffix}_conversion_${index}"("SELECT * FROM 
${tableName} ORDER BY k")
+            }
+            if (nullable && !(c.name in ['array', 'map', 'struct'])) {
+                sql "TRUNCATE TABLE ${tableName}"
+                load(tableName, c.name == 'decimal32' ? 'decimal128_null' : 
'int32_null', null)
+                sql 'sync'
+                "order_qt_${suffix}_typed_null"("SELECT * FROM ${tableName} 
ORDER BY k")
+            }
+        }
+    }
+}


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

Reply via email to