diff --git a/core/src/main/java/org/apache/iceberg/avro/Avro.java b/core/src/main/java/org/apache/iceberg/avro/Avro.java index 4a5136f58e71..f24fe621b8a3 100644 --- a/core/src/main/java/org/apache/iceberg/avro/Avro.java +++ b/core/src/main/java/org/apache/iceberg/avro/Avro.java @@ -506,6 +506,7 @@ public EqualityDeleteWriter buildEqualityWriter() throws IOException { Preconditions.checkArgument( spec.isUnpartitioned() || partition != null, "Partition must not be null for partitioned writes"); + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, rowSchema); meta("delete-type", "equality"); meta( diff --git a/core/src/main/java/org/apache/iceberg/formats/FileWriterBuilderImpl.java b/core/src/main/java/org/apache/iceberg/formats/FileWriterBuilderImpl.java index d0f9c6da2fbf..8ea9ad616a26 100644 --- a/core/src/main/java/org/apache/iceberg/formats/FileWriterBuilderImpl.java +++ b/core/src/main/java/org/apache/iceberg/formats/FileWriterBuilderImpl.java @@ -36,6 +36,7 @@ import org.apache.iceberg.encryption.EncryptedOutputFile; import org.apache.iceberg.encryption.EncryptionKeyMetadata; import org.apache.iceberg.io.DataWriter; +import org.apache.iceberg.io.DeleteSchemaUtil; import org.apache.iceberg.io.FileWriter; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; @@ -213,6 +214,9 @@ protected void validate() { spec.isUnpartitioned() || partition != null, "Invalid partition, does not match spec: %s", spec); + if (content == FileContent.EQUALITY_DELETES) { + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, schema); + } } /** Builder for creating {@link DataWriter} instances for writing data files. */ diff --git a/core/src/main/java/org/apache/iceberg/io/DeleteSchemaUtil.java b/core/src/main/java/org/apache/iceberg/io/DeleteSchemaUtil.java index 4af310d80e4d..314acb683a80 100644 --- a/core/src/main/java/org/apache/iceberg/io/DeleteSchemaUtil.java +++ b/core/src/main/java/org/apache/iceberg/io/DeleteSchemaUtil.java @@ -18,8 +18,11 @@ */ package org.apache.iceberg.io; +import java.util.Set; import org.apache.iceberg.MetadataColumns; import org.apache.iceberg.Schema; +import org.apache.iceberg.relocated.com.google.common.base.Preconditions; +import org.apache.iceberg.relocated.com.google.common.collect.Sets; import org.apache.iceberg.types.Types; public class DeleteSchemaUtil { @@ -54,4 +57,21 @@ public static Schema posDeleteReadSchema(Schema rowSchema) { rowSchema.asStruct(), MetadataColumns.DELETE_FILE_ROW_DOC)); } + + public static void validateEqualityFieldIds(int[] equalityFieldIds, Schema equalityDeleteSchema) { + Preconditions.checkArgument( + equalityFieldIds != null && equalityFieldIds.length > 0, + "Equality delete field IDs must not be null or empty"); + Preconditions.checkArgument(equalityDeleteSchema != null, "Schema must not be null"); + + Set seenFieldIds = Sets.newHashSetWithExpectedSize(equalityFieldIds.length); + for (int fieldId : equalityFieldIds) { + Preconditions.checkArgument( + seenFieldIds.add(fieldId), "Duplicate equality delete field ID: %s", fieldId); + Preconditions.checkArgument( + equalityDeleteSchema.findField(fieldId) != null, + "Invalid equality delete field ID: %s", + fieldId); + } + } } diff --git a/core/src/test/java/org/apache/iceberg/avro/TestAvroDeleteWriters.java b/core/src/test/java/org/apache/iceberg/avro/TestAvroDeleteWriters.java index 86bb74c5a397..12cc4023fbe3 100644 --- a/core/src/test/java/org/apache/iceberg/avro/TestAvroDeleteWriters.java +++ b/core/src/test/java/org/apache/iceberg/avro/TestAvroDeleteWriters.java @@ -19,6 +19,7 @@ package org.apache.iceberg.avro; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.File; import java.io.IOException; @@ -112,6 +113,57 @@ public void testEqualityDeleteWriter() throws IOException { assertThat(deletedRecords).as("Deleted records should match expected").isEqualTo(records); } + @Test + public void equalityDeleteWriterRejectsEmptyEqualityFieldIds() { + OutputFile out = new InMemoryOutputFile(); + + assertThatThrownBy( + () -> + Avro.writeDeletes(out) + .createWriterFunc(DataWriter::create) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(new int[0]) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Equality delete field IDs must not be null or empty"); + } + + @Test + public void equalityDeleteWriterRejectsDuplicateEqualityFieldIds() { + OutputFile out = new InMemoryOutputFile(); + + assertThatThrownBy( + () -> + Avro.writeDeletes(out) + .createWriterFunc(DataWriter::create) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(1, 1) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Duplicate equality delete field ID: 1"); + } + + @Test + public void equalityDeleteWriterRejectsMissingEqualityFieldId() { + OutputFile out = new InMemoryOutputFile(); + + assertThatThrownBy( + () -> + Avro.writeDeletes(out) + .createWriterFunc(DataWriter::create) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(99) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid equality delete field ID: 99"); + } + @Test public void testPositionDeleteWriter() throws IOException { Schema deleteSchema = diff --git a/core/src/test/java/org/apache/iceberg/formats/TestFormatModelRegistry.java b/core/src/test/java/org/apache/iceberg/formats/TestFormatModelRegistry.java index cda302c249c2..0210af0295ea 100644 --- a/core/src/test/java/org/apache/iceberg/formats/TestFormatModelRegistry.java +++ b/core/src/test/java/org/apache/iceberg/formats/TestFormatModelRegistry.java @@ -23,9 +23,20 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import org.apache.iceberg.FileContent; import org.apache.iceberg.FileFormat; +import org.apache.iceberg.Metrics; +import org.apache.iceberg.MetricsConfig; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.encryption.EncryptedFiles; import org.apache.iceberg.encryption.EncryptedOutputFile; +import org.apache.iceberg.encryption.EncryptionKeyMetadata; +import org.apache.iceberg.inmemory.InMemoryOutputFile; +import org.apache.iceberg.io.FileAppender; import org.apache.iceberg.io.InputFile; +import org.apache.iceberg.types.Types; import org.apache.iceberg.util.Pair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -124,6 +135,25 @@ public static void register() { } } + @Test + void equalityDeleteWriterRejectsMissingEqualityFieldId() { + FormatModelRegistry.register(new DummyParquetFormatModel(Object.class, Object.class)); + EncryptedOutputFile outputFile = + EncryptedFiles.encryptedOutput(new InMemoryOutputFile(), EncryptionKeyMetadata.EMPTY); + Schema schema = new Schema(Types.NestedField.required(1, "id", Types.LongType.get())); + + assertThatThrownBy( + () -> + FormatModelRegistry.equalityDeleteWriteBuilder( + FileFormat.PARQUET, Object.class, outputFile) + .schema(schema) + .spec(PartitionSpec.unpartitioned()) + .equalityFieldIds(99) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid equality delete field ID: 99"); + } + private static class DummyParquetFormatModel implements FormatModel { private final Class type; private final Class schemaType; @@ -152,7 +182,7 @@ public Class schemaType() { @Override public ModelWriteBuilder writeBuilder(EncryptedOutputFile outputFile) { - return null; + return new DummyModelWriteBuilder(); } @Override @@ -160,4 +190,74 @@ public ReadBuilder readBuilder(InputFile inputFile) { return null; } } + + private static class DummyModelWriteBuilder implements ModelWriteBuilder { + @Override + public ModelWriteBuilder schema(Schema schema) { + return this; + } + + @Override + public ModelWriteBuilder engineSchema(Object schema) { + return this; + } + + @Override + public ModelWriteBuilder set(String property, String value) { + return this; + } + + @Override + public ModelWriteBuilder meta(String property, String value) { + return this; + } + + @Override + public ModelWriteBuilder content(FileContent content) { + return this; + } + + @Override + public ModelWriteBuilder metricsConfig(MetricsConfig metricsConfig) { + return this; + } + + @Override + public ModelWriteBuilder overwrite() { + return this; + } + + @Override + public ModelWriteBuilder withFileEncryptionKey(ByteBuffer encryptionKey) { + return this; + } + + @Override + public ModelWriteBuilder withAADPrefix(ByteBuffer aadPrefix) { + return this; + } + + @Override + public FileAppender build() { + return new NoOpFileAppender(); + } + } + + private static class NoOpFileAppender implements FileAppender { + @Override + public void add(Object datum) {} + + @Override + public Metrics metrics() { + return null; + } + + @Override + public long length() { + return 0; + } + + @Override + public void close() {} + } } diff --git a/data/src/main/java/org/apache/iceberg/data/BaseFileWriterFactory.java b/data/src/main/java/org/apache/iceberg/data/BaseFileWriterFactory.java index 444c0d0226bd..a19a9f280279 100644 --- a/data/src/main/java/org/apache/iceberg/data/BaseFileWriterFactory.java +++ b/data/src/main/java/org/apache/iceberg/data/BaseFileWriterFactory.java @@ -35,6 +35,7 @@ import org.apache.iceberg.encryption.EncryptedOutputFile; import org.apache.iceberg.encryption.EncryptionKeyMetadata; import org.apache.iceberg.io.DataWriter; +import org.apache.iceberg.io.DeleteSchemaUtil; import org.apache.iceberg.io.FileWriterFactory; import org.apache.iceberg.orc.ORC; import org.apache.iceberg.parquet.Parquet; @@ -69,6 +70,10 @@ protected BaseFileWriterFactory( Schema equalityDeleteRowSchema, SortOrder equalityDeleteSortOrder, Map writerProperties) { + if (equalityDeleteRowSchema != null && equalityFieldIds != null) { + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, equalityDeleteRowSchema); + } + this.table = table; this.dataFileFormat = dataFileFormat; this.dataSchema = dataSchema; @@ -92,6 +97,10 @@ protected BaseFileWriterFactory( SortOrder equalityDeleteSortOrder, Schema positionDeleteRowSchema, Map writerProperties) { + if (equalityDeleteRowSchema != null && equalityFieldIds != null) { + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, equalityDeleteRowSchema); + } + this.table = table; this.dataFileFormat = dataFileFormat; this.dataSchema = dataSchema; @@ -115,6 +124,10 @@ protected BaseFileWriterFactory( Schema equalityDeleteRowSchema, SortOrder equalityDeleteSortOrder, Schema positionDeleteRowSchema) { + if (equalityDeleteRowSchema != null && equalityFieldIds != null) { + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, equalityDeleteRowSchema); + } + this.table = table; this.dataFileFormat = dataFileFormat; this.dataSchema = dataSchema; diff --git a/data/src/main/java/org/apache/iceberg/data/GenericAppenderFactory.java b/data/src/main/java/org/apache/iceberg/data/GenericAppenderFactory.java index dd78b48858a2..f7cd7c059e9d 100644 --- a/data/src/main/java/org/apache/iceberg/data/GenericAppenderFactory.java +++ b/data/src/main/java/org/apache/iceberg/data/GenericAppenderFactory.java @@ -35,6 +35,7 @@ import org.apache.iceberg.deletes.PositionDeleteWriter; import org.apache.iceberg.encryption.EncryptedOutputFile; import org.apache.iceberg.encryption.EncryptionUtil; +import org.apache.iceberg.io.DeleteSchemaUtil; import org.apache.iceberg.io.FileAppender; import org.apache.iceberg.io.FileAppenderFactory; import org.apache.iceberg.io.OutputFile; @@ -132,6 +133,10 @@ public GenericAppenderFactory( int[] equalityFieldIds, Schema eqDeleteRowSchema, Schema posDeleteRowSchema) { + if (eqDeleteRowSchema != null && equalityFieldIds != null) { + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, eqDeleteRowSchema); + } + this.table = table; this.config = config == null ? Maps.newHashMap() : config; diff --git a/data/src/main/java/org/apache/iceberg/data/RegistryBasedFileWriterFactory.java b/data/src/main/java/org/apache/iceberg/data/RegistryBasedFileWriterFactory.java index 868b41f5840b..d522f140a74e 100644 --- a/data/src/main/java/org/apache/iceberg/data/RegistryBasedFileWriterFactory.java +++ b/data/src/main/java/org/apache/iceberg/data/RegistryBasedFileWriterFactory.java @@ -36,6 +36,7 @@ import org.apache.iceberg.formats.FileWriterBuilder; import org.apache.iceberg.formats.FormatModelRegistry; import org.apache.iceberg.io.DataWriter; +import org.apache.iceberg.io.DeleteSchemaUtil; import org.apache.iceberg.io.FileWriterFactory; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; @@ -73,6 +74,10 @@ protected RegistryBasedFileWriterFactory( Map writerProperties, S inputSchema, S equalityDeleteInputSchema) { + if (equalityDeleteRowSchema != null && equalityFieldIds != null) { + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, equalityDeleteRowSchema); + } + this.table = table; this.dataFileFormat = dataFileFormat; this.inputType = inputType; diff --git a/data/src/test/java/org/apache/iceberg/TestGenericAppenderFactory.java b/data/src/test/java/org/apache/iceberg/TestGenericAppenderFactory.java index 5d940adaec58..b6b42ae8ea97 100644 --- a/data/src/test/java/org/apache/iceberg/TestGenericAppenderFactory.java +++ b/data/src/test/java/org/apache/iceberg/TestGenericAppenderFactory.java @@ -136,4 +136,33 @@ void createFactoryWithConflictConfig() { .hasMessageContaining( "Cannot set metrics properties when the table is provided, use table properties instead"); } + + @TestTemplate + void equalityFieldIdsAreValidatedAgainstEqualityDeleteRowSchema() { + int equalityFieldId = table.schema().findField("id").fieldId(); + + assertThatNoException() + .isThrownBy( + () -> + new GenericAppenderFactory( + null, + PartitionSpec.unpartitioned(), + new int[] {equalityFieldId}, + table.schema().select("id"))); + } + + @TestTemplate + void equalityFieldIdsAreRejectedWhenMissingFromEqualityDeleteRowSchema() { + int equalityFieldId = table.schema().findField("id").fieldId(); + + assertThatThrownBy( + () -> + new GenericAppenderFactory( + table.schema().select("id"), + PartitionSpec.unpartitioned(), + new int[] {equalityFieldId}, + table.schema().select("data"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid equality delete field ID: %s", equalityFieldId); + } } diff --git a/data/src/test/java/org/apache/iceberg/data/TestGenericFileWriterFactory.java b/data/src/test/java/org/apache/iceberg/data/TestGenericFileWriterFactory.java index fc8d420742cf..48ebae8b82d8 100644 --- a/data/src/test/java/org/apache/iceberg/data/TestGenericFileWriterFactory.java +++ b/data/src/test/java/org/apache/iceberg/data/TestGenericFileWriterFactory.java @@ -18,6 +18,9 @@ */ package org.apache.iceberg.data; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import java.io.IOException; import java.util.List; import org.apache.iceberg.Schema; @@ -64,4 +67,31 @@ protected StructLikeSet toSet(Iterable records) { @Override @TestTemplate public void testPositionDeleteWriterWithRow() throws IOException {} + + @TestTemplate + void equalityFieldIdsAreValidatedAgainstEqualityDeleteRowSchema() { + int equalityFieldId = table.schema().findField("id").fieldId(); + + assertThatNoException() + .isThrownBy( + () -> + newWriterFactory( + table.schema().select("data"), + List.of(equalityFieldId), + table.schema().select("id"))); + } + + @TestTemplate + void equalityFieldIdsAreRejectedWhenMissingFromEqualityDeleteRowSchema() { + int equalityFieldId = table.schema().findField("id").fieldId(); + + assertThatThrownBy( + () -> + newWriterFactory( + table.schema().select("id"), + List.of(equalityFieldId), + table.schema().select("data"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid equality delete field ID: %s", equalityFieldId); + } } diff --git a/orc/src/main/java/org/apache/iceberg/orc/ORC.java b/orc/src/main/java/org/apache/iceberg/orc/ORC.java index 9fb805246962..0b4f88cf6d2f 100644 --- a/orc/src/main/java/org/apache/iceberg/orc/ORC.java +++ b/orc/src/main/java/org/apache/iceberg/orc/ORC.java @@ -619,6 +619,7 @@ public EqualityDeleteWriter buildEqualityWriter() { Preconditions.checkArgument( spec.isUnpartitioned() || partition != null, "Partition must not be null for partitioned writes"); + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, rowSchema); meta("delete-type", "equality"); meta( diff --git a/orc/src/test/java/org/apache/iceberg/orc/TestOrcDeleteWriters.java b/orc/src/test/java/org/apache/iceberg/orc/TestOrcDeleteWriters.java index 112885bb00a4..6669c6da2b05 100644 --- a/orc/src/test/java/org/apache/iceberg/orc/TestOrcDeleteWriters.java +++ b/orc/src/test/java/org/apache/iceberg/orc/TestOrcDeleteWriters.java @@ -19,6 +19,7 @@ package org.apache.iceberg.orc; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.File; import java.io.IOException; @@ -106,6 +107,57 @@ public void testEqualityDeleteWriter() throws IOException { assertThat(deletedRecords).as("Deleted records should match expected").isEqualTo(records); } + @Test + public void equalityDeleteWriterRejectsEmptyEqualityFieldIds() { + OutputFile out = Files.localOutput(temp); + + assertThatThrownBy( + () -> + ORC.writeDeletes(out) + .createWriterFunc(GenericOrcWriter::buildWriter) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(new int[0]) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Equality delete field IDs must not be null or empty"); + } + + @Test + public void equalityDeleteWriterRejectsDuplicateEqualityFieldIds() { + OutputFile out = Files.localOutput(temp); + + assertThatThrownBy( + () -> + ORC.writeDeletes(out) + .createWriterFunc(GenericOrcWriter::buildWriter) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(1, 1) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Duplicate equality delete field ID: 1"); + } + + @Test + public void equalityDeleteWriterRejectsMissingEqualityFieldId() { + OutputFile out = Files.localOutput(temp); + + assertThatThrownBy( + () -> + ORC.writeDeletes(out) + .createWriterFunc(GenericOrcWriter::buildWriter) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(99) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid equality delete field ID: 99"); + } + @Test public void testPositionDeleteWriter() throws IOException { Schema deleteSchema = diff --git a/parquet/src/main/java/org/apache/iceberg/parquet/Parquet.java b/parquet/src/main/java/org/apache/iceberg/parquet/Parquet.java index 9119d16d86dd..8114acb2d731 100644 --- a/parquet/src/main/java/org/apache/iceberg/parquet/Parquet.java +++ b/parquet/src/main/java/org/apache/iceberg/parquet/Parquet.java @@ -1129,6 +1129,7 @@ public EqualityDeleteWriter buildEqualityWriter() throws IOException { Preconditions.checkArgument( spec.isUnpartitioned() || partition != null, "Partition must not be null for partitioned writes"); + DeleteSchemaUtil.validateEqualityFieldIds(equalityFieldIds, rowSchema); meta("delete-type", "equality"); meta( diff --git a/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDeleteWriters.java b/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDeleteWriters.java index 7e91f5d9eb63..e26d839b192f 100644 --- a/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDeleteWriters.java +++ b/parquet/src/test/java/org/apache/iceberg/parquet/TestParquetDeleteWriters.java @@ -19,6 +19,7 @@ package org.apache.iceberg.parquet; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.File; import java.io.IOException; @@ -111,6 +112,57 @@ public void testEqualityDeleteWriter() throws IOException { assertThat(deletedRecords).as("Deleted records should match expected").isEqualTo(records); } + @Test + public void equalityDeleteWriterRejectsEmptyEqualityFieldIds() { + OutputFile out = Files.localOutput(temp); + + assertThatThrownBy( + () -> + Parquet.writeDeletes(out) + .createWriterFunc(GenericParquetWriter::create) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(new int[0]) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Equality delete field IDs must not be null or empty"); + } + + @Test + public void equalityDeleteWriterRejectsDuplicateEqualityFieldIds() { + OutputFile out = Files.localOutput(temp); + + assertThatThrownBy( + () -> + Parquet.writeDeletes(out) + .createWriterFunc(GenericParquetWriter::create) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(1, 1) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Duplicate equality delete field ID: 1"); + } + + @Test + public void equalityDeleteWriterRejectsMissingEqualityFieldId() { + OutputFile out = Files.localOutput(temp); + + assertThatThrownBy( + () -> + Parquet.writeDeletes(out) + .createWriterFunc(GenericParquetWriter::create) + .overwrite() + .rowSchema(SCHEMA) + .withSpec(PartitionSpec.unpartitioned()) + .equalityFieldIds(99) + .buildEqualityWriter()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid equality delete field ID: 99"); + } + @Test public void testPositionDeleteWriter() throws IOException { Schema deleteSchema =