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

vinishjail97 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-xtable.git


The following commit(s) were added to refs/heads/main by this push:
     new 39d6cb45 [958] Mark only non-required parquet groups nullable, never 
the root message (#989)
39d6cb45 is described below

commit 39d6cb45e5cd1cb22748677bbe0a48f6835d04ff
Author: Sylwester Lachiewicz <[email protected]>
AuthorDate: Thu Oct 29 21:58:24 2026 +0100

    [958] Mark only non-required parquet groups nullable, never the root 
message (#989)
    
    Parquet gives the root MessageType the repetition REPEATED, so requiring a
    nullable child as well made the root record nullable whenever one column was
    optional, and Hudi then failed to build column stats on the ["null", record]
    union. The same condition reported an optional group whose fields are all
    required as non-nullable. A group's nullability is its repetition alone.
    
    Co-authored-by: Vinish Reddy <[email protected]>
---
 .../xtable/parquet/ParquetSchemaExtractor.java     |  6 ++--
 .../xtable/parquet/ITParquetConversionSource.java  |  5 ++--
 .../xtable/parquet/TestParquetSchemaExtractor.java | 33 ++++++++++++++++++++++
 3 files changed, 39 insertions(+), 5 deletions(-)

diff --git 
a/xtable-core/src/main/java/org/apache/xtable/parquet/ParquetSchemaExtractor.java
 
b/xtable-core/src/main/java/org/apache/xtable/parquet/ParquetSchemaExtractor.java
index 48144287..2cc1650b 100644
--- 
a/xtable-core/src/main/java/org/apache/xtable/parquet/ParquetSchemaExtractor.java
+++ 
b/xtable-core/src/main/java/org/apache/xtable/parquet/ParquetSchemaExtractor.java
@@ -30,6 +30,7 @@ import lombok.NoArgsConstructor;
 
 import org.apache.avro.Schema;
 import org.apache.parquet.schema.LogicalTypeAnnotation;
+import org.apache.parquet.schema.MessageType;
 import org.apache.parquet.schema.PrimitiveType;
 import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName;
 import org.apache.parquet.schema.Type;
@@ -319,9 +320,8 @@ public class ParquetSchemaExtractor {
         // RECORD Type (non-nullable elements)
         if (!schema.asGroupType().getName().equals("list")
             && !Arrays.asList("key_value", 
"map").contains(schema.asGroupType().getName())) {
-          boolean isNullable =
-              subFields.stream().anyMatch(ele -> ele.getSchema().isNullable())
-                  && isNullable(schema.asGroupType());
+          // The root message is the row itself and is never null; parquet 
marks it REPEATED.
+          boolean isNullable = !(schema instanceof MessageType) && 
isNullable(schema);
           return InternalSchema.builder()
               .name(schema.getName())
               .comment(null)
diff --git 
a/xtable-core/src/test/java/org/apache/xtable/parquet/ITParquetConversionSource.java
 
b/xtable-core/src/test/java/org/apache/xtable/parquet/ITParquetConversionSource.java
index cededf35..66773f2a 100644
--- 
a/xtable-core/src/test/java/org/apache/xtable/parquet/ITParquetConversionSource.java
+++ 
b/xtable-core/src/test/java/org/apache/xtable/parquet/ITParquetConversionSource.java
@@ -228,7 +228,7 @@ public class ITParquetConversionSource {
             RowFactory.create(
                 3, "Charlie", true, 35.2, new 
Timestamp(System.currentTimeMillis() + 2000)),
             RowFactory.create(
-                4, "David", false, 29.5, new 
Timestamp(System.currentTimeMillis() + 3000)),
+                4, null, false, 29.5, new Timestamp(System.currentTimeMillis() 
+ 3000)),
             RowFactory.create(
                 5, "Eve", true, 22.2, new Timestamp(System.currentTimeMillis() 
+ 4000)));
 
@@ -236,7 +236,8 @@ public class ITParquetConversionSource {
         DataTypes.createStructType(
             new StructField[] {
               DataTypes.createStructField("id", DataTypes.IntegerType, false),
-              DataTypes.createStructField("name", DataTypes.StringType, false),
+              // nullable, as Spark writes most columns; covers #958
+              DataTypes.createStructField("name", DataTypes.StringType, true),
               DataTypes.createStructField("hasSiblings", 
DataTypes.BooleanType, false),
               DataTypes.createStructField("age", DataTypes.DoubleType, false),
               DataTypes.createStructField(
diff --git 
a/xtable-core/src/test/java/org/apache/xtable/parquet/TestParquetSchemaExtractor.java
 
b/xtable-core/src/test/java/org/apache/xtable/parquet/TestParquetSchemaExtractor.java
index 8dbceaa5..13282198 100644
--- 
a/xtable-core/src/test/java/org/apache/xtable/parquet/TestParquetSchemaExtractor.java
+++ 
b/xtable-core/src/test/java/org/apache/xtable/parquet/TestParquetSchemaExtractor.java
@@ -396,6 +396,39 @@ public class TestParquetSchemaExtractor {
     Assertions.assertEquals(internalSchema, 
schemaExtractor.toInternalSchema(messageType, null));
   }
 
+  static Stream<Arguments> recordNullability() {
+    return Stream.of(
+        Arguments.of(
+            "root message with an optional column",
+            Types.buildMessage()
+                .required(PrimitiveTypeName.INT64)
+                .named("id")
+                .optional(PrimitiveTypeName.INT32)
+                .named("name")
+                .named("my_record"),
+            false),
+        Arguments.of(
+            "optional group with only required fields",
+            
Types.optionalGroup().required(PrimitiveTypeName.INT64).named("id").named("my_group"),
+            true),
+        Arguments.of(
+            "optional group with an optional field",
+            
Types.optionalGroup().optional(PrimitiveTypeName.INT64).named("id").named("my_group"),
+            true),
+        Arguments.of(
+            "required group with an optional field",
+            
Types.requiredGroup().optional(PrimitiveTypeName.INT64).named("id").named("my_group"),
+            false));
+  }
+
+  @ParameterizedTest(name = "{0}")
+  @MethodSource("recordNullability")
+  void testRecordNullability(String description, GroupType parquetSchema, 
boolean nullable) {
+    InternalSchema internalSchema = 
schemaExtractor.toInternalSchema(parquetSchema, null);
+    Assertions.assertEquals(InternalType.RECORD, internalSchema.getDataType());
+    Assertions.assertEquals(nullable, internalSchema.isNullable());
+  }
+
   static Stream<Arguments> listEncodings() {
     return Stream.of(
         Arguments.of(

Reply via email to