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 f8f4b7476c Spark 4.2: Test legacy position deletes when upgrading to
v3 tables (#18214)
f8f4b7476c is described below
commit f8f4b7476c42a592b6a50ef59dd9dfb1c21757ae
Author: Manu Zhang <[email protected]>
AuthorDate: Thu Oct 29 09:31:32 2026 +0800
Spark 4.2: Test legacy position deletes when upgrading to v3 tables (#18214)
Position delete files that contain deletes for more than one data file
must be kept in table metadata until all of their deletes are replaced
by DVs. Cover that in the reader and in the rewrite position deletes
action.
Generated-by: Claude Code
---
.../TestRewritePositionDeleteFilesAction.java | 37 ++++++
.../spark/source/TestSparkReaderDeletes.java | 143 +++++++++++++++++----
2 files changed, 153 insertions(+), 27 deletions(-)
diff --git
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRewritePositionDeleteFilesAction.java
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRewritePositionDeleteFilesAction.java
index ba7a2af2fe..411d08a1f0 100644
---
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRewritePositionDeleteFilesAction.java
+++
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/actions/TestRewritePositionDeleteFilesAction.java
@@ -890,6 +890,43 @@ public class TestRewritePositionDeleteFilesAction extends
CatalogTestBase {
.isEmpty();
}
+ @TestTemplate
+ public void rewriteSharedV2PositionDeletesToV3DVsRemovesLegacyDeleteFiles()
throws IOException {
+ Table table = createTableUnpartitioned(2, SCALE);
+ List<DataFile> dataFiles = TestHelpers.dataFiles(table);
+ writePosDeletesForFiles(table, 1, DELETES_SCALE, dataFiles);
+ assertThat(dataFiles).hasSize(2);
+
+ List<DeleteFile> deleteFiles = deleteFiles(table);
+ assertThat(deleteFiles).hasSize(1).allMatch(file -> file.format() ==
FileFormat.PARQUET);
+
+ List<Object[]> expectedRecords = records(table);
+ List<Object[]> expectedDeletes = deleteRecords(table);
+ assertThat(expectedRecords).hasSize(2000);
+ assertThat(expectedDeletes).hasSize(2000);
+ assertThat(dvRecords(table)).isEmpty();
+
+ table.updateProperties().set(TableProperties.FORMAT_VERSION, "3").commit();
+
+ Result result =
+ SparkActions.get(spark)
+ .rewritePositionDeletes(table)
+ .option(SizeBasedFileRewritePlanner.REWRITE_ALL, "true")
+ .execute();
+
+ List<DeleteFile> newDeleteFiles = deleteFiles(table);
+ assertThat(result.rewrittenDeleteFilesCount()).isEqualTo(1);
+ assertThat(result.addedDeleteFilesCount()).isEqualTo(2);
+ assertThat(newDeleteFiles).hasSize(2).allMatch(file -> file.format() ==
FileFormat.PUFFIN);
+ assertThat(newDeleteFiles).noneMatch(file -> file.format() ==
FileFormat.PARQUET);
+ assertThat(dvRecords(table)).hasSize(2);
+
+ List<Object[]> actualRecords = records(table);
+ List<Object[]> actualDeletes = deleteRecords(table);
+ assertEquals("Rows must match", expectedRecords, actualRecords);
+ assertEquals("Position deletes must match", expectedDeletes,
actualDeletes);
+ }
+
private List<Row> dvRecords(Table table) {
return spark
.read()
diff --git
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReaderDeletes.java
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReaderDeletes.java
index 0d61930571..a40ffb5eab 100644
---
a/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReaderDeletes.java
+++
b/spark/v4.2/spark/src/test/java/org/apache/iceberg/spark/source/TestSparkReaderDeletes.java
@@ -41,6 +41,7 @@ import org.apache.iceberg.DataFile;
import org.apache.iceberg.DataFiles;
import org.apache.iceberg.DeleteFile;
import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.FileScanTask;
import org.apache.iceberg.Files;
import org.apache.iceberg.MetadataColumns;
import org.apache.iceberg.Parameter;
@@ -93,6 +94,7 @@ import org.apache.spark.sql.catalyst.InternalRow;
import org.apache.spark.sql.internal.SQLConf;
import org.apache.spark.sql.types.StructType;
import org.apache.spark.sql.vectorized.ColumnarBatch;
+import org.assertj.core.groups.Tuple;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
@@ -175,33 +177,7 @@ public class TestSparkReaderDeletes extends
DeleteReadTests {
@Override
protected Table createTable(String name, Schema schema, PartitionSpec spec) {
- Table table = catalog.createTable(TableIdentifier.of("default", name),
schema);
- TableOperations ops = ((BaseTable) table).operations();
- TableMetadata meta = ops.current();
- ops.commit(meta, meta.upgradeToFormatVersion(formatVersion));
- table
- .updateProperties()
- .set(TableProperties.DEFAULT_FILE_FORMAT, format.name())
- .set(TableProperties.DATA_PLANNING_MODE, planningMode.modeName())
- .set(TableProperties.DELETE_PLANNING_MODE, planningMode.modeName())
- .set(TableProperties.FORMAT_VERSION, String.valueOf(formatVersion))
- .commit();
- if (format.equals(FileFormat.PARQUET) || format.equals(FileFormat.ORC)) {
- String vectorizationEnabled =
- format.equals(FileFormat.PARQUET)
- ? TableProperties.PARQUET_VECTORIZATION_ENABLED
- : TableProperties.ORC_VECTORIZATION_ENABLED;
- String batchSize =
- format.equals(FileFormat.PARQUET)
- ? TableProperties.PARQUET_BATCH_SIZE
- : TableProperties.ORC_BATCH_SIZE;
- table.updateProperties().set(vectorizationEnabled,
String.valueOf(vectorized)).commit();
- if (vectorized) {
- // split 7 records to two batches to cover more code paths
- table.updateProperties().set(batchSize, "4").commit();
- }
- }
- return table;
+ return createTable(name, schema, formatVersion);
}
@Override
@@ -415,6 +391,89 @@ public class TestSparkReaderDeletes extends
DeleteReadTests {
checkDeleteCount(4L);
}
+ @TestTemplate
+ public void sharedPositionDeletesAreRetainedDuringPartialDVReplacement()
throws IOException {
+ assumeThat(format).isEqualTo(FileFormat.PARQUET);
+ assumeThat(formatVersion).isEqualTo(3);
+
+ String tableName = "test3";
+ Table table = createTable(tableName, SCHEMA, 2);
+
+ GenericRecord record = GenericRecord.create(table.schema());
+ List<Record> firstFileRecords =
+ Lists.newArrayList(
+ record.copy("id", 1, "data", "a"),
+ record.copy("id", 2, "data", "b"),
+ record.copy("id", 3, "data", "c"));
+ List<Record> secondFileRecords =
+ Lists.newArrayList(
+ record.copy("id", 4, "data", "d"),
+ record.copy("id", 5, "data", "e"),
+ record.copy("id", 6, "data", "f"));
+
+ DataFile firstDataFile =
+ FileHelpers.writeDataFile(
+ table,
+ Files.localOutput(temp.resolve("junit" +
System.nanoTime()).toFile()),
+ TestHelpers.Row.of(),
+ firstFileRecords);
+ DataFile secondDataFile =
+ FileHelpers.writeDataFile(
+ table,
+ Files.localOutput(temp.resolve("junit" +
System.nanoTime()).toFile()),
+ TestHelpers.Row.of(),
+ secondFileRecords);
+
table.newAppend().appendFile(firstDataFile).appendFile(secondDataFile).commit();
+
+ Pair<DeleteFile, CharSequenceSet> sharedPositionDeletes =
+ FileHelpers.writeDeleteFile(
+ table,
+ Files.localOutput(temp.resolve("junit" +
System.nanoTime()).toFile()),
+ TestHelpers.Row.of(),
+ Lists.newArrayList(
+ Pair.of(firstDataFile.location(), 0L),
+ Pair.of(firstDataFile.location(), 1L),
+ Pair.of(secondDataFile.location(), 0L)),
+ 2);
+ table
+ .newRowDelta()
+ .addDeletes(sharedPositionDeletes.first())
+ .validateDataFilesExist(sharedPositionDeletes.second())
+ .commit();
+
+ table.updateProperties().set(TableProperties.FORMAT_VERSION, "3").commit();
+
+ Pair<DeleteFile, CharSequenceSet> secondFileDV =
+ FileHelpers.writeDeleteFile(
+ table,
+ Files.localOutput(temp.resolve("junit" +
System.nanoTime()).toFile()),
+ TestHelpers.Row.of(),
+ Lists.newArrayList(
+ Pair.of(secondDataFile.location(), 0L),
Pair.of(secondDataFile.location(), 1L)),
+ 3);
+ table
+ .newRowDelta()
+ .addDeletes(secondFileDV.first())
+ .validateDataFilesExist(secondFileDV.second())
+ .commit();
+
+ StructLikeSet actual = rowSet(tableName, table, "*");
+ StructLikeSet expected = rowSetWithoutIds(table, firstFileRecords, 1, 2);
+ expected.addAll(rowSetWithoutIds(table, secondFileRecords, 4, 5));
+ assertThat(actual).as("Table should contain expected
rows").isEqualTo(expected);
+
+ try (CloseableIterable<FileScanTask> tasks = table.newScan().planFiles()) {
+ List<FileScanTask> fileTasks = Lists.newArrayList(tasks);
+ assertThat(fileTasks).hasSize(2);
+ assertThat(fileTasks).allSatisfy(task ->
assertThat(task.deletes()).hasSize(1));
+ assertThat(fileTasks)
+ .extracting(task -> task.file().location(), task ->
task.deletes().get(0).format())
+ .containsExactlyInAnyOrder(
+ Tuple.tuple(firstDataFile.location(), FileFormat.PARQUET),
+ Tuple.tuple(secondDataFile.location(), FileFormat.PUFFIN));
+ }
+ }
+
@TestTemplate
public void testEqualityDeleteWithDeletedColumn() throws IOException {
String tableName = table.name().substring(table.name().lastIndexOf(".") +
1);
@@ -869,4 +928,34 @@ public class TestSparkReaderDeletes extends
DeleteReadTests {
records.add(record.copy("id", 122, "data", "g", "_deleted", false));
return records;
}
+
+ private Table createTable(String name, Schema schema, int
tableFormatVersion) {
+ Table table = catalog.createTable(TableIdentifier.of("default", name),
schema);
+ TableOperations ops = ((BaseTable) table).operations();
+ TableMetadata meta = ops.current();
+ ops.commit(meta, meta.upgradeToFormatVersion(tableFormatVersion));
+ table
+ .updateProperties()
+ .set(TableProperties.DEFAULT_FILE_FORMAT, format.name())
+ .set(TableProperties.DATA_PLANNING_MODE, planningMode.modeName())
+ .set(TableProperties.DELETE_PLANNING_MODE, planningMode.modeName())
+ .set(TableProperties.FORMAT_VERSION,
String.valueOf(tableFormatVersion))
+ .commit();
+ if (format.equals(FileFormat.PARQUET) || format.equals(FileFormat.ORC)) {
+ String vectorizationEnabled =
+ format.equals(FileFormat.PARQUET)
+ ? TableProperties.PARQUET_VECTORIZATION_ENABLED
+ : TableProperties.ORC_VECTORIZATION_ENABLED;
+ String batchSize =
+ format.equals(FileFormat.PARQUET)
+ ? TableProperties.PARQUET_BATCH_SIZE
+ : TableProperties.ORC_BATCH_SIZE;
+ table.updateProperties().set(vectorizationEnabled,
String.valueOf(vectorized)).commit();
+ if (vectorized) {
+ // split 7 records to two batches to cover more code paths
+ table.updateProperties().set(batchSize, "4").commit();
+ }
+ }
+ return table;
+ }
}