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

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


The following commit(s) were added to refs/heads/main by this push:
     new b289a90c66 Parquet: Add round-trip test for encrypted shredded variant 
columns (#17469)
b289a90c66 is described below

commit b289a90c6608e1cd0fda7f5fa10497a496e4451d
Author: Neelesh Salian <[email protected]>
AuthorDate: Mon Aug 3 10:52:58 2026 -0700

    Parquet: Add round-trip test for encrypted shredded variant columns (#17469)
    
    * Parquet: Add round-trip test for encrypted shredded variant columns
    
    * PR comment fix
    
    * Dedupe the tests
---
 .../iceberg/parquet/TestParquetDataWriter.java     | 334 ++++++++++++---------
 1 file changed, 191 insertions(+), 143 deletions(-)

diff --git 
a/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDataWriter.java 
b/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDataWriter.java
index 00891b507e..ae7d5441c9 100644
--- 
a/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDataWriter.java
+++ 
b/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDataWriter.java
@@ -20,10 +20,12 @@ package org.apache.iceberg.parquet;
 
 import static 
org.apache.iceberg.parquet.ParquetWritingTestUtils.createTempFile;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 import java.util.List;
 import java.util.Optional;
 import java.util.Random;
@@ -57,10 +59,12 @@ import 
org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.util.RandomUtil;
 import org.apache.iceberg.variants.Variant;
-import org.apache.iceberg.variants.VariantMetadata;
 import org.apache.iceberg.variants.VariantTestUtil;
 import org.apache.iceberg.variants.VariantValue;
 import org.apache.iceberg.variants.Variants;
+import org.apache.parquet.ParquetReadOptions;
+import org.apache.parquet.crypto.FileDecryptionProperties;
+import org.apache.parquet.crypto.ParquetCryptoRuntimeException;
 import org.apache.parquet.example.data.Group;
 import org.apache.parquet.hadoop.ParquetFileReader;
 import org.apache.parquet.hadoop.ParquetReader;
@@ -80,6 +84,11 @@ public class TestParquetDataWriter {
           Types.NestedField.optional(2, "data", Types.StringType.get()),
           Types.NestedField.optional(3, "binary", Types.BinaryType.get()));
 
+  private static final Schema VARIANT_SHREDDING_SCHEMA =
+      new Schema(
+          Types.NestedField.required(1, "id", Types.LongType.get()),
+          Types.NestedField.optional(2, "v", Types.VariantType.get()));
+
   private List<Record> records;
 
   @TempDir private Path temp;
@@ -380,17 +389,9 @@ public class TestParquetDataWriter {
                 .add(Types.NestedField.optional(4, "variant", 
Types.VariantType.get()))
                 .build());
 
-    ByteBuffer metadataBuffer = 
VariantTestUtil.createMetadata(ImmutableList.of("a", "b"), true);
-    VariantMetadata metadata = Variants.metadata(metadataBuffer);
-
-    ByteBuffer objectBuffer =
-        VariantTestUtil.createObject(
-            metadataBuffer,
-            ImmutableMap.of(
-                "a", Variants.of(123456789),
-                "b", Variants.of("string")));
-
-    Variant variant = Variant.of(metadata, Variants.value(metadata, 
objectBuffer));
+    Variant variant =
+        VariantTestUtil.variant(
+            ImmutableMap.of("a", Variants.of(123456789), "b", 
Variants.of("string")));
 
     // Create records with variant data
     GenericRecord record = GenericRecord.create(variantSchema);
@@ -406,11 +407,6 @@ public class TestParquetDataWriter {
 
   @Test
   public void testShreddingWriteReturnsBufferedAppender() throws IOException {
-    Schema variantSchema =
-        new Schema(
-            Types.NestedField.required(1, "id", Types.LongType.get()),
-            Types.NestedField.optional(2, "v", Types.VariantType.get()));
-
     VariantShreddingAnalyzer<Record, Void> testAnalyzer =
         new VariantShreddingAnalyzer<Record, Void>() {
           @Override
@@ -427,20 +423,12 @@ public class TestParquetDataWriter {
     OutputFile outputFile = Files.localOutput(createTempFile(temp));
 
     ParquetFormatModel<Record, Void, ParquetValueReader<?>> model =
-        ParquetFormatModel.create(
-            Record.class,
-            Void.class,
-            (icebergSchema, messageType, engineSchema) ->
-                GenericParquetWriter.create(icebergSchema, messageType),
-            (icebergSchema, fileSchema, engineSchema, idToConstant) ->
-                GenericParquetReaders.buildReader(icebergSchema, fileSchema),
-            testAnalyzer,
-            (Function<Void, UnaryOperator<Record>>) unused -> record -> 
record);
+        variantModel(testAnalyzer, unused -> record -> record);
 
     try (FileAppender<Record> appender =
         model
             .writeBuilder(EncryptedFiles.plainAsEncryptedOutput(outputFile))
-            .schema(variantSchema)
+            .schema(VARIANT_SHREDDING_SCHEMA)
             .setAll(ImmutableMap.of(TableProperties.PARQUET_SHRED_VARIANTS, 
"true"))
             .content(FileContent.DATA)
             .build()) {
@@ -450,28 +438,14 @@ public class TestParquetDataWriter {
 
   @Test
   public void testWriteBuilderReturnsDirectAppenderWithNullAnalyzer() throws 
IOException {
-    Schema variantSchema =
-        new Schema(
-            Types.NestedField.required(1, "id", Types.LongType.get()),
-            Types.NestedField.optional(2, "v", Types.VariantType.get()));
-
     OutputFile outputFile = Files.localOutput(createTempFile(temp));
 
-    ParquetFormatModel<Record, Void, ParquetValueReader<?>> model =
-        ParquetFormatModel.create(
-            Record.class,
-            Void.class,
-            (icebergSchema, messageType, engineSchema) ->
-                GenericParquetWriter.create(icebergSchema, messageType),
-            (icebergSchema, fileSchema, engineSchema, idToConstant) ->
-                GenericParquetReaders.buildReader(icebergSchema, fileSchema),
-            null,
-            (Function<Void, UnaryOperator<Record>>) null);
+    ParquetFormatModel<Record, Void, ParquetValueReader<?>> model = 
variantModel(null, null);
 
     try (FileAppender<Record> appender =
         model
             .writeBuilder(EncryptedFiles.plainAsEncryptedOutput(outputFile))
-            .schema(variantSchema)
+            .schema(VARIANT_SHREDDING_SCHEMA)
             .setAll(ImmutableMap.of(TableProperties.PARQUET_SHRED_VARIANTS, 
"true"))
             .content(FileContent.DATA)
             .build()) {
@@ -482,66 +456,14 @@ public class TestParquetDataWriter {
 
   @Test
   public void testFormatModelVariantShreddingRoundTrip() throws IOException {
-    Schema variantSchema =
-        new Schema(
-            Types.NestedField.required(1, "id", Types.LongType.get()),
-            Types.NestedField.optional(2, "v", Types.VariantType.get()));
-
-    VariantShreddingAnalyzer<Record, Void> analyzer =
-        new VariantShreddingAnalyzer<Record, Void>() {
-          @Override
-          protected List<VariantValue> extractVariantValues(List<Record> rows, 
int idx) {
-            List<VariantValue> values = Lists.newArrayList();
-            for (Record row : rows) {
-              Object obj = row.get(idx);
-              if (obj instanceof Variant) {
-                values.add(((Variant) obj).value());
-              }
-            }
-            return values;
-          }
-
-          @Override
-          protected int resolveColumnIndex(Void engineSchema, String 
columnName) {
-            // GenericRecord uses schema column order
-            return 
variantSchema.columns().indexOf(variantSchema.findField(columnName));
-          }
-        };
-
-    ByteBuffer metadataBuffer = 
VariantTestUtil.createMetadata(ImmutableList.of("a", "b"), true);
-    VariantMetadata metadata = Variants.metadata(metadataBuffer);
-    ByteBuffer objectBuffer =
-        VariantTestUtil.createObject(
-            metadataBuffer,
-            ImmutableMap.of(
-                "a", Variants.of(42),
-                "b", Variants.of("hello")));
-    Variant variant = Variant.of(metadata, Variants.value(metadata, 
objectBuffer));
-
-    GenericRecord record = GenericRecord.create(variantSchema);
-    List<Record> variantRecords =
-        ImmutableList.of(
-            record.copy(ImmutableMap.of("id", 1L, "v", variant)),
-            record.copy(ImmutableMap.of("id", 2L, "v", variant)),
-            record.copy(ImmutableMap.of("id", 3L, "v", variant)));
-
+    List<Record> variantRecords = variantShreddingRecords(42, "hello");
     OutputFile outputFile = Files.localOutput(createTempFile(temp));
-
-    ParquetFormatModel<Record, Void, ParquetValueReader<?>> model =
-        ParquetFormatModel.create(
-            Record.class,
-            Void.class,
-            (icebergSchema, messageType, engineSchema) ->
-                GenericParquetWriter.create(icebergSchema, messageType),
-            (icebergSchema, fileSchema, engineSchema, idToConstant) ->
-                GenericParquetReaders.buildReader(icebergSchema, fileSchema),
-            analyzer,
-            (Function<Void, UnaryOperator<Record>>) unused -> record1 -> 
record1);
+    ParquetFormatModel<Record, Void, ParquetValueReader<?>> model = 
variantShreddingModel();
 
     try (FileAppender<Record> appender =
         model
             .writeBuilder(EncryptedFiles.plainAsEncryptedOutput(outputFile))
-            .schema(variantSchema)
+            .schema(VARIANT_SHREDDING_SCHEMA)
             .setAll(
                 ImmutableMap.of(
                     TableProperties.PARQUET_SHRED_VARIANTS, "true",
@@ -554,57 +476,61 @@ public class TestParquetDataWriter {
       }
     }
 
-    // Verify shredded Parquet schema
-    try (ParquetFileReader reader =
-        ParquetFileReader.open(ParquetIO.file(outputFile.toInputFile()))) {
-      MessageType parquetSchema = 
reader.getFooter().getFileMetaData().getSchema();
-      GroupType variantGroup = parquetSchema.getType("v").asGroupType();
-      assertThat(variantGroup.containsField("metadata")).isTrue();
-      assertThat(variantGroup.containsField("value")).isTrue();
-      assertThat(variantGroup.containsField("typed_value")).isTrue();
+    assertShreddedVariant(outputFile, null, 42, "hello");
 
-      GroupType typedValue = variantGroup.getType("typed_value").asGroupType();
-      assertThat(typedValue.containsField("a")).isTrue();
-      assertThat(typedValue.containsField("b")).isTrue();
-    }
+    assertRoundTrip(outputFile, null, null, variantRecords);
+  }
 
-    // Verify data is in typed columns by reading raw Parquet groups
-    try (ParquetReader<Group> rawReader =
-        ParquetReader.builder(
-                new GroupReadSupport(), new 
org.apache.hadoop.fs.Path(outputFile.location()))
-            .build()) {
-      Group row = rawReader.read();
-      Group variantData = row.getGroup("v", 0);
-
-      assertThat(variantData.getFieldRepetitionCount("value"))
-          .as("value should be absent when fully shredded")
-          .isEqualTo(0);
-
-      Group typedValue = variantData.getGroup("typed_value", 0);
-      assertThat(typedValue.getGroup("a", 0).getInteger("typed_value", 0))
-          .as("typed_value.a should contain 42")
-          .isEqualTo(42);
-      assertThat(typedValue.getGroup("b", 0).getString("typed_value", 0))
-          .as("typed_value.b should contain hello")
-          .isEqualTo("hello");
-    }
+  @Test
+  public void testFormatModelVariantShreddingWithEncryption() throws 
IOException {
+    List<Record> variantRecords = variantShreddingRecords(123456789, "string");
+    ParquetFormatModel<Record, Void, ParquetValueReader<?>> model = 
variantShreddingModel();
 
-    // Verify data round-trips
-    List<Record> writtenRecords;
-    try (CloseableIterable<Record> reader =
-        Parquet.read(outputFile.toInputFile())
-            .project(variantSchema)
-            .createReaderFunc(
-                fileSchema -> GenericParquetReaders.buildReader(variantSchema, 
fileSchema))
+    OutputFile encryptedFile = Files.localOutput(createTempFile(temp));
+    ByteBuffer fileDek = ByteBuffer.allocate(16);
+    ByteBuffer aadPrefix = ByteBuffer.allocate(16);
+    SecureRandom random = new SecureRandom();
+    random.nextBytes(fileDek.array());
+    random.nextBytes(aadPrefix.array());
+
+    try (FileAppender<Record> appender =
+        model
+            .writeBuilder(EncryptedFiles.plainAsEncryptedOutput(encryptedFile))
+            .schema(VARIANT_SHREDDING_SCHEMA)
+            .withFileEncryptionKey(fileDek)
+            .withAADPrefix(aadPrefix)
+            .setAll(
+                ImmutableMap.of(
+                    TableProperties.PARQUET_SHRED_VARIANTS, "true",
+                    TableProperties.PARQUET_VARIANT_BUFFER_SIZE, "2"))
+            .content(FileContent.DATA)
             .build()) {
-      writtenRecords = Lists.newArrayList(reader);
+      assertThat(appender).isInstanceOf(BufferedFileAppender.class);
+      appender.addAll(variantRecords);
     }
 
-    assertThat(writtenRecords).hasSameSizeAs(variantRecords);
-    for (int i = 0; i < variantRecords.size(); i++) {
-      InternalTestHelpers.assertEquals(
-          variantSchema.asStruct(), variantRecords.get(i), 
writtenRecords.get(i));
-    }
+    assertThatThrownBy(
+            () ->
+                Parquet.read(encryptedFile.toInputFile())
+                    .project(VARIANT_SHREDDING_SCHEMA)
+                    .createReaderFunc(
+                        fileSchema ->
+                            
GenericParquetReaders.buildReader(VARIANT_SHREDDING_SCHEMA, fileSchema))
+                    .build()
+                    .iterator())
+        .isInstanceOf(ParquetCryptoRuntimeException.class)
+        .hasMessage("Trying to read file with encrypted footer. No keys 
available");
+
+    assertShreddedVariant(
+        encryptedFile,
+        FileDecryptionProperties.builder()
+            .withFooterKey(fileDek.array())
+            .withAADPrefix(aadPrefix.array())
+            .build(),
+        123456789,
+        "string");
+
+    assertRoundTrip(encryptedFile, fileDek, aadPrefix, variantRecords);
   }
 
   @ParameterizedTest
@@ -630,6 +556,128 @@ public class TestParquetDataWriter {
         .isLessThan(trackedFile.splitOffsets().size());
   }
 
+  private static ParquetFormatModel<Record, Void, ParquetValueReader<?>> 
variantModel(
+      VariantShreddingAnalyzer<Record, Void> analyzer,
+      Function<Void, UnaryOperator<Record>> transform) {
+    return ParquetFormatModel.create(
+        Record.class,
+        Void.class,
+        (icebergSchema, messageType, engineSchema) ->
+            GenericParquetWriter.create(icebergSchema, messageType),
+        (icebergSchema, fileSchema, engineSchema, idToConstant) ->
+            GenericParquetReaders.buildReader(icebergSchema, fileSchema),
+        analyzer,
+        transform);
+  }
+
+  private static ParquetFormatModel<Record, Void, ParquetValueReader<?>> 
variantShreddingModel() {
+    VariantShreddingAnalyzer<Record, Void> analyzer =
+        new VariantShreddingAnalyzer<Record, Void>() {
+          @Override
+          protected List<VariantValue> extractVariantValues(List<Record> rows, 
int idx) {
+            List<VariantValue> values = Lists.newArrayList();
+            for (Record row : rows) {
+              Object obj = row.get(idx);
+              if (obj instanceof Variant) {
+                values.add(((Variant) obj).value());
+              }
+            }
+            return values;
+          }
+
+          @Override
+          protected int resolveColumnIndex(Void engineSchema, String 
columnName) {
+            return VARIANT_SHREDDING_SCHEMA
+                .columns()
+                .indexOf(VARIANT_SHREDDING_SCHEMA.findField(columnName));
+          }
+        };
+
+    return variantModel(analyzer, unused -> input -> input);
+  }
+
+  private static List<Record> variantShreddingRecords(int aValue, String 
bValue) {
+    Variant variant =
+        VariantTestUtil.variant(
+            ImmutableMap.of("a", Variants.of(aValue), "b", 
Variants.of(bValue)));
+    GenericRecord record = GenericRecord.create(VARIANT_SHREDDING_SCHEMA);
+    return ImmutableList.of(
+        record.copy(ImmutableMap.of("id", 1L, "v", variant)),
+        record.copy(ImmutableMap.of("id", 2L, "v", variant)),
+        record.copy(ImmutableMap.of("id", 3L, "v", variant)));
+  }
+
+  private static void assertShreddedVariant(
+      OutputFile file, FileDecryptionProperties decryption, int aValue, String 
bValue)
+      throws IOException {
+    ParquetReadOptions.Builder options = ParquetReadOptions.builder();
+    if (decryption != null) {
+      options = options.withDecryption(decryption);
+    }
+    try (ParquetFileReader reader =
+        ParquetFileReader.open(ParquetIO.file(file.toInputFile()), 
options.build())) {
+      GroupType variantGroup =
+          
reader.getFooter().getFileMetaData().getSchema().getType("v").asGroupType();
+      assertThat(variantGroup.containsField("metadata")).isTrue();
+      assertThat(variantGroup.containsField("value")).isTrue();
+      assertThat(variantGroup.containsField("typed_value")).isTrue();
+
+      GroupType typedValueType = 
variantGroup.getType("typed_value").asGroupType();
+      assertThat(typedValueType.containsField("a")).isTrue();
+      assertThat(typedValueType.containsField("b")).isTrue();
+    }
+
+    ParquetReader.Builder<Group> builder =
+        ParquetReader.builder(
+            new GroupReadSupport(), new 
org.apache.hadoop.fs.Path(file.location()));
+    if (decryption != null) {
+      builder = builder.withDecryption(decryption);
+    }
+    try (ParquetReader<Group> rawReader = builder.build()) {
+      Group row;
+      while ((row = rawReader.read()) != null) {
+        Group variantData = row.getGroup("v", 0);
+
+        assertThat(variantData.getFieldRepetitionCount("value"))
+            .as("value should be absent when fully shredded")
+            .isEqualTo(0);
+
+        Group typedValue = variantData.getGroup("typed_value", 0);
+        assertThat(typedValue.getGroup("a", 0).getInteger("typed_value", 0))
+            .as("typed_value.a should contain " + aValue)
+            .isEqualTo(aValue);
+        assertThat(typedValue.getGroup("b", 0).getString("typed_value", 0))
+            .as("typed_value.b should contain " + bValue)
+            .isEqualTo(bValue);
+      }
+    }
+  }
+
+  private static void assertRoundTrip(
+      OutputFile file, ByteBuffer fileDek, ByteBuffer aadPrefix, List<Record> 
expected)
+      throws IOException {
+    Parquet.ReadBuilder builder =
+        Parquet.read(file.toInputFile())
+            .project(VARIANT_SHREDDING_SCHEMA)
+            .createReaderFunc(
+                fileSchema ->
+                    
GenericParquetReaders.buildReader(VARIANT_SHREDDING_SCHEMA, fileSchema));
+    if (fileDek != null) {
+      builder = 
builder.withFileEncryptionKey(fileDek).withAADPrefix(aadPrefix);
+    }
+
+    List<Record> writtenRecords;
+    try (CloseableIterable<Record> reader = builder.build()) {
+      writtenRecords = Lists.newArrayList(reader);
+    }
+
+    assertThat(writtenRecords).hasSameSizeAs(expected);
+    for (int i = 0; i < expected.size(); i++) {
+      InternalTestHelpers.assertEquals(
+          VARIANT_SHREDDING_SCHEMA.asStruct(), expected.get(i), 
writtenRecords.get(i));
+    }
+  }
+
   // Writes 30 records of 256 KB compressible JSON (~8 MB uncompressed) with a 
2 MB target.
   private DataFile writeCompressibleRecords(String codec, boolean 
trackUncompressed)
       throws IOException {

Reply via email to