ahmedabu98 commented on code in PR #40143:
URL: https://github.com/apache/beam/pull/40143#discussion_r4035622185


##########
sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java:
##########
@@ -949,4 +963,671 @@ private DataWriter<Record> createWriter(String file, 
@Nullable StructLike partit
   private Record record(int id, String name, int age) {
     return GenericRecord.create(icebergSchema).copy("id", id, "name", name, 
"age", age);
   }
+
+  // ---- ConvertToDataFile coverage check and pinned columns
+
+  private static final SchemaEvolutionConfig ADDITIONS =
+      SchemaEvolutionConfig.of(SchemaEvolutionOption.ALLOW_FIELD_ADDITION);
+
+  private PCollectionTuple convert(SchemaEvolutionConfig config, String... 
files) {
+    PCollectionTuple out =
+        pipeline
+            .apply("Create Input", Create.of(Arrays.asList(files)))
+            .apply(
+                ParDo.of(
+                        new AddFiles.ConvertToDataFile(
+                            catalogConfig, tableId.toString(), null, null, 
null, null, config))
+                    .withOutputTags(
+                        AddFiles.ConvertToDataFile.DATA_FILES,
+                        TupleTagList.of(AddFiles.ConvertToDataFile.ERRORS)));
+    
out.get(AddFiles.ConvertToDataFile.ERRORS).setRowSchema(AddFiles.ERROR_SCHEMA);
+    return out;
+  }
+
+  private String writeWithSchema(String name, Schema schema, Record... 
records) throws IOException {
+    String file = root + name;
+    DataWriter<Record> writer =
+        Parquet.writeData(Files.localOutput(file))
+            .schema(schema)
+            .withSpec(PartitionSpec.unpartitioned())
+            .createWriterFunc(GenericParquetWriter::create)
+            .build();
+    try {
+      for (Record record : records) {
+        writer.write(record);
+      }
+    } finally {
+      writer.close();
+    }
+    return file;
+  }
+
+  private String writeOneRecord(String name) throws IOException {
+    String file = root + name;
+    DataWriter<Record> writer = createWriter(file);
+    writer.write(record(1, "a", 1));
+    writer.close();
+    return file;
+  }
+
+  private static final Schema WIDER =
+      new Schema(
+          Types.NestedField.required(1, "id", Types.IntegerType.get()),
+          Types.NestedField.required(2, "name", Types.StringType.get()),
+          Types.NestedField.required(3, "age", Types.IntegerType.get()),
+          Types.NestedField.optional(4, "email", Types.StringType.get()));
+
+  private String writeWider(String name) throws IOException {
+    Record record = GenericRecord.create(WIDER);
+    record.setField("id", 1);
+    record.setField("name", "a");
+    record.setField("age", 1);
+    record.setField("email", "e");
+    return writeWithSchema(name, WIDER, record);
+  }
+
+  private void assertSingleError(PCollectionTuple out, String file, String 
contains) {
+    PAssert.that(out.get(AddFiles.ConvertToDataFile.DATA_FILES)).empty();
+    PAssert.that(out.get(AddFiles.ConvertToDataFile.ERRORS))
+        .satisfies(
+            rows -> {
+              Row row = Iterables.getOnlyElement(rows);
+              assertEquals(file, row.getString("file"));
+              assertThat(row.getString("error"), containsString(contains));
+              return null;
+            });
+  }
+
+  private void assertRegisters(PCollectionTuple out, long files) {
+    PAssert.that(out.get(AddFiles.ConvertToDataFile.ERRORS)).empty();
+    
PAssert.thatSingleton(out.get(AddFiles.ConvertToDataFile.DATA_FILES).apply(Count.globally()))
+        .isEqualTo(files);
+  }
+
+  @Test
+  public void testEmptySchemaTableWarnsAndStillRegisters() throws Exception {
+    catalog.createTable(tableId, new Schema());
+    String file = writeOneRecord("data.parquet");
+
+    PCollectionTuple out = convert(SchemaEvolutionConfig.disabled(), file);
+
+    assertRegisters(out, 1);
+    pipeline.run().waitUntilFinish();
+    logs.verifyWarn("has no columns");
+  }
+
+  /** Files without embedded field ids against a zero-column table still 
register. */
+  @Test
+  public void testEmptySchemaTableWithIdLessParquetStillRegisters() throws 
Exception {
+    catalog.createTable(tableId, new Schema());
+    File file = new File(temp.getRoot(), "idless.parquet");
+    org.apache.avro.Schema avro =
+        org.apache.avro.SchemaBuilder.record("r")
+            .fields()
+            .requiredInt("id")
+            .optionalString("name")
+            .name("address")
+            .type()
+            .record("address")
+            .fields()
+            .optionalString("city")
+            .endRecord()
+            .noDefault()
+            .endRecord();
+    try (org.apache.parquet.hadoop.ParquetWriter<Object> writer =
+        org.apache.parquet.avro.AvroParquetWriter.builder(
+                new org.apache.hadoop.fs.Path(file.getAbsolutePath()))
+            .withSchema(avro)
+            .build()) {
+      org.apache.avro.generic.GenericData.Record record =
+          new org.apache.avro.generic.GenericData.Record(avro);
+      record.put("id", 1);
+      record.put("name", "a");
+      org.apache.avro.generic.GenericData.Record address =
+          new 
org.apache.avro.generic.GenericData.Record(avro.getField("address").schema());
+      address.put("city", "c");
+      record.put("address", address);
+      writer.write(record);
+    }
+
+    PCollectionTuple out = convert(SchemaEvolutionConfig.disabled(), 
file.getAbsolutePath());
+
+    assertRegisters(out, 1);
+    pipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testCoveredFileRegistersWithEvolutionEnabled() throws Exception {
+    catalog.createTable(tableId, icebergSchema);
+    String file = writeOneRecord("data.parquet");
+
+    PCollectionTuple out = convert(ADDITIONS, file);
+
+    assertRegisters(out, 1);
+    pipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testUncoveredFileRoutesToErrorsWhenEvolutionEnabled() throws 
Exception {
+    catalog.createTable(tableId, icebergSchema);
+    String file = writeWider("wider.parquet");
+
+    PCollectionTuple out = convert(ADDITIONS, file);
+
+    assertSingleError(out, file, "does not cover the file");
+    PAssert.that(out.get(AddFiles.ConvertToDataFile.ERRORS))
+        .satisfies(
+            rows -> {
+              assertThat(
+                  Iterables.getOnlyElement(rows).getString("error"),
+                  containsString("add optional email string"));
+              return null;
+            });
+    pipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testExtraColumnsRegisterWhenEvolutionDisabled() throws Exception 
{
+    catalog.createTable(tableId, icebergSchema);
+    String file = writeWider("wider.parquet");
+
+    PCollectionTuple out = convert(SchemaEvolutionConfig.disabled(), file);
+
+    assertRegisters(out, 1);
+    pipeline.run().waitUntilFinish();
+  }
+
+  @Test
+  public void testUnreadableSchemaRoutesToErrorsWithConverterMessage() throws 
Exception {
+    catalog.createTable(tableId, icebergSchema);
+    // a legacy unannotated repeated field: readable Parquet, rejected by 
Iceberg's converter
+    org.apache.parquet.schema.MessageType legacy =
+        org.apache.parquet.schema.Types.buildMessage()
+            
.required(org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32)
+            .named("id")
+            
.repeated(org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32)
+            .named("vals")
+            .named("root");
+    File legacyFile = new File(temp.getRoot(), "legacy.parquet");
+    try 
(org.apache.parquet.hadoop.ParquetWriter<org.apache.parquet.example.data.Group> 
writer =
+        org.apache.parquet.hadoop.example.ExampleParquetWriter.builder(
+                new org.apache.hadoop.fs.Path(legacyFile.getAbsolutePath()))
+            .withType(legacy)
+            .build()) {
+      org.apache.parquet.example.data.Group group =
+          new 
org.apache.parquet.example.data.simple.SimpleGroupFactory(legacy).newGroup();
+      group.add("id", 1);
+      group.add("vals", 2);
+      writer.write(group);
+    }
+    String file = legacyFile.getAbsolutePath();
+
+    PCollectionTuple out = convert(ADDITIONS, file);
+
+    assertSingleError(out, file, 
AddFiles.ConvertToDataFile.UNREADABLE_SCHEMA_ERROR);
+    PAssert.that(out.get(AddFiles.ConvertToDataFile.ERRORS))
+        .satisfies(
+            rows -> {
+              assertThat(
+                  Iterables.getOnlyElement(rows).getString("error"),
+                  containsString("repetition REPEATED"));
+              return null;
+            });
+    pipeline.run().waitUntilFinish();
+  }
+
+  // ---- pinned columns
+  //
+  // A pin is enforced in two layers. A pinned column the table holds as 
REQUIRED is protected by
+  // the coverage check: a file that declares it optional with nulls, or lacks 
it, needs a
+  // relaxation of a pinned column, which SchemaDelta refuses, so the file is 
routed there
+  // (testPinnedRequiredColumnIsProtectedByCoverage). The per-file pin walk is 
reached only for
+  // pinned columns the table holds as OPTIONAL: columns the pre-pass added 
(it never creates them
+  // required) or pre-existing optional ones. The tests below therefore pin 
optional columns.

Review Comment:
   > it never creates them required
   
   From what I'm seeing, this is only true if the table already exists and it 
needs to add new pinned column. But if it needs to create the table itself then 
it will create those pinned columns required right? If that's intended behavior 
maybe we should mention it in javadoc somewhere



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to