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

SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new a4eddd18 fix: respect parent validity in Arrow nullability checks 
(#340)
a4eddd18 is described below

commit a4eddd180ed8a8a8b29b708e5032419121d652a4
Author: lxy <[email protected]>
AuthorDate: Thu Sep 17 11:36:37 2026 +0800

    fix: respect parent validity in Arrow nullability checks (#340)
---
 src/paimon/common/utils/arrow/arrow_utils.cpp      | 188 ++++++++++++++++-----
 src/paimon/common/utils/arrow/arrow_utils.h        |   6 +-
 src/paimon/common/utils/arrow/arrow_utils_test.cpp | 183 +++++++++++++++++++-
 3 files changed, 334 insertions(+), 43 deletions(-)

diff --git a/src/paimon/common/utils/arrow/arrow_utils.cpp 
b/src/paimon/common/utils/arrow/arrow_utils.cpp
index 5a022240..77e32168 100644
--- a/src/paimon/common/utils/arrow/arrow_utils.cpp
+++ b/src/paimon/common/utils/arrow/arrow_utils.cpp
@@ -39,7 +39,6 @@
 #include "arrow/util/compression.h"
 #include "fmt/format.h"
 #include "paimon/common/utils/arrow/status_utils.h"
-#include "paimon/common/utils/arrow/vector_utils.h"
 #include "paimon/common/utils/checked_cast.h"
 #include "paimon/common/utils/string_utils.h"
 #include "paimon/core/casting/casting_utils.h"
@@ -97,6 +96,153 @@ bool HasUndeclaredDictionaryChild(const 
std::shared_ptr<arrow::DataType>& type,
     return false;
 }
 
+// Positions in the current array which remain visible through every nullable 
ancestor.
+struct VisibleRange {
+    int64_t offset;
+    int64_t length;
+};
+
+using VisibleRanges = std::vector<VisibleRange>;
+
+void AppendVisibleRange(int64_t offset, int64_t length, VisibleRanges* ranges) 
{
+    if (length == 0) {
+        return;
+    }
+    if (!ranges->empty()) {
+        VisibleRange& last = ranges->back();
+        if (last.offset + last.length == offset) {
+            last.length += length;
+            return;
+        }
+    }
+    ranges->push_back({offset, length});
+}
+
+Status NullabilityMismatch(const arrow::Field& field) {
+    return Status::Invalid(fmt::format(
+        "CheckNullabilityMatch failed, field {} not nullable while data have 
null value",
+        field.name()));
+}
+
+Result<VisibleRanges> IntersectWithValidity(const arrow::Array& array,
+                                            const VisibleRanges& 
visible_ranges,
+                                            const arrow::Field& field) {
+    const bool nullable = field.nullable();
+    if (visible_ranges.empty()) {
+        return VisibleRanges{};
+    }
+
+    const int64_t null_count = array.null_count();
+    if (null_count == 0) {
+        return visible_ranges;
+    }
+    if (null_count == array.length()) {
+        if (!nullable) {
+            return NullabilityMismatch(field);
+        }
+        return VisibleRanges{};
+    }
+
+    VisibleRanges valid_ranges;
+    for (const VisibleRange& range : visible_ranges) {
+        int64_t run_start = -1;
+        const int64_t range_end = range.offset + range.length;
+        for (int64_t i = range.offset; i < range_end; ++i) {
+            if (array.IsValid(i)) {
+                if (run_start == -1) {
+                    run_start = i;
+                }
+            } else {
+                if (!nullable) {
+                    return NullabilityMismatch(field);
+                }
+                if (run_start != -1) {
+                    AppendVisibleRange(run_start, i - run_start, 
&valid_ranges);
+                    run_start = -1;
+                }
+            }
+        }
+        if (run_start != -1) {
+            AppendVisibleRange(run_start, range_end - run_start, 
&valid_ranges);
+        }
+    }
+    return valid_ranges;
+}
+
+template <typename ListArray>
+VisibleRanges GetVisibleValueRanges(const ListArray& array,
+                                    const VisibleRanges& 
visible_parent_ranges) {
+    VisibleRanges value_ranges;
+    for (const VisibleRange& range : visible_parent_ranges) {
+        int64_t value_offset = array.value_offset(range.offset);
+        int64_t value_end = array.value_offset(range.offset + range.length);
+        AppendVisibleRange(value_offset, value_end - value_offset, 
&value_ranges);
+    }
+    return value_ranges;
+}
+
+Status CheckFieldNullability(const std::shared_ptr<arrow::Field>& field,
+                             const std::shared_ptr<arrow::Array>& data,
+                             const VisibleRanges& visible_ranges) {
+    const std::shared_ptr<arrow::DataType>& type = field->type();
+    const bool needs_child_validation =
+        type->id() == arrow::Type::STRUCT || type->id() == arrow::Type::LIST ||
+        type->id() == arrow::Type::FIXED_SIZE_LIST || type->id() == 
arrow::Type::MAP;
+    if (!needs_child_validation &&
+        (field->nullable() || visible_ranges.empty() || data->null_count() == 
0)) {
+        return Status::OK();
+    }
+
+    PAIMON_ASSIGN_OR_RAISE(VisibleRanges valid_ranges,
+                           IntersectWithValidity(*data, visible_ranges, 
*field));
+
+    if (type->id() == arrow::Type::STRUCT) {
+        auto struct_type = checked_pointer_cast<arrow::StructType>(type);
+        auto struct_array = checked_pointer_cast<arrow::StructArray>(data);
+        for (int32_t i = 0; i < struct_type->num_fields(); ++i) {
+            PAIMON_RETURN_NOT_OK(
+                CheckFieldNullability(struct_type->field(i), 
struct_array->field(i), valid_ranges));
+        }
+    } else if (type->id() == arrow::Type::LIST) {
+        auto list_type = checked_pointer_cast<arrow::ListType>(type);
+        auto list_array = checked_pointer_cast<arrow::ListArray>(data);
+        VisibleRanges value_ranges = GetVisibleValueRanges(*list_array, 
valid_ranges);
+        PAIMON_RETURN_NOT_OK(
+            CheckFieldNullability(list_type->value_field(), 
list_array->values(), value_ranges));
+    } else if (type->id() == arrow::Type::FIXED_SIZE_LIST) {
+        auto vector_type = 
checked_pointer_cast<arrow::FixedSizeListType>(type);
+        auto vector_array = 
checked_pointer_cast<arrow::FixedSizeListArray>(data);
+        int32_t vector_length = vector_type->list_size();
+        int64_t required_values = (vector_array->offset() + 
vector_array->length()) * vector_length;
+        if (vector_array->values()->length() < required_values) {
+            return Status::Invalid(fmt::format(
+                "VECTOR field {} is invalid: VECTOR holds {} elements while {} 
rows of dimension "
+                "{} require {}",
+                field->name(), vector_array->values()->length(), 
vector_array->length(),
+                vector_length, required_values));
+        }
+
+        VisibleRanges value_ranges = GetVisibleValueRanges(*vector_array, 
valid_ranges);
+        // Paimon VECTOR values cannot contain null elements, irrespective of 
the Arrow child
+        // field's declared nullability.
+        std::shared_ptr<arrow::Field> value_field = 
vector_type->value_field()->WithNullable(false);
+        Status status = CheckFieldNullability(value_field, 
vector_array->values(), value_ranges);
+        if (!status.ok()) {
+            return Status::Invalid(
+                fmt::format("VECTOR field {} is invalid: {}", field->name(), 
status.message()));
+        }
+    } else if (type->id() == arrow::Type::MAP) {
+        auto map_type = checked_pointer_cast<arrow::MapType>(type);
+        auto map_array = checked_pointer_cast<arrow::MapArray>(data);
+        VisibleRanges value_ranges = GetVisibleValueRanges(*map_array, 
valid_ranges);
+        PAIMON_RETURN_NOT_OK(
+            CheckFieldNullability(map_type->key_field(), map_array->keys(), 
value_ranges));
+        PAIMON_RETURN_NOT_OK(
+            CheckFieldNullability(map_type->item_field(), map_array->items(), 
value_ranges));
+    }
+    return Status::OK();
+}
+
 bool NeedsNormalization(const std::shared_ptr<arrow::ArrayData>& data) {
     if (data->offset != 0) {
         return true;
@@ -407,7 +553,9 @@ Status ArrowUtils::CheckNullabilityMatch(const 
std::shared_ptr<arrow::Schema>& s
             struct_array->num_fields(), schema->num_fields()));
     }
     for (int32_t i = 0; i < schema->num_fields(); i++) {
-        PAIMON_RETURN_NOT_OK(InnerCheckNullabilityMatch(schema->field(i), 
struct_array->field(i)));
+        const std::shared_ptr<arrow::Array>& field_array = 
struct_array->field(i);
+        PAIMON_RETURN_NOT_OK(
+            CheckFieldNullability(schema->field(i), field_array, {{0, 
field_array->length()}}));
     }
     return Status::OK();
 }
@@ -533,42 +681,6 @@ bool ArrowUtils::EqualsIgnoreNullable(const 
std::shared_ptr<arrow::DataType>& ty
     return true;
 }
 
-Status ArrowUtils::InnerCheckNullabilityMatch(const 
std::shared_ptr<arrow::Field>& field,
-                                              const 
std::shared_ptr<arrow::Array>& data) {
-    if (PAIMON_UNLIKELY(!field->nullable() && data->null_count() != 0)) {
-        return Status::Invalid(fmt::format(
-            "CheckNullabilityMatch failed, field {} not nullable while data 
have null value",
-            field->name()));
-    }
-    auto type = field->type();
-    if (type->id() == arrow::Type::STRUCT) {
-        auto struct_type = 
checked_pointer_cast<arrow::StructType>(field->type());
-        auto struct_array = checked_pointer_cast<arrow::StructArray>(data);
-        for (int32_t i = 0; i < struct_type->num_fields(); ++i) {
-            PAIMON_RETURN_NOT_OK(
-                InnerCheckNullabilityMatch(struct_type->field(i), 
struct_array->field(i)));
-        }
-    } else if (type->id() == arrow::Type::LIST) {
-        auto list_type = checked_pointer_cast<arrow::ListType>(field->type());
-        auto list_array = checked_pointer_cast<arrow::ListArray>(data);
-        PAIMON_RETURN_NOT_OK(
-            InnerCheckNullabilityMatch(list_type->value_field(), 
list_array->values()));
-    } else if (type->id() == arrow::Type::FIXED_SIZE_LIST) {
-        Status status = VectorUtils::ValidateVectorElements(*data);
-        if (!status.ok()) {
-            return Status::Invalid(
-                fmt::format("VECTOR field {} is invalid: {}", field->name(), 
status.message()));
-        }
-    } else if (type->id() == arrow::Type::MAP) {
-        auto map_type = checked_pointer_cast<arrow::MapType>(field->type());
-        auto map_array = checked_pointer_cast<arrow::MapArray>(data);
-        PAIMON_RETURN_NOT_OK(InnerCheckNullabilityMatch(map_type->key_field(), 
map_array->keys()));
-        PAIMON_RETURN_NOT_OK(
-            InnerCheckNullabilityMatch(map_type->item_field(), 
map_array->items()));
-    }
-    return Status::OK();
-}
-
 Result<std::shared_ptr<arrow::StructArray>> 
ArrowUtils::RemoveFieldFromStructArray(
     const std::shared_ptr<arrow::StructArray>& struct_array, const 
std::string& field_name) {
     auto struct_type = 
checked_pointer_cast<arrow::StructType>(struct_array->type());
diff --git a/src/paimon/common/utils/arrow/arrow_utils.h 
b/src/paimon/common/utils/arrow/arrow_utils.h
index 2a213155..ac1bfd31 100644
--- a/src/paimon/common/utils/arrow/arrow_utils.h
+++ b/src/paimon/common/utils/arrow/arrow_utils.h
@@ -43,6 +43,8 @@ class PAIMON_EXPORT ArrowUtils {
     static Result<std::vector<int32_t>> CreateProjection(
         const std::shared_ptr<arrow::Schema>& src_schema, const 
arrow::FieldVector& target_fields);
 
+    /// Checks that every logically visible null matches the schema. Values 
hidden by a null
+    /// STRUCT, LIST, MAP, or FIXED_SIZE_LIST parent are ignored.
     static Status CheckNullabilityMatch(const std::shared_ptr<arrow::Schema>& 
schema,
                                         const std::shared_ptr<arrow::Array>& 
data);
 
@@ -149,10 +151,6 @@ class PAIMON_EXPORT ArrowUtils {
         const std::shared_ptr<arrow::StructArray>& batch,
         const std::shared_ptr<arrow::DataType>& logical_type, 
arrow::MemoryPool* pool,
         bool preserve_layout_recoverable_dictionaries);
-
- private:
-    static Status InnerCheckNullabilityMatch(const 
std::shared_ptr<arrow::Field>& field,
-                                             const 
std::shared_ptr<arrow::Array>& data);
 };
 
 }  // namespace paimon
diff --git a/src/paimon/common/utils/arrow/arrow_utils_test.cpp 
b/src/paimon/common/utils/arrow/arrow_utils_test.cpp
index ea3fd1e3..6625e15e 100644
--- a/src/paimon/common/utils/arrow/arrow_utils_test.cpp
+++ b/src/paimon/common/utils/arrow/arrow_utils_test.cpp
@@ -222,6 +222,186 @@ TEST(ArrowUtilsTest, TestCheckNullableMatchWithStruct) {
     }
 }
 
+TEST(ArrowUtilsTest, TestCheckNullableMatchParentNullMasksChildren) {
+    auto child = arrow::field("child", arrow::int32(), /*nullable=*/false);
+    auto struct_field = arrow::field("parent", arrow::struct_({child}), 
/*nullable=*/true);
+    auto vector_type =
+        arrow::fixed_size_list(arrow::field("item", arrow::float32(), 
/*nullable=*/false), 3);
+    auto vector_field = arrow::field("embedding", vector_type, 
/*nullable=*/true);
+    auto schema = arrow::schema({struct_field, vector_field});
+
+    // Do not build the complete batch with ArrayFromJSON: for a null STRUCT 
it appends a valid
+    // default value to primitive children, which cannot represent the hidden 
child null under test.
+    auto parent_with_validity = checked_pointer_cast<arrow::StructArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({child}), 
R"([null, [1]])")
+            .ValueOrDie());
+    std::shared_ptr<arrow::Array> child_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[null, 
1]").ValueOrDie();
+    std::shared_ptr<arrow::StructArray> parent =
+        arrow::StructArray::Make({child_array}, {child}, 
parent_with_validity->null_bitmap(),
+                                 parent_with_validity->null_count())
+            .ValueOrDie();
+    auto vector = checked_pointer_cast<arrow::FixedSizeListArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(vector_type, R"([null, [1.0, 
2.0, 3.0]])")
+            .ValueOrDie());
+    std::shared_ptr<arrow::StructArray> batch =
+        arrow::StructArray::Make({parent, vector}, {struct_field, 
vector_field}).ValueOrDie();
+
+    ASSERT_OK(ArrowUtils::CheckNullabilityMatch(schema, batch));
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchRejectsVisibleChildNulls) {
+    auto child = arrow::field("child", arrow::int32(), /*nullable=*/false);
+    auto struct_field = arrow::field("parent", arrow::struct_({child}), 
/*nullable=*/true);
+    std::shared_ptr<arrow::Array> struct_batch =
+        
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({struct_field}), 
R"([[[null]]])")
+            .ValueOrDie();
+    ASSERT_NOK_WITH_MSG(
+        ArrowUtils::CheckNullabilityMatch(arrow::schema({struct_field}), 
struct_batch),
+        "CheckNullabilityMatch failed, field child not nullable while data 
have null value");
+
+    auto vector_type =
+        arrow::fixed_size_list(arrow::field("item", arrow::float32(), 
/*nullable=*/false), 3);
+    auto vector_field = arrow::field("embedding", vector_type, 
/*nullable=*/true);
+    std::shared_ptr<arrow::Array> vector_batch =
+        
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_({vector_field}),
+                                                  R"([[[1.0, null, 3.0]]])")
+            .ValueOrDie();
+    ASSERT_NOK_WITH_MSG(
+        ArrowUtils::CheckNullabilityMatch(arrow::schema({vector_field}), 
vector_batch),
+        "VECTOR field embedding is invalid: CheckNullabilityMatch failed, 
field item not nullable "
+        "while data have null value");
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchListParentNullMasksChildren) {
+    auto value_field = arrow::field("value", arrow::int32(), 
/*nullable=*/false);
+    auto list_type = arrow::list(value_field);
+    auto list_field = arrow::field("list_column", list_type, 
/*nullable=*/true);
+    auto offsets =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[0, 1, 
2]").ValueOrDie();
+    auto validity_source = checked_pointer_cast<arrow::ListArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(list_type, "[null, 
[1]]").ValueOrDie());
+
+    std::shared_ptr<arrow::Array> hidden_values =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[null, 
1]").ValueOrDie();
+    std::shared_ptr<arrow::ListArray> hidden_list =
+        arrow::ListArray::FromArrays(list_type, *offsets, *hidden_values,
+                                     arrow::default_memory_pool(), 
validity_source->null_bitmap(),
+                                     /*null_count=*/1)
+            .ValueOrDie();
+    std::shared_ptr<arrow::StructArray> hidden_batch =
+        arrow::StructArray::Make({hidden_list}, {list_field}).ValueOrDie();
+    ASSERT_TRUE(hidden_batch->ValidateFull().ok());
+    ASSERT_OK(ArrowUtils::CheckNullabilityMatch(arrow::schema({list_field}), 
hidden_batch));
+
+    std::shared_ptr<arrow::Array> visible_values =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[null, 
null]").ValueOrDie();
+    std::shared_ptr<arrow::ListArray> visible_list =
+        arrow::ListArray::FromArrays(list_type, *offsets, *visible_values,
+                                     arrow::default_memory_pool(), 
validity_source->null_bitmap(),
+                                     /*null_count=*/1)
+            .ValueOrDie();
+    std::shared_ptr<arrow::StructArray> visible_batch =
+        arrow::StructArray::Make({visible_list}, {list_field}).ValueOrDie();
+    ASSERT_TRUE(visible_batch->ValidateFull().ok());
+    ASSERT_NOK_WITH_MSG(
+        ArrowUtils::CheckNullabilityMatch(arrow::schema({list_field}), 
visible_batch),
+        "CheckNullabilityMatch failed, field value not nullable while data 
have null value");
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchMapParentNullMasksChildren) {
+    auto key_field = arrow::field("key", arrow::int32(), /*nullable=*/false);
+    auto item_field = arrow::field("item", arrow::int32(), /*nullable=*/false);
+    auto map_type = std::make_shared<arrow::MapType>(key_field, item_field);
+    auto map_field = arrow::field("map_column", map_type, /*nullable=*/true);
+    auto offsets =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[0, 1, 
2]").ValueOrDie();
+    std::shared_ptr<arrow::Array> keys =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[1, 
2]").ValueOrDie();
+    auto validity_source = checked_pointer_cast<arrow::ListArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::list(arrow::int32()), 
"[null, [1]]")
+            .ValueOrDie());
+
+    // Arrow rejects physical null MAP keys during construction, so exercise 
masking with the
+    // non-nullable item field.
+    std::shared_ptr<arrow::Array> hidden_items =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[null, 
20]").ValueOrDie();
+    std::shared_ptr<arrow::Array> hidden_map =
+        arrow::MapArray::FromArrays(map_type, offsets, keys, hidden_items,
+                                    arrow::default_memory_pool(), 
validity_source->null_bitmap())
+            .ValueOrDie();
+    std::shared_ptr<arrow::StructArray> hidden_batch =
+        arrow::StructArray::Make({hidden_map}, {map_field}).ValueOrDie();
+    ASSERT_TRUE(hidden_batch->ValidateFull().ok());
+    ASSERT_OK(ArrowUtils::CheckNullabilityMatch(arrow::schema({map_field}), 
hidden_batch));
+
+    std::shared_ptr<arrow::Array> visible_items =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[null, 
null]").ValueOrDie();
+    std::shared_ptr<arrow::Array> visible_map =
+        arrow::MapArray::FromArrays(map_type, offsets, keys, visible_items,
+                                    arrow::default_memory_pool(), 
validity_source->null_bitmap())
+            .ValueOrDie();
+    std::shared_ptr<arrow::StructArray> visible_batch =
+        arrow::StructArray::Make({visible_map}, {map_field}).ValueOrDie();
+    ASSERT_TRUE(visible_batch->ValidateFull().ok());
+    ASSERT_NOK_WITH_MSG(
+        ArrowUtils::CheckNullabilityMatch(arrow::schema({map_field}), 
visible_batch),
+        "CheckNullabilityMatch failed, field item not nullable while data have 
null value");
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchWithSlicedList) {
+    auto value_field = arrow::field("value", arrow::int32(), 
/*nullable=*/false);
+    auto list_type = arrow::list(value_field);
+    auto list_field = arrow::field("list_column", list_type, 
/*nullable=*/true);
+    auto offsets =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[0, 1, 2, 
3]").ValueOrDie();
+    std::shared_ptr<arrow::Array> values =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[10, null, 
30]").ValueOrDie();
+    auto validity_source = checked_pointer_cast<arrow::ListArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(list_type, "[[10], null, 
[30]]").ValueOrDie());
+    std::shared_ptr<arrow::ListArray> list =
+        arrow::ListArray::FromArrays(list_type, *offsets, *values, 
arrow::default_memory_pool(),
+                                     validity_source->null_bitmap(), 
/*null_count=*/1)
+            .ValueOrDie();
+    std::shared_ptr<arrow::Array> sliced_list = list->Slice(/*offset=*/1, 
/*length=*/2);
+    ASSERT_EQ(sliced_list->offset(), 1);
+
+    std::shared_ptr<arrow::StructArray> batch =
+        arrow::StructArray::Make({sliced_list}, {list_field}).ValueOrDie();
+    ASSERT_TRUE(batch->ValidateFull().ok());
+    ASSERT_OK(ArrowUtils::CheckNullabilityMatch(arrow::schema({list_field}), 
batch));
+}
+
+TEST(ArrowUtilsTest, TestCheckNullableMatchDeepParentNullMasksChildren) {
+    auto leaf_field = arrow::field("leaf", arrow::int32(), /*nullable=*/false);
+    auto item_type = arrow::struct_({leaf_field});
+    auto item_field = arrow::field("item", item_type, /*nullable=*/false);
+    auto list_type = arrow::list(item_field);
+    auto list_field = arrow::field("list_column", list_type, 
/*nullable=*/true);
+    auto outer_field = arrow::field("outer", arrow::struct_({list_field}), 
/*nullable=*/false);
+
+    std::shared_ptr<arrow::Array> leaves =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[null, 
1]").ValueOrDie();
+    std::shared_ptr<arrow::StructArray> items =
+        arrow::StructArray::Make({leaves}, {leaf_field}).ValueOrDie();
+    auto offsets =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[0, 1, 
2]").ValueOrDie();
+    auto validity_source = checked_pointer_cast<arrow::ListArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::list(arrow::int32()), 
"[null, [1]]")
+            .ValueOrDie());
+    std::shared_ptr<arrow::ListArray> list =
+        arrow::ListArray::FromArrays(list_type, *offsets, *items, 
arrow::default_memory_pool(),
+                                     validity_source->null_bitmap(), 
/*null_count=*/1)
+            .ValueOrDie();
+    std::shared_ptr<arrow::StructArray> outer =
+        arrow::StructArray::Make({list}, {list_field}).ValueOrDie();
+    std::shared_ptr<arrow::StructArray> batch =
+        arrow::StructArray::Make({outer}, {outer_field}).ValueOrDie();
+
+    ASSERT_TRUE(batch->ValidateFull().ok());
+    ASSERT_OK(ArrowUtils::CheckNullabilityMatch(arrow::schema({outer_field}), 
batch));
+}
+
 TEST(ArrowUtilsTest, TestCheckNullableMatchWithList) {
     auto value_field = arrow::field("value", arrow::int32(), 
/*nullable=*/false);
     auto list_field = arrow::field("list_column", arrow::list(value_field), 
/*nullable=*/false);
@@ -272,7 +452,8 @@ TEST(ArrowUtilsTest, 
TestCheckNullableMatchRejectsNullVectorElement) {
 
     ASSERT_NOK_WITH_MSG(
         ArrowUtils::CheckNullabilityMatch(arrow::schema({vector_field}), 
struct_array),
-        "VECTOR field embedding is invalid: VECTOR cannot contain null 
elements");
+        "VECTOR field embedding is invalid: CheckNullabilityMatch failed, 
field item not nullable "
+        "while data have null value");
 }
 
 // Arrow accepts a FixedSizeList whose child is shorter than `length * 
list_size` when importing

Reply via email to