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

pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 5f75a12e29d GH-55124: [C++][Parquet] Allow masked nulls in required 
fields when writing (#55136)
5f75a12e29d is described below

commit 5f75a12e29d2ccf1c5527db0750c8b5ce6aa9ce1
Author: Zehua Zou <[email protected]>
AuthorDate: Tue Oct 27 16:59:22 2026 +0800

    GH-55124: [C++][Parquet] Allow masked nulls in required fields when writing 
(#55136)
    
    ### Rationale for this change
    
    The Parquet writer checks the leaf array's physical null count without 
accounting for the parent struct's validity bitmap. As a result, it rejects 
nulls in required child fields even when they are masked by null parent rows.
    
    ### What changes are included in this PR?
    
    Corrected the logic used by the column writer to validate the null values 
for required leaf fields.
    
    ### Are these changes tested?
    
    Yes.
    
    ### Are there any user-facing changes?
    
    No.
    
    ### Was AI used for this PR?
    
    In accordance to the [AI generation 
guidelines](https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code),
 please disclose below whether and how AI was used in this PR.
    
    **PR code and description written by:**
    
    - [x] Human
    - [x] AI
    
    **Reviewed before submission by:**
    
    - [x] Human
    - [ ] AI
    - [ ] Not reviewed
    
    * GitHub Issue: #55124
    
    Authored-by: Zehua Zou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/parquet/arrow/arrow_reader_writer_test.cc | 45 +++++++++++++++++++++++
 cpp/src/parquet/arrow/path_internal.cc            |  4 +-
 cpp/src/parquet/column_writer.cc                  | 23 +++++++++++-
 3 files changed, 69 insertions(+), 3 deletions(-)

diff --git a/cpp/src/parquet/arrow/arrow_reader_writer_test.cc 
b/cpp/src/parquet/arrow/arrow_reader_writer_test.cc
index d858962d472..421769521d1 100644
--- a/cpp/src/parquet/arrow/arrow_reader_writer_test.cc
+++ b/cpp/src/parquet/arrow/arrow_reader_writer_test.cc
@@ -3262,6 +3262,47 @@ TEST(ArrowReadWrite, NestedRequiredField) {
                        /*row_group_size=*/8);
 }
 
+TEST(ArrowReadWrite, NestedRequiredFieldWithMaskedNulls) {
+  // A required child null is valid when its parent is null.
+  // Reuse the child's bitmap to construct this case explicitly.
+  auto type =
+      ::arrow::struct_({::arrow::field("inner", ::arrow::int32(), 
/*nullable=*/false)});
+  auto inner = ::arrow::ArrayFromJSON(::arrow::int32(), "[42, 42, null, 42]");
+  ASSERT_OK_AND_ASSIGN(auto array, ::arrow::StructArray::Make({inner}, 
type->fields(),
+                                                              
inner->null_bitmap()));
+  ASSERT_EQ(array->null_count(), 1);
+  ASSERT_EQ(array->field(0)->null_count(), 1);
+
+  auto table = ::arrow::Table::Make(
+      ::arrow::schema({::arrow::field("outer", type, /*nullable=*/true)}), 
{array});
+  ASSERT_OK(table->ValidateFull());
+  CheckSimpleRoundtrip(table, /*row_group_size=*/4);
+
+  // Also test masked child nulls after null/empty lists in the same row group.
+  // Null/empty lists add definition levels without corresponding child values.
+  auto offsets = ::arrow::ArrayFromJSON(::arrow::int32(), "[null, 0, 0, 4]");
+  ASSERT_OK_AND_ASSIGN(auto list_array, 
::arrow::ListArray::FromArrays(*offsets, *array));
+  auto list_type = list_array->type();
+  auto list_table = ::arrow::Table::Make(
+      ::arrow::schema({::arrow::field("outer", std::move(list_type))}),
+      {std::move(list_array)});
+  ASSERT_OK(list_table->ValidateFull());
+  CheckSimpleRoundtrip(list_table, /*row_group_size=*/3);
+
+  // Exposing the same child null under a valid parent must fail.
+  ASSERT_OK_AND_ASSIGN(auto unmasked,
+                       ::arrow::StructArray::Make({std::move(inner)}, 
type->fields()));
+  auto invalid_table = ::arrow::Table::Make(table->schema(), 
{std::move(unmasked)});
+  // Test partially null and all-null child arrays.
+  for (const auto& invalid_slice : {invalid_table, invalid_table->Slice(2, 
1)}) {
+    ARROW_SCOPED_TRACE("num_rows = ", invalid_slice->num_rows());
+    auto sink = CreateOutputStream();
+    EXPECT_RAISES_WITH_MESSAGE_THAT(
+        Invalid, ::testing::HasSubstr("is declared non-nullable but contains 
nulls"),
+        WriteTable(*invalid_slice, ::arrow::default_memory_pool(), sink));
+  }
+}
+
 TEST(ArrowReadWrite, Decimal) {
   using ::arrow::field;
 
@@ -3843,6 +3884,10 @@ TEST(TestArrowWrite, InvalidSchema) {
   CheckWritingNonNullableColumnWithNulls(
       ::arrow::field("a", ::arrow::utf8(), /*nullable=*/false),
       R"([{"a": "foo"}, {"a": null}])");
+  CheckWritingNonNullableColumnWithNulls(
+      ::arrow::field("a", ::arrow::list(::arrow::field("item", 
::arrow::int32(),
+                                                       /*nullable=*/false))),
+      R"([{"a": null}, {"a": []}, {"a": [42, null]}])");
 }
 
 void DoNestedValidate(const std::shared_ptr<::arrow::DataType>& inner_type,
diff --git a/cpp/src/parquet/arrow/path_internal.cc 
b/cpp/src/parquet/arrow/path_internal.cc
index c626dcbf48b..48e054e145a 100644
--- a/cpp/src/parquet/arrow/path_internal.cc
+++ b/cpp/src/parquet/arrow/path_internal.cc
@@ -724,7 +724,9 @@ class PathBuilder {
     // and the array does in fact contain nulls, we will end up
     // traversing the null bitmap twice (once here and once when calculating
     // rep/def levels).
-    if (LazyNoNulls(array)) {
+    // Required leaf nulls must not lower definition levels: they may be 
masked by
+    // an ancestor, but otherwise WriteArrow() must reject them as invalid.
+    if (!nullable_in_parent_ || LazyNoNulls(array)) {
       info_.path.emplace_back(AllPresentTerminalNode{info_.max_def_level});
     } else if (LazyNullCount(array) == array.length()) {
       info_.path.emplace_back(AllNullsTerminalNode(info_.max_def_level - 1));
diff --git a/cpp/src/parquet/column_writer.cc b/cpp/src/parquet/column_writer.cc
index 3b4a873b519..9d5b3b7bcd0 100644
--- a/cpp/src/parquet/column_writer.cc
+++ b/cpp/src/parquet/column_writer.cc
@@ -1430,8 +1430,27 @@ class TypedColumnWriterImpl : public ColumnWriterImpl,
         (level_info_.def_level == level_info_.repeated_ancestor_def_level + 1) 
&&
         leaf_field_nullable;
     if (!leaf_field_nullable && leaf_array.null_count() != 0) {
-      return Status::Invalid("Column '", descr_->name(),
-                             "' is declared non-nullable but contains nulls");
+      bool has_unmasked_nulls = true;
+      if (level_info_.HasNullableValues()) {
+        has_unmasked_nulls = false;
+        int64_t leaf_index = 0;
+        for (int64_t i = 0; i < num_levels; ++i) {
+          // Null or empty lists have no corresponding slot in the leaf array.
+          if (def_levels[i] < level_info_.repeated_ancestor_def_level) {
+            continue;
+          }
+          // A required leaf only needs to be valid when all ancestors are 
present.
+          if (def_levels[i] == level_info_.def_level && 
leaf_array.IsNull(leaf_index)) {
+            has_unmasked_nulls = true;
+            break;
+          }
+          ++leaf_index;
+        }
+      }
+      if (has_unmasked_nulls) {
+        return Status::Invalid("Column '", descr_->name(),
+                               "' is declared non-nullable but contains 
nulls");
+      }
     }
     bool maybe_parent_nulls = level_info_.HasNullableValues() && 
!single_nullable_element;
     if (maybe_parent_nulls) {

Reply via email to