This is an automated email from the ASF dual-hosted git repository.
claudevdm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 68e762ecadb Default to parquet in AddFiles for extension less files.
(#40614)
68e762ecadb is described below
commit 68e762ecadbc5e5e351f6881baab4fa11a71ff0c
Author: claudevdm <[email protected]>
AuthorDate: Mon Oct 19 17:54:40 2026 -0400
Default to parquet in AddFiles for extension less files. (#40614)
* Assume files are parquet by default if no extension
* fix messge
---
.../IO_Iceberg_Integration_Tests.json | 2 +-
.../org/apache/beam/sdk/io/iceberg/AddFiles.java | 35 ++++-
.../beam/sdk/io/iceberg/ReadFooterSchema.java | 61 ++++++--
.../org/apache/beam/sdk/io/iceberg/AddFilesIT.java | 80 +++++++++++
.../apache/beam/sdk/io/iceberg/AddFilesTest.java | 157 +++++++++++++++++++++
.../beam/sdk/io/iceberg/ReadFooterSchemaTest.java | 52 +++++++
6 files changed, 364 insertions(+), 23 deletions(-)
diff --git a/.github/trigger_files/IO_Iceberg_Integration_Tests.json
b/.github/trigger_files/IO_Iceberg_Integration_Tests.json
index e1290fa50c3..b73af5e61a4 100644
--- a/.github/trigger_files/IO_Iceberg_Integration_Tests.json
+++ b/.github/trigger_files/IO_Iceberg_Integration_Tests.json
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run.",
- "modification": 11
+ "modification": 1
}
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
index cb54183e0a0..c72479aa39b 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/AddFiles.java
@@ -468,6 +468,8 @@ public class AddFiles extends
PTransform<PCollection<String>, PCollectionRowTupl
static final String PREFIX_ERROR = "File path did not start with the
specified prefix";
private static final String UNKNOWN_FORMAT_ERROR = "Could not determine
the file's format";
+ static final String NOT_PARQUET_ERROR =
+ UNKNOWN_FORMAT_ERROR + ": the file name has no extension and the file
is not Parquet: ";
static final String UNKNOWN_PARTITION_ERROR = "Could not determine the
file's partition: ";
static final String UNREADABLE_SCHEMA_ERROR = "Could not read the file's
schema: ";
static final String UNCOVERED_ERROR = "Table schema does not cover the
file after refresh: ";
@@ -626,10 +628,21 @@ public class AddFiles extends
PTransform<PCollection<String>, PCollectionRowTupl
return () -> {
FileFormat format;
- try {
- format = inferFormat(filePath);
- } catch (UnknownFormatException e) {
- return errorResult(filePath, UNKNOWN_FORMAT_ERROR, timestamp,
window, paneInfo);
+ @Nullable ParquetMetadata parquetFooter = null;
+ if (hasNoExtension(filePath)) {
+ try {
+ parquetFooter = ParquetFooters.read(filePath);
+ } catch (Exception e) {
+ return errorResult(
+ filePath, NOT_PARQUET_ERROR + errorMessage(e), timestamp,
window, paneInfo);
+ }
+ format = FileFormat.PARQUET;
+ } else {
+ try {
+ format = inferFormat(filePath);
+ } catch (UnknownFormatException e) {
+ return errorResult(filePath, UNKNOWN_FORMAT_ERROR, timestamp,
window, paneInfo);
+ }
}
// ---- Infrastructure phase. Failures propagate so the runner retries
the bundle;
@@ -670,8 +683,7 @@ public class AddFiles extends
PTransform<PCollection<String>, PCollectionRowTupl
}
// ---- Per-file phase: every failure below is one error row, never a
failed bundle.
- @Nullable ParquetMetadata parquetFooter = null;
- if (format.equals(FileFormat.PARQUET)) {
+ if (format.equals(FileFormat.PARQUET) && parquetFooter == null) {
try {
parquetFooter = ParquetFooters.read(filePath);
} catch (Exception e) {
@@ -1369,7 +1381,7 @@ public class AddFiles extends
PTransform<PCollection<String>, PCollectionRowTupl
return e.getMessage() != null ? e.getMessage() : e.toString();
}
- /** Tries to infer other file formats. Defaults to Parquet. */
+ /** Attempt to infer file format from extension. */
public static FileFormat inferFormat(String path) {
String lowerPath = path.toLowerCase();
@@ -1384,6 +1396,15 @@ public class AddFiles extends
PTransform<PCollection<String>, PCollectionRowTupl
}
}
+ /**
+ * A name with no extension, as Hive and other engines write by default,
says nothing about the
+ * format, so the file is read to see whether it is Parquet. A name with a
dot names some format.
+ */
+ static boolean hasNoExtension(String path) {
+ String name = path.substring(path.lastIndexOf('/') + 1);
+ return !name.isEmpty() && name.indexOf('.') < 0;
+ }
+
static class UnknownFormatException extends IllegalArgumentException {}
static class UnknownPartitionException extends IllegalStateException {
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
index 389f03490fe..f111ed4b855 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchema.java
@@ -161,29 +161,60 @@ class ReadFooterSchema extends DoFn<String,
CollectDistinctSchemas.SchemaGroup>
private Callable<ReadResult> createReadTask(
String filePath, Instant timestamp, BoundedWindow window, PaneInfo
paneInfo) {
return () -> {
- FileFormat format;
- try {
- format = AddFiles.inferFormat(filePath);
- } catch (AddFiles.UnknownFormatException e) {
- return new ReadResult(dryRunMarker(UNREADABLE_KEY), false, timestamp,
window, paneInfo);
- }
- if (!format.equals(FileFormat.PARQUET)) {
- return new ReadResult(
- dryRunMarker(UNCHECKED_FORMAT_KEY), false, timestamp, window,
paneInfo);
+ ParquetMetadata footer;
+ if (AddFiles.hasNoExtension(filePath)) {
+ try {
+ footer = ParquetFooters.read(filePath);
+ } catch (Exception e) {
+ return unknownFormat(timestamp, window, paneInfo);
+ }
+ } else {
+ FileFormat format;
+ try {
+ format = AddFiles.inferFormat(filePath);
+ } catch (AddFiles.UnknownFormatException e) {
+ return unknownFormat(timestamp, window, paneInfo);
+ }
+ if (!format.equals(FileFormat.PARQUET)) {
+ return new ReadResult(
+ dryRunMarker(UNCHECKED_FORMAT_KEY), false, timestamp, window,
paneInfo);
+ }
+ try {
+ footer = ParquetFooters.read(filePath);
+ } catch (Exception e) {
+ return unreadableFooter(filePath, e, timestamp, window, paneInfo);
+ }
}
try {
- ParquetMetadata footer = ParquetFooters.read(filePath);
return new ReadResult(FileSchemas.schemaGroup(footer), false,
timestamp, window, paneInfo);
} catch (Exception e) {
- LOG.warn(
- "Could not read the footer of {}; the file will not contribute to
schema inference: {}",
- filePath,
- AddFiles.errorMessage(e));
- return new ReadResult(dryRunMarker(UNREADABLE_KEY), true, timestamp,
window, paneInfo);
+ return unconvertibleSchema(filePath, e, timestamp, window, paneInfo);
}
};
}
+ private ReadResult unknownFormat(Instant timestamp, BoundedWindow window,
PaneInfo paneInfo) {
+ return new ReadResult(dryRunMarker(UNREADABLE_KEY), false, timestamp,
window, paneInfo);
+ }
+
+ private ReadResult unreadableFooter(
+ String filePath, Exception e, Instant timestamp, BoundedWindow window,
PaneInfo paneInfo) {
+ LOG.warn(
+ "Could not read the footer of {}; the file will not contribute to
schema inference: {}",
+ filePath,
+ AddFiles.errorMessage(e));
+ return new ReadResult(dryRunMarker(UNREADABLE_KEY), true, timestamp,
window, paneInfo);
+ }
+
+ private ReadResult unconvertibleSchema(
+ String filePath, Exception e, Instant timestamp, BoundedWindow window,
PaneInfo paneInfo) {
+ LOG.warn(
+ "Could not convert the schema of {}; the file will not contribute to
schema inference: {}",
+ filePath,
+ AddFiles.errorMessage(e));
+ return new ReadResult(dryRunMarker(UNREADABLE_KEY), false, timestamp,
window, paneInfo);
+ }
+
/** A dry run counts files that contribute no schema; a real run handles
them at registration. */
private CollectDistinctSchemas.@Nullable SchemaGroup dryRunMarker(String
key) {
if (!config.getDryRun()) {
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java
index a1bb7943175..276ea4b0ee9 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesIT.java
@@ -33,6 +33,8 @@ import static org.junit.Assert.assertTrue;
import com.google.api.client.http.HttpResponseException;
import com.google.api.services.storage.model.StorageObject;
import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Notification;
import com.google.cloud.storage.NotificationInfo;
import com.google.cloud.storage.Storage;
@@ -698,6 +700,84 @@ public class AddFilesIT {
s.run().waitUntilFinish();
}
+ /**
+ * Hive names Parquet files without an extension (000000_0), and Spark and
Hadoop leave a _SUCCESS
+ * marker beside them. The files register and read back, BigQuery included;
the marker is one
+ * error row and must not seed the table.
+ */
+ @Test
+ public void testBatchImportOfParquetFilesWithoutExtension() throws
IOException {
+ String parquetDir = format("gs://%s/%s/", DATA_BUCKET, dirName);
+ String tempDir = format("gs://%s/%s-tmp/", DATA_BUCKET, dirName);
+ Pipeline q = Pipeline.create();
+ org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(ROW_SCHEMA);
+ q.apply(Create.of(TEST_ROWS))
+ .setRowSchema(ROW_SCHEMA)
+ .apply(
+ MapElements.into(TypeDescriptor.of(GenericRecord.class))
+ .via(AvroUtils.getRowToGenericRecordFunction(avroSchema)))
+ .setCoder(AvroCoder.of(avroSchema))
+ .apply(
+ FileIO.<String, GenericRecord>writeDynamic()
+ .by(record -> String.valueOf(record.get("id")))
+ .via(ParquetIO.sink(avroSchema))
+ .withNaming(
+ id ->
+ (window, pane, numShards, shardIndex, compression) ->
+ format("%06d_%d", Long.parseLong(id), shardIndex))
+ .withTempDirectory(tempDir)
+ .to(parquetDir)
+ .withDestinationCoder(StringUtf8Coder.of()));
+ q.run().waitUntilFinish();
+ String marker = parquetDir + "_SUCCESS";
+ storage.create(
+ BlobInfo.newBuilder(BlobId.of(DATA_BUCKET, dirName +
"/_SUCCESS")).build(), new byte[0]);
+
+ GcsUtil gcsUtil =
TestPipeline.testingPipelineOptions().as(GcsOptions.class).getGcsUtil();
+ List<String> dataFiles = new ArrayList<>();
+ for (StorageObject object : gcsUtil.listObjects(DATA_BUCKET, dirName +
"/", null).getItems()) {
+ String path = format("gs://%s/%s", object.getBucket(), object.getName());
+ if (!path.equals(marker)) {
+ dataFiles.add(path);
+ }
+ }
+ LOG.info("Extensionless files: {}", dataFiles);
+ assertEquals(TEST_ROWS.size(), dataFiles.size());
+ for (String path : dataFiles) {
+ assertFalse(path, path.substring(path.lastIndexOf('/') +
1).contains("."));
+ }
+ List<String> inputs = new ArrayList<>(dataFiles);
+ inputs.add(marker);
+
+ Pipeline p = Pipeline.create();
+ PCollectionRowTuple tuple =
+ p.apply(Create.of(inputs))
+ .apply(
+ new AddFiles(
+
IcebergCatalogConfig.builder().setCatalogProperties(LAKEHOUSE_PROPS).build(),
+ namespace + "." + destTableName,
+ null,
+ null,
+ null,
+ TABLE_PROPS,
+ null,
+ null));
+ PAssert.that(tuple.get("errors"))
+ .satisfies(
+ rows -> {
+ Row row = Iterables.getOnlyElement(rows);
+ String error = String.valueOf(row.getString("error"));
+ LOG.info("Marker error row: {}", row);
+ assertEquals(marker, row.getString("file"));
+ assertTrue(error,
error.startsWith(AddFiles.ConvertToDataFile.NOT_PARQUET_ERROR));
+ return null;
+ });
+ p.run().waitUntilFinish();
+
+ assertTrue(checkTableHasRegisteredParquetFiles(dataFiles));
+ checkRecordsInDestinationTable(/* alsoCheckWithBigQueryIO= */ true);
+ }
+
private static void writeParquet(List<Row> rows, Schema schema, String dir,
String tempDir) {
Pipeline q = Pipeline.create();
org.apache.avro.Schema avroSchema = AvroUtils.toAvroSchema(schema);
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
index 0d5a41e0cc4..578d18c539d 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/AddFilesTest.java
@@ -18,6 +18,7 @@
package org.apache.beam.sdk.io.iceberg;
import static
org.apache.beam.sdk.io.iceberg.AddFiles.ConvertToDataFile.FIELD_ID_ERROR;
+import static
org.apache.beam.sdk.io.iceberg.AddFiles.ConvertToDataFile.NOT_PARQUET_ERROR;
import static
org.apache.beam.sdk.io.iceberg.AddFiles.ConvertToDataFile.PREFIX_ERROR;
import static
org.apache.beam.sdk.io.iceberg.AddFiles.ConvertToDataFile.UNKNOWN_PARTITION_ERROR;
import static
org.apache.beam.sdk.io.iceberg.AddFiles.ConvertToDataFile.getPartitionFromMetrics;
@@ -450,6 +451,105 @@ public class AddFilesTest {
pipeline.run().waitUntilFinish();
}
+ /** Hive names its output files like {@code 000000_0}, with no extension. */
+ @Test
+ public void testExtensionlessParquetFileRegisters() throws Exception {
+ catalog.createTable(tableId, icebergSchema);
+ String file = root + "000000_0";
+ DataWriter<Record> writer = createWriter(file);
+ writer.write(record(1, "Mark", 20));
+ writer.close();
+
+ PCollectionRowTuple output =
+ pipeline
+ .apply("Create Input", Create.of(file))
+ .apply(
+ new AddFiles(
+ catalogConfig, tableId.toString(), null, null, null, null,
null, null));
+ PAssert.that(output.get("errors")).empty();
+
+ pipeline.run().waitUntilFinish();
+
+ List<DataFile> registered = new ArrayList<>();
+ for (FileScanTask task : catalog.loadTable(tableId).newScan().planFiles())
{
+ registered.add(task.file());
+ }
+ assertEquals(1, registered.size());
+ assertEquals(file, registered.get(0).location());
+ assertEquals(FileFormat.PARQUET, registered.get(0).format());
+ assertEquals(1L, registered.get(0).recordCount());
+ }
+
+ /**
+ * A _SUCCESS marker has no extension either. It must become an error row
rather than seed the
+ * missing table, where a failed footer read would fail the job.
+ */
+ @Test
+ public void testExtensionlessFileThatIsNotParquetIsAnErrorRow() throws
Exception {
+ File marker = temp.newFile("_SUCCESS");
+
+ PCollectionRowTuple output =
+ pipeline
+ .apply("Create Input", Create.of(marker.getAbsolutePath()))
+ .apply(
+ new AddFiles(
+ catalogConfig, tableId.toString(), null, null, null, null,
null, null));
+ assertNotParquetErrorFor(output, marker.getAbsolutePath());
+
+ pipeline.run().waitUntilFinish();
+
+ assertFalse("the marker must not create the table",
catalog.tableExists(tableId));
+ }
+
+ @Test
+ public void testMissingTableIsCreatedFromAnExtensionlessFile() throws
Exception {
+ String file = writeWithoutFieldIds("000000_0", icebergSchema, record(1,
"a", 1));
+ File marker = temp.newFile("_SUCCESS");
+
+ PCollectionRowTuple output =
+ pipeline
+ .apply("Create Input", Create.of(file, marker.getAbsolutePath()))
+ .apply(addFiles(ADDITIONS));
+ assertNotParquetErrorFor(output, marker.getAbsolutePath());
+
+ PipelineResult result = pipeline.run();
+ result.waitUntilFinish();
+
+ assertEquals(Arrays.asList("id=1 name=a age=1"), readRows("id", "name",
"age"));
+ assertEquals(
+ "the marker is an unknown format, not a footer that failed to read",
+ 0,
+ counted(result, ReadFooterSchema.class,
ReadFooterSchema.FOOTER_READ_ERRORS_COUNTER));
+ }
+
+ /** Only a name with no extension is left to its contents; inferFormat stays
strict. */
+ @Test
+ public void testOnlyAnExtensionlessNameIsReadForItsFormat() {
+ assertTrue(AddFiles.hasNoExtension("gs://b/wh/sales.db/orders/000000_0"));
+ assertTrue(AddFiles.hasNoExtension("gs://b/t/part-00000-1a2b3c-c000"));
+ assertFalse(AddFiles.hasNoExtension("gs://b/t/data.txt"));
+ assertFalse(AddFiles.hasNoExtension("gs://b/t/.hidden"));
+ assertFalse(AddFiles.hasNoExtension("gs://b/t/"));
+ assertEquals(FileFormat.PARQUET,
AddFiles.inferFormat("gs://b/t/data.PARQUET"));
+ assertEquals(FileFormat.ORC, AddFiles.inferFormat("gs://b/t/data.orc"));
+ assertThrows(
+ AddFiles.UnknownFormatException.class, () ->
AddFiles.inferFormat("gs://b/t/000000_0"));
+ assertThrows(
+ AddFiles.UnknownFormatException.class, () ->
AddFiles.inferFormat("gs://b/t/data.txt"));
+ }
+
+ private static void assertNotParquetErrorFor(PCollectionRowTuple output,
String file) {
+ PAssert.that(output.get("errors"))
+ .satisfies(
+ rows -> {
+ Row row = Iterables.getOnlyElement(rows);
+ String error = String.valueOf(row.getString("error"));
+ assertEquals(file, row.getString("file"));
+ assertTrue(error, error.startsWith(NOT_PARQUET_ERROR));
+ return null;
+ });
+ }
+
@Test
public void testPartitionPrefixErrors() throws Exception {
// Drop unpartitioned table and create a partitioned one
@@ -1814,6 +1914,63 @@ public class AddFilesTest {
pipeline.run().waitUntilFinish();
}
+ /**
+ * Through the whole transform, so the pre-pass sees the file too: its
footer reads, so it is not
+ * a footer read error, and it costs one error row, not the job.
+ */
+ @Test
+ public void testUnconvertibleSchemaIsOneErrorRowWithEvolution() throws
Exception {
+ catalog.createTable(tableId, icebergSchema);
+ File legacy = new File(temp.getRoot(), "legacy.parquet");
+ writeLegacyRepeated(legacy);
+ String good = writeWithoutFieldIds("good.parquet", icebergSchema,
record(1, "a", 1));
+
+ PCollectionRowTuple output =
+ pipeline
+ .apply("Create Input", Create.of(legacy.getAbsolutePath(), good))
+ .apply(addFiles(ADDITIONS));
+ PAssert.that(output.get("errors"))
+ .satisfies(
+ rows -> {
+ Row row = Iterables.getOnlyElement(rows);
+ assertEquals(legacy.getAbsolutePath(), row.getString("file"));
+ assertTrue(
+ String.valueOf(row.getString("error")),
+ String.valueOf(row.getString("error"))
+
.startsWith(AddFiles.ConvertToDataFile.UNREADABLE_SCHEMA_ERROR));
+ return null;
+ });
+
+ PipelineResult result = pipeline.run();
+ result.waitUntilFinish();
+
+ assertEquals(1,
Iterables.size(catalog.loadTable(tableId).newScan().planFiles()));
+ assertEquals(
+ 0, counted(result, ReadFooterSchema.class,
ReadFooterSchema.FOOTER_READ_ERRORS_COUNTER));
+ }
+
+ /** Readable Parquet whose unannotated repeated field Iceberg's converter
rejects. */
+ private static void writeLegacyRepeated(File file) throws IOException {
+ 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");
+ 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(file.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);
+ }
+ }
+
@Test
public void testUnreadableSchemaRoutesToErrorsWithConverterMessage() throws
Exception {
catalog.createTable(tableId, icebergSchema);
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
index 4f17bc73840..a6c178a520f 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/ReadFooterSchemaTest.java
@@ -164,6 +164,25 @@ public class ReadFooterSchemaTest {
pipeline.run();
}
+ /** Hive names its output files like this, with no extension. */
+ @Test
+ public void testExtensionlessParquetEmitsSchema() throws IOException {
+ String file = writeParquet("000000_0", FLAT_SCHEMA, record(FLAT_SCHEMA,
"id", 1));
+
+ assertSchemas(run(file), FLAT_SCHEMA);
+
+ pipeline.run();
+ }
+
+ @Test
+ public void testExtensionlessNonParquetEmitsNothing() throws IOException {
+ String marker = writeBytes("_SUCCESS", new byte[0]);
+
+ PAssert.that(run(marker)).empty();
+
+ pipeline.run();
+ }
+
/**
* The schema is emitted exactly as declared; which columns proved null-free
travels beside it.
*/
@@ -250,6 +269,39 @@ public class ReadFooterSchemaTest {
return total;
}
+ /** The footer reads, but its schema does not convert: the file contributes
no schema. */
+ @Test
+ public void testUnconvertibleSchemaEmitsNothing() throws IOException {
+ File file = new File(temporaryFolder.getRoot(), "legacy.parquet");
+ writeLegacyRepeated(file);
+
+ PAssert.that(run(file.getAbsolutePath())).empty();
+
+ pipeline.run();
+ }
+
+ /** Readable Parquet whose unannotated repeated field Iceberg's converter
rejects. */
+ private static void writeLegacyRepeated(File file) throws IOException {
+ 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");
+ 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(file.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);
+ }
+ }
+
private PCollection<CollectDistinctSchemas.SchemaGroup> run(String... paths)
{
return pipeline
.apply(Create.of(Arrays.asList(paths)))